feat: add prepared lifecycle scene

This commit is contained in:
lda
2026-07-09 21:24:06 +07:00 Verified
parent 632d3a04eb
commit f19fa25d97
4 changed files with 228 additions and 1 deletions
@@ -0,0 +1,65 @@
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: /demo lifecycle detail/i })).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();
});
});
@@ -0,0 +1,81 @@
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="demo lifecycle detail"
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} -&gt; {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>
</>
);
};
@@ -8,6 +8,7 @@ import {
type SceneDefinition,
type SceneBeatDefinition,
} from "./storyboard.js";
import { DemoLifecycleScene } from "./DemoLifecycleScene.js";
import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
import { StageCaption } from "./StageCaption.js";
import { ArchitectureScene } from "./scenes/ArchitectureScene.js";
@@ -325,7 +326,9 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
return <AuthoringScene scene={scene} beat={beat} />;
case "agent":
return <AgentHandoffScene scene={scene} beat={beat} />;
case "demo":
case "demo-lifecycle":
return <DemoLifecycleScene scene={scene} beat={beat} demo={demo} />;
case "demo":
return (
<DemoWorkflowScene
scene={scene}
@@ -1066,6 +1066,84 @@
font-weight: 600;
}
/* Demo lifecycle scene */
.demo-lifecycle-scene {
display: grid;
grid-template-columns: minmax(18rem, 0.8fr) minmax(0, 1.2fr);
gap: 1rem;
min-height: 0;
}
.demo-lifecycle-scene__rail {
display: grid;
gap: 0.65rem;
margin: 0;
padding: 0;
list-style: none;
}
.demo-lifecycle-scene__rail li,
.demo-lifecycle-scene__proof {
border: 1px solid var(--stage-line);
border-radius: var(--presentation-panel-radius, 0.85rem);
background: var(--stage-surface);
color: var(--text-primary);
}
.demo-lifecycle-scene__rail li {
padding: 0.8rem 0.9rem;
opacity: 0.62;
}
.demo-lifecycle-scene__rail li[data-active="true"] {
border-color: var(--accent-cyan);
opacity: 1;
}
.demo-lifecycle-scene__rail strong,
.demo-lifecycle-scene__rail span {
display: block;
}
.demo-lifecycle-scene__rail span {
color: var(--text-muted);
font-size: 0.86rem;
}
.demo-lifecycle-scene__proof {
min-height: 0;
overflow: auto;
padding: 1rem 1.1rem;
scrollbar-width: none;
}
.demo-lifecycle-scene__proof::-webkit-scrollbar {
display: none;
}
.demo-lifecycle-scene__proof h3 {
margin: 0 0 1rem;
color: var(--accent-cyan);
font: 700 1.35rem/1 var(--font-interface);
}
.demo-lifecycle-scene__proof dl {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 0.55rem 0.9rem;
margin: 0;
}
.demo-lifecycle-scene__proof dt {
color: var(--text-muted);
font-weight: 700;
}
.demo-lifecycle-scene__proof dd {
margin: 0;
font-family: var(--font-mono);
}
.scene-body__lifecycle-arrow {
position: absolute;
right: -0.55rem;