feat: split authoring diagnosis and repair beats
This commit is contained in:
@@ -4,14 +4,18 @@ import {
|
|||||||
projectPreparedAuthoringThread,
|
projectPreparedAuthoringThread,
|
||||||
type AuthoringPhaseId,
|
type AuthoringPhaseId,
|
||||||
} from "./authoring-recording.js";
|
} from "./authoring-recording.js";
|
||||||
import type { Scene9SubmittedOverrides } from "./scene9-message-state.js";
|
import {
|
||||||
|
recordingPhaseForStep,
|
||||||
|
type PreparedLifecycleStepId,
|
||||||
|
} from "./authoring-projection.js";
|
||||||
|
import type { PreparedLifecycleSubmittedOverrides } from "./prepared-lifecycle-message-state.js";
|
||||||
|
|
||||||
type AuthoringConversationProps = {
|
type AuthoringConversationProps = {
|
||||||
readonly throughPhase: AuthoringPhaseId;
|
readonly throughPhase: PreparedLifecycleStepId | AuthoringPhaseId;
|
||||||
readonly activePhase: AuthoringPhaseId;
|
readonly activePhase: PreparedLifecycleStepId | AuthoringPhaseId;
|
||||||
readonly surface: "stage" | "dock";
|
readonly surface: "stage" | "dock";
|
||||||
readonly requestOverride?: string | undefined;
|
readonly requestOverride?: string | undefined;
|
||||||
readonly requestOverrides?: Scene9SubmittedOverrides | undefined;
|
readonly requestOverrides?: PreparedLifecycleSubmittedOverrides | undefined;
|
||||||
readonly scrollMode?: "active" | "start" | undefined;
|
readonly scrollMode?: "active" | "start" | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,8 +31,12 @@ export const AuthoringConversation = ({
|
|||||||
<AssistantOperatorThread
|
<AssistantOperatorThread
|
||||||
mode={surface === "stage" ? "full" : "dock"}
|
mode={surface === "stage" ? "full" : "dock"}
|
||||||
surface={surface}
|
surface={surface}
|
||||||
messages={projectPreparedAuthoringThread(throughPhase, requestOverride, requestOverrides)}
|
messages={projectPreparedAuthoringThread(
|
||||||
activeToolGroupId={authoringToolGroupId(activePhase)}
|
recordingPhaseForStep(throughPhase),
|
||||||
|
requestOverride,
|
||||||
|
requestOverrides,
|
||||||
|
)}
|
||||||
|
activeToolGroupId={authoringToolGroupId(recordingPhaseForStep(activePhase))}
|
||||||
scrollMode={scrollMode}
|
scrollMode={scrollMode}
|
||||||
ariaLabel="prepared authoring conversation"
|
ariaLabel="prepared authoring conversation"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import type { ComponentProps } from "react";
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { PresentationAssistantPane } from "./PresentationAssistantPane.js";
|
import { PresentationAssistantPane } from "./PresentationAssistantPane.js";
|
||||||
import {
|
import {
|
||||||
SCENE9_PHASE_PLACEHOLDERS,
|
PREPARED_LIFECYCLE_PHASE_PLACEHOLDERS,
|
||||||
SCENE9_PHASE_PROMPTS,
|
PREPARED_LIFECYCLE_PHASE_PROMPTS,
|
||||||
initialScene9MessageState,
|
initialPreparedLifecycleMessageState,
|
||||||
projectScene9Message,
|
projectPreparedLifecycleMessage,
|
||||||
} from "./scene9-message-state.js";
|
} from "./prepared-lifecycle-message-state.js";
|
||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
|
|
||||||
const renderPane = (
|
const renderPane = (
|
||||||
phase: Parameters<typeof projectScene9Message>[1],
|
phase: Parameters<typeof projectPreparedLifecycleMessage>[1],
|
||||||
options: Partial<ComponentProps<typeof PresentationAssistantPane>> = {},
|
options: Partial<ComponentProps<typeof PresentationAssistantPane>> = {},
|
||||||
) => {
|
) => {
|
||||||
const onDraftChange = options.onDraftChange ?? (() => undefined);
|
const onDraftChange = options.onDraftChange ?? (() => undefined);
|
||||||
@@ -22,7 +22,7 @@ const renderPane = (
|
|||||||
return render(
|
return render(
|
||||||
<PresentationAssistantPane
|
<PresentationAssistantPane
|
||||||
phase={phase}
|
phase={phase}
|
||||||
message={projectScene9Message(initialScene9MessageState, phase)}
|
message={projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, phase)}
|
||||||
submittedOverrides={{}}
|
submittedOverrides={{}}
|
||||||
runRequested={null}
|
runRequested={null}
|
||||||
onDraftChange={onDraftChange}
|
onDraftChange={onDraftChange}
|
||||||
@@ -69,7 +69,7 @@ describe("PresentationAssistantPane", () => {
|
|||||||
|
|
||||||
const input = screen.getByRole("textbox", { name: /message to authoring assistant/i });
|
const input = screen.getByRole("textbox", { name: /message to authoring assistant/i });
|
||||||
expect(input).toHaveValue("");
|
expect(input).toHaveValue("");
|
||||||
expect(input).toHaveAttribute("placeholder", SCENE9_PHASE_PLACEHOLDERS[phase]);
|
expect(input).toHaveAttribute("placeholder", PREPARED_LIFECYCLE_PHASE_PLACEHOLDERS[phase]);
|
||||||
expect(screen.getByRole("button", { name: /send message/i })).toBeDisabled();
|
expect(screen.getByRole("button", { name: /send message/i })).toBeDisabled();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -80,7 +80,7 @@ describe("PresentationAssistantPane", () => {
|
|||||||
renderPane(phase);
|
renderPane(phase);
|
||||||
|
|
||||||
expect(screen.getByRole("textbox", { name: /message to authoring assistant/i }))
|
expect(screen.getByRole("textbox", { name: /message to authoring assistant/i }))
|
||||||
.toHaveValue(SCENE9_PHASE_PROMPTS[phase]);
|
.toHaveValue(PREPARED_LIFECYCLE_PHASE_PROMPTS[phase]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,34 +4,37 @@ import { Textarea } from "../../components/ui/textarea.js";
|
|||||||
import { AuthoringConversation } from "./AuthoringConversation.js";
|
import { AuthoringConversation } from "./AuthoringConversation.js";
|
||||||
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||||
import type {
|
import type {
|
||||||
Scene9MessageProjection,
|
PreparedLifecycleMessageProjection,
|
||||||
Scene9SubmittedOverrides,
|
PreparedLifecycleSubmittedOverrides,
|
||||||
} from "./scene9-message-state.js";
|
} from "./prepared-lifecycle-message-state.js";
|
||||||
|
import type { PreparedLifecycleStepId } from "./authoring-projection.js";
|
||||||
import { PREPARED_COMPOSER_HELP, usePreparedComposerSubmit } from "./usePreparedComposerSubmit.js";
|
import { PREPARED_COMPOSER_HELP, usePreparedComposerSubmit } from "./usePreparedComposerSubmit.js";
|
||||||
|
|
||||||
export type PresentationAssistantPaneProps = {
|
export type PresentationAssistantPaneProps = {
|
||||||
readonly phase: AuthoringPhaseId;
|
readonly phase: PreparedLifecycleStepId | AuthoringPhaseId;
|
||||||
readonly visualRole?: "support";
|
readonly visualRole?: "support";
|
||||||
readonly message: Scene9MessageProjection;
|
readonly message: PreparedLifecycleMessageProjection;
|
||||||
readonly submittedOverrides: Scene9SubmittedOverrides;
|
readonly submittedOverrides: PreparedLifecycleSubmittedOverrides;
|
||||||
readonly runRequested: string | null;
|
readonly runRequested: string | null;
|
||||||
readonly onDraftChange: (draft: string) => void;
|
readonly onDraftChange: (draft: string) => void;
|
||||||
readonly onSubmit: (message: string) => void;
|
readonly onSubmit: (message: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const phaseLabels: Readonly<Record<AuthoringPhaseId, string>> = {
|
const phaseLabels: Readonly<Record<PreparedLifecycleStepId | AuthoringPhaseId, string>> = {
|
||||||
discover: "Discover",
|
discover: "Discover",
|
||||||
draft: "Draft",
|
draft: "Draft",
|
||||||
|
diagnose: "Diagnose",
|
||||||
|
repair: "Repair",
|
||||||
validate: "Validate",
|
validate: "Validate",
|
||||||
artifact: "Artifact",
|
artifact: "Artifact",
|
||||||
deployment: "Deployment",
|
deployment: "Deployment",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stable Scene 9 boundary for the prepared assistant surface.
|
* Stable prepared lifecycle boundary for the prepared assistant surface.
|
||||||
*
|
*
|
||||||
* The pane owns only the transient composer buffer. Submitted text is handed
|
* The pane owns only the transient composer buffer. Submitted text is handed
|
||||||
* back to the Scene 9 controller so the replay can project it later.
|
* back to the lifecycle controller so the replay can project it later.
|
||||||
*/
|
*/
|
||||||
export const PresentationAssistantPane = ({
|
export const PresentationAssistantPane = ({
|
||||||
phase,
|
phase,
|
||||||
@@ -83,22 +86,22 @@ export const PresentationAssistantPane = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<form className="presentation-assistant-pane__composer" onSubmit={submit}>
|
<form className="presentation-assistant-pane__composer" onSubmit={submit}>
|
||||||
<label htmlFor="scene9-authoring-message">Message to authoring assistant</label>
|
<label htmlFor="prepared-lifecycle-authoring-message">Message to authoring assistant</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="scene9-authoring-message"
|
id="prepared-lifecycle-authoring-message"
|
||||||
value={draft}
|
value={draft}
|
||||||
placeholder={message.placeholder}
|
placeholder={message.placeholder}
|
||||||
disabled={runRequested !== null}
|
disabled={runRequested !== null}
|
||||||
onChange={(event) => updateDraft(event.target.value)}
|
onChange={(event) => updateDraft(event.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
aria-describedby="scene9-authoring-message-help"
|
aria-describedby="prepared-lifecycle-authoring-message-help"
|
||||||
/>
|
/>
|
||||||
<div className="presentation-assistant-pane__composer-actions">
|
<div className="presentation-assistant-pane__composer-actions">
|
||||||
<Button type="submit" disabled={!canSubmit}>
|
<Button type="submit" disabled={!canSubmit}>
|
||||||
Send message
|
Send message
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p id="scene9-authoring-message-help" className="presentation-assistant-pane__composer-help">
|
<p id="prepared-lifecycle-authoring-message-help" className="presentation-assistant-pane__composer-help">
|
||||||
{PREPARED_COMPOSER_HELP}
|
{PREPARED_COMPOSER_HELP}
|
||||||
</p>
|
</p>
|
||||||
{runRequested !== null ? (
|
{runRequested !== null ? (
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { projectPreparedAuthoring } from "./authoring-recording.js";
|
import { projectPreparedAuthoring } from "./authoring-recording.js";
|
||||||
import { projectPreparedAuthoringPhase } from "./authoring-projection.js";
|
import {
|
||||||
|
projectPreparedAuthoringPhase,
|
||||||
|
projectPreparedLifecycleStep,
|
||||||
|
recordingPhaseForStep,
|
||||||
|
} from "./authoring-projection.js";
|
||||||
|
|
||||||
describe("projectPreparedAuthoringPhase", () => {
|
describe("projectPreparedAuthoringPhase", () => {
|
||||||
it("projects the discover phase with sources, capabilities, schema", () => {
|
it("projects the discover phase with sources, capabilities, schema", () => {
|
||||||
@@ -64,4 +68,37 @@ describe("projectPreparedAuthoringPhase", () => {
|
|||||||
expect(["success", "diagnostic"]).toContain(cmd.result);
|
expect(["success", "diagnostic"]).toContain(cmd.result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("splits one recorded validate phase into diagnosis and repair presentation steps", () => {
|
||||||
|
const diagnose = projectPreparedLifecycleStep("diagnose");
|
||||||
|
const repair = projectPreparedLifecycleStep("repair");
|
||||||
|
|
||||||
|
expect(diagnose.recordingPhase).toBe("validate");
|
||||||
|
expect(repair.recordingPhase).toBe("validate");
|
||||||
|
expect(diagnose.focus).toBe("diagnose");
|
||||||
|
expect(repair.focus).toBe("repair");
|
||||||
|
expect(diagnose.primaryCommand.title).toBe("workflow.draft_workspaces.validate");
|
||||||
|
expect(repair.primaryCommand.title).toBe("workflow.draft_workspaces.set_step_output_map");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects six presentation steps from five recorded phases", () => {
|
||||||
|
const steps = [
|
||||||
|
"discover",
|
||||||
|
"draft",
|
||||||
|
"diagnose",
|
||||||
|
"repair",
|
||||||
|
"artifact",
|
||||||
|
"deployment",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
expect(steps.map(recordingPhaseForStep)).toEqual([
|
||||||
|
"discover",
|
||||||
|
"draft",
|
||||||
|
"validate",
|
||||||
|
"validate",
|
||||||
|
"artifact",
|
||||||
|
"deployment",
|
||||||
|
]);
|
||||||
|
expect(steps.map((step) => projectPreparedLifecycleStep(step).step)).toEqual(steps);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { projectPreparedAuthoring, type AuthoringPhaseId, type PreparedAuthoringCommand } from "./authoring-recording.js";
|
import { projectPreparedAuthoring, type AuthoringPhaseId, type PreparedAuthoringCommand } from "./authoring-recording.js";
|
||||||
|
|
||||||
|
export type PreparedLifecycleStepId =
|
||||||
|
| "discover"
|
||||||
|
| "draft"
|
||||||
|
| "diagnose"
|
||||||
|
| "repair"
|
||||||
|
| "artifact"
|
||||||
|
| "deployment";
|
||||||
|
|
||||||
export type AuthoringPhaseProjection = {
|
export type AuthoringPhaseProjection = {
|
||||||
readonly phase: AuthoringPhaseId;
|
readonly phase: AuthoringPhaseId;
|
||||||
readonly beatId: string;
|
readonly beatId: string;
|
||||||
@@ -10,6 +18,13 @@ export type AuthoringPhaseProjection = {
|
|||||||
readonly visual: AuthoringPhaseVisualModel;
|
readonly visual: AuthoringPhaseVisualModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PreparedLifecycleStepProjection = AuthoringPhaseProjection & {
|
||||||
|
readonly step: PreparedLifecycleStepId;
|
||||||
|
readonly recordingPhase: AuthoringPhaseId;
|
||||||
|
readonly focus: "full" | "diagnose" | "repair";
|
||||||
|
readonly primaryCommand: PreparedAuthoringCommand;
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthoringPhaseVisualModel =
|
export type AuthoringPhaseVisualModel =
|
||||||
| {
|
| {
|
||||||
readonly kind: "inventory";
|
readonly kind: "inventory";
|
||||||
@@ -111,3 +126,34 @@ export const projectPreparedAuthoringPhase = (
|
|||||||
visual: visualForPhase(found.phase),
|
visual: visualForPhase(found.phase),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const recordingPhaseForStep = (
|
||||||
|
step: PreparedLifecycleStepId | AuthoringPhaseId,
|
||||||
|
): AuthoringPhaseId => {
|
||||||
|
if (step === "diagnose" || step === "repair") return "validate";
|
||||||
|
return step;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectPreparedLifecycleStep = (
|
||||||
|
step: PreparedLifecycleStepId,
|
||||||
|
): PreparedLifecycleStepProjection => {
|
||||||
|
const recordingPhase = recordingPhaseForStep(step);
|
||||||
|
const phase = projectPreparedAuthoringPhase(recordingPhase);
|
||||||
|
// Diagnose and repair are presentation choreography over one factual
|
||||||
|
// recording phase; they select distinct evidence without duplicating it.
|
||||||
|
const commandIndex = step === "repair" ? 1 : 0;
|
||||||
|
const primaryCommand = phase.commands[commandIndex];
|
||||||
|
if (primaryCommand === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`recorded phase "${recordingPhase}" has no command at index ${commandIndex} for presentation step "${step}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...phase,
|
||||||
|
step,
|
||||||
|
recordingPhase,
|
||||||
|
focus: step === "diagnose" || step === "repair" ? step : "full",
|
||||||
|
primaryCommand,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
+32
-32
@@ -1,15 +1,15 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
SCENE9_PHASE_PROMPTS,
|
PREPARED_LIFECYCLE_PHASE_PROMPTS,
|
||||||
initialScene9MessageState,
|
initialPreparedLifecycleMessageState,
|
||||||
projectScene9Message,
|
projectPreparedLifecycleMessage,
|
||||||
projectScene9SubmittedOverrides,
|
projectPreparedLifecycleSubmittedOverrides,
|
||||||
scene9MessageReducer,
|
preparedLifecycleMessageReducer,
|
||||||
} from "./scene9-message-state.js";
|
} from "./prepared-lifecycle-message-state.js";
|
||||||
|
|
||||||
describe("scene 9 staged message state", () => {
|
describe("prepared lifecycle staged message state", () => {
|
||||||
it("defines the exact prompt for every phase", () => {
|
it("defines the exact prompt for every phase", () => {
|
||||||
expect(SCENE9_PHASE_PROMPTS).toEqual({
|
expect(PREPARED_LIFECYCLE_PHASE_PROMPTS).toEqual({
|
||||||
discover: "",
|
discover: "",
|
||||||
draft: "Is the draft valid? Can you check and fix any issues?",
|
draft: "Is the draft valid? Can you check and fix any issues?",
|
||||||
validate: "",
|
validate: "",
|
||||||
@@ -20,12 +20,12 @@ describe("scene 9 staged message state", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("projects empty phases with a useful placeholder", () => {
|
it("projects empty phases with a useful placeholder", () => {
|
||||||
expect(projectScene9Message(initialScene9MessageState, "discover")).toMatchObject({
|
expect(projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, "discover")).toMatchObject({
|
||||||
draft: "",
|
draft: "",
|
||||||
prefill: "",
|
prefill: "",
|
||||||
placeholder: expect.any(String),
|
placeholder: expect.any(String),
|
||||||
});
|
});
|
||||||
expect(projectScene9Message(initialScene9MessageState, "validate")).toMatchObject({
|
expect(projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, "diagnose")).toMatchObject({
|
||||||
draft: "",
|
draft: "",
|
||||||
prefill: "",
|
prefill: "",
|
||||||
placeholder: expect.any(String),
|
placeholder: expect.any(String),
|
||||||
@@ -33,23 +33,23 @@ describe("scene 9 staged message state", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("projects the exact phase prefill without changing the draft", () => {
|
it("projects the exact phase prefill without changing the draft", () => {
|
||||||
expect(projectScene9Message(initialScene9MessageState, "draft")).toMatchObject({
|
expect(projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, "draft")).toMatchObject({
|
||||||
draft: "",
|
draft: "",
|
||||||
prefill: SCENE9_PHASE_PROMPTS.draft,
|
prefill: PREPARED_LIFECYCLE_PHASE_PROMPTS.draft,
|
||||||
});
|
});
|
||||||
expect(projectScene9Message(initialScene9MessageState, "artifact")).toMatchObject({
|
expect(projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, "artifact")).toMatchObject({
|
||||||
draft: "",
|
draft: "",
|
||||||
prefill: SCENE9_PHASE_PROMPTS.artifact,
|
prefill: PREPARED_LIFECYCLE_PHASE_PROMPTS.artifact,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves edited draft text exactly when submitting draft", () => {
|
it("preserves edited draft text exactly when submitting draft", () => {
|
||||||
const edited = scene9MessageReducer(initialScene9MessageState, {
|
const edited = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
|
||||||
type: "draft_edited",
|
type: "draft_edited",
|
||||||
draft: " Check only the report binding. ",
|
draft: " Check only the report binding. ",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(scene9MessageReducer(edited, { type: "draft_submitted" })).toEqual({
|
expect(preparedLifecycleMessageReducer(edited, { type: "draft_submitted" })).toEqual({
|
||||||
draft: edited.draft,
|
draft: edited.draft,
|
||||||
submittedOverrides: { validate: edited.draft },
|
submittedOverrides: { validate: edited.draft },
|
||||||
runRequested: null,
|
runRequested: null,
|
||||||
@@ -57,12 +57,12 @@ describe("scene 9 staged message state", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("stores artifact submissions under the deployment destination", () => {
|
it("stores artifact submissions under the deployment destination", () => {
|
||||||
const state = scene9MessageReducer(initialScene9MessageState, {
|
const state = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
|
||||||
type: "draft_edited",
|
type: "draft_edited",
|
||||||
draft: "Save this edited deployment request",
|
draft: "Save this edited deployment request",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(scene9MessageReducer(state, { type: "artifact_submitted" })).toEqual({
|
expect(preparedLifecycleMessageReducer(state, { type: "artifact_submitted" })).toEqual({
|
||||||
draft: state.draft,
|
draft: state.draft,
|
||||||
submittedOverrides: { deployment: state.draft },
|
submittedOverrides: { deployment: state.draft },
|
||||||
runRequested: null,
|
runRequested: null,
|
||||||
@@ -70,12 +70,12 @@ describe("scene 9 staged message state", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("stores discover submissions under the discover destination", () => {
|
it("stores discover submissions under the discover destination", () => {
|
||||||
const state = scene9MessageReducer(initialScene9MessageState, {
|
const state = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
|
||||||
type: "draft_edited",
|
type: "draft_edited",
|
||||||
draft: "Inspect the report source first.",
|
draft: "Inspect the report source first.",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(scene9MessageReducer(state, { type: "discover_submitted" })).toEqual({
|
expect(preparedLifecycleMessageReducer(state, { type: "discover_submitted" })).toEqual({
|
||||||
draft: state.draft,
|
draft: state.draft,
|
||||||
submittedOverrides: { discover: state.draft },
|
submittedOverrides: { discover: state.draft },
|
||||||
runRequested: null,
|
runRequested: null,
|
||||||
@@ -83,43 +83,43 @@ describe("scene 9 staged message state", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("ignores blank submits and keeps duplicate submits idempotent", () => {
|
it("ignores blank submits and keeps duplicate submits idempotent", () => {
|
||||||
const blank = { ...initialScene9MessageState, draft: " \n\t" };
|
const blank = { ...initialPreparedLifecycleMessageState, draft: " \n\t" };
|
||||||
expect(scene9MessageReducer(blank, { type: "draft_submitted" })).toBe(blank);
|
expect(preparedLifecycleMessageReducer(blank, { type: "draft_submitted" })).toBe(blank);
|
||||||
expect(scene9MessageReducer(blank, { type: "artifact_submitted" })).toBe(blank);
|
expect(preparedLifecycleMessageReducer(blank, { type: "artifact_submitted" })).toBe(blank);
|
||||||
expect(scene9MessageReducer(blank, { type: "run_requested" })).toBe(blank);
|
expect(preparedLifecycleMessageReducer(blank, { type: "run_requested" })).toBe(blank);
|
||||||
|
|
||||||
const submitted = scene9MessageReducer(
|
const submitted = preparedLifecycleMessageReducer(
|
||||||
scene9MessageReducer(initialScene9MessageState, {
|
preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
|
||||||
type: "draft_edited",
|
type: "draft_edited",
|
||||||
draft: "A draft override",
|
draft: "A draft override",
|
||||||
}),
|
}),
|
||||||
{ type: "draft_submitted" },
|
{ type: "draft_submitted" },
|
||||||
);
|
);
|
||||||
expect(scene9MessageReducer(submitted, { type: "draft_submitted" })).toBe(submitted);
|
expect(preparedLifecycleMessageReducer(submitted, { type: "draft_submitted" })).toBe(submitted);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("records the final run request without claiming execution", () => {
|
it("records the final run request without claiming execution", () => {
|
||||||
const state = scene9MessageReducer(initialScene9MessageState, {
|
const state = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
|
||||||
type: "draft_edited",
|
type: "draft_edited",
|
||||||
draft: "Run this deployment",
|
draft: "Run this deployment",
|
||||||
});
|
});
|
||||||
const requested = scene9MessageReducer(state, { type: "run_requested" });
|
const requested = preparedLifecycleMessageReducer(state, { type: "run_requested" });
|
||||||
|
|
||||||
expect(requested.runRequested).toBe("Run this deployment");
|
expect(requested.runRequested).toBe("Run this deployment");
|
||||||
expect(requested.submittedOverrides).toEqual({});
|
expect(requested.submittedOverrides).toEqual({});
|
||||||
expect(scene9MessageReducer(requested, { type: "run_requested" })).toBe(requested);
|
expect(preparedLifecycleMessageReducer(requested, { type: "run_requested" })).toBe(requested);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("projects only destination-phase overrides for the prepared thread", () => {
|
it("projects only destination-phase overrides for the prepared thread", () => {
|
||||||
const state = {
|
const state = {
|
||||||
...initialScene9MessageState,
|
...initialPreparedLifecycleMessageState,
|
||||||
submittedOverrides: {
|
submittedOverrides: {
|
||||||
validate: "Edited validation request",
|
validate: "Edited validation request",
|
||||||
deployment: "Edited deployment request",
|
deployment: "Edited deployment request",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(projectScene9SubmittedOverrides(state)).toEqual({
|
expect(projectPreparedLifecycleSubmittedOverrides(state)).toEqual({
|
||||||
validate: "Edited validation request",
|
validate: "Edited validation request",
|
||||||
deployment: "Edited deployment request",
|
deployment: "Edited deployment request",
|
||||||
});
|
});
|
||||||
+36
-29
@@ -1,9 +1,13 @@
|
|||||||
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||||
|
import {
|
||||||
|
recordingPhaseForStep,
|
||||||
|
type PreparedLifecycleStepId,
|
||||||
|
} from "./authoring-projection.js";
|
||||||
|
|
||||||
export type Scene9MessagePhase = AuthoringPhaseId;
|
export type PreparedLifecycleMessagePhase = PreparedLifecycleStepId | AuthoringPhaseId;
|
||||||
export type Scene9DestinationPhase = "discover" | "validate" | "deployment";
|
export type PreparedLifecycleDestinationPhase = "discover" | "validate" | "deployment";
|
||||||
|
|
||||||
export const SCENE9_PHASE_PROMPTS: Readonly<Record<Scene9MessagePhase, string>> = {
|
export const PREPARED_LIFECYCLE_PHASE_PROMPTS: Readonly<Record<AuthoringPhaseId, string>> = {
|
||||||
discover: "",
|
discover: "",
|
||||||
draft: "Is the draft valid? Can you check and fix any issues?",
|
draft: "Is the draft valid? Can you check and fix any issues?",
|
||||||
validate: "",
|
validate: "",
|
||||||
@@ -12,7 +16,7 @@ export const SCENE9_PHASE_PROMPTS: Readonly<Record<Scene9MessagePhase, string>>
|
|||||||
"The deployment lda_report_case_study.default is saved and valid. Shall we run it now?",
|
"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>> = {
|
export const PREPARED_LIFECYCLE_PHASE_PLACEHOLDERS: Readonly<Record<AuthoringPhaseId, string>> = {
|
||||||
discover: "Ask about the workflow authoring process.",
|
discover: "Ask about the workflow authoring process.",
|
||||||
draft: "Review the prepared draft.",
|
draft: "Review the prepared draft.",
|
||||||
validate: "Ask about the draft validation results.",
|
validate: "Ask about the draft validation results.",
|
||||||
@@ -20,23 +24,23 @@ export const SCENE9_PHASE_PLACEHOLDERS: Readonly<Record<Scene9MessagePhase, stri
|
|||||||
deployment: "Ask whether the saved deployment should run.",
|
deployment: "Ask whether the saved deployment should run.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Scene9SubmittedOverrides = Readonly<
|
export type PreparedLifecycleSubmittedOverrides = Readonly<
|
||||||
Partial<Record<Scene9DestinationPhase, string>>
|
Partial<Record<PreparedLifecycleDestinationPhase, string>>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type Scene9MessageState = {
|
export type PreparedLifecycleMessageState = {
|
||||||
readonly draft: string;
|
readonly draft: string;
|
||||||
readonly submittedOverrides: Scene9SubmittedOverrides;
|
readonly submittedOverrides: PreparedLifecycleSubmittedOverrides;
|
||||||
readonly runRequested: string | null;
|
readonly runRequested: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialScene9MessageState: Scene9MessageState = {
|
export const initialPreparedLifecycleMessageState: PreparedLifecycleMessageState = {
|
||||||
draft: "",
|
draft: "",
|
||||||
submittedOverrides: {},
|
submittedOverrides: {},
|
||||||
runRequested: null,
|
runRequested: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Scene9MessageAction =
|
export type PreparedLifecycleMessageAction =
|
||||||
| { readonly type: "draft_edited"; readonly draft: string }
|
| { readonly type: "draft_edited"; readonly draft: string }
|
||||||
| { readonly type: "discover_submitted" }
|
| { readonly type: "discover_submitted" }
|
||||||
| { readonly type: "draft_submitted" }
|
| { readonly type: "draft_submitted" }
|
||||||
@@ -46,9 +50,9 @@ export type Scene9MessageAction =
|
|||||||
const hasText = (text: string): boolean => text.trim().length > 0;
|
const hasText = (text: string): boolean => text.trim().length > 0;
|
||||||
|
|
||||||
const submitOverride = (
|
const submitOverride = (
|
||||||
state: Scene9MessageState,
|
state: PreparedLifecycleMessageState,
|
||||||
destination: Scene9DestinationPhase,
|
destination: PreparedLifecycleDestinationPhase,
|
||||||
): Scene9MessageState => {
|
): PreparedLifecycleMessageState => {
|
||||||
if (!hasText(state.draft) || state.submittedOverrides[destination] !== undefined) return state;
|
if (!hasText(state.draft) || state.submittedOverrides[destination] !== undefined) return state;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -60,10 +64,10 @@ const submitOverride = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scene9MessageReducer = (
|
export const preparedLifecycleMessageReducer = (
|
||||||
state: Scene9MessageState,
|
state: PreparedLifecycleMessageState,
|
||||||
action: Scene9MessageAction,
|
action: PreparedLifecycleMessageAction,
|
||||||
): Scene9MessageState => {
|
): PreparedLifecycleMessageState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "draft_edited":
|
case "draft_edited":
|
||||||
return state.runRequested === null ? { ...state, draft: action.draft } : state;
|
return state.runRequested === null ? { ...state, draft: action.draft } : state;
|
||||||
@@ -80,22 +84,25 @@ export const scene9MessageReducer = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Scene9MessageProjection = {
|
export type PreparedLifecycleMessageProjection = {
|
||||||
readonly draft: string;
|
readonly draft: string;
|
||||||
readonly prefill: string;
|
readonly prefill: string;
|
||||||
readonly placeholder: string;
|
readonly placeholder: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const projectScene9Message = (
|
export const projectPreparedLifecycleMessage = (
|
||||||
state: Scene9MessageState,
|
state: PreparedLifecycleMessageState,
|
||||||
phase: Scene9MessagePhase,
|
step: PreparedLifecycleMessagePhase,
|
||||||
): Scene9MessageProjection => ({
|
): PreparedLifecycleMessageProjection => {
|
||||||
draft: state.draft,
|
const phase = recordingPhaseForStep(step);
|
||||||
prefill: SCENE9_PHASE_PROMPTS[phase],
|
return {
|
||||||
placeholder: SCENE9_PHASE_PLACEHOLDERS[phase],
|
draft: state.draft,
|
||||||
});
|
prefill: PREPARED_LIFECYCLE_PHASE_PROMPTS[phase],
|
||||||
|
placeholder: PREPARED_LIFECYCLE_PHASE_PLACEHOLDERS[phase],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/** Returns the transcript-facing request overrides by destination phase. */
|
/** Returns the transcript-facing request overrides by destination phase. */
|
||||||
export const projectScene9SubmittedOverrides = (
|
export const projectPreparedLifecycleSubmittedOverrides = (
|
||||||
state: Scene9MessageState,
|
state: PreparedLifecycleMessageState,
|
||||||
): Scene9SubmittedOverrides => state.submittedOverrides;
|
): PreparedLifecycleSubmittedOverrides => state.submittedOverrides;
|
||||||
Reference in New Issue
Block a user