fix: address review findings for defense compositor

- S1: Remove global html/body overflow:hidden; scope to .presentation-route
- S2+P1: Add CSS selectors for data-stage-theme, data-chat-theme, data-chat-mode
- S3: Add distinct visual treatments per SceneView type (positioning, boundary, lifecycle, architecture, authoring, evaluation)
- S4+P2: Move discussion index to reducer state; Escape closes all overlays; remove miswired button
- P3: Add 8 missing Q&A discussion branches across lifecycle, authoring, architecture, evaluation, workflow-demo
- P4: Force replay now restores replay evidence
- P5: Add scene progress display and scene reset to presenter controls
- P6: Fix default dock mode to use composition.chatMode
- P7: Wrap decodeURIComponent in try-catch for malformed hashes
This commit is contained in:
lda
2026-07-04 17:31:52 +07:00 Verified
parent d80a4650eb
commit 43c7105575
11 changed files with 523 additions and 31 deletions
@@ -10,8 +10,10 @@ import { ChatDock } from "./ChatDock.js";
import {
initialPresentationState,
presentationReducer,
compositionForState,
} from "./presentation-state.js";
import { hashForLocation } from "./storyboard-navigation.js";
import { findScene } from "./storyboard.js";
import type { PresentationLocation } from "./storyboard.js";
import "./presentation.css";
@@ -154,7 +156,8 @@ export const PresentationRoute = () => {
const handleForceReplay = useCallback(() => {
demo.setMode("replay");
}, [demo]);
setEvidence(replayEvidence);
}, [demo, replayEvidence]);
const handleResetOverrides = useCallback(() => {
dispatch({ type: "set_stage_theme", theme: null });
@@ -162,7 +165,15 @@ export const PresentationRoute = () => {
dispatch({ type: "set_chat_mode", mode: null });
}, []);
const showChatDock = state.chatModeOverride === "dock" && state.location.kind !== "discussion";
const handleResetScene = useCallback(() => {
if (state.location.kind !== "main") return;
const scene = findScene(state.location.sceneId);
if (!scene || scene.beats.length === 0) return;
dispatch({ type: "jump", location: { kind: "main", sceneId: state.location.sceneId, beatId: scene.beats[0]!.id } });
}, [state.location]);
const composition = compositionForState(state);
const showChatDock = composition.chatMode === "dock" && state.location.kind !== "discussion";
return (
<main className="presentation-route" aria-label="lda.chat presentation">
@@ -179,6 +190,7 @@ export const PresentationRoute = () => {
closeOverlay={() => dispatch({ type: "close_overlay" })}
openDiscussion={handleOpenDiscussion}
closeDiscussion={handleCloseDiscussion}
toggleDiscussionIndex={() => dispatch({ type: "toggle_discussion_index" })}
/>
{showChatDock && <ChatDock openChat={() => dispatch({ type: "set_chat_mode", mode: "rail" })} />}
{state.controlsOpen && (
@@ -191,8 +203,8 @@ export const PresentationRoute = () => {
setChatTheme={(theme) => dispatch({ type: "set_chat_theme", theme })}
setChatMode={(mode) => dispatch({ type: "set_chat_mode", mode })}
forceReplay={handleForceReplay}
openDiscussionIndex={() => dispatch({ type: "toggle_controls" })}
resetOverrides={handleResetOverrides}
resetScene={handleResetScene}
/>
)}
<button
@@ -1,4 +1,3 @@
import { useState } from "react";
import type { EvidenceRecord } from "../app/state.js";
import type { AgentMessage } from "../demo/agent/events.js";
import { SceneBody } from "./SceneBody.js";
@@ -25,6 +24,7 @@ type PresentationStageProps = {
readonly closeOverlay: () => void;
readonly openDiscussion: (branchId: string) => void;
readonly closeDiscussion: () => void;
readonly toggleDiscussionIndex: () => void;
};
export const PresentationStage = ({
@@ -40,9 +40,9 @@ export const PresentationStage = ({
closeOverlay,
openDiscussion,
closeDiscussion,
toggleDiscussionIndex,
}: PresentationStageProps) => {
const composition = compositionForState(state);
const [indexOpen, setIndexOpen] = useState(false);
const isMainScene = state.location.kind === "main";
const currentScene = isMainScene ? findScene(state.location.sceneId) : null;
@@ -75,17 +75,17 @@ export const PresentationStage = ({
<button
type="button"
className="presentation-stage__discussion-toggle"
onClick={() => setIndexOpen(true)}
onClick={toggleDiscussionIndex}
>
Open discussion topics
</button>
)}
</>
)}
{indexOpen && (
{state.discussionIndexOpen && (
<DiscussionIndex
onSelect={(branchId) => {
setIndexOpen(false);
toggleDiscussionIndex();
openDiscussion(branchId);
}}
/>
@@ -17,8 +17,8 @@ describe("PresenterControls", () => {
setChatTheme: vi.fn(),
setChatMode: vi.fn(),
forceReplay: vi.fn(),
openDiscussionIndex: vi.fn(),
resetOverrides: vi.fn(),
resetScene: vi.fn(),
};
it("changes stage and chat themes independently", async () => {
@@ -13,8 +13,8 @@ type PresenterControlsProps = {
readonly setChatTheme: (theme: ChatTheme | null) => void;
readonly setChatMode: (mode: ChatMode | null) => void;
readonly forceReplay: () => void;
readonly openDiscussionIndex: () => void;
readonly resetOverrides: () => void;
readonly resetScene: () => void;
};
export const PresenterControls = ({
@@ -26,13 +26,19 @@ export const PresenterControls = ({
setChatTheme,
setChatMode,
forceReplay,
openDiscussionIndex,
resetOverrides,
resetScene,
}: PresenterControlsProps) => {
const composition = compositionForState(state);
const isMain = state.location.kind === "main";
const currentScene = isMain ? findScene(state.location.sceneId) : null;
const currentBeat = currentScene?.beats.find((b) => b.id === (state.location as MainLocation).beatId);
const beatIndex = currentScene && isMain
? currentScene.beats.findIndex((b) => b.id === (state.location as MainLocation).beatId)
: -1;
const totalBeats = currentScene?.beats.length ?? 0;
const sceneNumber = currentScene?.number ?? 0;
const totalScenes = mainScenes.length;
return (
<div className="presenter-controls" role="dialog" aria-label="presenter controls">
@@ -41,8 +47,8 @@ export const PresenterControls = ({
<button type="button" onClick={next}>Next</button>
</div>
<div className="presenter-controls__info">
<span>{currentScene?.title ?? "Discussion"}</span>
{currentBeat && <span> · {currentBeat.title}</span>}
<span>Scene {sceneNumber}/{totalScenes}: {currentScene?.title ?? "Discussion"}</span>
{currentBeat && <span> · {beatIndex + 1}/{totalBeats} {currentBeat.title}</span>}
</div>
<div className="presenter-controls__over">
<label>
@@ -84,7 +90,7 @@ export const PresenterControls = ({
<div className="presenter-controls__actions">
<span>{state.playbackMode === "replay" ? "Replay" : "Live"}</span>
<button type="button" onClick={forceReplay}>Force replay fallback</button>
<button type="button" onClick={openDiscussionIndex}>Open discussion index</button>
<button type="button" onClick={resetScene}>Reset scene</button>
<button type="button" onClick={resetOverrides}>Reset overrides</button>
</div>
</div>
+142 -7
View File
@@ -29,6 +29,134 @@ const NarrativeScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBe
</>
);
const PositioningScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow={`Act I · ${scene.claimClass}`} title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__positioning-grid">
<div className="scene-body__positioning-card">
<strong>Tool loops</strong>
<span>Direct action, no lifecycle</span>
</div>
<div className="scene-body__positioning-card">
<strong>Scripts</strong>
<span>Simple, debuggable</span>
</div>
<div className="scene-body__positioning-card scene-body__positioning-card--active">
<strong>lda.chat</strong>
<span>Typed lifecycle contracts</span>
</div>
<div className="scene-body__positioning-card">
<strong>Agent graphs</strong>
<span>Shared durability</span>
</div>
<div className="scene-body__positioning-card">
<strong>MCP</strong>
<span>Capability protocol</span>
</div>
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
const BoundaryScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__boundary">
<div className="scene-body__boundary-side">
<h3>Planner</h3>
<p>External LLM proposes and revises workflow structure.</p>
</div>
<div className="scene-body__boundary-divider" />
<div className="scene-body__boundary-side">
<h3>Runtime</h3>
<p>Validates, executes, records, and resumes deterministically.</p>
</div>
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
const LifecycleScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__lifecycle">
{["Draft", "Artifact", "Deployment", "Run"].map((stage, i) => (
<div key={stage} className="scene-body__lifecycle-stage">
<span className="scene-body__lifecycle-number">{i + 1}</span>
<strong>{stage}</strong>
{i < 3 && <span className="scene-body__lifecycle-arrow"></span>}
</div>
))}
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
const ArchitectureScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__architecture">
<div className="scene-body__architecture-layer">Client operations</div>
<div className="scene-body__architecture-arrow"></div>
<div className="scene-body__architecture-layer">Transport / JSON-RPC</div>
<div className="scene-body__architecture-arrow"></div>
<div className="scene-body__architecture-layer">Runtime & providers</div>
<div className="scene-body__architecture-arrow"></div>
<div className="scene-body__architecture-layer scene-body__architecture-layer--node">NodeUse</div>
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
const AuthoringScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__authoring">
{["Discover", "Author", "Diagnose", "Repair"].map((step, i) => (
<div key={step} className="scene-body__authoring-step">
<span className="scene-body__authoring-number">{i + 1}</span>
<strong>{step}</strong>
{i < 3 && <span className="scene-body__authoring-arrow"></span>}
</div>
))}
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
const EvaluationScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow={`Act I · ${scene.claimClass}`} title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__evaluation">
<div className="scene-body__evaluation-stat">
<strong>36</strong>
<span>trials</span>
</div>
<div className="scene-body__evaluation-stat">
<strong>2</strong>
<span>challenges</span>
</div>
<div className="scene-body__evaluation-stat">
<strong>3</strong>
<span>waves</span>
</div>
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
const AgentHandoffScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow="Agent handoff" title={scene.title}>
@@ -80,14 +208,17 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneB
switch (scene.view) {
case "narrative":
case "positioning":
case "boundary":
case "lifecycle":
case "architecture":
case "authoring":
case "evaluation":
case "conclusion":
return <NarrativeScene scene={scene} beat={beat} />;
case "positioning":
return <PositioningScene scene={scene} beat={beat} />;
case "boundary":
return <BoundaryScene scene={scene} beat={beat} />;
case "lifecycle":
return <LifecycleScene scene={scene} beat={beat} />;
case "architecture":
return <ArchitectureScene scene={scene} beat={beat} />;
case "authoring":
return <AuthoringScene scene={scene} beat={beat} />;
case "agent":
return <AgentHandoffScene scene={scene} beat={beat} />;
case "demo":
@@ -100,6 +231,10 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneB
selectNode={selectNode}
/>
);
case "evaluation":
return <EvaluationScene scene={scene} beat={beat} />;
case "conclusion":
return <NarrativeScene scene={scene} beat={beat} />;
default:
return assertNever(scene.view);
}
@@ -27,6 +27,7 @@ export type PresentationState = {
readonly chatThemeOverride: ChatTheme | null;
readonly chatModeOverride: ChatMode | null;
readonly controlsOpen: boolean;
readonly discussionIndexOpen: boolean;
};
export type PresentationAction =
@@ -44,7 +45,8 @@ export type PresentationAction =
| { readonly type: "set_stage_theme"; readonly theme: StageTheme | null }
| { readonly type: "set_chat_theme"; readonly theme: ChatTheme | null }
| { readonly type: "set_chat_mode"; readonly mode: ChatMode | null }
| { readonly type: "toggle_controls" };
| { readonly type: "toggle_controls" }
| { readonly type: "toggle_discussion_index" };
export const initialPresentationState: PresentationState = {
location: defaultMainLocation,
@@ -56,6 +58,7 @@ export const initialPresentationState: PresentationState = {
chatThemeOverride: null,
chatModeOverride: null,
controlsOpen: false,
discussionIndexOpen: false,
};
const compositionForLocation = (
@@ -173,6 +176,8 @@ export const presentationReducer = (
case "close_overlay": {
if (state.selectedNodeId !== null) return { ...state, selectedNodeId: null };
if (state.evidenceModeOverride !== null) return { ...state, evidenceModeOverride: null };
if (state.discussionIndexOpen) return { ...state, discussionIndexOpen: false };
if (state.controlsOpen) return { ...state, controlsOpen: false };
if (state.location.kind === "discussion") {
return {
...state,
@@ -192,5 +197,7 @@ export const presentationReducer = (
return { ...state, chatModeOverride: action.mode };
case "toggle_controls":
return { ...state, controlsOpen: !state.controlsOpen };
case "toggle_discussion_index":
return { ...state, discussionIndexOpen: !state.discussionIndexOpen };
}
};
@@ -3,11 +3,9 @@
--evidence-width: 0rem;
}
html, body {
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100%;
}
.presentation-route {
@@ -28,6 +26,92 @@ html, body {
var(--chat-width) minmax(0, 1fr) var(--evidence-width);
}
/* Stage themes */
.presentation-stage[data-stage-theme="paper"] {
background: oklch(0.97 0.005 80);
color: oklch(0.15 0.01 80);
}
.presentation-stage[data-stage-theme="night"] {
background: oklch(0.12 0.025 250);
color: oklch(0.96 0.01 250);
}
/* Paper theme child-element overrides */
.presentation-stage[data-stage-theme="paper"] .stage-caption__eyebrow {
color: oklch(0.45 0.04 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__evidence {
color: oklch(0.5 0.03 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-rail__scene {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-rail__scene[data-active="true"] {
background: oklch(0.35 0.12 80);
color: oklch(0.97 0.005 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-rail__beat {
background: oklch(0.7 0.04 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-rail__beat--active {
background: oklch(0.3 0.08 80);
}
.presentation-stage[data-stage-theme="paper"] .presentation-stage__mode {
color: oklch(0.5 0.03 80);
}
.presentation-stage[data-stage-theme="paper"] .presentation-stage__discussion-toggle {
border-color: oklch(0.7 0.05 80);
background: oklch(0.9 0.01 80);
color: oklch(0.2 0.02 80);
}
/* Chat themes */
.presentation-stage[data-chat-theme="light"] .presentation-stage__chat {
background: oklch(0.96 0.005 80);
color: oklch(0.18 0.01 80);
}
.presentation-stage[data-chat-theme="dark"] .presentation-stage__chat {
background: oklch(0.14 0.025 250);
color: oklch(0.92 0.01 250);
}
/* Chat modes */
.presentation-stage[data-chat-mode="hidden"] {
grid-template:
"primary evidence" minmax(0, 1fr)
"rail rail" auto /
minmax(0, 1fr) var(--evidence-width);
}
.presentation-stage[data-chat-mode="hidden"] .presentation-stage__chat {
display: none;
}
.presentation-stage[data-chat-mode="rail"] {
--chat-width: 16rem;
}
.presentation-stage[data-chat-mode="dock"] {
grid-template:
"primary evidence" minmax(0, 1fr)
"rail rail" auto /
minmax(0, 1fr) var(--evidence-width);
}
.presentation-stage[data-chat-mode="dock"] .presentation-stage__chat {
display: none;
}
.presentation-stage__chat,
.presentation-stage__primary,
.presentation-stage__evidence {
@@ -451,6 +535,168 @@ html, body {
color: oklch(0.65 0.2 25);
}
/* Scene body visual treatments */
.scene-body__positioning-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: 0.5rem;
margin-top: 0.5rem;
}
.scene-body__positioning-card {
padding: 0.65rem 0.75rem;
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.16 0.025 250);
border-radius: 0.5rem;
font-size: 0.8rem;
}
.scene-body__positioning-card strong {
display: block;
margin-bottom: 0.15rem;
}
.scene-body__positioning-card span {
color: oklch(0.72 0.03 250);
font-size: 0.75rem;
}
.scene-body__positioning-card--active {
border-color: oklch(0.7 0.16 195);
background: oklch(0.2 0.06 195);
}
.scene-body__boundary {
display: flex;
gap: 1rem;
margin-top: 0.75rem;
align-items: stretch;
}
.scene-body__boundary-side {
flex: 1;
padding: 0.75rem;
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.16 0.025 250);
border-radius: 0.5rem;
}
.scene-body__boundary-side h3 {
margin: 0 0 0.35rem;
font-size: 0.95rem;
}
.scene-body__boundary-side p {
margin: 0;
font-size: 0.8rem;
color: oklch(0.78 0.03 250);
}
.scene-body__boundary-divider {
width: 2px;
background: oklch(0.5 0.08 195);
border-radius: 1px;
align-self: stretch;
}
.scene-body__lifecycle,
.scene-body__authoring {
display: flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.75rem;
flex-wrap: wrap;
}
.scene-body__lifecycle-stage,
.scene-body__authoring-step {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.16 0.025 250);
border-radius: 0.5rem;
font-size: 0.85rem;
}
.scene-body__lifecycle-number,
.scene-body__authoring-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.4rem;
height: 1.4rem;
border-radius: 50%;
background: oklch(0.7 0.16 195);
color: oklch(0.12 0.025 250);
font-size: 0.7rem;
font-weight: 600;
}
.scene-body__lifecycle-arrow,
.scene-body__authoring-arrow {
color: oklch(0.5 0.06 250);
font-size: 1.1rem;
}
.scene-body__architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
margin-top: 0.75rem;
}
.scene-body__architecture-layer {
width: 100%;
max-width: 20rem;
padding: 0.55rem 0.75rem;
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.16 0.025 250);
border-radius: 0.4rem;
text-align: center;
font-size: 0.85rem;
}
.scene-body__architecture-layer--node {
border-color: oklch(0.7 0.16 195);
background: oklch(0.2 0.06 195);
}
.scene-body__architecture-arrow {
color: oklch(0.5 0.06 250);
font-size: 0.9rem;
}
.scene-body__evaluation {
display: flex;
gap: 1.5rem;
margin-top: 0.75rem;
}
.scene-body__evaluation-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.65rem 1rem;
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.16 0.025 250);
border-radius: 0.5rem;
}
.scene-body__evaluation-stat strong {
font-size: 1.6rem;
color: oklch(0.7 0.16 195);
}
.scene-body__evaluation-stat span {
font-size: 0.75rem;
color: oklch(0.72 0.03 250);
text-transform: uppercase;
letter-spacing: 0.05em;
}
@media (max-height: 760px) {
.scene-rail {
padding: 0.25rem 0.5rem;
@@ -23,6 +23,11 @@ describe("storyboard navigation", () => {
expect(locationFromHash("#discuss/nope")).toEqual(defaultMainLocation);
});
it("falls back for malformed percent-encoded hashes", () => {
expect(locationFromHash("#scene/%ZZ/%ZZ")).toEqual(defaultMainLocation);
expect(locationFromHash("#discuss/%ZZ")).toEqual(defaultMainLocation);
});
it("advances within a scene before advancing to the next scene", () => {
expect(nextMainLocation({ kind: "main", sceneId: "thesis", beatId: "title" })).toEqual({
kind: "main",
@@ -22,8 +22,14 @@ export const locationFromHash = (hash: string): PresentationLocation => {
const raw = hash.replace(/^#/, "");
const sceneMatch = raw.match(/^scene\/([^/]+)\/(.+)$/);
if (sceneMatch) {
const sceneId = decodeURIComponent(sceneMatch[1]!);
const beatId = decodeURIComponent(sceneMatch[2]!);
let sceneId: string;
let beatId: string;
try {
sceneId = decodeURIComponent(sceneMatch[1]!);
beatId = decodeURIComponent(sceneMatch[2]!);
} catch {
return defaultMainLocation;
}
const scene = findScene(sceneId);
if (scene && scene.beats.some((b) => b.id === beatId)) {
return { kind: "main", sceneId: scene.id as MainLocation["sceneId"], beatId };
@@ -32,7 +38,12 @@ export const locationFromHash = (hash: string): PresentationLocation => {
}
const discussMatch = raw.match(/^discuss\/(.+)$/);
if (discussMatch) {
const branchId = decodeURIComponent(discussMatch[1]!);
let branchId: string;
try {
branchId = decodeURIComponent(discussMatch[1]!);
} catch {
return defaultMainLocation;
}
if (findDiscussionBranch(branchId)) {
return { kind: "discussion", branchId: branchId as DiscussionLocation["branchId"] };
}
@@ -41,15 +41,21 @@ describe("defense storyboard catalog", () => {
expect(findBeat("interrupt-evidence", "trace")?.chatMode).toBe("dock");
});
it("defines the five positioning discussion branches", () => {
expect(discussionBranches.map((branch) => branch.id)).toEqual([
it("defines discussion branches across multiple scenes", () => {
expect(discussionBranches.length).toBeGreaterThanOrEqual(5);
const positioningBranches = discussionBranches.filter((b) => b.parentSceneId === "positioning");
expect(positioningBranches.map((b) => b.id)).toEqual([
"direct-orchestration",
"generated-scripts",
"hosted-automation",
"durable-agent-graphs",
"mcp-agent-scale",
]);
expect(discussionBranches.every((branch) => branch.parentSceneId === "positioning")).toBe(true);
for (const branch of discussionBranches) {
expect(branch.title.length).toBeGreaterThan(0);
expect(branch.summary.length).toBeGreaterThan(0);
expect(branch.evidencePointer.length).toBeGreaterThan(0);
}
});
it("exposes a valid default location", () => {
@@ -284,6 +284,70 @@ export const discussionBranches = defineDiscussionBranches([
evidencePointer: "Thesis: Model Context Protocol; Cloudflare Code Mode; Anthropic code execution with MCP",
summary: "MCP is a capability protocol; progressive discovery addresses large agent-facing surfaces.",
},
{
id: "lifecycle-states",
parentSceneId: "lifecycle",
title: "Lifecycle state machine",
claimClass: "implemented",
evidencePointer: "Thesis Workflow Lifecycle; lifecycle state transitions",
summary: "Draft, artifact, deployment, and run form a linear lifecycle with typed transitions.",
},
{
id: "raw-plan-import",
parentSceneId: "authoring",
title: "Raw plan import",
claimClass: "implemented",
evidencePointer: "Thesis Authoring; raw plan import and compilation",
summary: "Raw plans are compiled into validated workflow definitions with typed operations.",
},
{
id: "validation-diagnostics",
parentSceneId: "authoring",
title: "Validation and diagnostics",
claimClass: "implemented",
evidencePointer: "Thesis Validation; diagnostic repair hints",
summary: "Structured diagnostics identify invalid state and suggest repair hints.",
},
{
id: "typed-interrupts",
parentSceneId: "interrupt-evidence",
title: "Typed interrupt contracts",
claimClass: "implemented",
evidencePointer: "Thesis Typed Interrupts; interrupt schemas",
summary: "Interrupts are typed resume-request payloads with schema-backed review boundaries.",
},
{
id: "run-persistence",
parentSceneId: "architecture",
title: "Run persistence and inspection",
claimClass: "implemented",
evidencePointer: "Thesis Runtime; run persistence and trace events",
summary: "Persisted runs store output, status, and trace for post-hoc inspection.",
},
{
id: "provider-security",
parentSceneId: "architecture",
title: "Provider-neutral security",
claimClass: "implemented",
evidencePointer: "Thesis Architecture; provider-neutral capability resolution",
summary: "Provider-neutral capabilities avoid vendor lock-in while maintaining security boundaries.",
},
{
id: "evaluation-validity",
parentSceneId: "evaluation",
title: "Evaluation validity bounds",
claimClass: "evaluated",
evidencePointer: "Thesis Evaluation; bounded validity",
summary: "Manual audit separates task completion from valid product-surface evidence.",
},
{
id: "replay-provenance",
parentSceneId: "workflow-demo",
title: "Replay provenance",
claimClass: "implemented",
evidencePointer: "Thesis Prepared Replay; replay evidence chain",
summary: "Replay evidence is projected from the canonical recording for deterministic demonstration.",
},
]);
export type DiscussionBranchId = (typeof discussionBranches)[number]["id"];