feat: show prepared workflow authoring lifecycle
This commit is contained in:
@@ -1,65 +0,0 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
|
||||||
import { initialDemoTimelineState } from "../demo/timeline/reducer.js";
|
|
||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
|
||||||
import { DemoLifecycleScene } from "./DemoLifecycleScene.js";
|
|
||||||
import { findBeat, findScene } from "./storyboard.js";
|
|
||||||
|
|
||||||
const recording = loadCanonicalDemoRecording();
|
|
||||||
const demo: DemoTimelineController = {
|
|
||||||
state: {
|
|
||||||
...initialDemoTimelineState,
|
|
||||||
mode: "replay",
|
|
||||||
phase: "paused",
|
|
||||||
events: recording.events,
|
|
||||||
appliedCount: recording.events.length,
|
|
||||||
autoplay: false,
|
|
||||||
},
|
|
||||||
inFlight: false,
|
|
||||||
interruptPayload: null,
|
|
||||||
output: null,
|
|
||||||
trace: null,
|
|
||||||
missingDeploymentMessage: null,
|
|
||||||
recordingId: recording.recordingId,
|
|
||||||
canStart: true,
|
|
||||||
setMode: vi.fn(),
|
|
||||||
start: vi.fn(),
|
|
||||||
pause: vi.fn(),
|
|
||||||
play: vi.fn(),
|
|
||||||
next: vi.fn(async () => {}),
|
|
||||||
submitSelectedIssues: vi.fn(async () => {}),
|
|
||||||
cancelReview: vi.fn(async () => {}),
|
|
||||||
restart: vi.fn(),
|
|
||||||
primeReplayToStage: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderBeat = (beatId: "draft" | "artifact" | "deployment" | "ready-run") => {
|
|
||||||
const scene = findScene("prepared-lifecycle");
|
|
||||||
const beat = findBeat("prepared-lifecycle", beatId);
|
|
||||||
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
|
|
||||||
render(<DemoLifecycleScene scene={scene} beat={beat} demo={demo} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("DemoLifecycleScene", () => {
|
|
||||||
it("renders prepared draft context honestly", () => {
|
|
||||||
renderBeat("draft");
|
|
||||||
expect(screen.getByRole("region", { name: "prepared workflow lifecycle" })).toHaveAttribute("data-active-lifecycle", "draft");
|
|
||||||
expect(screen.getByText("prepared context")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("examples/lda_report_workflow")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders artifact and deployment facts from replay evidence", () => {
|
|
||||||
renderBeat("deployment");
|
|
||||||
expect(screen.getByText("lda_report_case_study.default")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("block")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/local\.lda_docs/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders run readiness without claiming output exists yet", () => {
|
|
||||||
renderBeat("ready-run");
|
|
||||||
expect(screen.getByText("run_recorded_lda_report")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("interrupted")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText(/created issues/i)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
|
||||||
import { projectDemoLifecycleFacts } from "./demo-lifecycle-facts.js";
|
|
||||||
import { StageCaption } from "./StageCaption.js";
|
|
||||||
import type { SceneBeatDefinition, SceneDefinition } from "./storyboard.js";
|
|
||||||
|
|
||||||
type DemoLifecycleSceneProps = {
|
|
||||||
readonly scene: SceneDefinition;
|
|
||||||
readonly beat: SceneBeatDefinition;
|
|
||||||
readonly demo: DemoTimelineController;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stages = [
|
|
||||||
{ id: "draft", label: "Draft", description: "Prepared authoring context" },
|
|
||||||
{ id: "artifact", label: "Artifact", description: "Immutable versioned workflow" },
|
|
||||||
{ id: "deployment", label: "Deployment", description: "Configured source bindings" },
|
|
||||||
{ id: "ready-run", label: "Run-ready", description: "Persisted run can start" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const DemoLifecycleScene = ({ scene, beat, demo }: DemoLifecycleSceneProps) => {
|
|
||||||
const facts = projectDemoLifecycleFacts(demo);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StageCaption eyebrow="Product lifecycle" title={scene.title}>
|
|
||||||
<p>{beat.caption}</p>
|
|
||||||
</StageCaption>
|
|
||||||
<section
|
|
||||||
className="demo-lifecycle-scene"
|
|
||||||
aria-label="prepared workflow lifecycle"
|
|
||||||
data-active-lifecycle={beat.id}
|
|
||||||
>
|
|
||||||
<ol className="demo-lifecycle-scene__rail">
|
|
||||||
{stages.map((stage) => (
|
|
||||||
<li key={stage.id} data-active={stage.id === beat.id ? "true" : "false"}>
|
|
||||||
<strong>{stage.label}</strong>
|
|
||||||
<span>{stage.description}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<article className="demo-lifecycle-scene__proof">
|
|
||||||
<h3>{stages.find((stage) => stage.id === beat.id)?.label ?? "Lifecycle"}</h3>
|
|
||||||
{beat.id === "draft" && (
|
|
||||||
<dl>
|
|
||||||
<dt>Workflow</dt><dd>{facts.draft.label}</dd>
|
|
||||||
<dt>Status</dt><dd>{facts.draft.status}</dd>
|
|
||||||
<dt>Source</dt><dd>{facts.draft.source}</dd>
|
|
||||||
</dl>
|
|
||||||
)}
|
|
||||||
{beat.id === "artifact" && (
|
|
||||||
<dl>
|
|
||||||
<dt>Artifact</dt><dd>{facts.artifact.id}</dd>
|
|
||||||
<dt>Version</dt><dd>{facts.artifact.version ?? "unavailable"}</dd>
|
|
||||||
</dl>
|
|
||||||
)}
|
|
||||||
{beat.id === "deployment" && (
|
|
||||||
<dl>
|
|
||||||
<dt>Deployment</dt><dd>{facts.deployment.id}</dd>
|
|
||||||
<dt>Drift policy</dt><dd>{facts.deployment.driftPolicy}</dd>
|
|
||||||
<dt>Bindings</dt>
|
|
||||||
<dd>
|
|
||||||
<ul>
|
|
||||||
{facts.deployment.bindings.map(([from, to]) => (
|
|
||||||
<li key={`${from}:${to}`}>{from} -> {to}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
)}
|
|
||||||
{beat.id === "ready-run" && (
|
|
||||||
<dl>
|
|
||||||
<dt>Run</dt><dd>{facts.run.id ?? "not started"}</dd>
|
|
||||||
<dt>Status</dt><dd>{facts.run.status}</dd>
|
|
||||||
<dt>Deployment</dt><dd>{facts.deployment.id}</dd>
|
|
||||||
</dl>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -420,6 +420,18 @@ describe("SceneBody", () => {
|
|||||||
expect(openDiscussion).toHaveBeenCalledWith("where-is-ai-agent");
|
expect(openDiscussion).toHaveBeenCalledWith("where-is-ai-agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders Scene 9 discover beat with prepared authoring phases", () => {
|
||||||
|
renderSceneBodyAtMainLocation("prepared-lifecycle", "discover");
|
||||||
|
expect(screen.getByLabelText("authoring phase rail")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Discover")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 9 artifact beat with compiled artifact evidence", () => {
|
||||||
|
renderSceneBodyAtMainLocation("prepared-lifecycle", "artifact");
|
||||||
|
expect(screen.getByText("Artifact")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/art_x9y8z7/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders evidence before discussion links so the chip lane cannot cover evidence text", () => {
|
it("renders evidence before discussion links so the chip lane cannot cover evidence text", () => {
|
||||||
const location: PresentationLocation = { kind: "main", sceneId: "positioning", beatId: "landscape", focusPath: [] };
|
const location: PresentationLocation = { kind: "main", sceneId: "positioning", beatId: "landscape", focusPath: [] };
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type SceneDefinition,
|
type SceneDefinition,
|
||||||
type SceneBeatDefinition,
|
type SceneBeatDefinition,
|
||||||
} from "./storyboard.js";
|
} from "./storyboard.js";
|
||||||
import { DemoLifecycleScene } from "./DemoLifecycleScene.js";
|
import { PreparedAuthoringLifecycleScene } from "./authoring/PreparedAuthoringLifecycleScene.js";
|
||||||
import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
|
import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
|
||||||
import { StageCaption } from "./StageCaption.js";
|
import { StageCaption } from "./StageCaption.js";
|
||||||
import { ConclusionScene } from "./conclusion/ConclusionScene.js";
|
import { ConclusionScene } from "./conclusion/ConclusionScene.js";
|
||||||
@@ -311,7 +311,7 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
|
|||||||
case "agent":
|
case "agent":
|
||||||
return <AgentHandoffScene scene={scene} beat={beat} />;
|
return <AgentHandoffScene scene={scene} beat={beat} />;
|
||||||
case "demo-lifecycle":
|
case "demo-lifecycle":
|
||||||
return <DemoLifecycleScene scene={scene} beat={beat} demo={demo} />;
|
return <PreparedAuthoringLifecycleScene scene={scene} beat={beat} />;
|
||||||
case "demo":
|
case "demo":
|
||||||
return (
|
return (
|
||||||
<DemoWorkflowScene
|
<DemoWorkflowScene
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { findBeat, findScene } from "../storyboard.js";
|
||||||
|
import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleScene.js";
|
||||||
|
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
|
const renderBeat = (beatId: string) => {
|
||||||
|
const scene = findScene("prepared-lifecycle");
|
||||||
|
const beat = findBeat("prepared-lifecycle", beatId);
|
||||||
|
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
|
||||||
|
return render(<PreparedAuthoringLifecycleScene scene={scene} beat={beat} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("PreparedAuthoringLifecycleScene", () => {
|
||||||
|
it("discover shows sources, capabilities, and schema", () => {
|
||||||
|
renderBeat("discover");
|
||||||
|
expect(screen.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(screen.getAllByText(/graph|routes/i).length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validate shows diagnosis and repair", () => {
|
||||||
|
renderBeat("validate");
|
||||||
|
expect(screen.getByText("Validate")).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText(/diagnos|repair/i).length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("artifact shows immutable ID and version", () => {
|
||||||
|
renderBeat("artifact");
|
||||||
|
expect(screen.getByText("Artifact")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/art_x9y8z7/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deployment shows bindings and validation", () => {
|
||||||
|
renderBeat("deployment");
|
||||||
|
expect(screen.getByText("Deployment")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/dep_m4n5p6/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders an Agent trace trigger", () => {
|
||||||
|
renderBeat("discover");
|
||||||
|
expect(screen.getByRole("button", { name: "Agent trace" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trace is initially closed", () => {
|
||||||
|
renderBeat("discover");
|
||||||
|
expect(screen.queryByRole("dialog", { name: "Authoring trace" })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens the trace panel when Agent trace is clicked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderBeat("draft");
|
||||||
|
await user.click(screen.getByRole("button", { name: "Agent trace" }));
|
||||||
|
expect(screen.getByRole("dialog", { name: "Authoring trace" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a compact orientation rail", () => {
|
||||||
|
renderBeat("artifact");
|
||||||
|
const rail = screen.getByLabelText("authoring phase rail");
|
||||||
|
expect(rail).toBeInTheDocument();
|
||||||
|
expect(rail.children.length).toBeGreaterThanOrEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("highlights the active phase in the rail", () => {
|
||||||
|
renderBeat("deployment");
|
||||||
|
const rail = screen.getByLabelText("authoring phase rail");
|
||||||
|
const active = rail.querySelector("[data-active='true']");
|
||||||
|
expect(active).toBeInTheDocument();
|
||||||
|
expect(active).toHaveTextContent("Deployment");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { projectPreparedAuthoringPhase } from "./authoring-projection.js";
|
||||||
|
import { AuthoringTracePanel } from "./AuthoringTracePanel.js";
|
||||||
|
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||||
|
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
||||||
|
import { StageCaption } from "../StageCaption.js";
|
||||||
|
|
||||||
|
type PreparedAuthoringLifecycleSceneProps = {
|
||||||
|
readonly scene: SceneDefinition;
|
||||||
|
readonly beat: SceneBeatDefinition;
|
||||||
|
};
|
||||||
|
|
||||||
|
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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scene 9 — Prepared workflow authoring lifecycle.
|
||||||
|
*
|
||||||
|
* Each beat shows a compact orientation rail and one dominant phase projection
|
||||||
|
* sourced from the prepared authoring recording. A persistent "Agent trace"
|
||||||
|
* trigger opens the AuthoringTracePanel overlay.
|
||||||
|
*
|
||||||
|
* The receipt bridges Scene 8's full conversation and Scene 9's separate
|
||||||
|
* trace panel without morphing runtime-owned components.
|
||||||
|
*/
|
||||||
|
export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthoringLifecycleSceneProps) => {
|
||||||
|
const beatId = beat.id as AuthoringPhaseId;
|
||||||
|
const [traceOpen, setTraceOpen] = useState(false);
|
||||||
|
const projection = projectPreparedAuthoringPhase(beatId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StageCaption eyebrow="Prepared workflow" title={scene.title}>
|
||||||
|
<p>{beat.caption}</p>
|
||||||
|
</StageCaption>
|
||||||
|
<section
|
||||||
|
className="prepared-lifecycle-scene"
|
||||||
|
aria-label="prepared workflow authoring lifecycle"
|
||||||
|
data-active-phase={beatId}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<article className="prepared-lifecycle-scene__projection">
|
||||||
|
<div className="prepared-lifecycle-scene__summary">
|
||||||
|
<p>{projection.summary}</p>
|
||||||
|
</div>
|
||||||
|
<div className="prepared-lifecycle-scene__commands">
|
||||||
|
{projection.commands.map((cmd, i) => (
|
||||||
|
<div key={i} className="prepared-lifecycle-scene__command">
|
||||||
|
<code>$ {cmd.command}</code>
|
||||||
|
<span className="prepared-lifecycle-scene__command-result" data-result={cmd.result}>
|
||||||
|
{cmd.result === "success" ? "✓" : "⚡"} {cmd.result}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
<div className="prepared-lifecycle-scene__receipt">
|
||||||
|
<AuthoringTracePanel
|
||||||
|
phase={beatId}
|
||||||
|
open={traceOpen}
|
||||||
|
onOpen={() => setTraceOpen(true)}
|
||||||
|
onClose={() => setTraceOpen(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
|
||||||
import { initialDemoTimelineState } from "../demo/timeline/reducer.js";
|
|
||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
|
||||||
import { projectDemoLifecycleFacts } from "./demo-lifecycle-facts.js";
|
|
||||||
|
|
||||||
const controller = (eventMode: "recorded" | "empty" = "recorded"): DemoTimelineController => {
|
|
||||||
const recording = loadCanonicalDemoRecording();
|
|
||||||
return {
|
|
||||||
state: {
|
|
||||||
...initialDemoTimelineState,
|
|
||||||
mode: "replay",
|
|
||||||
phase: "paused",
|
|
||||||
events: eventMode === "recorded" ? recording.events : [],
|
|
||||||
appliedCount: eventMode === "recorded" ? recording.events.length : 0,
|
|
||||||
autoplay: false,
|
|
||||||
},
|
|
||||||
inFlight: false,
|
|
||||||
interruptPayload: null,
|
|
||||||
output: null,
|
|
||||||
trace: null,
|
|
||||||
missingDeploymentMessage: null,
|
|
||||||
recordingId: recording.recordingId,
|
|
||||||
canStart: true,
|
|
||||||
setMode: vi.fn(),
|
|
||||||
start: vi.fn(),
|
|
||||||
pause: vi.fn(),
|
|
||||||
play: vi.fn(),
|
|
||||||
next: vi.fn(async () => {}),
|
|
||||||
submitSelectedIssues: vi.fn(async () => {}),
|
|
||||||
cancelReview: vi.fn(async () => {}),
|
|
||||||
restart: vi.fn(),
|
|
||||||
primeReplayToStage: vi.fn(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("projectDemoLifecycleFacts", () => {
|
|
||||||
it("projects prepared draft context without pretending it was runtime evidence", () => {
|
|
||||||
const facts = projectDemoLifecycleFacts(controller());
|
|
||||||
expect(facts.draft.label).toBe("lda report workflow");
|
|
||||||
expect(facts.draft.source).toContain("examples/lda_report_workflow");
|
|
||||||
expect(facts.draft.status).toBe("prepared context");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("projects artifact and deployment facts from deployment inspect evidence", () => {
|
|
||||||
const facts = projectDemoLifecycleFacts(controller());
|
|
||||||
expect(facts.artifact).toEqual({ id: "lda_report_case_study", version: 1 });
|
|
||||||
expect(facts.deployment.id).toBe("lda_report_case_study.default");
|
|
||||||
expect(facts.deployment.driftPolicy).toBe("block");
|
|
||||||
expect(facts.deployment.bindings).toContainEqual(["local.lda_docs", "local.lda_docs"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("projects run readiness from the run start event", () => {
|
|
||||||
const facts = projectDemoLifecycleFacts(controller());
|
|
||||||
expect(facts.run.id).toBe("run_recorded_lda_report");
|
|
||||||
expect(facts.run.status).toBe("interrupted");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("falls back honestly when replay evidence has not loaded yet", () => {
|
|
||||||
const facts = projectDemoLifecycleFacts(controller("empty"));
|
|
||||||
|
|
||||||
expect(facts.artifact).toEqual({ id: "unavailable", version: null });
|
|
||||||
expect(facts.deployment).toEqual({
|
|
||||||
id: "unavailable",
|
|
||||||
driftPolicy: "unavailable",
|
|
||||||
bindings: [],
|
|
||||||
});
|
|
||||||
expect(facts.run).toEqual({ id: null, status: "not started" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
|
||||||
import type { DemoEvent } from "../demo/timeline/models.js";
|
|
||||||
|
|
||||||
export type DemoLifecycleFacts = {
|
|
||||||
readonly draft: {
|
|
||||||
readonly label: string;
|
|
||||||
readonly source: string;
|
|
||||||
readonly status: string;
|
|
||||||
};
|
|
||||||
readonly artifact: {
|
|
||||||
readonly id: string;
|
|
||||||
readonly version: number | null;
|
|
||||||
};
|
|
||||||
readonly deployment: {
|
|
||||||
readonly id: string;
|
|
||||||
readonly driftPolicy: string;
|
|
||||||
readonly bindings: ReadonlyArray<readonly [string, string]>;
|
|
||||||
};
|
|
||||||
readonly run: {
|
|
||||||
readonly id: string | null;
|
|
||||||
readonly status: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const deploymentInspect = (demo: DemoTimelineController): DemoEvent | undefined =>
|
|
||||||
demo.state.events.find((event) => event.stage === "deployment_check");
|
|
||||||
|
|
||||||
const runStart = (demo: DemoTimelineController): DemoEvent | undefined =>
|
|
||||||
demo.state.events.find((event) => event.stage === "run_start");
|
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
|
|
||||||
const stringField = (record: Record<string, unknown> | undefined, field: string): string | undefined => {
|
|
||||||
const value = record?.[field];
|
|
||||||
return typeof value === "string" ? value : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const numberField = (record: Record<string, unknown> | undefined, field: string): number | undefined => {
|
|
||||||
const value = record?.[field];
|
|
||||||
return typeof value === "number" ? value : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const readBindings = (value: unknown): ReadonlyArray<readonly [string, string]> => {
|
|
||||||
if (!Array.isArray(value)) return [];
|
|
||||||
return value.flatMap((entry) => {
|
|
||||||
if (!Array.isArray(entry) || entry.length !== 2) return [];
|
|
||||||
const [from, to] = entry;
|
|
||||||
return typeof from === "string" && typeof to === "string" ? [[from, to] as const] : [];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Presentation-only lifecycle facts. Draft context is prepared example context;
|
|
||||||
* artifact/deployment/run facts come from replay evidence when available.
|
|
||||||
*/
|
|
||||||
export const projectDemoLifecycleFacts = (demo: DemoTimelineController): DemoLifecycleFacts => {
|
|
||||||
const deployment = deploymentInspect(demo);
|
|
||||||
const deploymentInterpreted = isRecord(deployment?.interpreted) ? deployment.interpreted : undefined;
|
|
||||||
const run = runStart(demo);
|
|
||||||
const runInterpreted = isRecord(run?.interpreted) ? run.interpreted : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
draft: {
|
|
||||||
label: "lda report workflow",
|
|
||||||
source: "examples/lda_report_workflow",
|
|
||||||
status: "prepared context",
|
|
||||||
},
|
|
||||||
artifact: {
|
|
||||||
id: stringField(deploymentInterpreted, "artifactId") ?? "unavailable",
|
|
||||||
version: numberField(deploymentInterpreted, "artifactVersion") ?? null,
|
|
||||||
},
|
|
||||||
deployment: {
|
|
||||||
id: stringField(deploymentInterpreted, "id") ?? "unavailable",
|
|
||||||
driftPolicy: stringField(deploymentInterpreted, "driftPolicy") ?? "unavailable",
|
|
||||||
bindings: readBindings(deploymentInterpreted?.["bindings"]),
|
|
||||||
},
|
|
||||||
run: {
|
|
||||||
id: stringField(runInterpreted, "runId") ?? run?.resultingIds.runId ?? null,
|
|
||||||
status: stringField(runInterpreted, "status") ?? "not started",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -2739,3 +2739,100 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Scene 9 — Prepared Workflow Authoring Lifecycle.
|
||||||
|
Compact orientation rail plus dominant phase projection.
|
||||||
|
*/
|
||||||
|
.prepared-lifecycle-scene {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__rail {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.45rem 0.55rem;
|
||||||
|
background: color-mix(in oklch, var(--stage-inset) 55%, transparent);
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__rail li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.25rem 0.45rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
font: 600 0.65rem/1 var(--font-interface);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__rail li[data-active="true"] {
|
||||||
|
background: color-mix(in oklch, var(--accent-cyan) 14%, transparent);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__projection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
background: color-mix(in oklch, var(--stage-surface) 60%, var(--stage-inset));
|
||||||
|
border: 1px solid var(--stage-line);
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__summary p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__commands {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__command {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.3rem 0.45rem;
|
||||||
|
background: color-mix(in oklch, var(--stage-inset) 65%, transparent);
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__command code {
|
||||||
|
font-family: var(--font-evidence);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__command-result {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__command-result[data-result="success"] {
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__command-result[data-result="diagnostic"] {
|
||||||
|
color: var(--accent-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__receipt {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.35rem 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ describe("defense storyboard catalog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps chat out of the way during proof-heavy demo beats", () => {
|
it("keeps chat out of the way during proof-heavy demo beats", () => {
|
||||||
|
expect(findBeat("prepared-lifecycle", "discover")?.chatMode).toBe("hidden");
|
||||||
expect(findBeat("prepared-lifecycle", "draft")?.chatMode).toBe("hidden");
|
expect(findBeat("prepared-lifecycle", "draft")?.chatMode).toBe("hidden");
|
||||||
|
expect(findBeat("prepared-lifecycle", "validate")?.chatMode).toBe("hidden");
|
||||||
|
expect(findBeat("prepared-lifecycle", "artifact")?.chatMode).toBe("hidden");
|
||||||
expect(findBeat("prepared-lifecycle", "deployment")?.chatMode).toBe("hidden");
|
expect(findBeat("prepared-lifecycle", "deployment")?.chatMode).toBe("hidden");
|
||||||
expect(findBeat("run-from-deployment", "input")?.chatMode).toBe("hidden");
|
expect(findBeat("run-from-deployment", "input")?.chatMode).toBe("hidden");
|
||||||
expect(findBeat("run-from-deployment", "graph")?.chatMode).toBe("hidden");
|
expect(findBeat("run-from-deployment", "graph")?.chatMode).toBe("hidden");
|
||||||
@@ -72,10 +75,11 @@ describe("defense storyboard catalog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("defines the lifecycle story beats before run evidence", () => {
|
it("defines the lifecycle story beats before run evidence", () => {
|
||||||
expect(findBeat("prepared-lifecycle", "draft")?.caption).toMatch(/prepared authoring/i);
|
expect(findBeat("prepared-lifecycle", "discover")?.caption).toMatch(/sources|capabilities|schemas/i);
|
||||||
expect(findBeat("prepared-lifecycle", "artifact")?.caption).toMatch(/artifact/i);
|
expect(findBeat("prepared-lifecycle", "draft")?.caption).toMatch(/draft/i);
|
||||||
expect(findBeat("prepared-lifecycle", "deployment")?.caption).toMatch(/source/i);
|
expect(findBeat("prepared-lifecycle", "validate")?.caption).toMatch(/diagnose|repair/i);
|
||||||
expect(findBeat("prepared-lifecycle", "ready-run")?.caption).toMatch(/ready/i);
|
expect(findBeat("prepared-lifecycle", "artifact")?.caption).toMatch(/compile|artifact/i);
|
||||||
|
expect(findBeat("prepared-lifecycle", "deployment")?.caption).toMatch(/deploy|bindings/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defines focused run, interrupt, and evidence beats", () => {
|
it("defines focused run, interrupt, and evidence beats", () => {
|
||||||
|
|||||||
@@ -182,10 +182,11 @@ export const mainScenes = defineScenes([
|
|||||||
stageTheme: "night",
|
stageTheme: "night",
|
||||||
view: "demo-lifecycle",
|
view: "demo-lifecycle",
|
||||||
beats: [
|
beats: [
|
||||||
sceneBeat("draft", "Prepared draft", "Prepared authoring context creates a reusable report workflow.", { chatMode: "hidden", chatTheme: "light" }),
|
sceneBeat("discover", "Discover capabilities", "Inspect available sources, capabilities, and schemas before authoring.", { chatMode: "hidden", chatTheme: "light" }),
|
||||||
sceneBeat("artifact", "Saved artifact", "The workflow is preserved as a versioned artifact.", { chatMode: "hidden", chatTheme: "light" }),
|
sceneBeat("draft", "Author draft", "Create a workflow draft with report generation steps and routes.", { chatMode: "hidden", chatTheme: "light" }),
|
||||||
sceneBeat("deployment", "Deployment bindings", "Deployment binds workflow requirements to configured local sources.", { chatMode: "hidden", chatTheme: "light" }),
|
sceneBeat("validate", "Validate and repair", "Bind sources and validate the draft; diagnose and repair issues.", { chatMode: "hidden", chatTheme: "light" }),
|
||||||
sceneBeat("ready-run", "Ready to run", "The deployment is ready to start a persisted run from workflow input.", { chatMode: "hidden", chatTheme: "light" }),
|
sceneBeat("artifact", "Compile artifact", "Compile the validated draft into an immutable artifact.", { chatMode: "hidden", chatTheme: "light" }),
|
||||||
|
sceneBeat("deployment", "Deploy and validate", "Save deployment bindings and validate readiness.", { chatMode: "hidden", chatTheme: "light" }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user