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
@@ -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;
};