feat: unify prepared authoring lifecycle visual

This commit is contained in:
lda
2026-07-13 12:42:04 +07:00 Verified
parent 3eef7e35c0
commit 8414b07272
7 changed files with 382 additions and 80 deletions
@@ -51,6 +51,18 @@ describe("AuthoringPhaseVisual", () => {
);
});
it("keeps diagnosis and repair attached to the recorded validate phase", () => {
for (const focus of ["diagnose", "repair"] as const) {
render(<AuthoringPhaseVisual projection={projectPreparedAuthoringPhase("validate")} focus={focus} />);
expect(screen.getByRole("region", { name: /validation repair evidence/i })).toHaveAttribute(
"data-authoring-recording-phase",
"validate",
);
cleanup();
}
});
it.each(["diagnose", "repair"] as const)("marks %s evidence as the primary visual", (focus) => {
render(<AuthoringPhaseVisual projection={projectPreparedAuthoringPhase("validate")} focus={focus} />);
expect(screen.getByRole("region", { name: /validation repair evidence/i })).toHaveAttribute(
@@ -44,9 +44,11 @@ const GraphVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["vis
const RepairVisual = ({
visual,
focus,
recordingPhase,
}: {
readonly visual: Extract<AuthoringPhaseProjection["visual"], { kind: "repair" }>;
readonly focus: "full" | "diagnose" | "repair";
readonly recordingPhase: AuthoringPhaseProjection["phase"];
}) => (
<section
className="authoring-visual authoring-visual--repair"
@@ -54,6 +56,7 @@ const RepairVisual = ({
data-presentation-surface="editorial"
data-visual-role="primary"
data-authoring-focus={focus}
data-authoring-recording-phase={recordingPhase}
>
<div className="authoring-repair__diagnostic"><AlertTriangle aria-hidden="true" /><span>Diagnostic</span><strong>{visual.diagnostic}</strong></div>
<ArrowRight className="authoring-repair__connector" aria-hidden="true" />
@@ -86,7 +89,9 @@ export const AuthoringPhaseVisual = ({ projection, focus = "full" }: AuthoringPh
switch (projection.visual.kind) {
case "inventory": return <InventoryVisual visual={projection.visual} />;
case "graph": return <GraphVisual visual={projection.visual} />;
case "repair": return <RepairVisual visual={projection.visual} focus={focus} />;
// Diagnose and repair deliberately share this factual validate visual while
// the lifecycle scene supplies the distinct recorded command and focus.
case "repair": return <RepairVisual visual={projection.visual} focus={focus} recordingPhase={projection.phase} />;
case "artifact": return <ArtifactVisual visual={projection.visual} />;
case "bindings": return <BindingsVisual visual={projection.visual} />;
}
@@ -20,29 +20,56 @@ describe("PreparedAuthoringLifecycleScene", () => {
"data-presentation-surface",
"editorial",
);
expect(screen.getByText("Discover")).toBeInTheDocument();
expect(within(screen.getByRole("list", { name: /prepared authoring lifecycle/i })).getByText("Discover"))
.toBeInTheDocument();
expect(screen.getAllByText(/sources|capabilities|schema/i).length).toBeGreaterThanOrEqual(1);
});
it("draft shows graph or routes", () => {
renderBeat("draft");
expect(screen.getByText("Draft")).toBeInTheDocument();
expect(within(screen.getByRole("list", { name: /prepared authoring lifecycle/i })).getByText("Author"))
.toBeInTheDocument();
expect(screen.getAllByText(/graph|routes/i).length).toBeGreaterThanOrEqual(1);
});
it("validate shows diagnosis and repair", () => {
renderBeat("validate");
it("diagnose shows the structured validation operation in its frame", () => {
renderBeat("diagnose");
expect(screen.getByRole("region", { name: "prepared workflow authoring lifecycle" })).toHaveAttribute(
"data-presentation-surface",
"editorial",
);
expect(screen.getByText("Validate")).toBeInTheDocument();
expect(screen.getAllByText(/diagnos|repair/i).length).toBeGreaterThanOrEqual(1);
const frame = screen.getByRole("region", { name: /active authoring operation/i });
expect(frame).toHaveTextContent("workflow.draft_workspaces.validate");
expect(frame).toHaveTextContent("wf draft validate lda_report_workflow");
expect(frame).toHaveTextContent(/structured missing-output diagnostic/i);
expect(frame).toHaveAttribute("data-authoring-step", "diagnose");
expect(screen.getByRole("region", { name: "validation repair evidence" })).toHaveAttribute(
"data-authoring-focus",
"diagnose",
);
});
it("repair shows the output-map operation over the same validation visual", () => {
const scene = findScene("prepared-lifecycle");
const repairBeat = findBeat("prepared-lifecycle", "repair");
if (!scene || !repairBeat) throw new Error("missing prepared-lifecycle/repair");
const { rerender } = renderBeat("diagnose");
rerender(<PreparedAuthoringLifecycleScene scene={scene} beat={repairBeat} />);
const frame = screen.getByRole("region", { name: /active authoring operation/i });
expect(frame).toHaveTextContent("workflow.draft_workspaces.set_step_output_map");
expect(frame).toHaveTextContent(/wf draft set-output lda_report_workflow/i);
expect(frame).toHaveAttribute("data-authoring-step", "repair");
expect(screen.getByRole("region", { name: "validation repair evidence" })).toHaveAttribute(
"data-authoring-focus",
"repair",
);
});
it("artifact shows immutable ID and version", () => {
renderBeat("artifact");
expect(screen.getByText("Artifact")).toBeInTheDocument();
expect(within(screen.getByRole("list", { name: /prepared authoring lifecycle/i })).getByText("Artifact"))
.toBeInTheDocument();
expect(screen.getAllByText(/lda_report_case_study/i).length).toBeGreaterThanOrEqual(1);
});
@@ -55,7 +82,8 @@ describe("PreparedAuthoringLifecycleScene", () => {
it.each([
["discover", "discovery evidence"],
["draft", "draft graph evidence"],
["validate", "validation repair evidence"],
["diagnose", "validation repair evidence"],
["repair", "validation repair evidence"],
["artifact", "artifact evidence"],
["deployment", "deployment binding evidence"],
] as const)("renders %s as the primary phase visual", (beatId, label) => {
@@ -68,9 +96,11 @@ describe("PreparedAuthoringLifecycleScene", () => {
const workspace = screen.getByRole("region", { name: "prepared workflow authoring lifecycle" });
const assistant = screen.getByRole("complementary", { name: /prepared authoring assistant/i });
const frame = screen.getByRole("region", { name: "active authoring operation" });
const visual = screen.getByRole("region", { name: "draft graph evidence" });
expect(workspace).toContainElement(assistant);
expect(workspace).toContainElement(frame);
expect(workspace).toContainElement(visual);
expect(assistant).toHaveAttribute("data-phase", "draft");
expect(assistant).toHaveAttribute("data-surface", "prepared-replay");
@@ -78,7 +108,7 @@ describe("PreparedAuthoringLifecycleScene", () => {
expect(assistant.querySelector('[data-surface="stage"]')).toBeInTheDocument();
expect(workspace.querySelector(".prepared-lifecycle-scene__dock")).not.toBeInTheDocument();
expect(workspace.querySelector("[role='dialog']")).not.toBeInTheDocument();
expect(workspace.querySelector("[aria-label='authoring phase rail']"))
expect(workspace.querySelector("[aria-label='prepared authoring lifecycle']"))
.toBeInTheDocument();
expect(visual).toHaveAttribute("data-presentation-surface", "editorial");
expect(visual).toHaveAttribute("data-visual-role", "primary");
@@ -101,7 +131,8 @@ describe("PreparedAuthoringLifecycleScene", () => {
it.each([
["draft", true],
["artifact", true],
["validate", false],
["diagnose", false],
["repair", false],
["deployment", false],
] as const)("%s submission advances only when its beat owns the transition", async (beatId, advances) => {
const user = userEvent.setup();
@@ -109,7 +140,9 @@ describe("PreparedAuthoringLifecycleScene", () => {
renderBeat(beatId, onAdvance);
const input = screen.getByRole("textbox", { name: /message to authoring assistant/i });
if (beatId === "validate") await user.type(input, "Review the validation result.");
if (beatId === "diagnose" || beatId === "repair") {
await user.type(input, "Review the validation result.");
}
await user.click(screen.getByRole("button", { name: /send message/i }));
expect(onAdvance).toHaveBeenCalledTimes(advances ? 1 : 0);
@@ -118,31 +151,33 @@ describe("PreparedAuthoringLifecycleScene", () => {
it.each([
["discover", "Discover"],
["draft", "Draft"],
["validate", "Validate"],
["diagnose", "Diagnose"],
["repair", "Repair"],
["artifact", "Artifact"],
["deployment", "Deployment"],
] as const)("marks %s as the active lifecycle evidence beat", (beatId, label) => {
renderBeat(beatId);
const evidence = screen.getByRole("region", { name: /active lifecycle evidence/i });
const evidence = screen.getByRole("region", { name: /active authoring operation/i });
const workspace = screen.getByRole("region", { name: "prepared workflow authoring lifecycle" });
expect(evidence).toHaveAttribute("data-visual-role", "lifecycle-primary");
expect(workspace.querySelectorAll('[data-visual-role="lifecycle-primary"]')).toHaveLength(1);
expect(screen.getByLabelText("authoring phase rail").querySelector('[data-active="true"]'))
expect(screen.getByRole("list", { name: /prepared authoring lifecycle/i }).querySelector('[data-active="true"]'))
.toHaveTextContent(label);
});
it("synchronizes the assistant phase, active group, rail, and visual", () => {
renderBeat("validate");
renderBeat("diagnose");
expect(screen.getByRole("complementary", { name: /prepared authoring assistant/i }))
.toHaveAttribute("data-phase", "validate");
.toHaveAttribute("data-phase", "diagnose");
expect(screen.getByRole("button", { name: /validate.*2 tool calls/i }))
.toHaveAttribute("aria-expanded", "true");
expect(screen.getByRole("button", { name: /draft.*3 tool calls/i }))
.toHaveAttribute("aria-expanded", "false");
expect(screen.getByLabelText("authoring phase rail").querySelector("[data-active='true']"))
.toHaveTextContent("Validate");
expect(screen.getByRole("list", { name: /prepared authoring lifecycle/i })
.querySelector("[data-active='true']"))
.toHaveTextContent("Diagnose");
const chat = screen.getByRole("log", { name: "prepared authoring conversation" });
expect(chat).toHaveAttribute("data-surface", "stage");
expect(screen.getByRole("region", { name: "validation repair evidence" })).toHaveAttribute(
@@ -152,22 +187,25 @@ describe("PreparedAuthoringLifecycleScene", () => {
});
it("does not render the obsolete trace modal or receipt", () => {
renderBeat("validate");
renderBeat("diagnose");
expect(screen.queryByRole("button", { name: "Agent trace" })).not.toBeInTheDocument();
expect(screen.queryByRole("dialog", { name: "Authoring trace" })).not.toBeInTheDocument();
expect(screen.queryByLabelText("prepared authoring receipt")).not.toBeInTheDocument();
});
it("renders a compact orientation rail", () => {
it("renders the six-step prepared authoring lifecycle rail", () => {
renderBeat("artifact");
const rail = screen.getByLabelText("authoring phase rail");
const rail = screen.getByRole("list", { name: /prepared authoring lifecycle/i });
expect(rail).toBeInTheDocument();
expect(rail.children.length).toBeGreaterThanOrEqual(5);
expect(within(rail).getAllByRole("listitem")).toHaveLength(6);
expect(within(rail).getByText("Diagnose")).toBeInTheDocument();
expect(within(rail).getByText("Repair")).toBeInTheDocument();
expect(within(rail).getByText("Sources, capabilities, schemas")).toBeInTheDocument();
});
it("highlights the active phase in the rail", () => {
renderBeat("deployment");
const rail = screen.getByLabelText("authoring phase rail");
const rail = screen.getByRole("list", { name: /prepared authoring lifecycle/i });
const active = rail.querySelector("[data-active='true']");
expect(active).toBeInTheDocument();
expect(active).toHaveTextContent("Deployment");
@@ -181,7 +219,7 @@ describe("PreparedAuthoringLifecycleScene", () => {
render(<PreparedAuthoringLifecycleScene scene={scene} beat={unexpectedBeat} />);
expect(screen.getByRole("region", { name: "discovery evidence" })).toBeInTheDocument();
expect(screen.getByLabelText("authoring phase rail").querySelector("[data-active='true']"))
expect(screen.getByRole("list", { name: /prepared authoring lifecycle/i }).querySelector("[data-active='true']"))
.toHaveTextContent("Discover");
});
});
@@ -1,14 +1,16 @@
import { useReducer } from "react";
import { projectPreparedAuthoringPhase } from "./authoring-projection.js";
import {
projectPreparedLifecycleStep,
type PreparedLifecycleStepId,
} from "./authoring-projection.js";
import { AuthoringPhaseVisual } from "./AuthoringPhaseVisual.js";
import { PresentationAssistantPane } from "./PresentationAssistantPane.js";
import type { AuthoringPhaseId } from "./authoring-recording.js";
import {
initialScene9MessageState,
projectScene9Message,
projectScene9SubmittedOverrides,
scene9MessageReducer,
} from "./scene9-message-state.js";
initialPreparedLifecycleMessageState,
projectPreparedLifecycleMessage,
projectPreparedLifecycleSubmittedOverrides,
preparedLifecycleMessageReducer,
} from "./prepared-lifecycle-message-state.js";
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
import { StageCaption } from "../StageCaption.js";
@@ -18,13 +20,18 @@ type PreparedAuthoringLifecycleSceneProps = {
readonly onAdvance?: (() => void) | undefined;
};
const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }[] = [
{ id: "discover", label: "Discover" },
{ id: "draft", label: "Draft" },
{ id: "validate", label: "Validate" },
{ id: "artifact", label: "Artifact" },
{ id: "deployment", label: "Deployment" },
];
const steps = [
{ id: "discover", label: "Discover", detail: "Sources, capabilities, schemas" },
{ id: "draft", label: "Author", detail: "Create and edit mutable Draft" },
{ id: "diagnose", label: "Diagnose", detail: "Structured validation result" },
{ id: "repair", label: "Repair", detail: "Focused output-map edit" },
{ id: "artifact", label: "Artifact", detail: "Immutable versioned definition" },
{ id: "deployment", label: "Deployment", detail: "Bind and validate sources" },
] as const satisfies readonly {
readonly id: PreparedLifecycleStepId;
readonly label: string;
readonly detail: string;
}[];
/**
* Scene 9 — Prepared workflow authoring lifecycle.
@@ -34,66 +41,96 @@ const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }
*/
export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance }: PreparedAuthoringLifecycleSceneProps) => {
const [messageState, dispatch] = useReducer(
scene9MessageReducer,
initialScene9MessageState,
preparedLifecycleMessageReducer,
initialPreparedLifecycleMessageState,
);
// Storyboard beats normally match these IDs. Discovery is a safe projection
// if a future beat reaches this scene before its authoring mapping is added.
const beatId = phases.find((phase) => phase.id === beat.id)?.id ?? "discover";
const projection = projectPreparedAuthoringPhase(beatId);
const activeStep = steps.find((candidate) => candidate.id === beat.id) ?? steps[0]!;
const step = activeStep.id;
const projection = projectPreparedLifecycleStep(step);
const activeStepIndex = steps.findIndex((candidate) => candidate.id === step);
return (
<>
{/* Keep external chrome to scene orientation; beat-specific copy belongs in the frame. */}
<StageCaption eyebrow="Prepared workflow" title={scene.title}>
<p>{beat.caption}</p>
{null}
</StageCaption>
<section
className="prepared-lifecycle-scene"
aria-label="prepared workflow authoring lifecycle"
data-active-phase={beatId}
data-active-phase={step}
data-recording-phase={projection.recordingPhase}
data-primary-surface="authoring-phase"
data-support-surface="prepared-chat"
data-presentation-surface="editorial"
>
<PresentationAssistantPane
phase={beatId}
phase={step}
visualRole="support"
message={projectScene9Message(messageState, beatId)}
submittedOverrides={projectScene9SubmittedOverrides(messageState)}
message={projectPreparedLifecycleMessage(messageState, step)}
submittedOverrides={projectPreparedLifecycleSubmittedOverrides(messageState)}
runRequested={messageState.runRequested}
onDraftChange={(draft) => dispatch({ type: "draft_edited", draft })}
onSubmit={(submittedText) => {
dispatch({ type: "draft_edited", draft: submittedText });
if (beatId === "discover") {
if (step === "discover") {
dispatch({ type: "discover_submitted" });
}
if (beatId === "draft") {
if (step === "draft") {
dispatch({ type: "draft_submitted" });
onAdvance?.();
}
if (beatId === "artifact") {
if (step === "artifact") {
dispatch({ type: "artifact_submitted" });
onAdvance?.();
}
if (beatId === "deployment") dispatch({ type: "run_requested" });
if (step === "deployment") dispatch({ type: "run_requested" });
}}
/>
<div className="prepared-lifecycle-scene__presentation">
<ol className="prepared-lifecycle-scene__rail" aria-label="authoring phase rail">
{phases.map((phase) => (
<li key={phase.id} data-active={phase.id === beatId ? "true" : "false"}>
<strong>{phase.label}</strong>
<ol className="prepared-lifecycle-scene__rail" aria-label="prepared authoring lifecycle">
{steps.map((candidate, index) => (
<li
key={candidate.id}
data-active={candidate.id === step ? "true" : "false"}
data-complete={index < activeStepIndex ? "true" : "false"}
>
<span className="prepared-lifecycle-scene__ordinal" aria-hidden="true">
{String(index + 1).padStart(2, "0")}
</span>
<strong>{candidate.label}</strong>
<span className="prepared-lifecycle-scene__detail">{candidate.detail}</span>
</li>
))}
</ol>
<article
className="prepared-lifecycle-scene__projection"
className="prepared-lifecycle-scene__frame"
role="region"
aria-label="active lifecycle evidence"
aria-label="active authoring operation"
data-authoring-step={step}
data-recording-phase={projection.recordingPhase}
data-visual-role="lifecycle-primary"
key={beatId}
>
<AuthoringPhaseVisual projection={projection} />
<header className="prepared-lifecycle-scene__frame-header">
<div>
<span className="prepared-lifecycle-scene__frame-step">{activeStep.label}</span>
<h2>{beat.title}</h2>
<p>{beat.caption}</p>
</div>
<dl className="prepared-lifecycle-scene__evidence">
<div>
<dt>Method</dt>
<dd><code>{projection.primaryCommand.title}</code></dd>
</div>
<div>
<dt>Equivalent CLI</dt>
<dd><code>{projection.primaryCommand.command}</code></dd>
</div>
</dl>
</header>
<AuthoringPhaseVisual projection={projection} focus={projection.focus} />
</article>
</div>
</section>