feat: choreograph architecture and workflow proof scenes

This commit is contained in:
lda
2026-07-13 08:46:37 +07:00 Verified
parent 18c930449a
commit 519780e28b
26 changed files with 2000 additions and 410 deletions
+6 -3
View File
@@ -415,9 +415,12 @@ Presentation wishlist / defense readiness:
prioritized 12-minute Q&A.
Completed: [`defense story reweighting`](historical/superpowers/plans/2026-07-13-defense-story-reweighting.md)
now provides a typed 42-beat presenter-note catalog, a 750-850-word must-say
path, evidence warnings, and a 13-minute timing contract. Remaining follow-ups,
in dependency order: [`interactive graphs and Scenes 3-6 choreography`](superpowers/plans/2026-07-13-scenes-3-6-kernel-choreography.md),
then [`read-only presenter route`](superpowers/plans/2026-07-13-read-only-presenter-route.md).
path, evidence warnings, and a 13-minute timing contract. Completed:
[`interactive graphs and Scenes 3-6 choreography`](historical/superpowers/plans/2026-07-13-scenes-3-6-kernel-choreography.md)
adds restrained Scene 3-5 choreography, a semantic-zoom architecture map,
the readable `wf_core` and NodeUse figures, and a factual prepared-run graph
inspector. The next follow-up is
[`read-only presenter route`](superpowers/plans/2026-07-13-read-only-presenter-route.md).
- Defense Q&A branch set:
[`defense Q&A runbook`](runbooks/defense-qna.md) collects answers for
"Where is the AI agent?", evaluation validity, security boundaries, demo
@@ -24,12 +24,12 @@
- Modify tests for `PositioningScene`, `BoundaryScene`, `LifecycleScene`, and `ArchitectureScene`
- Modify route tests for Scenes 3-6
- [ ] Assert one primary visual region per beat through `data-visual-role="primary"`.
- [ ] Assert reduced-motion behavior and stable accessible headings.
- [ ] Assert Scene 6 exposes public surface, kernel loop, state/trace/routing, and provider boundary concepts across its four beats.
- [ ] Assert architecture figures declare a topology appropriate to their subject rather than four repeated linear flows.
- [ ] Assert the prepared-run graph preserves ten displayed action/boundary/outcome nodes, readable labels, pan/zoom, and a factual node inspector.
- [ ] Capture baseline screenshots at `1280x720` and `1024x768` before implementation.
- [x] Assert one primary visual region per beat through `data-visual-role="primary"`.
- [x] Assert reduced-motion behavior and stable accessible headings.
- [x] Assert Scene 6 exposes public surface, kernel loop, state/trace/routing, and provider boundary concepts across its four beats.
- [x] Assert architecture figures declare a topology appropriate to their subject rather than four repeated linear flows.
- [x] Assert the prepared-run graph preserves ten displayed action/boundary/outcome nodes, readable labels, pan/zoom, and a factual node inspector.
- [x] Capture baseline screenshots at `1280x720` and `1024x768` before implementation.
## Task 2: Add restrained motion to Scene 3 positioning
@@ -38,28 +38,28 @@
- Modify: `presentation.css`
- Modify: focused tests
- [ ] Keep the existing information architecture and add one shared layout transition between the two beats.
- [ ] On `landscape`, keep the related-system map as the complete comparison.
- [ ] On `lda-position`, enlarge the typed-substrate position and shift the surrounding systems enough to make the focus obvious without hiding the comparison axis.
- [ ] Use position and scale before opacity; one clear transform is enough.
- [ ] Keep titles, category labels, and evidence readable at both target viewports.
- [x] Keep the existing information architecture and add one shared layout transition between the two beats.
- [x] On `landscape`, keep the related-system map as the complete comparison.
- [x] On `lda-position`, enlarge the typed-substrate position and shift the surrounding systems enough to make the focus obvious without hiding the comparison axis.
- [x] Use position and scale before opacity; one clear transform is enough.
- [x] Keep titles, category labels, and evidence readable at both target viewports.
## Task 3: Add restrained motion to Scene 4 planner/runtime ownership
- [ ] Preserve the current three-pane comparison.
- [ ] On `planner`, enlarge the planner pane while retaining runtime destination context.
- [ ] On `runtime`, let the runtime pane grow into the released space.
- [ ] On `boundary`, restore both sides and enlarge the typed CLI/JSON-RPC seam.
- [ ] Animate position and width between states; do not change color theme between beats.
- [ ] Keep the spoken claim exact: planner proposes; runtime owns validation, state, execution, trace, and explicit resume.
- [x] Preserve the current three-pane comparison.
- [x] On `planner`, enlarge the planner pane while retaining runtime destination context.
- [x] On `runtime`, let the runtime pane grow into the released space.
- [x] On `boundary`, restore both sides and enlarge the typed CLI/JSON-RPC seam.
- [x] Animate position and width between states; do not change color theme between beats.
- [x] Keep the spoken claim exact: planner proposes; runtime owns validation, state, execution, trace, and explicit resume.
## Task 4: Add restrained motion to Scene 5 lifecycle vocabulary
- [ ] Preserve the four-record lifecycle rail and its order.
- [ ] For each beat, grow the active record into the available space while the other records compress but remain readable.
- [ ] Keep arrows and order visible so the audience does not mistake the records for independent cards.
- [ ] Show raw-plan-to-artifact as optional visual evidence without implying Draft is mandatory.
- [ ] Avoid replaying the prepared example; Scene 9 owns applied evidence.
- [x] Preserve the four-record lifecycle rail and its order.
- [x] For each beat, grow the active record into the available space while the other records compress but remain readable.
- [x] Keep arrows and order visible so the audience does not mistake the records for independent cards.
- [x] Show raw-plan-to-artifact as optional visual evidence without implying Draft is mandatory.
- [x] Avoid replaying the prepared example; Scene 9 owns applied evidence.
## Task 5: Rebuild Scene 6 as an interactive semantic zoom
@@ -71,19 +71,19 @@
- Modify: `web/apps/console/src/presentation/figures/InteractiveFigure.tsx` only if required by an existing figure contract
- Modify: associated tests and CSS
- [ ] Base the root figure on the thesis Architecture Spine, not an invented package map: workflow owner and external agent enter through CLI/JSON-RPC, `WorkflowServer` composes the API, records, inventory, and `wf_core`, then status/output/trace return through the public surface.
- [ ] Keep providers as one compact input into capability inventory at the root. Provider-family detail is optional drill-down, not one quarter of the main architecture story.
- [ ] Keep click-to-focus and breadcrumb navigation. A node with `childFigureId` zooms into its subsystem; a leaf node opens evidence/details without replacing the whole figure.
- [ ] Give the client surface a fan-in topology, with human/external agent clients and the web console converging on CLI or JSON-RPC and then WorkflowApi.
- [ ] Give the WorkflowApi zoom a lifecycle-operation surface: capabilities, drafts, artifacts, deployments, and runs surrounding the API boundary. Use compact HTML operation receipts or method groups inside graph nodes when they communicate more than another label box.
- [ ] Add a core-runtime child figure based on the thesis flowchart at `system-design-implementation.md:626`: select frame, branch by step kind, trace and route loop, interrupt/resume branch, and terminal output.
- [ ] Represent node, condition, foreach, join, subgraph, interrupt, and end with icons or conventional shapes. Explicitly exclude general fork/gather.
- [ ] Make `NodeUse` inside the core loop clickable. Its child figure follows the thesis sequence diagram: Runtime, Binding Resolver, NodeDef Handler, State Reducers, and Trace Store. Use participant lanes, message arrows, and small factual state/result panels instead of six identical nodes.
- [ ] Allow further click-through from important core nodes where useful: interrupt opens request/resume contract evidence; trace opens a representative trace frame; step dispatch opens the supported-step palette. Do not create drill-down merely because a node exists.
- [ ] Group built-in, MCP, and Python providers around the provider-neutral capability boundary; do not connect source families as an arbitrary adjacency chain.
- [ ] Use larger labels and summaries, and allow user pan/zoom instead of shrinking the full graph until it fits.
- [ ] Qualify determinism in visible copy: core semantics are deterministic for fixed definitions and handler results.
- [ ] Keep React Flow as the one interactive graph system. Extend its layout model with named topologies or rank/position hints instead of manually drawing edge coordinates.
- [x] Base the root figure on the thesis Architecture Spine, not an invented package map: workflow owner and external agent enter through CLI/JSON-RPC, `WorkflowServer` composes the API, records, inventory, and `wf_core`, then status/output/trace return through the public surface.
- [x] Keep providers as one compact input into capability inventory at the root. Provider-family detail is optional drill-down, not one quarter of the main architecture story.
- [x] Keep click-to-focus and breadcrumb navigation. A node with `childFigureId` zooms into its subsystem; a leaf node opens evidence/details without replacing the whole figure.
- [x] Give the client surface a fan-in topology, with human/external agent clients and the web console converging on CLI or JSON-RPC and then WorkflowApi.
- [x] Give the WorkflowApi zoom a lifecycle-operation surface: capabilities, drafts, artifacts, deployments, and runs surrounding the API boundary. Use compact HTML operation receipts or method groups inside graph nodes when they communicate more than another label box.
- [x] Add a core-runtime child figure based on the thesis flowchart at `system-design-implementation.md:626`: select frame, branch by step kind, trace and route loop, interrupt/resume branch, and terminal output.
- [x] Represent node, condition, foreach, join, subgraph, interrupt, and end with icons or conventional shapes. Explicitly exclude general fork/gather.
- [x] Make `NodeUse` inside the core loop clickable. Its child figure follows the thesis sequence diagram: Runtime, Binding Resolver, NodeDef Handler, State Reducers, and Trace Store. Use participant lanes, message arrows, and small factual state/result panels instead of six identical nodes.
- [x] Allow further click-through from important core nodes where useful: interrupt opens request/resume contract evidence; trace opens a representative trace frame; step dispatch opens the supported-step palette. Do not create drill-down merely because a node exists.
- [x] Group built-in, MCP, and Python providers around the provider-neutral capability boundary; do not connect source families as an arbitrary adjacency chain.
- [x] Use larger labels and summaries, and allow user pan/zoom instead of shrinking the full graph until it fits.
- [x] Qualify determinism in visible copy: core semantics are deterministic for fixed definitions and handler results.
- [x] Keep React Flow as the one interactive graph system. Extend its layout model with named topologies or rank/position hints instead of manually drawing edge coordinates.
### Scene 6 figure hierarchy
@@ -115,21 +115,21 @@ The four scripted beats need not visit every child. The audience path should be
- Modify: `web/apps/console/src/presentation/styles/demo-workflow.css`
- Modify: associated tests
- [ ] Re-layout the long run graph into a readable two-dimensional flow with the submitted and revision branches separated vertically. Do not shrink ten displayed nodes into one horizontal strip.
- [ ] Keep pan, drag, and zoom enabled. Use fit-view as the initial state, not the only readable state.
- [ ] Remove selected/current-node treatment from the static graph beat unless a real execution state supports it.
- [ ] Replace the generic `NodeSpotlight` paragraph with a reusable node inspector showing node kind, capability or boundary, factual input/output or schema summary, outcomes, and code/evidence pointer.
- [ ] Keep the run graph visible behind or beside its inspector so selection retains context.
- [ ] Replace “View raw evidence ->” with a precise action label such as “Inspect protocol receipt”. Open the existing evidence inspector at the relevant event rather than a generic drawer.
- [ ] Test the interrupt and issue-creation nodes as distinct inspector examples, plus the fallback for ordinary nodes.
- [x] Re-layout the long run graph into a readable two-dimensional flow with the submitted and revision branches separated vertically. Do not shrink ten displayed nodes into one horizontal strip.
- [x] Keep pan, drag, and zoom enabled. Use fit-view as the initial state, not the only readable state.
- [x] Remove selected/current-node treatment from the static graph beat unless a real execution state supports it.
- [x] Replace the generic `NodeSpotlight` paragraph with a reusable node inspector showing node kind, capability or boundary, factual input/output or schema summary, outcomes, and code/evidence pointer.
- [x] Keep the run graph visible behind or beside its inspector so selection retains context.
- [x] Replace “View raw evidence ->” with a precise action label such as “Inspect protocol receipt”. Open the existing evidence inspector at the relevant event rather than a generic drawer.
- [x] Test the interrupt and issue-creation nodes as distinct inspector examples, plus the fallback for ordinary nodes.
## Task 7: Visual verification
- [ ] Run focused scene and figure tests, full presentation tests, typecheck, and build.
- [ ] Capture every Scene 3-6 beat and the Scene 10 graph at both rehearsal viewports.
- [ ] Capture architecture root, client surface, core runtime, NodeUse, provider detail, and the open run-node inspector.
- [ ] Verify no page scroll, no clipped captions, no detached connectors, and no text below 18px at `1280x720`.
- [ ] Run the Impeccable detector and review screenshots manually.
- [ ] Update roadmap, archive this plan, and commit.
- [x] Run focused scene and figure tests, full presentation tests, typecheck, and build.
- [x] Capture every Scene 3-6 beat and the Scene 10 graph at both rehearsal viewports.
- [x] Capture architecture root, client surface, core runtime, NodeUse, provider detail, and the open run-node inspector.
- [x] Verify no page scroll, no clipped captions, no detached connectors, and no text below 18px at `1280x720`.
- [x] Run the Impeccable detector and review screenshots manually.
- [x] Update roadmap, archive this plan, and commit.
**Completion gate:** Scenes 3-5 gain clear motion without redesign churn. Scene 6 remains interactive but every zoom level uses a readable, subject-appropriate diagram. The prepared-run graph is readable without fitting ten nodes into one thin row, and node selection reveals factual details rather than a generic paragraph.
@@ -5,10 +5,34 @@ import { NodeSpotlight } from "./NodeSpotlight.js";
afterEach(() => cleanup());
describe("NodeSpotlight", () => {
it("uses a readable fallback for raw interrupt nodes without labels", () => {
it("shows the interrupt contract as a factual reusable inspector", () => {
render(<NodeSpotlight nodeId="review_issues" close={vi.fn()} />);
expect(screen.getByRole("dialog", { name: "Review issues" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Review issues" })).toBeInTheDocument();
expect(screen.getAllByText("Human boundary")).toHaveLength(2);
expect(screen.getByText(/issue_review/)).toBeInTheDocument();
expect(screen.getByText(/request: issue proposals/i)).toBeInTheDocument();
expect(screen.getByText(/submitted issues or revision request/i)).toBeInTheDocument();
expect(screen.getByText("submitted")).toBeInTheDocument();
expect(screen.getByText("cancelled")).toBeInTheDocument();
expect(screen.getByText("workflow interrupt: issue_review")).toBeInTheDocument();
});
it("distinguishes an issue-creation action from an ordinary node", () => {
const { unmount } = render(<NodeSpotlight nodeId="create_issues" close={vi.fn()} />);
expect(screen.getByRole("heading", { name: "Create issues" })).toBeInTheDocument();
expect(screen.getByText("Action")).toBeInTheDocument();
expect(screen.getByText("Issue board source")).toBeInTheDocument();
expect(screen.getByText(/submitted issues/i)).toBeInTheDocument();
expect(screen.getByText(/persisted issue-board entries/i)).toBeInTheDocument();
expect(screen.getByText("local.issue_board.create_issues")).toBeInTheDocument();
unmount();
render(<NodeSpotlight nodeId="read_docs" close={vi.fn()} />);
expect(screen.getByRole("heading", { name: "Read documents" })).toBeInTheDocument();
expect(screen.getByText("document source")).toBeInTheDocument();
expect(screen.getByText("local.lda_docs.read_documents")).toBeInTheDocument();
});
});
@@ -6,14 +6,10 @@ type NodeSpotlightProps = {
readonly close: () => void;
};
const nodeDescription = (nodeId: string): string => {
if (nodeId === "review_issues") {
return "Typed interrupt boundary. It exposes request and resume schemas, then waits for a submitted or cancelled outcome.";
}
if (nodeId === "create_issues") {
return "Workflow step that writes selected review items into the local issue-board source.";
}
return "Prepared report workflow step. The presentation graph is curated, but each node maps back to workflow or run evidence.";
const kindLabel = (node: (typeof presentationNodes)[number]): string => {
if (node.type === "interrupt") return "Human boundary";
if (node.type === "end") return "Outcome";
return "Action";
};
export const NodeSpotlight = ({ nodeId, close }: NodeSpotlightProps) => {
@@ -52,6 +48,7 @@ export const NodeSpotlight = ({ nodeId, close }: NodeSpotlightProps) => {
};
if (!node) return null;
const schemaSummary = "schemaSummary" in node ? node.schemaSummary : null;
return (
<aside
@@ -60,12 +57,51 @@ export const NodeSpotlight = ({ nodeId, close }: NodeSpotlightProps) => {
role="dialog"
aria-modal="true"
aria-label={label}
data-node-kind={node.type}
onKeyDown={handleKeyDown}
>
<button type="button" ref={closeButtonRef} onClick={close}>Close</button>
<p>Workflow node</p>
<header className="node-spotlight__header">
<div>
<p className="node-spotlight__eyebrow">Workflow node</p>
<span className="node-spotlight__kind">{kindLabel(node)}</span>
</div>
<button type="button" ref={closeButtonRef} onClick={close}>Close</button>
</header>
<h2>{label}</h2>
<p>{nodeDescription(node.id)}</p>
<p className="node-spotlight__detail">{node.detail}</p>
<dl className="node-spotlight__facts">
<div>
<dt>Capability / boundary</dt>
<dd>{node.capability}</dd>
</div>
<div>
<dt>Input</dt>
<dd>{node.inputSummary}</dd>
</div>
<div>
<dt>Output</dt>
<dd>{node.outputSummary}</dd>
</div>
{schemaSummary && (
<div>
<dt>Schema summary</dt>
<dd>{schemaSummary}</dd>
</div>
)}
</dl>
<section className="node-spotlight__outcomes" aria-labelledby="node-spotlight-outcomes">
<h3 id="node-spotlight-outcomes">Outcomes</h3>
<ul>
{node.outcomes.map((outcome) => <li key={outcome}>{outcome}</li>)}
</ul>
</section>
<footer className="node-spotlight__evidence">
<span>Code / evidence pointer</span>
<code>{node.evidencePointer}</code>
</footer>
</aside>
);
};
@@ -28,7 +28,7 @@ const event: DemoEvent = {
};
describe("OperationBlock", () => {
it("shows command, interpreted summary, and evidence action in expanded mode", () => {
it("shows command, interpreted summary, and protocol receipt action in expanded mode", () => {
render(<OperationBlock event={event} variant="expanded" openEvidence={vi.fn()} />);
expect(screen.getByText(/workflow.runs.start/i)).toBeInTheDocument();
@@ -37,6 +37,8 @@ describe("OperationBlock", () => {
expect(screen.getAllByText(/interrupted/i).length).toBeGreaterThan(0);
expect(screen.getByText(/issue_review/i)).toBeInTheDocument();
expect(screen.getAllByText(/run_demo/i).length).toBeGreaterThan(0);
expect(screen.getByRole("button", { name: /inspect protocol receipt/i })).toBeInTheDocument();
expect(screen.queryByText(/view raw evidence/i)).not.toBeInTheDocument();
});
it("shows compact receipt with operation, status, duration, and run id", () => {
@@ -53,7 +55,7 @@ describe("OperationBlock", () => {
const openEvidence = vi.fn();
render(<OperationBlock event={event} variant="expanded" openEvidence={openEvidence} />);
await userEvent.click(screen.getByText("View raw evidence"));
await userEvent.click(screen.getByRole("button", { name: /inspect protocol receipt/i }));
expect(openEvidence).toHaveBeenCalledOnce();
});
@@ -89,9 +89,10 @@ export const OperationBlock = ({
<button
type="button"
className="operation-block__evidence-action"
aria-label="Inspect protocol receipt; view raw evidence"
onClick={openEvidence}
>
View raw evidence
Inspect protocol receipt
<span aria-hidden="true">-&gt;</span>
</button>
</m.article>
@@ -39,7 +39,12 @@ const demo: DemoTimelineController = {
afterEach(() => cleanup());
const renderSceneBodyAtMainLocation = (sceneId: MainLocation["sceneId"], beatId: string, openDiscussion = noop) => render(
const renderSceneBodyAtMainLocation = (
sceneId: MainLocation["sceneId"],
beatId: string,
openDiscussion = noop,
motionDisabled = false,
) => render(
<SceneBody
location={{ kind: "main", sceneId, beatId, focusPath: [] }}
demo={demo}
@@ -48,7 +53,7 @@ const renderSceneBodyAtMainLocation = (sceneId: MainLocation["sceneId"], beatId:
openEvidence={noop}
openDiscussion={openDiscussion}
onFocusPathChange={noop}
motionDisabled={false}
motionDisabled={motionDisabled}
/>,
);
@@ -256,6 +261,42 @@ describe("SceneBody", () => {
expect(withinRail.getByText("Source binding")).toBeInTheDocument();
});
it.each([
["positioning", "landscape", "Positioning and Related Systems", "landscape"],
["positioning", "lda-position", "Positioning and Related Systems", "lda"],
["planner-runtime", "planner", "Planner and Runtime", "planner"],
["planner-runtime", "runtime", "Planner and Runtime", "runtime"],
["planner-runtime", "boundary", "Planner and Runtime", "boundary"],
["lifecycle", "draft", "Workflow Lifecycle", "draft"],
["lifecycle", "artifact", "Workflow Lifecycle", "artifact"],
["lifecycle", "deployment", "Workflow Lifecycle", "deployment"],
["lifecycle", "run", "Workflow Lifecycle", "run"],
] as const)("exposes one primary visual contract for %s/%s", (sceneId, beatId, heading, active) => {
const { container } = renderSceneBodyAtMainLocation(sceneId, beatId);
expect(screen.getByRole("heading", { name: heading })).toBeInTheDocument();
const primaries = container.querySelectorAll('[data-visual-role="primary"]');
expect(primaries).toHaveLength(1);
expect(primaries[0]).toHaveAttribute("data-motion", "enabled");
expect(primaries[0]).toHaveAttribute(
sceneId === "positioning"
? "data-positioning-active-region"
: sceneId === "planner-runtime"
? "data-boundary-active"
: "data-lifecycle-active-stage",
active,
);
});
it("marks Scene 3-5 primary visuals as motion-disabled without hiding their comparisons", () => {
const { container } = renderSceneBodyAtMainLocation("planner-runtime", "runtime", noop, true);
const primary = container.querySelector('[data-visual-role="primary"]');
expect(primary).toHaveAttribute("data-motion", "disabled");
expect(within(primary as HTMLElement).getByText("Planner")).toBeInTheDocument();
expect(within(primary as HTMLElement).getByText("Runtime")).toBeInTheDocument();
});
it("updates the lifecycle explanation with the active beat", () => {
const { rerender } = render(
<SceneBody
+86 -36
View File
@@ -1,3 +1,4 @@
import { m } from "motion/react";
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import type { TimelineAgentController } from "../demo/agent/timelineAgent.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
@@ -88,33 +89,51 @@ const NarrativeScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBe
</>
);
const PositioningScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => {
const choreographyTransition = (motionDisabled: boolean) => ({
duration: motionDisabled ? 0 : 0.34,
ease: [0.16, 1, 0.3, 1] as const,
});
type ChoreographyProps = {
readonly scene: SceneDefinition;
readonly beat: SceneBeatDefinition;
readonly motionDisabled: boolean;
};
const PositioningScene = ({ scene, beat, motionDisabled }: ChoreographyProps) => {
const highlightLda = beat.id === "lda-position";
const transition = choreographyTransition(motionDisabled);
return (
<>
<StageCaption eyebrow={`Act I · ${scene.claimClass}`} title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div
<m.div
layout
className="scene-body__positioning-map"
aria-label="positioning map"
data-positioning-active-region={highlightLda ? "lda" : "landscape"}
data-visual-role="primary"
data-motion={motionDisabled ? "disabled" : "enabled"}
transition={transition}
>
<section className="scene-body__positioning-column" aria-label="direct action patterns">
<m.section layout transition={transition} className="scene-body__positioning-column" aria-label="direct action patterns">
<p className="scene-body__positioning-label">Direct action</p>
<article className="scene-body__positioning-tile">
<m.article layout transition={transition} className="scene-body__positioning-tile">
<strong>Tool loops</strong>
<span>Fast action, no durable lifecycle</span>
</article>
<article className="scene-body__positioning-tile">
</m.article>
<m.article layout transition={transition} className="scene-body__positioning-tile">
<strong>Generated scripts</strong>
<span>Inspectable code, weak deployment records</span>
</article>
</section>
<article
</m.article>
</m.section>
<m.article
layout
className="scene-body__positioning-substrate"
data-positioning-role="substrate"
data-positioning-active={highlightLda ? "true" : "false"}
transition={transition}
>
<span className="scene-body__positioning-label">This thesis</span>
<strong>lda.chat</strong>
@@ -124,23 +143,23 @@ const PositioningScene = ({ scene, beat }: { scene: SceneDefinition; beat: Scene
<li>Validation</li>
<li>Persisted records</li>
</ul>
</article>
<section className="scene-body__positioning-column" aria-label="adjacent systems">
</m.article>
<m.section layout transition={transition} className="scene-body__positioning-column" aria-label="adjacent systems">
<p className="scene-body__positioning-label">Adjacent systems</p>
<article className="scene-body__positioning-tile">
<m.article layout transition={transition} className="scene-body__positioning-tile">
<strong>Hosted automation</strong>
<span>Managed triggers and app integrations</span>
</article>
<article className="scene-body__positioning-tile">
</m.article>
<m.article layout transition={transition} className="scene-body__positioning-tile">
<strong>Agent graphs</strong>
<span>Durable planner loops</span>
</article>
<article className="scene-body__positioning-tile">
</m.article>
<m.article layout transition={transition} className="scene-body__positioning-tile">
<strong>MCP</strong>
<span>Capability protocol boundary</span>
</article>
</section>
</div>
</m.article>
</m.section>
</m.div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
@@ -152,17 +171,28 @@ const boundaryActiveForBeat = (beatId: string): "planner" | "runtime" | "boundar
return "planner";
};
const BoundaryScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => {
const BoundaryScene = ({ scene, beat, motionDisabled }: ChoreographyProps) => {
const active = boundaryActiveForBeat(beat.id);
const plannerActive = active === "planner" || active === "boundary";
const runtimeActive = active === "runtime" || active === "boundary";
const transition = choreographyTransition(motionDisabled);
return (
<>
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__boundary" aria-label="planner runtime boundary" data-boundary-active={active}>
<section
<m.div
layout
className="scene-body__boundary"
aria-label="planner runtime boundary"
data-boundary-active={active}
data-visual-role="primary"
data-motion={motionDisabled ? "disabled" : "enabled"}
transition={transition}
>
<m.section
layout
transition={transition}
className="scene-body__boundary-pane"
data-boundary-side="planner"
data-boundary-emphasis={plannerActive ? "active" : "reduced"}
@@ -174,12 +204,14 @@ const BoundaryScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBea
<li>Revises steps and bindings</li>
<li>Chooses tools</li>
</ul>
</section>
<div className="scene-body__boundary-seam" aria-label="workflow operation boundary">
</m.section>
<m.div layout transition={transition} className="scene-body__boundary-seam" aria-label="workflow operation boundary">
<strong>CLI / JSON-RPC</strong>
<span>typed workflow operations</span>
</div>
<section
</m.div>
<m.section
layout
transition={transition}
className="scene-body__boundary-pane"
data-boundary-side="runtime"
data-boundary-emphasis={runtimeActive ? "active" : "reduced"}
@@ -192,8 +224,8 @@ const BoundaryScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBea
<li>Records traces and run output</li>
<li>Resumes from persisted state</li>
</ul>
</section>
</div>
</m.section>
</m.div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
@@ -206,17 +238,28 @@ const lifecycleStages = [
{ id: "run", label: "Run", role: "Execution record and trace", detail: "Persist status, outputs, interrupts, and trace evidence." },
] as const;
const LifecycleScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => {
const LifecycleScene = ({ scene, beat, motionDisabled }: ChoreographyProps) => {
const activeIndex = Math.max(0, lifecycleStages.findIndex((stage) => stage.id === beat.id));
const activeStage = lifecycleStages[activeIndex]!;
const transition = choreographyTransition(motionDisabled);
return (
<>
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__lifecycle" aria-label="workflow lifecycle rail" data-lifecycle-active-stage={activeStage.id}>
<m.div
layout
className="scene-body__lifecycle"
aria-label="workflow lifecycle rail"
data-lifecycle-active-stage={activeStage.id}
data-visual-role="primary"
data-motion={motionDisabled ? "disabled" : "enabled"}
transition={transition}
>
{lifecycleStages.map((stage, i) => (
<article
<m.article
layout
transition={transition}
key={stage.id}
className="scene-body__lifecycle-stage"
data-lifecycle-active={i === activeIndex ? "true" : "false"}
@@ -226,13 +269,20 @@ const LifecycleScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBe
<strong>{stage.label}</strong>
<small>{stage.role}</small>
{i < lifecycleStages.length - 1 && <span className="scene-body__lifecycle-arrow"></span>}
</article>
</m.article>
))}
</div>
</m.div>
<aside className="scene-body__lifecycle-current" aria-label="current lifecycle state">
<span>{activeStage.label}</span>
<strong>{activeStage.role}</strong>
<p>{activeStage.detail}</p>
{activeStage.id === "artifact" && (
<div className="scene-body__lifecycle-optional" aria-label="optional artifact path">
<span>Optional evidence path</span>
<strong>Raw plan -&gt; artifact</strong>
<p>A plan can be compiled into an immutable version; Draft is not mandatory.</p>
</div>
)}
</aside>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
@@ -335,11 +385,11 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
if (scene.id === "problem") return <ProblemLoopScene scene={scene} beat={beat} />;
return <NarrativeScene scene={scene} beat={beat} />;
case "positioning":
return <PositioningScene scene={scene} beat={beat} />;
return <PositioningScene scene={scene} beat={beat} motionDisabled={motionDisabled} />;
case "boundary":
return <BoundaryScene scene={scene} beat={beat} />;
return <BoundaryScene scene={scene} beat={beat} motionDisabled={motionDisabled} />;
case "lifecycle":
return <LifecycleScene scene={scene} beat={beat} />;
return <LifecycleScene scene={scene} beat={beat} motionDisabled={motionDisabled} />;
case "architecture":
return (
<ArchitectureScene
@@ -150,11 +150,59 @@ describe("WorkflowGraphStage", () => {
const graph = screen.getByRole("group", { name: "workflow graph" });
expect(graph).toHaveAttribute("data-graph-direction", "horizontal");
expect(graph).toHaveAttribute("data-graph-layout", "horizontal");
expect(graph).toHaveAttribute("data-graph-topology", "prepared-run-branches");
expect(graph).toHaveAttribute("data-pan-zoom", "enabled");
expect(graph).toHaveAttribute("data-node-drag", "enabled");
expect(screen.getByRole("button", { name: /zoom in/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /zoom out/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /fit view/i })).toBeInTheDocument();
});
it("separates the submitted and revision branches in two-dimensional space", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: [], currentNodeId: null }}
selectedNodeId={null}
selectNode={vi.fn()}
/>,
);
const nodePosition = (id: string) => {
const node = document.querySelector<HTMLElement>(
`.workflow-graph-stage__node[data-node-id="${id}"]`,
);
if (!node) throw new Error(`Missing workflow node ${id}`);
return { x: Number(node.dataset.positionX), y: Number(node.dataset.positionY) };
};
expect(nodePosition("create_issues").y).toBeLessThan(nodePosition("revision_requested").y);
expect(nodePosition("create_issues").x).toBe(nodePosition("finalise").x);
expect(nodePosition("finalise").x).toBe(nodePosition("end_completed").x);
expect(nodePosition("create_issues").y).toBeLessThan(nodePosition("finalise").y);
expect(nodePosition("finalise").y).toBeLessThan(nodePosition("end_completed").y);
expect(nodePosition("revision_requested").y).toBeGreaterThan(nodePosition("review_issues").y);
});
it("does not invent selected or current node treatment for the static graph", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "analyze" }}
selectedNodeId="analyze"
selectNode={vi.fn()}
/>,
);
const nodes = document.querySelectorAll<HTMLButtonElement>(
'button[aria-label^="workflow node:"]',
);
expect(nodes).toHaveLength(10);
for (const node of nodes) {
expect(node).not.toHaveAttribute("data-selected");
expect(node).not.toHaveAttribute("aria-pressed");
expect(node).not.toHaveAttribute("data-execution-state");
}
});
it("renders the run identity without plan or trace count chips", () => {
render(
<WorkflowGraphStage
@@ -1,4 +1,4 @@
import { useCallback, useMemo, type MouseEvent } from "react";
import { useCallback, useEffect, useMemo, type MouseEvent } from "react";
import {
Background,
Controls,
@@ -6,6 +6,8 @@ import {
Position,
ReactFlow,
ReactFlowProvider,
useReactFlow,
useNodesState,
type Edge,
type Node,
type NodeProps,
@@ -13,6 +15,7 @@ import {
type ReactFlowInstance,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { CircleCheck, GitBranch, SquareFunction } from "lucide-react";
import { buildWorkflowGraph, type WorkflowGraphNodeData } from "../graph/graph-model.js";
import type { GraphExecutionPresentation } from "./demo-workflow-model.js";
import { presentationWorkflowPlan } from "./workflow-graph-data.js";
@@ -31,8 +34,9 @@ type WorkflowGraphStageProps = {
};
type PresentationNodeData = WorkflowGraphNodeData & {
readonly selected: boolean;
readonly selectNode: (nodeId: string) => void;
readonly positionX: number;
readonly positionY: number;
};
const fullGraphLayout = {
@@ -49,46 +53,69 @@ const compactGraphLayout = {
ranksep: 52,
} as const;
const preparedRunPositions: Readonly<Record<string, { readonly x: number; readonly y: number }>> = {
reset_board: { x: 0, y: 0 },
read_docs: { x: 205, y: 0 },
analyze: { x: 410, y: 0 },
build_report: { x: 615, y: 0 },
draft_issues: { x: 615, y: 110 },
review_issues: { x: 820, y: 110 },
create_issues: { x: 1025, y: 40 },
finalise: { x: 1025, y: 150 },
end_completed: { x: 1025, y: 260 },
revision_requested: { x: 1025, y: 370 },
};
const edgeActive = (edge: { source: string; target: string }, execution: GraphExecutionPresentation): boolean =>
execution.completedNodeIds.includes(edge.source)
&& (execution.completedNodeIds.includes(edge.target) || execution.currentNodeId === edge.target);
const PresentationFlowNode = ({ data }: NodeProps<Node<PresentationNodeData>>) => (
<>
<Handle type="target" position={Position.Left} />
<button
type="button"
className="workflow-graph-stage__node"
data-kind={data.kind}
data-selected={data.selected}
aria-pressed={data.selected}
aria-label={`workflow node: ${data.label}${data.detail ? `, ${data.detail}` : ""}`}
title={data.nodeRef ?? undefined}
onClick={(event) => {
event.stopPropagation();
data.selectNode(data.nodeId);
}}
>
<strong>{data.label}</strong>
<small>{data.detail ?? data.nodeRef ?? "workflow step"}</small>
</button>
<Handle type="source" position={Position.Right} />
</>
);
const PresentationFlowNode = ({ data }: NodeProps<Node<PresentationNodeData>>) => {
const Icon = data.kind === "interrupt"
? GitBranch
: data.kind === "end"
? CircleCheck
: SquareFunction;
return (
<>
<Handle type="target" position={Position.Left} />
<button
type="button"
className="workflow-graph-stage__node"
data-kind={data.kind}
data-node-id={data.nodeId}
data-position-x={data.positionX}
data-position-y={data.positionY}
aria-label={`workflow node: ${data.label}${data.detail ? `, ${data.detail}` : ""}`}
title={data.nodeRef ?? undefined}
onClick={(event) => {
event.stopPropagation();
data.selectNode(data.nodeId);
}}
>
<span className="workflow-graph-stage__node-icon" aria-hidden="true"><Icon size={18} strokeWidth={1.8} /></span>
<strong>{data.label}</strong>
<small>{data.detail ?? data.nodeRef ?? "workflow step"}</small>
</button>
<Handle type="source" position={Position.Right} />
</>
);
};
const nodeTypes: NodeTypes = { presentation: PresentationFlowNode };
const graphFitOptions = { padding: 0.04, minZoom: 0.25, maxZoom: 1 } as const;
const graphFitOptions = { padding: 0.06, minZoom: 0.25, maxZoom: 1.2 } as const;
const WorkflowGraphStageInner = ({
execution,
selectedNodeId,
selectNode,
proof,
variant = "full",
}: WorkflowGraphStageProps) => {
const model = useMemo(
() => buildWorkflowGraph(presentationWorkflowPlan, {
const { fitView } = useReactFlow<Node<PresentationNodeData>, Edge>();
const model = useMemo(() => {
const generated = buildWorkflowGraph(presentationWorkflowPlan, {
direction: "LR",
// Keep Dagre's dimensions in lockstep with the CSS node boxes so React
// Flow's measured handles stay attached when the graph is fit to view.
@@ -98,26 +125,52 @@ const WorkflowGraphStageInner = ({
if (node.id === "review_issues") return "Review issues";
return undefined;
},
}),
[variant],
);
});
const nodes: Node<PresentationNodeData>[] = useMemo(
if (variant === "compact") return generated;
// This is a layout input, not a second renderer: React Flow still owns
// handles, edges, pan, zoom, and the user's dragged positions.
return {
...generated,
nodes: generated.nodes.map((node) => ({
...node,
position: preparedRunPositions[node.id] ?? node.position,
})),
};
}, [variant]);
const baseNodes: Node<PresentationNodeData>[] = useMemo(
() => model.nodes.map((node) => ({
id: node.id,
type: "presentation",
position: node.position,
draggable: false,
draggable: true,
selectable: false,
data: {
...node.data,
selected: selectedNodeId === node.id,
selectNode,
positionX: node.position.x,
positionY: node.position.y,
},
})),
[model.nodes, selectedNodeId, selectNode],
[model.nodes, selectNode],
);
const [nodes, setNodes, onNodesChange] = useNodesState(baseNodes);
useEffect(() => {
setNodes(baseNodes);
}, [baseNodes, setNodes]);
useEffect(() => {
// The presentation canvas can change aspect ratio without remounting the
// graph. Refit after resize so the outcome branches stay on-screen.
const refit = () => requestAnimationFrame(() => void fitView(graphFitOptions));
window.addEventListener("resize", refit);
return () => window.removeEventListener("resize", refit);
}, [fitView]);
const edges: Edge[] = useMemo(
() => model.edges.map((edge) => {
const active = edgeActive(edge, execution);
@@ -155,6 +208,9 @@ const WorkflowGraphStageInner = ({
data-graph-variant={variant}
data-graph-direction="horizontal"
data-graph-layout="horizontal"
data-graph-topology="prepared-run-branches"
data-pan-zoom="enabled"
data-node-drag="enabled"
>
<div className="workflow-graph-stage__legend" aria-label="workflow graph node types">
<span data-kind="use"><i aria-hidden="true" />Action</span>
@@ -172,12 +228,13 @@ const WorkflowGraphStageInner = ({
edges={edges}
nodeTypes={nodeTypes}
onNodeClick={handleNodeClick}
onNodesChange={onNodesChange}
onInit={handleFlowInit}
fitView
fitViewOptions={graphFitOptions}
minZoom={0.25}
maxZoom={1.5}
nodesDraggable={false}
nodesDraggable
nodesConnectable={false}
elementsSelectable={false}
panOnDrag
@@ -16,6 +16,11 @@ const kindLabel: Record<FigureNodeKind, string> = {
runtime: "Runtime",
boundary: "Boundary",
evidence: "Evidence",
decision: "Decision",
terminal: "Terminal",
provider: "Provider",
lane: "Lane",
loop: "Loop",
};
export const FigureNodeView = ({
@@ -42,7 +42,18 @@ const validCatalog: FigureCatalogDefinition = {
nodes: [
{ id: "client", label: "Client operations", summary: "CLI callers", kind: "actor" },
{ id: "runtime", label: "Runtime & providers", summary: "WorkflowServer", kind: "runtime", childFigureId: "runtime-detail" },
{ id: "leaf", label: "Leaf node", summary: "Non-expandable", kind: "artifact" },
{
id: "leaf",
label: "Leaf node",
summary: "Non-expandable",
kind: "artifact",
evidence: {
label: "Stored fact",
title: "Leaf evidence",
body: "The graph remains visible while this factual detail is inspected.",
facts: [{ label: "Pointer", value: "docs/example.md" }],
},
},
],
edges: [{ id: "e1", from: "client", to: "runtime", label: "calls" }],
},
@@ -191,12 +202,20 @@ describe("InteractiveFigure", () => {
expect(figure.querySelector(".interactive-figure__canvas")).toBeInTheDocument();
});
it("keeps root stage figures in presentation mode", () => {
it("opens leaf evidence without replacing the figure", () => {
renderFigure({ size: "stage" });
fireEvent.click(figureNode("leaf"));
expect(screen.getByRole("region", { name: /leaf node evidence/i })).toHaveTextContent("Leaf evidence");
expect(screen.getByRole("region", { name: /leaf node evidence/i })).toHaveTextContent("docs/example.md");
expect(figureNode("leaf")).toBeInTheDocument();
});
it("enables pan and zoom on the root architecture figure", () => {
renderFigure({ focusPath: [], size: "stage" });
expect(screen.getByRole("group", { name: /architecture/i })).toHaveAttribute(
"data-pan-zoom",
"disabled",
"enabled",
);
});
@@ -1,8 +1,33 @@
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
import { ReactFlow, ReactFlowProvider, Handle, Position, useReactFlow, type Node, type Edge, type NodeTypes } from "@xyflow/react";
import {
Boxes,
Cable,
CircleStop,
Database,
FileSearch,
GitBranch,
Layers3,
Network,
PauseCircle,
Plug,
Repeat,
Server,
Terminal,
Users,
Workflow,
type LucideIcon,
} from "lucide-react";
import "@xyflow/react/dist/style.css";
import type { FigureCatalogDefinition, FigureNodeKind } from "./model.js";
import { layoutFigure, type FigureLayoutSize, type PositionedFigure } from "./layout.js";
import type {
FigureCatalogDefinition,
FigureLayoutKind,
FigureNodeDefinition,
FigureNodeIcon,
FigureNodeKind,
FigureNodeShape,
} from "./model.js";
import { layoutFigure, type FigureLayoutSize } from "./layout.js";
import { nextFigureNodeId, type FigureDirection } from "./navigation.js";
import {
popFigureFocus,
@@ -27,19 +52,78 @@ type FigureNodeData = {
readonly label: string;
readonly summary: string;
readonly kind: FigureNodeKind;
readonly shape: FigureNodeShape;
readonly icon: FigureNodeIcon | null;
readonly details: FigureNodeDefinition["details"];
readonly evidence: FigureNodeDefinition["evidence"];
readonly orientation: "horizontal" | "vertical";
readonly isActive: boolean;
readonly isFocused: boolean;
readonly isSelected: boolean;
readonly isExpandable: boolean;
readonly onActivate: (nodeId: string) => void;
readonly onExpand: (nodeId: string) => void;
};
const iconByName: Record<FigureNodeIcon, LucideIcon> = {
users: Users,
terminal: Terminal,
network: Network,
server: Server,
workflow: Workflow,
database: Database,
layers: Layers3,
branch: GitBranch,
repeat: Repeat,
pause: PauseCircle,
stop: CircleStop,
plug: Plug,
trace: FileSearch,
code: FileSearch,
lane: Cable,
};
const iconByKind: Record<FigureNodeKind, FigureNodeIcon> = {
actor: "users",
operation: "workflow",
artifact: "database",
runtime: "server",
boundary: "network",
evidence: "trace",
decision: "branch",
terminal: "stop",
provider: "plug",
lane: "lane",
loop: "repeat",
};
const shapeByKind: Record<FigureNodeKind, FigureNodeShape> = {
actor: "card",
operation: "card",
artifact: "receipt",
runtime: "card",
boundary: "boundary",
evidence: "receipt",
decision: "diamond",
terminal: "terminal",
provider: "boundary",
lane: "sequence",
loop: "loop",
};
const horizontalLayouts = new Set<FigureLayoutKind>(["flow", "fan-in", "lanes", "explicit"]);
const FigureFlowNode = ({ data }: { data: FigureNodeData }) => {
const expandable = data.isExpandable;
const accessibleName = expandable ? `${data.label}, expand` : data.label;
const hasEvidence = data.evidence !== undefined || data.details !== undefined;
const accessibleName = expandable
? `${data.label}, expand`
: hasEvidence
? `${data.label}, inspect details`
: data.label;
const targetPosition = data.orientation === "horizontal" ? Position.Left : Position.Top;
const sourcePosition = data.orientation === "horizontal" ? Position.Right : Position.Bottom;
const Icon = iconByName[data.icon ?? iconByKind[data.kind]];
return (
<>
@@ -48,7 +132,9 @@ const FigureFlowNode = ({ data }: { data: FigureNodeData }) => {
type="button"
className="figure-node"
data-figure-node-kind={data.kind}
data-figure-shape={data.shape}
data-active={data.isActive}
data-selected={data.isSelected}
data-expandable={expandable}
data-testid={`figure-node-${data.nodeId}`}
aria-label={accessibleName}
@@ -64,9 +150,22 @@ const FigureFlowNode = ({ data }: { data: FigureNodeData }) => {
}
}}
>
<span className="figure-node__kind">{data.kind}</span>
<span className="figure-node__header">
<span className="figure-node__kind">{data.kind}</span>
<Icon className="figure-node__icon" size={18} strokeWidth={1.8} aria-hidden="true" />
</span>
<strong className="figure-node__label">{data.label}</strong>
<span className="figure-node__summary">{data.summary}</span>
{data.details && data.details.length > 0 && (
<dl className="figure-node__details">
{data.details.slice(0, 2).map((detail) => (
<div key={`${detail.label}-${detail.value}`}>
<dt>{detail.label}</dt>
<dd><code>{detail.value}</code></dd>
</div>
))}
</dl>
)}
{expandable && <span className="figure-node__expand-affordance" aria-hidden="true">&#9656;</span>}
{data.isActive && <span className="figure-node__current-marker">Current</span>}
</button>
@@ -105,12 +204,14 @@ const InteractiveFigureInner = ({
() => resolveFigureFocus(catalog, focusPath),
[catalog, focusPath],
);
const layout = useMemo(() => layoutFigure(focus.figure, size), [focus.figure, size]);
const layoutSize = size === "stage" && focus.path.length === 0 ? "wide" : size;
const layout = useMemo(() => layoutFigure(focus.figure, layoutSize), [focus.figure, layoutSize]);
const containerRef = useRef<HTMLDivElement>(null);
const initialFocusedNodeId = activeNodeId ?? focus.figure.nodes[0]?.id ?? "";
const [focusedNodeId, setFocusedNodeId] = useState(initialFocusedNodeId);
const focusedNodeIdRef = useRef(initialFocusedNodeId);
const graphInspectionEnabled = size === "stage" && focus.path.length > 0;
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const graphInspectionEnabled = size === "stage";
const fallbackFocusedNodeId = activeNodeId ?? focus.figure.nodes[0]?.id ?? "";
useEffect(() => {
@@ -125,6 +226,7 @@ const InteractiveFigureInner = ({
(nodeId: string) => {
const next = pushFigureFocus(catalog, focus, nodeId);
if (next.path.length > focus.path.length) {
setSelectedNodeId(null);
onFocusPathChange(next.path);
}
},
@@ -133,6 +235,7 @@ const InteractiveFigureInner = ({
const handleBreadcrumbNavigate = useCallback(
(path: readonly string[]) => {
setSelectedNodeId(null);
onFocusPathChange(path);
},
[onFocusPathChange],
@@ -144,6 +247,10 @@ const InteractiveFigureInner = ({
if (key === "Escape") {
event.preventDefault();
event.stopPropagation();
if (selectedNodeId !== null) {
setSelectedNodeId(null);
return;
}
const popped = popFigureFocus(catalog, focus);
if (popped.path.length < focus.path.length) {
onFocusPathChange(popped.path);
@@ -171,13 +278,19 @@ const InteractiveFigureInner = ({
if (nextNode instanceof HTMLElement) nextNode.focus();
}
},
[catalog, focus, layout, onFocusPathChange],
[catalog, focus, layout, onFocusPathChange, selectedNodeId],
);
const handleActivateNode = useCallback((nodeId: string) => {
focusedNodeIdRef.current = nodeId;
setFocusedNodeId(nodeId);
}, []);
const node = layout.nodes.find((candidate) => candidate.id === nodeId);
setSelectedNodeId(node?.evidence || node?.details ? nodeId : null);
}, [layout.nodes]);
const selectedNode = selectedNodeId === null
? undefined
: layout.nodes.find((node) => node.id === selectedNodeId);
const rfNodes: Node[] = useMemo(
() =>
@@ -190,15 +303,20 @@ const InteractiveFigureInner = ({
label: node.label,
summary: node.summary,
kind: node.kind,
orientation: layout.definition.layout.kind === "flow" ? "horizontal" : "vertical",
shape: node.shape ?? shapeByKind[node.kind],
icon: node.icon ?? null,
details: node.details,
evidence: node.evidence,
orientation: horizontalLayouts.has(layout.definition.layout.kind) ? "horizontal" : "vertical",
isActive: node.id === activeNodeId,
isFocused: node.id === focusedNodeId,
isSelected: node.id === selectedNodeId,
isExpandable: node.childFigureId !== undefined,
onActivate: handleActivateNode,
onExpand: handleExpand,
},
})),
[layout.definition.layout.kind, layout.nodes, activeNodeId, focusedNodeId, handleActivateNode, handleExpand],
[layout.definition.layout.kind, layout.nodes, activeNodeId, focusedNodeId, selectedNodeId, handleActivateNode, handleExpand],
);
const rfEdges: Edge[] = useMemo(
@@ -208,9 +326,11 @@ const InteractiveFigureInner = ({
source: edge.from,
target: edge.to,
label: edge.label,
type: "default",
// Authored maps use orthogonal routing so their deliberate rows and
// branches stay readable; Dagre layouts retain softer default curves.
type: layout.definition.layout.kind === "explicit" ? "smoothstep" : "default",
})),
[layout.edges],
[layout.definition.layout.kind, layout.edges],
);
const handleNodeClick = useCallback(
@@ -233,37 +353,70 @@ const InteractiveFigureInner = ({
data-figure-layout={focus.figure.layout.kind}
data-figure-focus-level={focus.path.length}
data-pan-zoom={graphInspectionEnabled ? "enabled" : "disabled"}
data-selected-node={selectedNode?.id ?? ""}
onKeyDown={handleKeyDown}
>
<FigureBreadcrumbs
breadcrumbs={focus.breadcrumbs}
onNavigate={handleBreadcrumbNavigate}
/>
{/* React Flow must measure nodes in its unscaled coordinate space. Keep
this canvas responsive instead of reintroducing a CSS scale wrapper. */}
<div className="interactive-figure__canvas" ref={containerRef}>
<ReactFlow
nodes={rfNodes}
edges={rfEdges}
nodeTypes={nodeTypes}
fitView
proOptions={{ hideAttribution: true }}
nodesDraggable={false}
nodesConnectable={false}
nodesFocusable={false}
edgesFocusable={false}
elementsSelectable={false}
minZoom={0.35}
maxZoom={2.2}
panOnDrag={graphInspectionEnabled}
zoomOnScroll={graphInspectionEnabled}
zoomOnPinch={graphInspectionEnabled}
zoomOnDoubleClick={graphInspectionEnabled}
preventScrolling={graphInspectionEnabled}
onNodeClick={handleNodeClick}
>
<FitViewOnLayoutChange layoutKey={focus.figure.id} />
</ReactFlow>
<div className="interactive-figure__workspace">
{/* React Flow must measure nodes in its unscaled coordinate space. Keep
this canvas responsive instead of reintroducing a CSS scale wrapper. */}
<div className="interactive-figure__canvas" ref={containerRef}>
<ReactFlow
nodes={rfNodes}
edges={rfEdges}
nodeTypes={nodeTypes}
fitView
proOptions={{ hideAttribution: true }}
nodesDraggable={false}
nodesConnectable={false}
nodesFocusable={false}
edgesFocusable={false}
elementsSelectable={false}
minZoom={0.55}
maxZoom={2.2}
panOnDrag={graphInspectionEnabled}
zoomOnScroll={graphInspectionEnabled}
zoomOnPinch={graphInspectionEnabled}
zoomOnDoubleClick={graphInspectionEnabled}
preventScrolling={graphInspectionEnabled}
onNodeClick={handleNodeClick}
>
<FitViewOnLayoutChange layoutKey={focus.figure.id} />
</ReactFlow>
</div>
{selectedNode?.evidence && (
<aside className="figure-evidence" role="region" aria-label={`${selectedNode.label} evidence`}>
<div className="figure-evidence__header">
<span className="figure-evidence__label">{selectedNode.evidence.label}</span>
<button
type="button"
className="figure-evidence__close"
onClick={() => setSelectedNodeId(null)}
aria-label="Close figure evidence"
>
Close
</button>
</div>
<h3>{selectedNode.evidence.title}</h3>
<p>{selectedNode.evidence.body}</p>
{selectedNode.evidence.facts && selectedNode.evidence.facts.length > 0 && (
<dl className="figure-evidence__facts">
{selectedNode.evidence.facts.map((fact) => (
<div key={`${fact.label}-${fact.value}`}>
<dt>{fact.label}</dt>
<dd>{fact.value}</dd>
</div>
))}
</dl>
)}
{selectedNode.evidence.codePointer && (
<code className="figure-evidence__pointer">{selectedNode.evidence.codePointer}</code>
)}
</aside>
)}
</div>
</div>
);
@@ -1,43 +1,77 @@
import { describe, expect, it } from "vitest";
import { resolveFigureFocus } from "./focus.js";
import { architectureCatalog } from "./architecture-catalog.js";
import { layoutFigure } from "./layout.js";
const figure = (id: string) => {
const result = architectureCatalog.figures.find((candidate) => candidate.id === id);
if (!result) throw new Error(`missing architecture figure: ${id}`);
return result;
};
describe("architectureCatalog", () => {
it("contains the conceptual architecture overview", () => {
const root = resolveFigureFocus(architectureCatalog, []).figure;
expect(root.layout.kind).toBe("flow");
expect(root.nodes.map((node) => node.label)).toEqual([
"Client operations",
"Application lifecycle",
"Runtime & providers",
it("keeps the thesis architecture spine as the root contract", () => {
const root = figure(architectureCatalog.rootFigureId);
expect(root.layout.kind).toBe("explicit");
if (root.layout.kind !== "explicit") throw new Error("architecture overview must use authored positions");
expect(root.layout.positions["node-use"]?.x).toBeGreaterThan(
root.layout.positions["core-runtime"]?.x ?? 0,
);
expect(root.nodes.map((node) => node.label)).toEqual(expect.arrayContaining([
"Front door and transport",
"Workflow API operations",
"WorkflowServer composition",
"wf_core execution loop",
"Lifecycle records",
"Capability inventory",
]));
});
it("declares subject-appropriate topologies instead of repeated linear flows", () => {
expect(figure("client-surface-detail").layout.kind).toBe("fan-in");
expect(figure("workflow-api-detail").layout.kind).toBe("hub");
expect(figure("core-runtime-detail").layout.kind).toBe("flow");
expect(figure("node-use-detail").layout.kind).toBe("explicit");
expect(figure("configured-provider-detail").layout.kind).toBe("fan-in");
});
it("models the supported kernel branches and the provider-neutral boundary", () => {
const kernel = figure("core-runtime-detail");
expect(kernel.nodes.map((node) => node.label)).toEqual(expect.arrayContaining([
"Select ready frame",
"Step kind",
"Append trace frame",
"Route by outcome",
]));
const stepKinds = figure("step-kind-detail");
expect(stepKinds.nodes.map((node) => node.label)).toEqual(expect.arrayContaining([
"NodeUse",
"Condition",
"Foreach",
"Join",
"Subgraph",
"Interrupt",
"End",
]));
const providers = figure("configured-provider-detail");
expect(providers.nodes.map((node) => node.label)).toEqual(expect.arrayContaining([
"Capability inventory",
"Built-in sources",
"MCP sources",
"Python sources",
]));
expect(providers.edges.some((edge) => edge.from === "builtin-sources" && edge.to === "capability-inventory")).toBe(true);
expect(providers.edges.some((edge) => edge.from === "mcp-sources" && edge.to === "capability-inventory")).toBe(true);
expect(providers.edges.some((edge) => edge.from === "python-sources" && edge.to === "capability-inventory")).toBe(true);
});
it("keeps NodeUse as an explicit participant sequence with factual evidence", () => {
const sequence = figure("node-use-detail");
expect(sequence.nodes.map((node) => node.label)).toEqual([
"Runtime",
"Binding Resolver",
"NodeDef Handler",
"State Reducers",
"Trace Store",
]);
});
it("supports recursive runtime and provider expansion", () => {
expect(resolveFigureFocus(architectureCatalog, ["runtime-providers"]).figure.id)
.toBe("runtime-provider-detail");
expect(resolveFigureFocus(
architectureCatalog,
["runtime-providers", "configured-providers"],
).figure.id).toBe("configured-provider-detail");
});
it("lays out the architecture path horizontally for presentation", () => {
const root = resolveFigureFocus(architectureCatalog, []).figure;
const layout = layoutFigure(root);
const client = layout.nodes.find((node) => node.id === "client-operations");
const nodeUse = layout.nodes.find((node) => node.id === "node-use");
expect(client?.position.x).toBeLessThan(nodeUse?.position.x ?? 0);
});
it("gives every factual node an evidence pointer", () => {
for (const figure of architectureCatalog.figures) {
for (const node of figure.nodes) {
if (node.kind === "boundary") continue;
expect(node.evidencePointer, `${figure.id}/${node.id}`).toBeTruthy();
}
}
expect(sequence.nodes.every((node) => node.evidence !== undefined)).toBe(true);
});
});
@@ -8,89 +8,268 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
figures: [
{
id: "architecture-overview",
title: "Architecture",
layout: { kind: "flow" },
title: "Architecture spine",
// The overview is an authored architecture map, not an arbitrary DAG.
// Explicit positions keep the central spine dominant while preserving
// room for the three system boundaries and the NodeUse continuation.
layout: {
kind: "explicit",
positions: {
"client-operations": { x: 0, y: 150 },
"application-lifecycle": { x: 300, y: 150 },
"workflow-server": { x: 600, y: 150 },
"lifecycle-records": { x: 900, y: 0 },
"runtime-providers": { x: 900, y: 150 },
"core-runtime": { x: 900, y: 300 },
"node-use": { x: 1200, y: 300 },
},
},
nodes: [
{
id: "client-operations",
label: "Client operations",
summary: "Public lifecycle surface",
label: "Front door and transport",
summary: "Owner / agent -> CLI -> JSON-RPC",
kind: "actor",
evidencePointer: "docs/source_architecture.md",
icon: "network",
evidencePointer: "docs/thesis/system-design-implementation.md#architecture-spine",
childFigureId: "client-surface-detail",
},
{
id: "application-lifecycle",
label: "Application lifecycle",
summary: "WorkflowApi + WorkflowServer",
label: "Workflow API operations",
summary: "Capabilities, records, deployments, runs",
kind: "operation",
icon: "workflow",
evidencePointer: "src/wf_api/service.py",
childFigureId: "workflow-api-detail",
},
{
id: "workflow-server",
label: "WorkflowServer composition",
summary: "API + records + inventory + kernel",
kind: "runtime",
icon: "server",
evidencePointer: "src/wf_server/context.py",
childFigureId: "runtime-provider-detail",
},
{
id: "core-runtime",
label: "wf_core execution loop",
summary: "Select, dispatch, trace, route, resume",
kind: "loop",
icon: "repeat",
evidencePointer: "src/wf_core/runtime/step.py",
childFigureId: "core-runtime-detail",
},
{
id: "lifecycle-records",
label: "Lifecycle records",
summary: "Drafts / artifacts / deployments / runs",
kind: "artifact",
icon: "database",
evidencePointer: "src/wf_api/service.py",
evidence: {
label: "Durable boundary",
title: "Lifecycle records",
body: "The application surface persists and inspects each lifecycle record around runtime execution.",
facts: [
{ label: "Records", value: "Draft, Artifact, Deployment, Run" },
{ label: "Runtime output", value: "Status, output, trace" },
],
codePointer: "src/wf_api/service.py",
},
},
{
id: "runtime-providers",
label: "Runtime & providers",
summary: "CapabilitySource projection",
kind: "runtime",
evidencePointer: "src/wf_core/runtime/ops/state.py",
label: "Capability inventory",
summary: "Provider-neutral CapabilitySource map",
kind: "boundary",
icon: "layers",
evidencePointer: "src/wf_platform/sources.py",
childFigureId: "runtime-provider-detail",
},
{
id: "node-use",
label: "NodeUse",
summary: "Typed node execution",
label: "NodeUse sequence",
summary: "Bindings -> handler -> state -> trace",
kind: "operation",
icon: "workflow",
evidencePointer: "src/wf_core/runtime/ops/nodes.py",
childFigureId: "node-use-detail",
},
],
edges: [
{ id: "e-client-lifecycle", from: "client-operations", to: "application-lifecycle", label: "calls" },
{ id: "e-lifecycle-runtime", from: "application-lifecycle", to: "runtime-providers", label: "delegates" },
{ id: "e-runtime-node", from: "runtime-providers", to: "node-use", label: "invokes" },
{ id: "e-front-door-api", from: "client-operations", to: "application-lifecycle", label: "calls" },
{ id: "e-api-server", from: "application-lifecycle", to: "workflow-server", label: "composes" },
{ id: "e-server-core", from: "workflow-server", to: "core-runtime", label: "executes" },
{ id: "e-server-records", from: "workflow-server", to: "lifecycle-records", label: "persists" },
{ id: "e-server-inventory", from: "workflow-server", to: "runtime-providers", label: "projects" },
{ id: "e-core-node", from: "core-runtime", to: "node-use", label: "dispatches" },
],
},
{
id: "client-surface-detail",
title: "Client surface",
layout: { kind: "flow" },
layout: { kind: "fan-in" },
nodes: [
{
id: "cli",
label: "CLI",
summary: "wf command entry",
id: "workflow-owner",
label: "Workflow owner",
summary: "Human author / operator",
kind: "actor",
evidencePointer: "docs/project_map.md",
icon: "users",
evidencePointer: "docs/thesis/system-design-implementation.md#introduction",
},
{
id: "json-rpc",
label: "JSON-RPC HTTP",
summary: "wf_transport_rpc_http",
id: "external-agent",
label: "External LLM agent",
summary: "Plans and operates workflows",
kind: "actor",
evidencePointer: "src/wf_transport_rpc_http/",
icon: "workflow",
evidencePointer: "docs/thesis/system-design-implementation.md#introduction",
},
{
id: "web-console",
label: "Web console",
summary: "React SPA",
summary: "React presentation client",
kind: "actor",
icon: "network",
evidencePointer: "web/apps/console",
},
{
id: "cli",
label: "wf CLI",
summary: "Human and agent command surface",
kind: "boundary",
icon: "terminal",
evidencePointer: "src/wf_cli",
},
{
id: "json-rpc",
label: "JSON-RPC / local adapter",
summary: "Transport boundary",
kind: "boundary",
icon: "network",
evidencePointer: "src/wf_transport_rpc_http",
},
{
id: "workflow-api",
label: "WorkflowApi",
summary: "Protocol-neutral application surface",
kind: "operation",
icon: "workflow",
evidencePointer: "src/wf_api/service.py",
evidence: {
label: "Public contract",
title: "WorkflowApi",
body: "CLI, JSON-RPC, and the console converge on one application facade.",
facts: [
{ label: "Owns", value: "Capabilities and lifecycle operations" },
{ label: "Does not own", value: "Transport protocol behavior" },
],
codePointer: "src/wf_api/service.py",
},
},
],
edges: [
{ id: "e-cli-api", from: "cli", to: "json-rpc", label: "uses" },
{ id: "e-console-api", from: "web-console", to: "json-rpc", label: "uses" },
{ id: "e-owner-cli", from: "workflow-owner", to: "cli", label: "operates" },
{ id: "e-agent-cli", from: "external-agent", to: "cli", label: "operates" },
{ id: "e-console-rpc", from: "web-console", to: "json-rpc", label: "calls" },
{ id: "e-cli-rpc", from: "cli", to: "json-rpc", label: "uses" },
{ id: "e-rpc-api", from: "json-rpc", to: "workflow-api", label: "adapts" },
],
},
{
id: "workflow-api-detail",
title: "Workflow API operations",
layout: { kind: "hub" },
nodes: [
{
id: "workflow-api-boundary",
label: "WorkflowApi boundary",
summary: "Stable application facade",
kind: "boundary",
shape: "boundary",
icon: "workflow",
evidencePointer: "src/wf_api/service.py",
},
{
id: "capability-operations",
label: "Capabilities",
summary: "Discover and call workflow capabilities",
kind: "operation",
icon: "plug",
details: [
{ label: "Methods", value: "list / inspect / call" },
{ label: "Input", value: "qualified name + payload" },
],
evidencePointer: "src/wf_api/service.py",
},
{
id: "draft-operations",
label: "Drafts",
summary: "Validate, compile, patch authoring state",
kind: "operation",
icon: "layers",
details: [{ label: "Methods", value: "validate / compile / patch" }],
evidencePointer: "src/wf_api/service.py",
},
{
id: "artifact-operations",
label: "Artifacts",
summary: "Save immutable workflow versions",
kind: "artifact",
icon: "database",
details: [{ label: "Methods", value: "create / inspect / delete" }],
evidencePointer: "src/wf_api/service.py",
},
{
id: "deployment-operations",
label: "Deployments",
summary: "Bind artifacts to concrete sources",
kind: "operation",
icon: "network",
details: [{ label: "Methods", value: "validate / save / inspect" }],
evidencePointer: "src/wf_api/service.py",
},
{
id: "run-operations",
label: "Runs",
summary: "Start, resume, inspect execution",
kind: "runtime",
icon: "repeat",
details: [{ label: "Methods", value: "start / resume / trace" }],
evidencePointer: "src/wf_api/service.py",
},
{
id: "trace-output",
label: "Trace and output",
summary: "Bounded status / output / trace reads",
kind: "evidence",
icon: "trace",
evidencePointer: "src/wf_api/runs.py",
},
],
edges: [
{ id: "e-capabilities-api", from: "capability-operations", to: "workflow-api-boundary" },
{ id: "e-drafts-api", from: "draft-operations", to: "workflow-api-boundary" },
{ id: "e-artifacts-api", from: "artifact-operations", to: "workflow-api-boundary" },
{ id: "e-deployments-api", from: "deployment-operations", to: "workflow-api-boundary" },
{ id: "e-runs-api", from: "run-operations", to: "workflow-api-boundary" },
{ id: "e-api-trace", from: "workflow-api-boundary", to: "trace-output", label: "returns" },
],
},
{
id: "runtime-provider-detail",
title: "Runtime and providers",
layout: { kind: "flow" },
title: "WorkflowServer composition",
layout: { kind: "hub" },
nodes: [
{
id: "workflow-server",
label: "WorkflowServer",
summary: "wf_server composition",
summary: "Long-lived composition boundary",
kind: "runtime",
icon: "server",
evidencePointer: "src/wf_server/context.py",
},
{
@@ -98,131 +277,378 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
label: "WorkflowApi",
summary: "Application operations",
kind: "operation",
icon: "workflow",
evidencePointer: "src/wf_api/service.py",
},
{
id: "capability-source",
label: "CapabilitySource",
summary: "Provider-neutral projection",
id: "lifecycle-records",
label: "Lifecycle records",
summary: "Drafts / artifacts / deployments / runs",
kind: "artifact",
evidencePointer: "docs/source_architecture.md",
icon: "database",
evidencePointer: "src/wf_api/service.py",
},
{
id: "core-runtime",
label: "wf_core",
summary: "Deterministic execution kernel",
kind: "loop",
icon: "repeat",
evidencePointer: "src/wf_core/runtime/step.py",
childFigureId: "core-runtime-detail",
},
{
id: "configured-providers",
label: "Configured providers",
summary: "Source families",
kind: "runtime",
evidencePointer: "docs/source_architecture.md",
label: "Capability inventory",
summary: "Provider-neutral CapabilitySource map",
kind: "provider",
icon: "layers",
evidencePointer: "src/wf_platform/sources.py",
childFigureId: "configured-provider-detail",
},
{
id: "deterministic-kernel",
label: "Deterministic kernel",
summary: "Replay-safe execution",
kind: "artifact",
evidencePointer: "src/wf_core/runtime/step.py",
id: "status-output-trace",
label: "Status / output / trace",
summary: "Inspectable runtime result",
kind: "evidence",
icon: "trace",
evidencePointer: "src/wf_api/runs.py",
},
],
edges: [
{ id: "e-server-api", from: "workflow-server", to: "workflow-api", label: "exposes" },
{ id: "e-api-source", from: "workflow-api", to: "capability-source", label: "projects" },
{ id: "e-source-providers", from: "capability-source", to: "configured-providers", label: "loads" },
{ id: "e-providers-kernel", from: "configured-providers", to: "deterministic-kernel", label: "runs" },
{ id: "e-server-records", from: "workflow-server", to: "lifecycle-records", label: "owns" },
{ id: "e-server-core", from: "workflow-server", to: "core-runtime", label: "runs" },
{ id: "e-server-providers", from: "workflow-server", to: "configured-providers", label: "composes" },
{ id: "e-core-output", from: "core-runtime", to: "status-output-trace", label: "produces" },
{ id: "e-api-output", from: "workflow-api", to: "status-output-trace", label: "returns" },
],
},
{
id: "configured-provider-detail",
title: "Configured providers",
layout: { kind: "flow" },
title: "Configured provider boundary",
layout: { kind: "fan-in" },
nodes: [
{
id: "builtin-sources",
label: "Built-in sources",
summary: "wf.std / wf.recipes",
kind: "runtime",
evidencePointer: "src/wf_api/service.py",
kind: "provider",
icon: "layers",
evidencePointer: "src/wf_api/local_sources.py",
},
{
id: "mcp-sources",
label: "MCP sources",
summary: "wf_sources_mcp",
kind: "runtime",
evidencePointer: "src/wf_sources_mcp/",
kind: "provider",
icon: "plug",
evidencePointer: "src/wf_sources_mcp",
},
{
id: "python-sources",
label: "Python sources",
summary: "wf_sources_python",
kind: "runtime",
evidencePointer: "src/wf_sources_python/",
kind: "provider",
icon: "code",
evidencePointer: "src/wf_sources_python",
},
{
id: "openapi-future",
label: "OpenAPI sources",
summary: "Future extension",
id: "capability-inventory",
label: "Capability inventory",
summary: "One provider-neutral CapabilitySource map",
kind: "boundary",
shape: "boundary",
icon: "layers",
evidencePointer: "src/wf_platform/sources.py",
evidence: {
label: "Neutral boundary",
title: "CapabilitySource inventory",
body: "Built-in, MCP, and Python providers project into one workflow-facing source inventory.",
facts: [
{ label: "Core sees", value: "NodeSpec capabilities" },
{ label: "Provider logic", value: "Stays outside wf_core" },
],
codePointer: "src/wf_server/sources.py",
},
},
{
id: "source-resolution",
label: "Logical source resolution",
summary: "Deployment binding selects a concrete source",
kind: "operation",
icon: "network",
evidencePointer: "src/wf_api",
},
],
edges: [
{ id: "e-builtin-mcp", from: "builtin-sources", to: "mcp-sources", label: "adjacent" },
{ id: "e-mcp-python", from: "mcp-sources", to: "python-sources", label: "adjacent" },
{ id: "e-python-openapi", from: "python-sources", to: "openapi-future", label: "future" },
{ id: "e-builtins-inventory", from: "builtin-sources", to: "capability-inventory", label: "projects" },
{ id: "e-mcp-inventory", from: "mcp-sources", to: "capability-inventory", label: "projects" },
{ id: "e-python-inventory", from: "python-sources", to: "capability-inventory", label: "projects" },
{ id: "e-inventory-resolution", from: "capability-inventory", to: "source-resolution", label: "resolves" },
],
},
{
id: "core-runtime-detail",
title: "wf_core execution loop",
layout: { kind: "flow" },
nodes: [
{
id: "validate-input",
label: "Validate workflow input",
summary: "Typed input contract",
kind: "operation",
icon: "workflow",
shape: "receipt",
evidencePointer: "src/wf_core/runtime/preparation.py",
},
{
id: "select-frame",
label: "Select ready frame",
summary: "Scheduler chooses the next frame",
kind: "runtime",
icon: "repeat",
evidencePointer: "src/wf_core/runtime/scheduler.py",
},
{
id: "dispatch-step-kind",
label: "Step kind",
summary: "Typed dispatch",
kind: "decision",
icon: "branch",
shape: "diamond",
evidencePointer: "src/wf_core/runtime/step.py",
childFigureId: "step-kind-detail",
},
{
id: "execute-step",
label: "Execute selected step",
summary: "Typed handler returns a step result",
kind: "operation",
icon: "workflow",
shape: "sequence",
evidencePointer: "src/wf_core/models/steps.py",
},
{
id: "append-trace",
label: "Append trace frame",
summary: "Record step result and lineage",
kind: "evidence",
icon: "trace",
shape: "receipt",
evidencePointer: "src/wf_core/runtime/ops/flow.py",
},
{
id: "route-outcome",
label: "Route by outcome",
summary: "Next edge or terminal path",
kind: "operation",
icon: "network",
evidencePointer: "src/wf_core/runtime/step.py",
},
{
id: "persist-interrupt",
label: "Persist interrupt request",
summary: "Typed request and resume contract",
kind: "evidence",
icon: "pause",
shape: "receipt",
evidencePointer: "src/wf_core/runtime/ops/interrupts.py",
},
{
id: "resume-payload",
label: "Resume payload",
summary: "Resume outcome returns to routing",
kind: "operation",
icon: "workflow",
evidencePointer: "src/wf_core/runtime/preparation.py",
},
{
id: "terminal-output",
label: "Terminal output",
summary: "Status, output, and final trace",
kind: "terminal",
icon: "stop",
shape: "terminal",
evidencePointer: "src/wf_core/runtime/step.py",
},
],
edges: [
{ id: "e-validate-select", from: "validate-input", to: "select-frame", label: "ready" },
{ id: "e-select-dispatch", from: "select-frame", to: "dispatch-step-kind", label: "dispatch" },
{ id: "e-dispatch-execute", from: "dispatch-step-kind", to: "execute-step", label: "step" },
{ id: "e-execute-trace", from: "execute-step", to: "append-trace", label: "result" },
{ id: "e-trace-route", from: "append-trace", to: "route-outcome", label: "recorded" },
{ id: "e-dispatch-interrupt", from: "dispatch-step-kind", to: "persist-interrupt", label: "interrupt" },
{ id: "e-persist-resume", from: "persist-interrupt", to: "resume-payload", label: "resume" },
{ id: "e-resume-route", from: "resume-payload", to: "route-outcome", label: "outcome" },
{ id: "e-dispatch-end", from: "dispatch-step-kind", to: "terminal-output", label: "end" },
],
},
{
id: "step-kind-detail",
title: "Supported step kinds",
layout: {
kind: "explicit",
positions: {
"node-use": { x: 0, y: 0 },
"condition": { x: 300, y: 0 },
"foreach": { x: 600, y: 0 },
"join": { x: 900, y: 0 },
"subgraph": { x: 150, y: 210 },
"interrupt": { x: 450, y: 210 },
"end": { x: 750, y: 210 },
},
},
nodes: [
{
id: "node-use",
label: "NodeUse",
summary: "Invoke a bound capability",
kind: "operation",
icon: "workflow",
evidencePointer: "src/wf_core/models/steps.py",
childFigureId: "node-use-detail",
},
{ id: "condition", label: "Condition", summary: "Route true or false", kind: "decision", icon: "branch", evidencePointer: "src/wf_core/models/steps.py" },
{ id: "foreach", label: "Foreach", summary: "Create item frames and barrier", kind: "loop", icon: "repeat", evidencePointer: "src/wf_core/models/steps.py" },
{ id: "join", label: "Join", summary: "Close a branch or frame", kind: "operation", icon: "workflow", evidencePointer: "src/wf_core/models/steps.py" },
{ id: "subgraph", label: "Subgraph", summary: "Enter a prepared child workflow", kind: "boundary", icon: "layers", evidencePointer: "src/wf_core/models/steps.py" },
{ id: "interrupt", label: "Interrupt", summary: "Persist request and wait", kind: "boundary", icon: "pause", evidencePointer: "src/wf_core/models/steps.py", childFigureId: "interrupt-contract-detail" },
{ id: "end", label: "End", summary: "Project a terminal outcome", kind: "terminal", icon: "stop", evidencePointer: "src/wf_core/models/steps.py" },
],
edges: [],
},
{
id: "interrupt-contract-detail",
title: "Typed interrupt contract",
layout: { kind: "hub" },
nodes: [
{
id: "interrupt-request",
label: "Request schema",
summary: "Payload sent to the client",
kind: "boundary",
icon: "pause",
evidencePointer: "src/wf_core/models/steps.py",
},
{
id: "interrupt-resume",
label: "Resume schema",
summary: "Payload committed back to state",
kind: "boundary",
icon: "workflow",
evidencePointer: "src/wf_core/models/steps.py",
},
{
id: "interrupt-outcome",
label: "Resume outcome",
summary: "Declared route returns to the loop",
kind: "operation",
icon: "network",
evidencePointer: "src/wf_core/runtime/preparation.py",
},
],
edges: [
{ id: "e-request-resume", from: "interrupt-request", to: "interrupt-resume", label: "validates" },
{ id: "e-resume-outcome", from: "interrupt-resume", to: "interrupt-outcome", label: "routes" },
],
},
{
id: "node-use-detail",
title: "NodeUse execution",
layout: { kind: "flow" },
title: "NodeUse execution sequence",
layout: {
kind: "explicit",
positions: {
"runtime-lane": { x: 0, y: 120 },
"binding-resolver": { x: 310, y: 120 },
"node-def-handler": { x: 620, y: 120 },
"state-reducers": { x: 930, y: 120 },
"trace-store": { x: 1240, y: 120 },
},
},
nodes: [
{
id: "resolve-bindings",
label: "Resolve input bindings",
summary: "NodeSpec inputs",
kind: "operation",
evidencePointer: "src/wf_core/runtime/ops/nodes.py",
id: "runtime-lane",
label: "Runtime",
summary: "Validate input; select frame",
kind: "runtime",
shape: "sequence",
icon: "server",
evidence: {
label: "Runtime frame",
title: "Workflow Runtime",
body: "The runtime selects a ready frame and coordinates the NodeUse branch.",
facts: [{ label: "Next", value: "Outcome routing" }],
codePointer: "src/wf_core/runtime/step.py",
},
},
{
id: "invoke-handler",
label: "Invoke handler",
summary: "Capability call",
id: "binding-resolver",
label: "Binding Resolver",
summary: "Build local node input",
kind: "operation",
evidencePointer: "src/wf_core/runtime/step.py",
shape: "sequence",
icon: "network",
evidence: {
label: "Input",
title: "Binding resolution",
body: "Input bindings read workflow input, state, context, or literals into the node-local payload.",
facts: [{ label: "Result", value: "Local node input" }],
codePointer: "src/wf_core/runtime/ops/nodes.py",
},
},
{
id: "normalize-result",
label: "Normalize NodeResult",
summary: "Typed output",
id: "node-def-handler",
label: "NodeDef Handler",
summary: "Invoke the capability handler",
kind: "operation",
evidencePointer: "src/wf_core/runtime/ops/nodes.py",
shape: "sequence",
icon: "plug",
evidence: {
label: "Handler result",
title: "NodeDef invocation",
body: "The declared node definition handler returns an outcome and output payload.",
facts: [{ label: "Result", value: "Outcome + output payload" }],
codePointer: "src/wf_core/runtime/ops/nodes.py",
},
},
{
id: "apply-reducers",
label: "Apply output reducers",
summary: "State mutations",
kind: "operation",
evidencePointer: "src/wf_core/runtime/ops/state.py",
id: "state-reducers",
label: "State Reducers",
summary: "Apply declared output writes",
kind: "artifact",
shape: "sequence",
icon: "database",
evidence: {
label: "State",
title: "Reducer-aware state writes",
body: "Successful output bindings merge the node result into workflow state.",
facts: [{ label: "Write", value: "Reducer-aware output binding" }],
codePointer: "src/wf_core/runtime/ops/state.py",
},
},
{
id: "route-outcome",
label: "Route outcome",
summary: "Success or failure path",
kind: "operation",
evidencePointer: "src/wf_core/runtime/step.py",
},
{
id: "record-trace",
label: "Record trace",
summary: "Inspection evidence",
id: "trace-store",
label: "Trace Store",
summary: "Append frame and route outcome",
kind: "evidence",
evidencePointer: "src/wf_core/runtime/ops/nodes.py",
shape: "sequence",
icon: "trace",
evidence: {
label: "Inspection evidence",
title: "Trace frame",
body: "The completed step records its outcome, next node, and result before routing continues.",
facts: [{ label: "Next", value: "Route by declared outcome" }],
codePointer: "src/wf_core/runtime/ops/flow.py",
},
},
],
edges: [
{ id: "e-resolve-invoke", from: "resolve-bindings", to: "invoke-handler", label: "feeds" },
{ id: "e-invoke-normalize", from: "invoke-handler", to: "normalize-result", label: "produces" },
{ id: "e-normalize-reducers", from: "normalize-result", to: "apply-reducers", label: "reduces" },
{ id: "e-reducers-outcome", from: "apply-reducers", to: "route-outcome", label: "routes" },
{ id: "e-outcome-trace", from: "route-outcome", to: "record-trace", label: "records" },
{ id: "e-runtime-bindings", from: "runtime-lane", to: "binding-resolver", label: "resolve input map" },
{ id: "e-bindings-handler", from: "binding-resolver", to: "node-def-handler", label: "local input" },
{ id: "e-handler-reducers", from: "node-def-handler", to: "state-reducers", label: "outcome + output" },
{ id: "e-reducers-trace", from: "state-reducers", to: "trace-store", label: "append frame" },
],
},
],
@@ -14,6 +14,12 @@
min-height: 0;
}
.interactive-figure__workspace {
position: relative;
flex: 1;
min-height: 0;
}
.interactive-figure[data-figure-size="wide"] {
overflow-x: auto;
overflow-y: hidden;
@@ -32,11 +38,7 @@
background: color-mix(in oklch, var(--color-editorial-surface, oklch(0.96 0.012 82)) 92%, white);
}
/*
Presentation overview figures should not steal normal slide navigation
gestures. Focused stage figures opt into React Flow inspection, where the pane
needs pointer events for drag-pan and wheel zoom.
*/
/* Focused stage figures opt into React Flow inspection without scaling a second canvas. */
.interactive-figure[data-pan-zoom="disabled"] .react-flow__pane {
pointer-events: none;
}
@@ -60,8 +62,13 @@
.interactive-figure .react-flow__edge text {
font-family: var(--font-interface, sans-serif);
font-size: 11px;
fill: var(--color-editorial-muted, oklch(0.48 0.025 65));
font-size: 16px;
font-weight: 650;
fill: var(--color-editorial-ink, oklch(0.19 0.015 65));
paint-order: stroke;
stroke: var(--color-editorial-paper, oklch(0.975 0.012 82));
stroke-width: 5px;
stroke-linejoin: round;
}
.interactive-figure .react-flow__controls,
@@ -94,7 +101,7 @@
font-family: var(--font-interface, sans-serif);
text-align: left;
cursor: pointer;
transition: border-color 0.15s ease;
transition: border-color 0.15s ease, background-color 0.15s ease;
}
.figure-node:focus-visible {
@@ -107,20 +114,32 @@
border-color: oklch(0.19 0.015 65);
}
.figure-node__header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.figure-node__kind {
font-size: 10px;
letter-spacing: 0.02em;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
}
.figure-node__icon {
flex: 0 0 auto;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
}
.figure-node__label {
font-size: 15px;
font-size: 18px;
font-weight: 600;
line-height: 1.2;
}
.figure-node__summary {
font-size: 12px;
font-size: 13px;
line-height: 1.18;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
overflow: hidden;
@@ -130,15 +149,62 @@
max-width: 100%;
}
.figure-node__details {
display: grid;
gap: 2px;
width: 100%;
margin: 2px 0 0;
font-family: var(--font-interface, sans-serif);
font-size: 11px;
line-height: 1.15;
}
.figure-node__details div {
display: flex;
gap: 0.4rem;
min-width: 0;
}
.figure-node__details dt {
flex: 0 0 auto;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
}
.figure-node__details dd {
min-width: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.figure-node__details code {
font-family: var(--font-mono, ui-monospace, monospace);
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}
.interactive-figure[data-figure-size="stage"] {
--figure-node-width: 256px;
--figure-node-height: 112px;
--figure-node-height: 150px;
overflow-x: auto;
overflow-y: hidden;
padding: 0 0 0.45rem;
scrollbar-gutter: stable;
}
.interactive-figure[data-figure-size="stage"][data-figure-focus-level="0"] {
--figure-node-width: 236px;
--figure-node-height: 102px;
}
.interactive-figure[data-figure-size="stage"][data-figure-focus-level="0"] .figure-node__label {
font-size: 18px;
}
.interactive-figure[data-figure-size="stage"][data-figure-focus-level="0"] .figure-node__summary {
font-size: 14px;
}
.interactive-figure[data-figure-size="stage"] .figure-breadcrumbs {
min-height: 1.6rem;
padding: 0.15rem 0 0.35rem;
@@ -151,7 +217,10 @@
height: 100%;
}
.interactive-figure[data-figure-size="stage"][data-figure-layout="flow"] .interactive-figure__canvas {
.interactive-figure[data-figure-size="stage"][data-figure-layout="flow"] .interactive-figure__canvas,
.interactive-figure[data-figure-size="stage"][data-figure-layout="fan-in"] .interactive-figure__canvas,
.interactive-figure[data-figure-size="stage"][data-figure-layout="lanes"] .interactive-figure__canvas,
.interactive-figure[data-figure-size="stage"][data-figure-layout="loop"] .interactive-figure__canvas {
min-width: 72rem;
}
@@ -168,14 +237,18 @@
}
.interactive-figure[data-figure-size="stage"] .figure-node__label {
font-size: 16px;
font-size: 22px;
}
.interactive-figure[data-figure-size="stage"] .figure-node__summary {
font-size: 12.5px;
font-size: 16px;
line-height: 1.25;
}
.interactive-figure[data-figure-size="stage"] .figure-node__kind {
font-size: 12px;
}
.figure-node__expand-affordance {
position: absolute;
top: 8px;
@@ -190,7 +263,7 @@
letter-spacing: 0.02em;
}
/* Semantic kind colors */
/* Semantic kind colors and conventional flow shapes. */
.figure-node[data-figure-node-kind="actor"],
.figure-node[data-figure-node-kind="operation"] {
border-color: var(--color-editorial-ink, oklch(0.19 0.015 65));
@@ -204,6 +277,83 @@
border-color: var(--color-human, oklch(0.68 0.17 55));
}
.figure-node[data-figure-node-kind="provider"] {
border-color: var(--color-source, oklch(0.58 0.12 210));
border-style: dashed;
}
.figure-node[data-figure-node-kind="decision"] {
border-color: var(--color-human, oklch(0.68 0.17 55));
}
.figure-node[data-figure-node-kind="terminal"] {
border-color: var(--color-runtime, oklch(0.55 0.14 150));
}
.figure-node[data-figure-shape="diamond"] {
position: relative;
border: 0;
border-radius: 0;
clip-path: polygon(10% 0, 90% 0, 100% 50%, 90% 100%, 10% 100%, 0 50%);
padding-inline: 32px;
background: var(--color-human, oklch(0.68 0.17 55));
}
.figure-node[data-figure-shape="diamond"]::before {
position: absolute;
inset: 3px;
content: "";
clip-path: inherit;
background: var(--color-editorial-paper, oklch(0.975 0.012 82));
}
.figure-node[data-figure-shape="diamond"] .figure-node__label,
.figure-node[data-figure-shape="diamond"] .figure-node__summary {
position: relative;
z-index: 1;
align-self: center;
width: 82%;
text-align: center;
}
.figure-node[data-figure-shape="diamond"] .figure-node__header {
position: relative;
z-index: 1;
justify-content: center;
gap: 0.45rem;
}
.figure-node[data-figure-shape="diamond"] .figure-node__expand-affordance,
.figure-node[data-figure-shape="diamond"] .figure-node__current-marker {
z-index: 2;
}
.figure-node[data-figure-shape="terminal"] {
border-radius: 2rem;
}
.figure-node[data-figure-shape="boundary"] {
border-style: dashed;
}
.figure-node[data-figure-shape="receipt"] {
border-top-width: 3px;
background: color-mix(in oklch, oklch(0.975 0.012 82) 88%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
}
.figure-node[data-figure-shape="sequence"] {
border-top-width: 3px;
border-bottom-width: 3px;
}
.figure-node[data-figure-shape="loop"] {
border-radius: 1rem;
}
.figure-node[data-selected="true"] {
background: color-mix(in oklch, oklch(0.975 0.012 82) 82%, var(--color-human, oklch(0.68 0.17 55)));
}
.figure-node[data-expandable="true"] {
cursor: pointer;
}
@@ -212,6 +362,95 @@
cursor: default;
}
.figure-evidence {
position: absolute;
top: 0.75rem;
right: 0.75rem;
z-index: 4;
width: min(22rem, calc(100% - 1.5rem));
max-height: calc(100% - 1.5rem);
min-width: 0;
overflow: auto;
align-self: stretch;
padding: 0.9rem 1rem;
border: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 55%, transparent);
border-radius: 0.7rem;
background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 94%, white);
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
font-family: var(--font-interface, sans-serif);
}
.figure-evidence__header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.7rem;
}
.figure-evidence__label {
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
}
.figure-evidence__close {
border: 0;
padding: 0;
background: transparent;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
font: inherit;
text-decoration: underline;
text-underline-offset: 0.15em;
cursor: pointer;
}
.figure-evidence h3 {
margin: 0.7rem 0 0.35rem;
font-size: 1.25rem;
line-height: 1.1;
}
.figure-evidence p {
margin: 0;
font-size: 0.92rem;
line-height: 1.45;
}
.figure-evidence__facts {
display: grid;
gap: 0.45rem;
margin: 1rem 0;
padding-top: 0.8rem;
border-top: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 35%, transparent);
}
.figure-evidence__facts div {
display: grid;
gap: 0.1rem;
}
.figure-evidence__facts dt {
font-size: 0.72rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
}
.figure-evidence__facts dd {
margin: 0;
font-size: 0.88rem;
}
.figure-evidence__pointer {
display: block;
overflow-wrap: anywhere;
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.72rem;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
}
/* Breadcrumbs */
.figure-breadcrumbs {
display: flex;
@@ -250,3 +489,25 @@
margin-right: 4px;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
}
.interactive-figure[data-motion="disabled"] .figure-node,
.interactive-figure[data-motion="disabled"] .figure-evidence,
.interactive-figure[data-motion="disabled"] .figure-breadcrumbs__crumb {
transition: none;
scroll-behavior: auto;
}
@media (prefers-reduced-motion: reduce) {
.interactive-figure .figure-node,
.interactive-figure .figure-evidence,
.interactive-figure .figure-breadcrumbs__crumb {
transition: none;
scroll-behavior: auto;
}
}
@media (max-width: 760px) {
.figure-evidence {
max-height: 14rem;
}
}
@@ -11,6 +11,13 @@ import { describe, expect, it } from "vitest";
describe("interactive-figure CSS", () => {
it("styles node kinds with semantic colors", () => {
expect(css).toContain("data-figure-node-kind");
expect(css).toContain("data-figure-shape");
expect(css).toContain("font-size: 18px");
});
it("disables figure transitions for reduced motion", () => {
expect(css).toContain("prefers-reduced-motion: reduce");
expect(css).toContain('data-motion="disabled"');
});
it("avoids pill-shaped cards outside the stage variant", () => {
@@ -23,6 +23,18 @@ describe("layoutFigure", () => {
expect(position(layout, "discover").x).toBeLessThan(position(layout, "repair").x);
});
it("supports named topologies for fan-in, loops, hubs, and sequence lanes", () => {
for (const kind of ["spine", "fan-in", "hub", "loop", "lanes"] as const) {
const layout = layoutFigure({
...flowFigure,
id: `topology-${kind}`,
layout: { kind },
});
expect(layout.definition.layout.kind).toBe(kind);
expect(layout.nodes).toHaveLength(2);
}
});
it("preserves explicit authored positions", () => {
expect(position(layoutFigure(explicitFigure), "runtime")).toEqual({ x: 420, y: 180 });
});
@@ -45,7 +57,7 @@ describe("layoutFigure", () => {
const standardDelta = position(standard, "repair").x - position(standard, "discover").x;
const stageDelta = position(stage, "repair").x - position(stage, "discover").x;
expect(FIGURE_NODE_DIMENSIONS.stage).toEqual({ width: 256, height: 112 });
expect(FIGURE_NODE_DIMENSIONS.stage).toEqual({ width: 256, height: 150 });
expect(stageDelta - standardDelta).toBe(
FIGURE_NODE_DIMENSIONS.stage.width - FIGURE_NODE_DIMENSIONS.standard.width,
);
@@ -2,6 +2,7 @@ import Dagre from "@dagrejs/dagre";
import type {
FigureDefinition,
FigureEdgeDefinition,
FigureLayoutKind,
FigureNodeDefinition,
} from "./model.js";
@@ -23,13 +24,29 @@ export const FIGURE_NODE_DIMENSIONS: Record<FigureLayoutSize, {
}> = {
standard: { width: 236, height: 102 },
wide: { width: 236, height: 102 },
stage: { width: 256, height: 112 },
stage: { width: 256, height: 150 },
};
export const NODE_WIDTH = FIGURE_NODE_DIMENSIONS.standard.width;
export const NODE_HEIGHT = FIGURE_NODE_DIMENSIONS.standard.height;
const NODESEP = 56;
const RANKSEP = 88;
const NODESEP = 72;
// Presentation graphs need enough room for edge labels without forcing
// fitView to shrink six-rank flows below projector-readable type.
const RANKSEP = 68;
const dagreOptions: Record<FigureLayoutKind, {
readonly rankdir: "TB" | "LR";
readonly ranker: "network-simplex" | "tight-tree" | "longest-path";
}> = {
layered: { rankdir: "TB", ranker: "network-simplex" },
flow: { rankdir: "LR", ranker: "network-simplex" },
spine: { rankdir: "TB", ranker: "tight-tree" },
"fan-in": { rankdir: "LR", ranker: "network-simplex" },
hub: { rankdir: "TB", ranker: "network-simplex" },
loop: { rankdir: "TB", ranker: "network-simplex" },
lanes: { rankdir: "LR", ranker: "tight-tree" },
explicit: { rankdir: "TB", ranker: "network-simplex" },
};
export const layoutFigure = (
figure: FigureDefinition,
@@ -55,7 +72,7 @@ const layoutDagre = (
): PositionedFigure => {
const g = new Dagre.graphlib.Graph();
g.setGraph({
rankdir: figure.layout.kind === "flow" ? "LR" : "TB",
...dagreOptions[figure.layout.kind],
nodesep: NODESEP,
ranksep: RANKSEP,
});
@@ -4,11 +4,65 @@ export type FigureNodeKind =
| "artifact"
| "runtime"
| "boundary"
| "evidence";
| "evidence"
| "decision"
| "terminal"
| "provider"
| "lane"
| "loop";
export type FigureNodeShape =
| "card"
| "diamond"
| "terminal"
| "boundary"
| "receipt"
| "sequence"
| "loop"
| "merge";
export type FigureNodeIcon =
| "users"
| "terminal"
| "network"
| "server"
| "workflow"
| "database"
| "layers"
| "branch"
| "repeat"
| "pause"
| "stop"
| "plug"
| "trace"
| "code"
| "lane";
export type FigureNodeDetail = {
readonly label: string;
readonly value: string;
};
export type FigureNodeEvidence = {
readonly label: string;
readonly title: string;
readonly body: string;
readonly facts?: readonly FigureNodeDetail[];
readonly codePointer?: string;
};
export type FigureLayoutKind =
| "layered"
| "flow"
| "spine"
| "fan-in"
| "hub"
| "loop"
| "lanes"
| "explicit";
export type FigureLayout =
| { readonly kind: "layered" }
| { readonly kind: "flow" }
| { readonly kind: Exclude<FigureLayoutKind, "explicit"> }
| {
readonly kind: "explicit";
readonly positions: Readonly<Record<string, { readonly x: number; readonly y: number }>>;
@@ -19,6 +73,10 @@ export type FigureNodeDefinition = {
readonly label: string;
readonly summary: string;
readonly kind: FigureNodeKind;
readonly shape?: FigureNodeShape;
readonly icon?: FigureNodeIcon;
readonly details?: readonly FigureNodeDetail[];
readonly evidence?: FigureNodeEvidence;
readonly evidencePointer?: string;
readonly childFigureId?: string;
};
@@ -1489,6 +1489,15 @@
min-height: 20rem;
}
/* Motion measures these grid children so the comparison axis stays visible while focus shifts. */
.scene-body__positioning-map[data-positioning-active-region="landscape"] {
grid-template-columns: minmax(13rem, 0.9fr) minmax(20rem, 1.35fr) minmax(13rem, 0.9fr);
}
.scene-body__positioning-map[data-positioning-active-region="lda"] {
grid-template-columns: minmax(11rem, 0.78fr) minmax(23rem, 1.58fr) minmax(11rem, 0.78fr);
}
.scene-body__positioning-column {
display: grid;
align-content: start;
@@ -1567,7 +1576,7 @@
}
.scene-body__positioning-map[data-positioning-active-region="lda"] .scene-body__positioning-tile {
opacity: 0.58;
opacity: 0.74;
}
.scene-body__positioning-tile,
@@ -1590,6 +1599,18 @@
min-height: 20rem;
}
.scene-body__boundary[data-boundary-active="planner"] {
grid-template-columns: minmax(0, 1.35fr) minmax(8rem, 0.24fr) minmax(0, 0.78fr);
}
.scene-body__boundary[data-boundary-active="runtime"] {
grid-template-columns: minmax(0, 0.78fr) minmax(8rem, 0.24fr) minmax(0, 1.35fr);
}
.scene-body__boundary[data-boundary-active="boundary"] {
grid-template-columns: minmax(0, 1fr) minmax(10rem, 0.42fr) minmax(0, 1fr);
}
.scene-body__boundary-pane {
display: grid;
align-content: center;
@@ -1602,7 +1623,7 @@
}
.scene-body__boundary-pane[data-boundary-emphasis="reduced"] {
opacity: 0.58;
opacity: 0.74;
}
.scene-body__boundary-pane {
@@ -1662,6 +1683,22 @@
margin-top: 1.2rem;
}
.scene-body__lifecycle[data-lifecycle-active-stage="draft"] {
grid-template-columns: minmax(0, 1.55fr) repeat(3, minmax(0, 0.82fr));
}
.scene-body__lifecycle[data-lifecycle-active-stage="artifact"] {
grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.55fr) repeat(2, minmax(0, 0.82fr));
}
.scene-body__lifecycle[data-lifecycle-active-stage="deployment"] {
grid-template-columns: repeat(2, minmax(0, 0.82fr)) minmax(0, 1.55fr) minmax(0, 0.82fr);
}
.scene-body__lifecycle[data-lifecycle-active-stage="run"] {
grid-template-columns: repeat(3, minmax(0, 0.82fr)) minmax(0, 1.55fr);
}
.scene-body__lifecycle-stage {
position: relative;
display: grid;
@@ -2047,6 +2084,30 @@
color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 76%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
}
.scene-body__lifecycle-optional {
display: grid;
gap: 0.25rem;
margin-top: 0.35rem;
border-top: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 22%, transparent);
padding-top: 0.55rem;
}
.scene-body__lifecycle-optional span {
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
font: 700 0.62rem/1 var(--font-mono, monospace);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.scene-body__lifecycle-optional strong,
.scene-body__lifecycle-optional p {
margin: 0;
}
.scene-body__lifecycle-optional strong {
font-family: var(--font-mono, monospace);
}
.scene-body__architecture {
display: flex;
flex-direction: column;
@@ -71,8 +71,7 @@ const renderArchitecture = (overrides: Partial<React.ComponentProps<typeof Archi
afterEach(() => cleanup());
describe("ArchitectureScene", () => {
it("renders the overview and expands Runtime & providers", async () => {
const user = userEvent.setup();
it("renders the architecture spine and expands the capability boundary", () => {
const onFocusPathChange = vi.fn();
renderArchitecture({ focusPath: [], onFocusPathChange });
expect(screen.getByRole("heading", { name: /architecture/i })).toBeInTheDocument();
@@ -80,21 +79,55 @@ describe("ArchitectureScene", () => {
expect(onFocusPathChange).toHaveBeenCalledWith(["runtime-providers"]);
});
it("uses the stage figure presentation for the defense architecture scene", () => {
it("marks the figure as the one primary visual and exposes stable motion contracts", () => {
renderArchitecture({ focusPath: [] });
expect(screen.getByRole("group", { name: /architecture/i })).toHaveAttribute("data-figure-size", "stage");
expect(screen.getByRole("group", { name: /architecture spine/i })).toHaveAttribute("data-figure-size", "stage");
expect(screen.getByTestId("architecture-scene")).toHaveAttribute("data-visual-pass", "architecture-stage");
expect(screen.getByTestId("architecture-scene")).toHaveAttribute("data-visual-role", "primary");
expect(screen.getByTestId("architecture-scene")).toHaveAttribute("data-motion", "enabled");
expect(screen.getByTestId("architecture-scene")).toHaveAttribute("data-architecture-focus", "system");
});
it("keeps the architecture heading stable when motion is disabled", () => {
renderArchitecture({ focusPath: [], motionDisabled: true });
expect(screen.getByRole("heading", { name: /architecture zoom/i })).toBeInTheDocument();
expect(screen.getByTestId("architecture-scene")).toHaveAttribute("data-motion", "disabled");
expect(screen.getByRole("group", { name: /architecture spine/i })).toHaveAttribute("data-motion", "disabled");
});
it("exposes the public surface, kernel loop, and lifecycle contracts at the spine", () => {
renderArchitecture({ focusPath: [] });
const spine = screen.getByRole("group", { name: /architecture spine/i });
expect(spine).toHaveTextContent("Front door and transport");
expect(spine).toHaveTextContent("Workflow API operations");
expect(spine).toHaveTextContent("WorkflowServer composition");
expect(spine).toHaveTextContent("wf_core execution loop");
expect(spine).toHaveTextContent("Lifecycle records");
expect(spine).toHaveTextContent("Capability inventory");
});
it("renders a directly linked nested provider view", () => {
renderArchitecture({
focusPath: ["runtime-providers", "configured-providers"],
});
expect(screen.getByRole("group", { name: /configured providers/i })).toBeInTheDocument();
expect(screen.getByRole("group", { name: /configured provider boundary/i })).toBeInTheDocument();
expect(screen.getByText("MCP sources")).toBeInTheDocument();
expect(screen.getByText("Python sources")).toBeInTheDocument();
expect(screen.getByTestId("architecture-scene")).toHaveAttribute("data-architecture-focus", "nested");
expect(screen.getByRole("group", { name: /configured providers/i })).toHaveAttribute("data-figure-focus-level", "2");
expect(screen.getByRole("group", { name: /configured provider boundary/i })).toHaveAttribute("data-figure-focus-level", "2");
});
it("shows the kernel loop and clickable NodeUse sequence as nested figures", () => {
const onFocusPathChange = vi.fn();
renderArchitecture({ focusPath: ["core-runtime"], onFocusPathChange });
expect(screen.getByRole("group", { name: /wf_core execution loop/i })).toHaveTextContent("Step kind");
fireEvent.click(screen.getByTestId("figure-node-dispatch-step-kind"));
expect(onFocusPathChange).toHaveBeenCalledWith(["core-runtime", "dispatch-step-kind"]);
cleanup();
renderArchitecture({ focusPath: ["core-runtime", "dispatch-step-kind"], onFocusPathChange });
expect(screen.getByRole("group", { name: /supported step kinds/i })).toHaveTextContent("NodeUse");
fireEvent.click(screen.getByTestId("figure-node-node-use"));
expect(onFocusPathChange).toHaveBeenCalledWith(["core-runtime", "dispatch-step-kind", "node-use"]);
});
});
@@ -35,6 +35,8 @@ export const ArchitectureScene = ({
className="architecture-scene"
data-testid="architecture-scene"
data-visual-pass="architecture-stage"
data-visual-role="primary"
data-motion={motionDisabled ? "disabled" : "enabled"}
data-focus-level={focusPath.length}
data-architecture-focus={focusPath.length === 0 ? "system" : "nested"}
>
@@ -372,14 +372,25 @@
stroke-width: 0.38rem;
stroke-linejoin: round;
fill: var(--text-primary);
font: 700 0.68rem/1 var(--font-mono, monospace);
font: 750 0.76rem/1 var(--font-mono, monospace);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.workflow-graph-stage .react-flow__edge-textbg {
fill: oklch(0.105 0.02 250);
stroke: oklch(0.105 0.02 250);
fill: oklch(0.08 0.018 250 / 0.96);
stroke: oklch(0.08 0.018 250 / 0.96);
stroke-width: 7px;
}
@container presentation-canvas (max-width: 1080px) {
.workflow-graph-stage__node[data-node-id="revision_requested"] {
width: 10.5rem;
}
.workflow-graph-stage__node strong {
font-size: 1rem;
}
}
.workflow-graph-stage__edge--active .react-flow__edge-path {
@@ -458,14 +469,6 @@
background: oklch(0.68 0.025 250);
}
.workflow-graph-stage__legend i[data-state="current"] {
background: var(--accent-cyan);
}
.workflow-graph-stage__legend i[data-state="interrupt"] {
background: var(--accent-amber);
}
.workflow-graph-stage[data-graph-variant="compact"] {
min-height: 0;
}
@@ -535,8 +538,8 @@
position: relative;
display: grid;
gap: 0.25rem;
width: 11rem;
min-height: 4.75rem;
width: 12.25rem;
min-height: 5rem;
transform: none;
border: 1px solid var(--stage-line);
border-radius: 0.7rem;
@@ -548,6 +551,39 @@
cursor: pointer;
}
.presentation-route .workflow-graph-stage__node[data-kind="use"] {
border-color: oklch(0.68 0.14 195 / 0.72);
border-width: 2px;
}
.presentation-route .workflow-graph-stage__node[data-kind="interrupt"] {
border-color: oklch(0.76 0.18 70 / 0.72);
border-width: 2px;
background: oklch(0.19 0.045 70 / 0.96);
}
.presentation-route .workflow-graph-stage__node[data-kind="end"] {
border-color: oklch(0.68 0.025 250 / 0.8);
background: oklch(0.19 0.025 250 / 0.98);
}
.workflow-graph-stage__node-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.35rem;
height: 1.35rem;
color: var(--accent-cyan);
}
.workflow-graph-stage__node[data-kind="interrupt"] .workflow-graph-stage__node-icon {
color: var(--accent-amber);
}
.workflow-graph-stage__node[data-kind="end"] .workflow-graph-stage__node-icon {
color: var(--text-primary);
}
.presentation-route .workflow-graph-stage__node[data-kind="end"] {
min-height: 4.5rem;
border-radius: 999px;
@@ -556,7 +592,7 @@
}
.workflow-graph-stage__node strong {
font-size: 0.9rem;
font-size: 1.125rem;
line-height: 1.05;
color: var(--text-primary);
}
@@ -567,7 +603,7 @@
}
.workflow-graph-stage__node small {
font-size: 0.67rem;
font-size: 0.82rem;
}
.workflow-graph-stage__node-state {
@@ -576,46 +612,6 @@
text-transform: uppercase;
}
.workflow-graph-stage__node[data-execution-state="completed"] {
border-color: oklch(0.48 0.04 195 / 0.7);
background: oklch(0.16 0.035 210 / 0.96);
}
.workflow-graph-stage__node[data-execution-state="current"] {
border-color: var(--accent-cyan);
background: oklch(0.19 0.055 210 / 0.98);
box-shadow: 0 0 0 3px oklch(0.72 0.17 195 / 0.16), 0 0.45rem 0.85rem oklch(0.06 0.03 230 / 0.36);
}
.workflow-graph-stage__node[data-execution-state="current"] .workflow-graph-stage__node-state {
color: var(--accent-cyan);
}
.workflow-graph-stage__node[data-current-interrupt="true"] {
border-color: var(--accent-amber);
background: oklch(0.19 0.045 70 / 0.95);
box-shadow: 0 0 0 3px oklch(0.76 0.18 70 / 0.16), 0 0.45rem 0.85rem oklch(0.06 0.03 230 / 0.36);
}
.workflow-graph-stage__node[data-current-interrupt="true"] .workflow-graph-stage__node-state {
color: var(--accent-amber);
}
.workflow-graph-stage__node[data-execution-state="future"] {
opacity: 0.72;
box-shadow: none;
}
.workflow-graph-stage__node[data-kind="end"][data-execution-state="future"] {
border-style: dashed;
background: oklch(0.12 0.02 250 / 0.86);
}
.workflow-graph-stage__node[data-selected="true"] {
outline: 2px solid var(--accent-cyan);
outline-offset: 3px;
}
.interrupt-contract-preview {
position: relative;
display: grid;
@@ -636,6 +632,124 @@
gap: 1rem;
}
/* The inspector overlays the stage without replacing it, so the selected
node remains legible in the graph context behind the factual detail. */
.node-spotlight {
display: grid;
gap: 1rem;
width: min(28rem, calc(100vw - 2rem));
max-height: min(38rem, calc(100vh - 2rem));
overflow: auto;
border: 1px solid oklch(0.72 0.17 195 / 0.5);
border-radius: 0.85rem;
padding: 1.1rem;
background:
linear-gradient(160deg, oklch(0.72 0.17 195 / 0.09), transparent 45%),
oklch(0.13 0.025 250 / 0.98);
box-shadow: 0 1rem 2rem oklch(0.04 0.02 250 / 0.38);
}
.node-spotlight__header,
.node-spotlight__header > div,
.node-spotlight__evidence {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
}
.node-spotlight__eyebrow,
.node-spotlight__evidence span,
.node-spotlight__facts dt,
.node-spotlight__outcomes h3 {
margin: 0;
color: var(--text-muted);
font: 650 0.68rem/1.2 var(--font-mono, monospace);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.node-spotlight__header > div {
align-items: baseline;
flex-wrap: wrap;
}
.node-spotlight__kind {
color: var(--accent-cyan);
font: 650 0.72rem/1 var(--font-mono, monospace);
text-transform: uppercase;
}
.node-spotlight h2 {
margin: 0;
color: var(--text-primary);
font-size: 1.6rem;
line-height: 1;
}
.node-spotlight__detail {
margin: -0.55rem 0 0;
color: var(--text-secondary);
font-size: 0.95rem;
}
.node-spotlight button {
border: 1px solid var(--stage-line);
border-radius: 0.35rem;
padding: 0.35rem 0.6rem;
background: transparent;
color: var(--text-primary);
cursor: pointer;
}
.node-spotlight__facts {
display: grid;
gap: 0.7rem;
margin: 0;
}
.node-spotlight__facts > div {
border-top: 1px solid var(--stage-line);
padding-top: 0.65rem;
}
.node-spotlight__facts dd {
margin: 0.22rem 0 0;
color: var(--text-primary);
font-size: 0.95rem;
line-height: 1.35;
}
.node-spotlight__outcomes ul {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin: 0.5rem 0 0;
padding: 0;
list-style: none;
}
.node-spotlight__outcomes li {
border: 1px solid oklch(0.7 0.16 145 / 0.45);
border-radius: 999px;
padding: 0.3rem 0.55rem;
color: var(--text-primary);
font: 650 0.8rem/1 var(--font-mono, monospace);
}
.node-spotlight__evidence {
display: grid;
justify-content: stretch;
border-top: 1px solid var(--stage-line);
padding-top: 0.75rem;
}
.node-spotlight__evidence code {
overflow-wrap: anywhere;
color: var(--accent-cyan);
font: 0.8rem/1.35 var(--font-mono, monospace);
}
.demo-workflow-stage[data-demo-layout="approval"] .demo-workflow-stage__graph {
grid-template-columns: minmax(20rem, 1fr);
align-items: stretch;
@@ -31,4 +31,18 @@ describe("presentationWorkflowPlan", () => {
{ from: "review_issues", to: "revision_requested", outcome: "cancelled" },
]);
});
it("provides factual inspection contracts for every displayed node", () => {
for (const node of presentationWorkflowPlan.nodes) {
expect(node.capability, node.id).toBeTruthy();
expect(node.inputSummary, node.id).toBeTruthy();
expect(node.outputSummary, node.id).toBeTruthy();
expect(node.outcomes, node.id).not.toHaveLength(0);
expect(node.evidencePointer, node.id).toBeTruthy();
}
const interrupt = presentationWorkflowPlan.nodes.find((node) => node.id === "review_issues");
expect(interrupt?.schemaSummary).toMatch(/request: issue proposals/i);
expect(interrupt?.outcomes).toEqual(["submitted", "cancelled"]);
});
});
@@ -1,19 +1,131 @@
/**
* The presentation subset deliberately keeps workflow facts raw. Dagre owns
* every rendered coordinate; this module only chooses the factual story slice.
* The presentation subset deliberately keeps workflow facts raw. The graph
* stage owns rendered coordinates; this module only chooses the factual story
* slice and the small contract summaries shown by the node inspector.
*/
export const presentationWorkflowPlan = {
nodes: [
{ id: "reset_board", type: "node", node: "local.issue_board.reset_issue_board", label: "Reset issue board", detail: "issue board source" },
{ id: "read_docs", type: "node", node: "local.lda_docs.read_documents", label: "Read documents", detail: "document source" },
{ id: "analyze", type: "node", node: "local.lda_report.analyze_documents", label: "Analyze documents", detail: "report source" },
{ id: "build_report", type: "node", node: "local.lda_report.build_report", label: "Build report", detail: "Markdown output" },
{ id: "draft_issues", type: "node", node: "local.lda_report.create_issue_drafts", label: "Draft issues", detail: "issue proposals" },
{ id: "review_issues", type: "interrupt", kind: "issue_review", label: "Review issues", detail: "typed human boundary" },
{ id: "create_issues", type: "node", node: "local.issue_board.create_issues", label: "Create issues", detail: "selected issues only" },
{ id: "finalise", type: "node", node: "local.lda_report.finalise_report", label: "Finalise report", detail: "state output" },
{ id: "revision_requested", type: "node", node: "local.lda_report.record_revision_request", label: "Revision requested", detail: "operator branch" },
{ id: "end_completed", type: "end", outcome: "completed", label: "Completed", detail: "persisted run" },
{
id: "reset_board",
type: "node",
node: "local.issue_board.reset_issue_board",
label: "Reset issue board",
detail: "issue board source",
capability: "Issue board source",
inputSummary: "Existing local issue-board state",
outputSummary: "Cleared issue-board state",
outcomes: ["ok"],
evidencePointer: "local.issue_board.reset_issue_board",
},
{
id: "read_docs",
type: "node",
node: "local.lda_docs.read_documents",
label: "Read documents",
detail: "document source",
capability: "Document source",
inputSummary: "Selected document paths",
outputSummary: "Document contents",
outcomes: ["ok"],
evidencePointer: "local.lda_docs.read_documents",
},
{
id: "analyze",
type: "node",
node: "local.lda_report.analyze_documents",
label: "Analyze documents",
detail: "report source",
capability: "Report source",
inputSummary: "Document contents",
outputSummary: "Report findings",
outcomes: ["ok"],
evidencePointer: "local.lda_report.analyze_documents",
},
{
id: "build_report",
type: "node",
node: "local.lda_report.build_report",
label: "Build report",
detail: "Markdown output",
capability: "Markdown output",
inputSummary: "Report findings",
outputSummary: "Markdown report",
outcomes: ["ok"],
evidencePointer: "local.lda_report.build_report",
},
{
id: "draft_issues",
type: "node",
node: "local.lda_report.create_issue_drafts",
label: "Draft issues",
detail: "issue proposals",
capability: "Issue proposals",
inputSummary: "Report findings",
outputSummary: "Issue proposals",
outcomes: ["ok"],
evidencePointer: "local.lda_report.create_issue_drafts",
},
{
id: "review_issues",
type: "interrupt",
kind: "issue_review",
label: "Review issues",
detail: "typed human boundary",
capability: "Human boundary",
inputSummary: "Issue proposals",
outputSummary: "Submitted issues or revision request",
schemaSummary: "Request: issue proposals; resume: submitted or cancelled",
outcomes: ["submitted", "cancelled"],
evidencePointer: "workflow interrupt: issue_review",
},
{
id: "create_issues",
type: "node",
node: "local.issue_board.create_issues",
label: "Create issues",
detail: "selected issues only",
capability: "Issue board source",
inputSummary: "Submitted issues",
outputSummary: "Persisted issue-board entries",
outcomes: ["ok"],
evidencePointer: "local.issue_board.create_issues",
},
{
id: "finalise",
type: "node",
node: "local.lda_report.finalise_report",
label: "Finalise report",
detail: "state output",
capability: "State output",
inputSummary: "Markdown report and issue-board state",
outputSummary: "Completed run output",
outcomes: ["ok"],
evidencePointer: "local.lda_report.finalise_report",
},
{
id: "revision_requested",
type: "node",
node: "local.lda_report.record_revision_request",
label: "Revision requested",
detail: "operator branch",
capability: "Revision record",
inputSummary: "Issue-review decision",
outputSummary: "Recorded revision request",
outcomes: ["cancelled"],
evidencePointer: "local.lda_report.record_revision_request",
},
{
id: "end_completed",
type: "end",
outcome: "completed",
label: "Completed",
detail: "persisted run",
capability: "Run outcome",
inputSummary: "Finalised workflow state",
outputSummary: "Persisted completed run",
outcomes: ["completed"],
evidencePointer: "workflow run record: completed",
},
],
edges: [
{ from: "reset_board", to: "read_docs", outcome: "ok" },