docs: plan scene 10 guided product moment
This commit is contained in:
+23
-9
@@ -38,6 +38,7 @@ Design contracts:
|
||||
- [`demo autoplay and replay`](superpowers/specs/2026-07-03-demo-autoplay-replay.md)
|
||||
- [`defense presentation storyboard`](superpowers/specs/2026-07-04-defense-presentation-storyboard-design.md)
|
||||
- [`adaptive presentation canvas and evidence inspector`](superpowers/specs/2026-07-05-adaptive-presentation-canvas-design.md)
|
||||
- [`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md)
|
||||
|
||||
Implementation order:
|
||||
|
||||
@@ -129,9 +130,15 @@ Implementation order:
|
||||
approval, graph focus, and evidence transitions into one deterministic
|
||||
presenter sequence. Implementation:
|
||||
[`guided run beat gates`](historical/superpowers/plans/2026-07-09-guided-run-beat-gates.md).
|
||||
18. Future: presenter companion, Scene 10 product graph, final scene visuals,
|
||||
18. Active: make Scene 10 a guided product moment with direct-link replay
|
||||
readiness and one dominant proof surface per approval/resume/output/trace
|
||||
beat. Design:
|
||||
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
|
||||
Plan:
|
||||
[`Scene 10 guided product moment plan`](superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
|
||||
19. Future: presenter companion, Scene 10 product graph, final scene visuals,
|
||||
evidence assets, and rehearsal timing.
|
||||
19. Add a static slide/appendix shell only after presentation mode is clear.
|
||||
20. Add a static slide/appendix shell only after presentation mode is clear.
|
||||
Astro remains an option, not the default next surface.
|
||||
|
||||
Presentation wishlist / defense readiness:
|
||||
@@ -177,9 +184,15 @@ Presentation wishlist / defense readiness:
|
||||
routes stage-aware, with body/support/action regions instead of plain
|
||||
document cards. Implementation:
|
||||
[`presentation discussion modal composition`](historical/superpowers/plans/2026-07-09-presentation-discussion-modal-composition.md).
|
||||
10. Presentation craft pass: tune remaining motion, evidence receipt
|
||||
10. Active: Scene 10 guided product moment primes replay state for direct
|
||||
hashes and stages approval, resume, output, and trace as one readable
|
||||
product flow. Design:
|
||||
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
|
||||
Plan:
|
||||
[`Scene 10 guided product moment plan`](superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
|
||||
11. Future: presentation craft pass for remaining motion, evidence receipt
|
||||
placement, route-level caption contrast, and graph visual language after
|
||||
the discussion layer is stable.
|
||||
Scene 10 is stable.
|
||||
- Presenter runbook:
|
||||
[`defense presentation runbook`](runbooks/defense-presentation.md) covers
|
||||
exact URLs, keyboard controls, live RPC/demo fallback steps, story spine,
|
||||
@@ -195,15 +208,16 @@ Presentation wishlist / defense readiness:
|
||||
"run prepared agent" affordance, render tool calls/results with a standard
|
||||
modern AI-app vocabulary, and expose approval requests as normal chat events
|
||||
that can also drive presentation actions.
|
||||
- Schema form approval surface: render typed interrupt request/resume schemas as
|
||||
reusable form UI shared by chat, Scene 10, and future console approval flows.
|
||||
The current `{ "type": "object" }` proof is enough for architecture evidence,
|
||||
but the product demo needs a real schema-derived approval form with submitted
|
||||
and cancelled outcomes.
|
||||
- Completed: guided run beat gates connect Scene 10 schema approval, chat
|
||||
approval, graph focus, and evidence transitions into one deterministic
|
||||
presenter sequence. Implementation:
|
||||
[`guided run beat gates`](historical/superpowers/plans/2026-07-09-guided-run-beat-gates.md).
|
||||
- Active: Scene 10 guided product moment removes delayed direct-link readiness
|
||||
and gives approval, resume, output, and trace one dominant proof surface each.
|
||||
Design:
|
||||
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
|
||||
Plan:
|
||||
[`Scene 10 guided product moment plan`](superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
|
||||
- Evidence assets and rehearsal timing: prepare fallback screenshots/recordings,
|
||||
expected run states, and a timed walkthrough checklist for a 15-minute defense.
|
||||
- Presenter companion feasibility: decide whether phone/laptop control is local
|
||||
|
||||
@@ -0,0 +1,996 @@
|
||||
# Scene 10 Guided Product Moment Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make Scene 10 behave like a guided product flow: direct links are immediately ready, approval decisions clearly change state, and each beat has one dominant proof surface.
|
||||
|
||||
**Architecture:** Add presentation-owned beat readiness projection, extend `useDemoTimeline` with replay priming, and recompose Scene 10 around a guided product moment component. Replay priming is a display projection over the canonical recording; it must not fabricate live runtime evidence.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, Vitest, Testing Library, Playwright smoke, existing `useDemoTimeline`, existing `SchemaApprovalSurface`, existing presentation storyboard.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Do not add a new transport, store, graph engine, or agent runtime.
|
||||
- Do not fabricate cancelled replay evidence; the canonical recording only contains the submitted branch.
|
||||
- Direct hashes for approval, resume, output, and trace must be immediately informative.
|
||||
- Live execution must still run sequentially when the chat action starts a live workflow.
|
||||
- `/console` must remain unaffected.
|
||||
- Add comments around replay priming because it is presentation projection, not runtime execution.
|
||||
- Keep styling within existing presentation CSS files and tokens.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create `web/apps/console/src/presentation/demo-beat-requirements.ts`
|
||||
- Maps demo scene beats to the minimum replay event stage needed for direct-link readiness.
|
||||
- Create `web/apps/console/src/presentation/demo-beat-requirements.test.ts`
|
||||
- Pins requirement mapping for Scene 9 and Scene 10 beats.
|
||||
- Modify `web/apps/console/src/demo/useDemoTimeline.ts`
|
||||
- Adds `primeReplayToStage(stage)` to project the canonical recording to a display-ready state.
|
||||
- Modify `web/apps/console/src/demo/useDemoTimeline.test.tsx`
|
||||
- Tests replay priming and live-mode non-interference.
|
||||
- Modify `web/apps/console/src/presentation/PresentationRoute.tsx`
|
||||
- Calls replay priming when the current route enters a demo beat.
|
||||
- Modify `web/apps/console/src/presentation/PresentationRoute.test.tsx`
|
||||
- Tests immediate readiness and decision consequences from direct hashes.
|
||||
- Create `web/apps/console/src/presentation/GuidedProductMoment.tsx`
|
||||
- Owns Scene 10 hierarchy for approval/resume/output/trace.
|
||||
- Create `web/apps/console/src/presentation/GuidedProductMoment.test.tsx`
|
||||
- Pins hero surface selection and decision copy.
|
||||
- Modify `web/apps/console/src/presentation/DemoWorkflowScene.tsx`
|
||||
- Delegates Scene 10 beats to `GuidedProductMoment`.
|
||||
- Modify `web/apps/console/src/presentation/styles/demo-workflow.css`
|
||||
- Adds guided moment layout styles.
|
||||
- Modify `docs/current_roadmap.md`
|
||||
- Marks this plan active/completed at the right time.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add beat readiness requirements
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `web/apps/console/src/presentation/demo-beat-requirements.ts`
|
||||
- Create: `web/apps/console/src/presentation/demo-beat-requirements.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
import type { DemoEvent } from "../demo/timeline/models.js";
|
||||
|
||||
export type DemoBeatRequirement = {
|
||||
readonly requiredStage: DemoEvent["stage"] | null;
|
||||
readonly reason: string;
|
||||
};
|
||||
|
||||
export const requirementForDemoBeat = (
|
||||
sceneId: string,
|
||||
beatId: string,
|
||||
): DemoBeatRequirement;
|
||||
```
|
||||
|
||||
- Consumes:
|
||||
- `DemoEvent["stage"]` from existing replay model.
|
||||
|
||||
- [ ] **Step 1: Write failing model tests**
|
||||
|
||||
Create `web/apps/console/src/presentation/demo-beat-requirements.test.ts`:
|
||||
|
||||
```ts
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { requirementForDemoBeat } from "./demo-beat-requirements.js";
|
||||
|
||||
describe("requirementForDemoBeat", () => {
|
||||
it.each([
|
||||
["workflow-demo", "operation", "run_start"],
|
||||
["workflow-demo", "graph", "run_start"],
|
||||
["workflow-demo", "interrupt", "interrupt"],
|
||||
["interrupt-evidence", "approval", "interrupt"],
|
||||
["interrupt-evidence", "resume", "run_resume"],
|
||||
["interrupt-evidence", "output", "run_resume"],
|
||||
["interrupt-evidence", "trace", "trace_read"],
|
||||
] as const)("maps %s/%s to %s", (sceneId, beatId, stage) => {
|
||||
expect(requirementForDemoBeat(sceneId, beatId).requiredStage).toBe(stage);
|
||||
});
|
||||
|
||||
it("returns no required stage for non-demo beats", () => {
|
||||
expect(requirementForDemoBeat("thesis", "title")).toEqual({
|
||||
requiredStage: null,
|
||||
reason: "No demo replay state needed.",
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/demo-beat-requirements.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because the file does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement mapping**
|
||||
|
||||
Create `web/apps/console/src/presentation/demo-beat-requirements.ts`:
|
||||
|
||||
```ts
|
||||
import type { DemoEvent } from "../demo/timeline/models.js";
|
||||
|
||||
export type DemoBeatRequirement = {
|
||||
readonly requiredStage: DemoEvent["stage"] | null;
|
||||
readonly reason: string;
|
||||
};
|
||||
|
||||
const requirements: Readonly<Record<string, DemoBeatRequirement>> = {
|
||||
"workflow-demo/operation": {
|
||||
requiredStage: "run_start",
|
||||
reason: "Operation beat needs the recorded run start.",
|
||||
},
|
||||
"workflow-demo/graph": {
|
||||
requiredStage: "run_start",
|
||||
reason: "Graph beat needs the persisted run id.",
|
||||
},
|
||||
"workflow-demo/interrupt": {
|
||||
requiredStage: "interrupt",
|
||||
reason: "Interrupt beat needs the typed review payload.",
|
||||
},
|
||||
"interrupt-evidence/approval": {
|
||||
requiredStage: "interrupt",
|
||||
reason: "Approval beat needs the typed review payload before controls can act.",
|
||||
},
|
||||
"interrupt-evidence/resume": {
|
||||
requiredStage: "run_resume",
|
||||
reason: "Resume beat needs the recorded resume operation.",
|
||||
},
|
||||
"interrupt-evidence/output": {
|
||||
requiredStage: "run_resume",
|
||||
reason: "Output beat needs the resumed run output.",
|
||||
},
|
||||
"interrupt-evidence/trace": {
|
||||
requiredStage: "trace_read",
|
||||
reason: "Trace beat needs the recorded trace read.",
|
||||
},
|
||||
};
|
||||
|
||||
export const requirementForDemoBeat = (
|
||||
sceneId: string,
|
||||
beatId: string,
|
||||
): DemoBeatRequirement =>
|
||||
requirements[`${sceneId}/${beatId}`] ?? {
|
||||
requiredStage: null,
|
||||
reason: "No demo replay state needed.",
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/demo-beat-requirements.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/apps/console/src/presentation/demo-beat-requirements.ts web/apps/console/src/presentation/demo-beat-requirements.test.ts
|
||||
git commit -m "feat: map demo beats to replay readiness"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add replay priming to the demo timeline
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `web/apps/console/src/demo/useDemoTimeline.ts`
|
||||
- Test: `web/apps/console/src/demo/useDemoTimeline.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes:
|
||||
- `DemoEvent["stage"]`
|
||||
- canonical `DemoRecording`
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
readonly primeReplayToStage: (stage: DemoEvent["stage"] | null) => void;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write failing timeline tests**
|
||||
|
||||
In `web/apps/console/src/demo/useDemoTimeline.test.tsx`, add:
|
||||
|
||||
```tsx
|
||||
it("primes replay to the interrupt stage immediately", () => {
|
||||
const { result } = renderHook(() => useDemoTimeline("http://127.0.0.1:8765/rpc", vi.fn()));
|
||||
|
||||
act(() => result.current.start("replay"));
|
||||
act(() => result.current.primeReplayToStage("interrupt"));
|
||||
|
||||
expect(result.current.state.mode).toBe("replay");
|
||||
expect(result.current.state.phase).toBe("review");
|
||||
expect(result.current.interruptPayload).not.toBeNull();
|
||||
expect(result.current.state.events[result.current.state.appliedCount - 1]?.stage).toBe("interrupt");
|
||||
});
|
||||
|
||||
it("primes replay to the resume stage with output projection", () => {
|
||||
const { result } = renderHook(() => useDemoTimeline("http://127.0.0.1:8765/rpc", vi.fn()));
|
||||
|
||||
act(() => result.current.start("replay"));
|
||||
act(() => result.current.primeReplayToStage("run_resume"));
|
||||
|
||||
expect(result.current.state.mode).toBe("replay");
|
||||
expect(result.current.state.phase).toBe("paused");
|
||||
expect(result.current.output?.created_issues).toHaveLength(1);
|
||||
expect(result.current.state.events[result.current.state.appliedCount - 1]?.stage).toBe("run_resume");
|
||||
});
|
||||
|
||||
it("does not prime live timelines", () => {
|
||||
const { result } = renderHook(() => useDemoTimeline("http://127.0.0.1:8765/rpc", vi.fn()));
|
||||
|
||||
act(() => result.current.start("live"));
|
||||
act(() => result.current.primeReplayToStage("interrupt"));
|
||||
|
||||
expect(result.current.state.mode).toBe("live");
|
||||
expect(result.current.state.events).toEqual([]);
|
||||
expect(result.current.interruptPayload).toBeNull();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/demo/useDemoTimeline.test.tsx
|
||||
```
|
||||
|
||||
Expected: FAIL because `primeReplayToStage` does not exist.
|
||||
|
||||
- [ ] **Step 3: Extend controller type**
|
||||
|
||||
In `web/apps/console/src/demo/useDemoTimeline.ts`, import the event type if
|
||||
not already imported as a value type:
|
||||
|
||||
```ts
|
||||
import type { DemoEvent, DemoRecording } from "./timeline/models.js";
|
||||
```
|
||||
|
||||
Add to `DemoTimelineController`:
|
||||
|
||||
```ts
|
||||
readonly primeReplayToStage: (stage: DemoEvent["stage"] | null) => void;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add projection helpers**
|
||||
|
||||
In `useDemoTimeline.ts`, add helper functions near `deriveMissingMessage`:
|
||||
|
||||
```ts
|
||||
const phaseForPrimedStage = (
|
||||
stage: DemoEvent["stage"] | null,
|
||||
): DemoTimelineState["phase"] => {
|
||||
if (stage === "interrupt") return "review";
|
||||
if (stage === "completed") return "completed";
|
||||
if (stage === "failed") return "failed";
|
||||
return "paused";
|
||||
};
|
||||
|
||||
const appliedCountForStage = (
|
||||
events: ReadonlyArray<DemoEvent>,
|
||||
stage: DemoEvent["stage"],
|
||||
): number => {
|
||||
const index = events.findIndex((event) => event.stage === stage);
|
||||
return index === -1 ? 0 : index + 1;
|
||||
};
|
||||
```
|
||||
|
||||
Add a local helper inside the hook after state setters are declared:
|
||||
|
||||
```ts
|
||||
const projectTransientState = useCallback((events: ReadonlyArray<DemoEvent>, appliedCount: number) => {
|
||||
const appliedEvents = events.slice(0, appliedCount);
|
||||
const interruptEvent = appliedEvents.find((event) => event.stage === "interrupt");
|
||||
const resumeEvent = appliedEvents.find((event) => event.stage === "run_resume");
|
||||
const traceEvent = appliedEvents.find((event) => event.stage === "trace_read");
|
||||
const completedEvent = appliedEvents.find((event) => event.stage === "completed");
|
||||
|
||||
if (interruptEvent?.interpreted) {
|
||||
const interpreted = interruptEvent.interpreted as { payload: LdaReportInterruptPayload };
|
||||
setInterruptPayload(parseLdaReportInterruptPayload(interpreted.payload));
|
||||
}
|
||||
if (resumeEvent?.interpreted) {
|
||||
const interpreted = resumeEvent.interpreted as { output: LdaReportOutput };
|
||||
setOutput(parseLdaReportOutput(interpreted.output));
|
||||
}
|
||||
if (traceEvent?.interpreted) {
|
||||
setTrace(decodeTracePage(traceEvent.interpreted));
|
||||
}
|
||||
if (completedEvent?.interpreted) {
|
||||
const interpreted = completedEvent.interpreted as { output: LdaReportOutput; trace: TracePage };
|
||||
setOutput(parseLdaReportOutput(interpreted.output));
|
||||
setTrace(decodeTracePage(interpreted.trace));
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
If `parseLdaReportInterruptPayload` is not currently used in this file, use it
|
||||
here instead of a raw cast so malformed replay data fails visibly in tests.
|
||||
|
||||
- [ ] **Step 5: Add reducer action for priming**
|
||||
|
||||
In `web/apps/console/src/demo/timeline/reducer.ts`, add action:
|
||||
|
||||
```ts
|
||||
| {
|
||||
readonly type: "prime_replay";
|
||||
readonly events: ReadonlyArray<DemoEvent>;
|
||||
readonly appliedCount: number;
|
||||
readonly phase: DemoTimelinePhase;
|
||||
}
|
||||
```
|
||||
|
||||
Add reducer case:
|
||||
|
||||
```ts
|
||||
case "prime_replay":
|
||||
return {
|
||||
mode: "replay",
|
||||
phase: action.phase,
|
||||
events: action.events,
|
||||
appliedCount: action.appliedCount,
|
||||
autoplay: false,
|
||||
error: null,
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Implement `primeReplayToStage`**
|
||||
|
||||
In `useDemoTimeline.ts`, add:
|
||||
|
||||
```ts
|
||||
const primeReplayToStage = useCallback((stage: DemoEvent["stage"] | null) => {
|
||||
if (stage === null || state.mode !== "replay") return;
|
||||
const recording = activeRecording.current;
|
||||
if (!recording) return;
|
||||
const appliedCount = appliedCountForStage(recording.events, stage);
|
||||
if (appliedCount === 0) return;
|
||||
|
||||
// Presentation priming projects the reviewed recording to the current beat.
|
||||
// It is not runtime execution and must never run while the live timeline is active.
|
||||
resetRuntime();
|
||||
projectTransientState(recording.events, appliedCount);
|
||||
dispatch({
|
||||
type: "prime_replay",
|
||||
events: recording.events,
|
||||
appliedCount,
|
||||
phase: phaseForPrimedStage(stage),
|
||||
});
|
||||
}, [projectTransientState, resetRuntime, state.mode]);
|
||||
```
|
||||
|
||||
Return it from the controller.
|
||||
|
||||
- [ ] **Step 7: Run tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/demo/useDemoTimeline.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add web/apps/console/src/demo/useDemoTimeline.ts web/apps/console/src/demo/useDemoTimeline.test.tsx web/apps/console/src/demo/timeline/reducer.ts
|
||||
git commit -m "feat: prime replay timeline for demo beats"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Prime direct demo routes from PresentationRoute
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `web/apps/console/src/presentation/PresentationRoute.tsx`
|
||||
- Test: `web/apps/console/src/presentation/PresentationRoute.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes:
|
||||
- `requirementForDemoBeat(sceneId, beatId)`
|
||||
- `demo.primeReplayToStage(stage)`
|
||||
- Produces:
|
||||
- Direct demo hashes are immediately display-ready.
|
||||
|
||||
- [ ] **Step 1: Add failing direct-route readiness tests**
|
||||
|
||||
In `web/apps/console/src/presentation/PresentationRoute.test.tsx`, add:
|
||||
|
||||
```tsx
|
||||
it("opens approval with enabled approval controls immediately after priming", async () => {
|
||||
window.location.hash = "#scene/interrupt-evidence/approval";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
|
||||
expect(await screen.findByRole("button", { name: "Submit" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("opens resume with resume operation proof immediately after priming", async () => {
|
||||
window.location.hash = "#scene/interrupt-evidence/resume";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
|
||||
expect(await screen.findByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
Do not use arbitrary timeouts in these tests. `findBy*` is enough for React
|
||||
effects; the UI should not wait for the replay autoplay timer.
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/PresentationRoute.test.tsx
|
||||
```
|
||||
|
||||
Expected: FAIL because direct routes still depend on autoplay progression.
|
||||
|
||||
- [ ] **Step 3: Wire route priming**
|
||||
|
||||
In `PresentationRoute.tsx`, import:
|
||||
|
||||
```ts
|
||||
import { requirementForDemoBeat } from "./demo-beat-requirements.js";
|
||||
```
|
||||
|
||||
Add effect after replay start effect:
|
||||
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (state.location.kind !== "main") return;
|
||||
if (demo.state.mode !== "replay") return;
|
||||
|
||||
const requirement = requirementForDemoBeat(
|
||||
state.location.sceneId,
|
||||
state.location.beatId,
|
||||
);
|
||||
demo.primeReplayToStage(requirement.requiredStage);
|
||||
}, [demo.state.mode, demo.primeReplayToStage, state.location]);
|
||||
```
|
||||
|
||||
Keep this effect separate from the replay `start("replay")` effect. The
|
||||
separation makes the unusual presentation projection easier to reason about.
|
||||
|
||||
- [ ] **Step 4: Run route tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/PresentationRoute.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/apps/console/src/presentation/PresentationRoute.tsx web/apps/console/src/presentation/PresentationRoute.test.tsx
|
||||
git commit -m "feat: prime presentation demo routes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add guided Scene 10 product moment component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `web/apps/console/src/presentation/GuidedProductMoment.tsx`
|
||||
- Create: `web/apps/console/src/presentation/GuidedProductMoment.test.tsx`
|
||||
- Modify: `web/apps/console/src/presentation/DemoWorkflowScene.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes:
|
||||
- `SceneBeatDefinition`
|
||||
- `DemoTimelineController`
|
||||
- `InterruptContractPresentation | null`
|
||||
- `OperationPresentation | null`
|
||||
- `DemoApprovalActions | undefined`
|
||||
- `openEvidence: () => void`
|
||||
- Produces:
|
||||
|
||||
```tsx
|
||||
export type GuidedProductMomentProps = {
|
||||
readonly beat: SceneBeatDefinition;
|
||||
readonly demo: DemoTimelineController;
|
||||
readonly contract: InterruptContractPresentation | null;
|
||||
readonly operation: OperationPresentation | null;
|
||||
readonly approvalActions?: DemoApprovalActions | undefined;
|
||||
readonly openEvidence: () => void;
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write failing component tests**
|
||||
|
||||
Create `web/apps/console/src/presentation/GuidedProductMoment.test.tsx`:
|
||||
|
||||
```tsx
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||
import { projectInterruptContract, projectOperationPresentation } from "./demo-workflow-model.js";
|
||||
import { GuidedProductMoment } from "./GuidedProductMoment.js";
|
||||
import { findBeat } from "./storyboard.js";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
const recording = loadCanonicalDemoRecording();
|
||||
const runStart = recording.events.find((event) => event.stage === "run_start")!;
|
||||
const runResume = recording.events.find((event) => event.stage === "run_resume")!;
|
||||
const contract = projectInterruptContract(runStart, runResume);
|
||||
const resumeOperation = projectOperationPresentation(runResume);
|
||||
const demo = {
|
||||
state: { mode: "replay", phase: "review", events: recording.events, appliedCount: 3, autoplay: false, error: null },
|
||||
inFlight: false,
|
||||
} as DemoTimelineController;
|
||||
|
||||
describe("GuidedProductMoment", () => {
|
||||
it("makes approval the primary product decision", () => {
|
||||
render(
|
||||
<GuidedProductMoment
|
||||
beat={findBeat("interrupt-evidence", "approval")!}
|
||||
demo={demo}
|
||||
contract={contract}
|
||||
operation={null}
|
||||
approvalActions={{
|
||||
state: "ready",
|
||||
canSubmit: true,
|
||||
canCancel: true,
|
||||
submit: vi.fn(async () => {}),
|
||||
cancel: vi.fn(async () => {}),
|
||||
}}
|
||||
openEvidence={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
|
||||
expect(screen.getByText(/Run is paused/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("makes resume operation proof primary on resume beat", () => {
|
||||
render(
|
||||
<GuidedProductMoment
|
||||
beat={findBeat("interrupt-evidence", "resume")!}
|
||||
demo={demo}
|
||||
contract={contract}
|
||||
operation={resumeOperation}
|
||||
openEvidence={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
|
||||
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/GuidedProductMoment.test.tsx
|
||||
```
|
||||
|
||||
Expected: FAIL because the component does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement `GuidedProductMoment`**
|
||||
|
||||
Create `web/apps/console/src/presentation/GuidedProductMoment.tsx`:
|
||||
|
||||
```tsx
|
||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||
import type { DemoApprovalActions } from "./demo-approval-actions.js";
|
||||
import type {
|
||||
InterruptContractPresentation,
|
||||
OperationPresentation,
|
||||
} from "./demo-workflow-model.js";
|
||||
import { DemoOutcomePanel } from "./DemoOutcomePanel.js";
|
||||
import { InterruptContractPreview } from "./InterruptContractPreview.js";
|
||||
import { OperationBlock } from "./OperationBlock.js";
|
||||
import type { SceneBeatDefinition } from "./storyboard.js";
|
||||
import { demoBeatLensForBeat } from "./demo-workflow-model.js";
|
||||
|
||||
export type GuidedProductMomentProps = {
|
||||
readonly beat: SceneBeatDefinition;
|
||||
readonly demo: DemoTimelineController;
|
||||
readonly contract: InterruptContractPresentation | null;
|
||||
readonly operation: OperationPresentation | null;
|
||||
readonly approvalActions?: DemoApprovalActions | undefined;
|
||||
readonly openEvidence: () => void;
|
||||
};
|
||||
|
||||
const momentForBeat = (beatId: string): "approval" | "resume" | "output" | "trace" =>
|
||||
beatId === "resume" || beatId === "output" || beatId === "trace" ? beatId : "approval";
|
||||
|
||||
const statusCopy = (
|
||||
moment: ReturnType<typeof momentForBeat>,
|
||||
approvalActions?: DemoApprovalActions,
|
||||
): string => {
|
||||
if (moment !== "approval") return "Same persisted run; inspect the proof below.";
|
||||
if (approvalActions?.state === "submitted") return "Submitted. Same run resumed.";
|
||||
if (approvalActions?.state === "cancelled") {
|
||||
return "Cancelled in presentation replay. No resume evidence is shown.";
|
||||
}
|
||||
return "Run is paused. Submit resumes this same run.";
|
||||
};
|
||||
|
||||
export const GuidedProductMoment = ({
|
||||
beat,
|
||||
demo,
|
||||
contract,
|
||||
operation,
|
||||
approvalActions,
|
||||
openEvidence,
|
||||
}: GuidedProductMomentProps) => {
|
||||
const moment = momentForBeat(beat.id);
|
||||
const lens = demoBeatLensForBeat(beat.id);
|
||||
return (
|
||||
<section className="guided-product-moment" aria-label="current product moment" data-moment={moment}>
|
||||
<header className="guided-product-moment__header">
|
||||
<span>{lens.eyebrow}</span>
|
||||
<strong>{lens.headline}</strong>
|
||||
<p>{statusCopy(moment, approvalActions)}</p>
|
||||
</header>
|
||||
|
||||
<div className="guided-product-moment__primary">
|
||||
{moment === "approval" && contract ? (
|
||||
<InterruptContractPreview
|
||||
contract={contract}
|
||||
mode="approval"
|
||||
hero
|
||||
approvalActions={approvalActions}
|
||||
/>
|
||||
) : null}
|
||||
{moment === "resume" && demo.state.events.find((event) => event.stage === "run_resume") ? (
|
||||
<OperationBlock
|
||||
event={demo.state.events.find((event) => event.stage === "run_resume")!}
|
||||
variant="expanded"
|
||||
openEvidence={openEvidence}
|
||||
/>
|
||||
) : null}
|
||||
{(moment === "output" || moment === "trace") ? (
|
||||
<DemoOutcomePanel
|
||||
beatId={beat.id}
|
||||
lens={lens}
|
||||
operation={operation}
|
||||
contract={contract}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
After writing this, remove duplicate `find()` calls by assigning `runResume`
|
||||
before the return if the linter or reviewer flags it.
|
||||
|
||||
- [ ] **Step 4: Delegate Scene 10 beats**
|
||||
|
||||
In `DemoWorkflowScene.tsx`, import:
|
||||
|
||||
```ts
|
||||
import { GuidedProductMoment } from "./GuidedProductMoment.js";
|
||||
```
|
||||
|
||||
Before the existing return, add:
|
||||
|
||||
```ts
|
||||
const isGuidedScene10 = scene.id === "interrupt-evidence";
|
||||
```
|
||||
|
||||
Inside the stage div, render the guided moment for Scene 10 beats:
|
||||
|
||||
```tsx
|
||||
{isGuidedScene10 ? (
|
||||
<GuidedProductMoment
|
||||
beat={beat}
|
||||
demo={demo}
|
||||
contract={contract}
|
||||
operation={currentOperation}
|
||||
approvalActions={approvalActions}
|
||||
openEvidence={openEvidence}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* existing graph/operation/outcome rendering for Scene 9 */}
|
||||
</>
|
||||
)}
|
||||
```
|
||||
|
||||
Keep the continuity rail and discussion links unchanged.
|
||||
|
||||
- [ ] **Step 5: Run tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/GuidedProductMoment.test.tsx src/presentation/DemoWorkflowScene.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS. Update existing Scene 10 tests only where they assert old layout internals that are intentionally replaced.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add web/apps/console/src/presentation/GuidedProductMoment.tsx web/apps/console/src/presentation/GuidedProductMoment.test.tsx web/apps/console/src/presentation/DemoWorkflowScene.tsx web/apps/console/src/presentation/DemoWorkflowScene.test.tsx
|
||||
git commit -m "feat: stage scene 10 as guided product moment"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Style the guided moment and pin visual hierarchy
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `web/apps/console/src/presentation/styles/demo-workflow.css`
|
||||
- Test: `web/apps/console/src/presentation/GuidedProductMoment.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes:
|
||||
- `.guided-product-moment`
|
||||
- `data-moment="approval" | "resume" | "output" | "trace"`
|
||||
- Produces:
|
||||
- One dominant primary surface per beat.
|
||||
|
||||
- [ ] **Step 1: Add class contract tests**
|
||||
|
||||
In `GuidedProductMoment.test.tsx`, add:
|
||||
|
||||
```tsx
|
||||
it("marks the primary surface for visual hierarchy", () => {
|
||||
render(
|
||||
<GuidedProductMoment
|
||||
beat={findBeat("interrupt-evidence", "approval")!}
|
||||
demo={demo}
|
||||
contract={contract}
|
||||
operation={null}
|
||||
approvalActions={{
|
||||
state: "ready",
|
||||
canSubmit: true,
|
||||
canCancel: true,
|
||||
submit: vi.fn(async () => {}),
|
||||
cancel: vi.fn(async () => {}),
|
||||
}}
|
||||
openEvidence={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveClass("guided-product-moment");
|
||||
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/GuidedProductMoment.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS if Task 4 already produced the class and hero attribute.
|
||||
|
||||
- [ ] **Step 3: Add CSS**
|
||||
|
||||
In `web/apps/console/src/presentation/styles/demo-workflow.css`, add:
|
||||
|
||||
```css
|
||||
.guided-product-moment {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.guided-product-moment__header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.55fr) minmax(0, 1fr);
|
||||
gap: 0.35rem 1rem;
|
||||
align-items: end;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border: 1px solid var(--stage-line);
|
||||
border-radius: 0.75rem;
|
||||
background: color-mix(in oklch, var(--stage-surface) 86%, transparent);
|
||||
}
|
||||
|
||||
.guided-product-moment__header span {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.guided-product-moment__header strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.guided-product-moment__header p {
|
||||
grid-column: 2;
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.guided-product-moment__primary {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.guided-product-moment[data-moment="approval"] .interrupt-contract-preview {
|
||||
max-width: min(58rem, 100%);
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.guided-product-moment[data-moment="resume"] .operation-block {
|
||||
max-width: min(62rem, 100%);
|
||||
justify-self: center;
|
||||
}
|
||||
```
|
||||
|
||||
Adjust class names to match current CSS tokens if the file uses different
|
||||
variables. Do not add a third presentation palette.
|
||||
|
||||
- [ ] **Step 4: Run CSS-related tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/GuidedProductMoment.test.tsx src/presentation/DemoWorkflowScene.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/apps/console/src/presentation/styles/demo-workflow.css web/apps/console/src/presentation/GuidedProductMoment.test.tsx
|
||||
git commit -m "style: focus scene 10 product moment hierarchy"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Docs, verification, and plan archive
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
- Move: `docs/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md` to `docs/historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Produces:
|
||||
- Roadmap completion entry.
|
||||
- Archived plan after implementation.
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/demo/useDemoTimeline.test.tsx src/presentation/demo-beat-requirements.test.ts src/presentation/PresentationRoute.test.tsx src/presentation/GuidedProductMoment.test.tsx src/presentation/DemoWorkflowScene.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run typecheck**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console typecheck
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Run build**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console build
|
||||
```
|
||||
|
||||
Expected: PASS. Existing chunk-size warning is acceptable.
|
||||
|
||||
- [ ] **Step 4: Browser smoke**
|
||||
|
||||
With `pnpm dev` running, inspect or screenshot:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:5173/present#scene/interrupt-evidence/approval
|
||||
http://127.0.0.1:5173/present#scene/interrupt-evidence/resume
|
||||
http://127.0.0.1:5173/present#scene/interrupt-evidence/output
|
||||
http://127.0.0.1:5173/present#scene/interrupt-evidence/trace
|
||||
```
|
||||
|
||||
Required checks:
|
||||
|
||||
- Approval controls are enabled without waiting for autoplay.
|
||||
- Resume operation is visible immediately on the resume hash.
|
||||
- Output beat shows result proof, not approval controls.
|
||||
- Trace beat shows trace/evidence proof, not approval controls.
|
||||
- Cancel still does not show resume evidence in replay.
|
||||
|
||||
- [ ] **Step 5: Update roadmap**
|
||||
|
||||
In `docs/current_roadmap.md`, mark this slice completed:
|
||||
|
||||
```md
|
||||
18. Completed: Scene 10 guided product moment primes replay state for direct
|
||||
hashes and stages approval, resume, output, and trace as one readable
|
||||
product flow. Design:
|
||||
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
|
||||
Implementation:
|
||||
[`Scene 10 guided product moment plan`](historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
|
||||
```
|
||||
|
||||
Renumber following items.
|
||||
|
||||
- [ ] **Step 6: Archive plan**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git mv docs/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md docs/historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Diff hygiene**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: no whitespace errors; only intended files listed.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/current_roadmap.md docs/historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md
|
||||
git commit -m "docs: complete scene 10 guided product moment"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: Direct readiness, approval consequences, replay truthfulness,
|
||||
hierarchy, tests, docs, and smoke verification are covered.
|
||||
- Placeholder scan: No TODO/TBD placeholders remain.
|
||||
- Type consistency: `DemoBeatRequirement`, `requirementForDemoBeat`,
|
||||
`primeReplayToStage`, and `GuidedProductMoment` signatures are consistent.
|
||||
- Scope check: This is one focused slice. Chat replacement and presenter
|
||||
companion remain future work.
|
||||
@@ -0,0 +1,211 @@
|
||||
# Scene 10 Guided Product Moment Design
|
||||
|
||||
## Purpose
|
||||
|
||||
Scene 10 should be the strongest product proof in the defense presentation. It
|
||||
shows that `lda.chat` owns a durable run, pauses at a typed human boundary,
|
||||
accepts an operator decision, resumes the same run, and leaves inspectable
|
||||
output/trace evidence.
|
||||
|
||||
The current implementation is functionally correct but still reads like several
|
||||
widgets placed on a slide. The next slice should turn it into a guided product
|
||||
moment: one dominant visual per beat, immediate readiness on deep links, and
|
||||
clear transitions from decision to proof.
|
||||
|
||||
## Problem
|
||||
|
||||
The approval beat currently has three UX problems:
|
||||
|
||||
1. **Delayed readiness:** a direct link to
|
||||
`#scene/interrupt-evidence/approval` starts replay from the beginning, so the
|
||||
approval controls remain disabled until autoplay reaches the interrupt.
|
||||
2. **Weak hierarchy:** graph, receipt, schema approval, continuity rail, and
|
||||
discussion links compete. The viewer must infer which part matters now.
|
||||
3. **Unclear consequence:** after Submit or Cancel, the UI changes technically,
|
||||
but it does not strongly explain what happened to the persisted run.
|
||||
|
||||
These are not backend problems. They are presentation-state and composition
|
||||
problems.
|
||||
|
||||
## Design Goal
|
||||
|
||||
Make Scene 10 feel like a product flow:
|
||||
|
||||
```text
|
||||
paused persisted run
|
||||
-> schema-backed operator decision
|
||||
-> same run resumes
|
||||
-> output appears outside chat
|
||||
-> trace/evidence remains inspectable
|
||||
```
|
||||
|
||||
The presenter should be able to say less because the screen already answers:
|
||||
|
||||
- What is waiting?
|
||||
- What decision is required?
|
||||
- What happens after Submit?
|
||||
- Why is Cancel not fake evidence?
|
||||
- Where is the proof?
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- Scene 10 and immediately related demo timeline projection.
|
||||
- Replay readiness for direct hashes in Scenes 9 and 10.
|
||||
- Visual hierarchy for approval/resume/output/trace beats.
|
||||
- Clear copy for live vs replay proof.
|
||||
- Tests that pin direct-link readiness and post-decision outcomes.
|
||||
|
||||
Out of scope:
|
||||
|
||||
- Replacing chat with AI Elements or AI SDK.
|
||||
- Building a presenter companion.
|
||||
- Adding a new workflow graph engine.
|
||||
- Changing Python workflow runtime behavior.
|
||||
- Adding a new cancelled replay recording.
|
||||
|
||||
## Core Interaction
|
||||
|
||||
### Direct route readiness
|
||||
|
||||
When the route opens a demo beat, replay state should be primed to the minimum
|
||||
event boundary needed by that beat.
|
||||
|
||||
Examples:
|
||||
|
||||
- `workflow-demo/operation`: needs `run_start`.
|
||||
- `workflow-demo/interrupt`: needs `interrupt`.
|
||||
- `interrupt-evidence/approval`: needs `interrupt`.
|
||||
- `interrupt-evidence/resume`: needs `run_resume`.
|
||||
- `interrupt-evidence/output`: needs `completed` or `run_resume` output.
|
||||
- `interrupt-evidence/trace`: needs `trace_read` or `completed` trace.
|
||||
|
||||
This is a presentation projection, not a runtime mutation. Live mode still
|
||||
executes sequentially when the operator starts the prepared workflow.
|
||||
|
||||
### Approval
|
||||
|
||||
On the approval beat:
|
||||
|
||||
- Submit is enabled immediately when replay has been primed to the interrupt.
|
||||
- Submit applies the existing submitted replay branch and jumps to `resume`.
|
||||
- Cancel marks the decision as cancelled and stays on approval.
|
||||
- Cancel must not display `workflow.runs.resume` in replay.
|
||||
- The UI should state that replay has no cancelled run evidence.
|
||||
|
||||
### Hierarchy
|
||||
|
||||
Each beat gets one dominant visual:
|
||||
|
||||
- Approval: schema-backed decision surface.
|
||||
- Resume: operation block for `workflow.runs.resume`.
|
||||
- Output: result/output panel.
|
||||
- Trace: trace/evidence receipt.
|
||||
|
||||
The graph is context, not the hero, during approval and resume. It may remain as
|
||||
a compact strip or mini-map, but it must not compete with the decision or
|
||||
operation proof.
|
||||
|
||||
## Components
|
||||
|
||||
### Beat Readiness Projection
|
||||
|
||||
Add a small model that maps beat IDs to required replay stages.
|
||||
|
||||
```ts
|
||||
export type DemoBeatRequirement = {
|
||||
readonly requiredStage: DemoEvent["stage"] | null;
|
||||
readonly reason: string;
|
||||
};
|
||||
```
|
||||
|
||||
This model should live near the existing demo presentation model, not in the
|
||||
generic timeline reducer. It is presentation-specific.
|
||||
|
||||
### Guided Product Moment
|
||||
|
||||
Add a presentation component for Scene 10 composition. It can reuse existing
|
||||
building blocks but owns the hierarchy.
|
||||
|
||||
```tsx
|
||||
<GuidedProductMoment
|
||||
beat={beat}
|
||||
demo={demo}
|
||||
contract={contract}
|
||||
operation={operation}
|
||||
approvalActions={approvalActions}
|
||||
openEvidence={openEvidence}
|
||||
/>
|
||||
```
|
||||
|
||||
It should avoid nested cards. Use a clear primary region, a slim context region,
|
||||
and a proof footer/receipt.
|
||||
|
||||
### Decision Status Copy
|
||||
|
||||
The approval surface should show one of these states:
|
||||
|
||||
- Ready: "Run is paused. Submit resumes this same run."
|
||||
- Submitted: "Submitted. Same run resumed."
|
||||
- Cancelled: "Cancelled in presentation replay. No resume evidence is shown."
|
||||
|
||||
Do not describe replay cancellation as a real persisted cancelled run unless a
|
||||
live run actually produced that evidence.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```text
|
||||
PresentationRoute location
|
||||
-> beat readiness requirement
|
||||
-> useDemoTimeline primes replay events for that beat
|
||||
-> SceneBody/DemoWorkflowScene receives ready demo state
|
||||
-> GuidedProductMoment renders the single dominant proof
|
||||
```
|
||||
|
||||
Approval actions stay route-owned:
|
||||
|
||||
```text
|
||||
SchemaApprovalSurface
|
||||
-> DemoApprovalActions
|
||||
-> PresentationRoute
|
||||
-> useDemoTimeline submit/cancel
|
||||
-> hash jump or local cancelled state
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
Route tests:
|
||||
|
||||
- Direct approval hash renders enabled Submit/Cancel without waiting for
|
||||
autoplay.
|
||||
- Direct resume hash renders `workflow.runs.resume` without waiting for
|
||||
autoplay.
|
||||
- Submit advances to resume and shows resume proof.
|
||||
- Cancel stays on approval, shows cancelled copy, and does not show resume
|
||||
evidence after a delay.
|
||||
|
||||
Model tests:
|
||||
|
||||
- Beat requirement mapping covers all demo beats.
|
||||
- Unknown beat returns no requirement.
|
||||
|
||||
Component tests:
|
||||
|
||||
- Approval beat marks the decision surface as the hero.
|
||||
- Resume beat marks the operation block as the hero.
|
||||
- Output/trace beats do not show approval controls.
|
||||
|
||||
Visual smoke:
|
||||
|
||||
- Capture approval, resume, output, and trace at 1280x720.
|
||||
- Approval should have one dominant decision surface.
|
||||
- Resume should have one dominant operation proof.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- A direct link to approval is immediately interactive.
|
||||
- Submit and Cancel consequences are clear without verbal explanation.
|
||||
- No fake cancelled replay evidence appears.
|
||||
- Scene 10 reads as one product flow rather than a collection of widgets.
|
||||
- Existing `/console` behavior is unchanged.
|
||||
Reference in New Issue
Block a user