feat: model scene 9 staged messages

This commit is contained in:
lda
2026-07-12 07:48:57 +07:00 Verified
parent e2e476a490
commit 7b9817fe45
2 changed files with 212 additions and 0 deletions
@@ -0,0 +1,114 @@
import { describe, expect, it } from "vitest";
import {
SCENE9_PHASE_PROMPTS,
initialScene9MessageState,
projectScene9Message,
projectScene9SubmittedOverrides,
scene9MessageReducer,
} from "./scene9-message-state.js";
describe("scene 9 staged message state", () => {
it("defines the exact prompt for every phase", () => {
expect(SCENE9_PHASE_PROMPTS).toEqual({
discover: "",
draft: "Is the draft valid? Can you check and fix any issues?",
validate: "",
artifact: "Now save everything as a deployment and make sure the bindings are valid.",
deployment:
"The deployment lda_report_case_study.default is saved and valid. Shall we run it now?",
});
});
it("projects empty phases with a useful placeholder", () => {
expect(projectScene9Message(initialScene9MessageState, "discover")).toMatchObject({
draft: "",
prefill: "",
placeholder: expect.any(String),
});
expect(projectScene9Message(initialScene9MessageState, "validate")).toMatchObject({
draft: "",
prefill: "",
placeholder: expect.any(String),
});
});
it("projects the exact phase prefill without changing the draft", () => {
expect(projectScene9Message(initialScene9MessageState, "draft")).toMatchObject({
draft: "",
prefill: SCENE9_PHASE_PROMPTS.draft,
});
expect(projectScene9Message(initialScene9MessageState, "artifact")).toMatchObject({
draft: "",
prefill: SCENE9_PHASE_PROMPTS.artifact,
});
});
it("preserves edited draft text exactly when submitting draft", () => {
const edited = scene9MessageReducer(initialScene9MessageState, {
type: "draft_edited",
draft: " Check only the report binding. ",
});
expect(scene9MessageReducer(edited, { type: "draft_submitted" })).toEqual({
draft: edited.draft,
submittedOverrides: { validate: edited.draft },
runRequested: null,
});
});
it("stores artifact submissions under the deployment destination", () => {
const state = scene9MessageReducer(initialScene9MessageState, {
type: "draft_edited",
draft: "Save this edited deployment request",
});
expect(scene9MessageReducer(state, { type: "artifact_submitted" })).toEqual({
draft: state.draft,
submittedOverrides: { deployment: state.draft },
runRequested: null,
});
});
it("ignores blank submits and keeps duplicate submits idempotent", () => {
const blank = { ...initialScene9MessageState, draft: " \n\t" };
expect(scene9MessageReducer(blank, { type: "draft_submitted" })).toBe(blank);
expect(scene9MessageReducer(blank, { type: "artifact_submitted" })).toBe(blank);
expect(scene9MessageReducer(blank, { type: "run_requested" })).toBe(blank);
const submitted = scene9MessageReducer(
scene9MessageReducer(initialScene9MessageState, {
type: "draft_edited",
draft: "A draft override",
}),
{ type: "draft_submitted" },
);
expect(scene9MessageReducer(submitted, { type: "draft_submitted" })).toBe(submitted);
});
it("records the final run request without claiming execution", () => {
const state = scene9MessageReducer(initialScene9MessageState, {
type: "draft_edited",
draft: "Run this deployment",
});
const requested = scene9MessageReducer(state, { type: "run_requested" });
expect(requested.runRequested).toBe("Run this deployment");
expect(requested.submittedOverrides).toEqual({});
expect(scene9MessageReducer(requested, { type: "run_requested" })).toBe(requested);
});
it("projects only destination-phase overrides for the prepared thread", () => {
const state = {
...initialScene9MessageState,
submittedOverrides: {
validate: "Edited validation request",
deployment: "Edited deployment request",
},
};
expect(projectScene9SubmittedOverrides(state)).toEqual({
validate: "Edited validation request",
deployment: "Edited deployment request",
});
});
});
@@ -0,0 +1,98 @@
import type { AuthoringPhaseId } from "./authoring-recording.js";
export type Scene9MessagePhase = AuthoringPhaseId;
export type Scene9DestinationPhase = "validate" | "deployment";
export const SCENE9_PHASE_PROMPTS: Readonly<Record<Scene9MessagePhase, string>> = {
discover: "",
draft: "Is the draft valid? Can you check and fix any issues?",
validate: "",
artifact: "Now save everything as a deployment and make sure the bindings are valid.",
deployment:
"The deployment lda_report_case_study.default is saved and valid. Shall we run it now?",
};
export const SCENE9_PHASE_PLACEHOLDERS: Readonly<Record<Scene9MessagePhase, string>> = {
discover: "Ask about the workflow authoring process.",
draft: "Review the prepared draft.",
validate: "Ask about the draft validation results.",
artifact: "Save the validated workflow as a deployment.",
deployment: "Ask whether the saved deployment should run.",
};
export type Scene9SubmittedOverrides = Readonly<
Partial<Record<Scene9DestinationPhase, string>>
>;
export type Scene9MessageState = {
readonly draft: string;
readonly submittedOverrides: Scene9SubmittedOverrides;
readonly runRequested: string | null;
};
export const initialScene9MessageState: Scene9MessageState = {
draft: "",
submittedOverrides: {},
runRequested: null,
};
export type Scene9MessageAction =
| { readonly type: "draft_edited"; readonly draft: string }
| { readonly type: "draft_submitted" }
| { readonly type: "artifact_submitted" }
| { readonly type: "run_requested" };
const hasText = (text: string): boolean => text.trim().length > 0;
const submitOverride = (
state: Scene9MessageState,
destination: Scene9DestinationPhase,
): Scene9MessageState => {
if (!hasText(state.draft) || state.submittedOverrides[destination] !== undefined) return state;
return {
...state,
submittedOverrides: {
...state.submittedOverrides,
[destination]: state.draft,
},
};
};
export const scene9MessageReducer = (
state: Scene9MessageState,
action: Scene9MessageAction,
): Scene9MessageState => {
switch (action.type) {
case "draft_edited":
return state.runRequested === null ? { ...state, draft: action.draft } : state;
case "draft_submitted":
return submitOverride(state, "validate");
case "artifact_submitted":
return submitOverride(state, "deployment");
case "run_requested":
return state.runRequested === null && hasText(state.draft)
? { ...state, runRequested: state.draft }
: state;
}
};
export type Scene9MessageProjection = {
readonly draft: string;
readonly prefill: string;
readonly placeholder: string;
};
export const projectScene9Message = (
state: Scene9MessageState,
phase: Scene9MessagePhase,
): Scene9MessageProjection => ({
draft: state.draft,
prefill: SCENE9_PHASE_PROMPTS[phase],
placeholder: SCENE9_PHASE_PLACEHOLDERS[phase],
});
/** Keeps the transcript-facing map limited to the two staged handoff destinations. */
export const projectScene9SubmittedOverrides = (
state: Scene9MessageState,
): Scene9SubmittedOverrides => state.submittedOverrides;