feat: make architecture figure expandable
- Create validated architecture catalog with 5 recursive figures - ArchitectureScene delegates to InteractiveFigure with catalog - SceneBody routes architecture view to ArchitectureScene - Architecture beats carry canonical figure state (focusPath, activeNodeId) - Remove old inline architectureLayers constant
This commit is contained in:
@@ -75,6 +75,12 @@ export const PresentationStage = ({
|
||||
selectedNodeId={state.selectedNodeId}
|
||||
selectNode={selectNode}
|
||||
openEvidence={openEvidence}
|
||||
onFocusPathChange={(path) => {
|
||||
if (state.location.kind === "main") {
|
||||
jump({ ...state.location, focusPath: path });
|
||||
}
|
||||
}}
|
||||
motionDisabled={false}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -46,6 +46,8 @@ describe("SceneBody", () => {
|
||||
selectedNodeId={null}
|
||||
selectNode={noop}
|
||||
openEvidence={noop}
|
||||
onFocusPathChange={noop}
|
||||
motionDisabled={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("heading", { name: /Positioning and Related Systems/i })).toBeInTheDocument();
|
||||
@@ -61,6 +63,8 @@ describe("SceneBody", () => {
|
||||
selectedNodeId={null}
|
||||
selectNode={noop}
|
||||
openEvidence={noop}
|
||||
onFocusPathChange={noop}
|
||||
motionDisabled={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByLabelText(/workflow graph/i)).toBeInTheDocument();
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||
import { findBeat, findScene, type PresentationLocation, type SceneDefinition, type SceneBeatDefinition } from "./storyboard.js";
|
||||
import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
|
||||
import { StageCaption } from "./StageCaption.js";
|
||||
import { ArchitectureScene } from "./scenes/ArchitectureScene.js";
|
||||
|
||||
type SceneBodyProps = {
|
||||
readonly location: PresentationLocation;
|
||||
@@ -9,6 +10,8 @@ type SceneBodyProps = {
|
||||
readonly selectedNodeId: string | null;
|
||||
readonly selectNode: (nodeId: string) => void;
|
||||
readonly openEvidence: () => void;
|
||||
readonly onFocusPathChange: (path: readonly string[]) => void;
|
||||
readonly motionDisabled: boolean;
|
||||
};
|
||||
|
||||
const NarrativeScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||
@@ -106,34 +109,6 @@ const LifecycleScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBe
|
||||
</>
|
||||
);
|
||||
|
||||
const architectureLayers = [
|
||||
{ id: "client", label: "Client operations" },
|
||||
{ id: "api", label: "Transport / JSON-RPC" },
|
||||
{ id: "runtime", label: "Runtime & providers" },
|
||||
{ id: "node-use", label: "NodeUse", isNode: true },
|
||||
];
|
||||
|
||||
const ArchitectureScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||
<>
|
||||
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
|
||||
<p>{beat.caption}</p>
|
||||
</StageCaption>
|
||||
<div className="scene-body__architecture">
|
||||
{architectureLayers.map((layer, i) => (
|
||||
<div key={layer.id}>
|
||||
<div
|
||||
className={`scene-body__architecture-layer${layer.isNode ? " scene-body__architecture-layer--node" : ""}${beat.id === layer.id ? " scene-body__architecture-layer--active" : ""}`}
|
||||
>
|
||||
{layer.label}
|
||||
</div>
|
||||
{i < architectureLayers.length - 1 && <div className="scene-body__architecture-arrow">↓</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
||||
</>
|
||||
);
|
||||
|
||||
const authoringSteps = [
|
||||
{ id: "discover", label: "Discover" },
|
||||
{ id: "author", label: "Author" },
|
||||
@@ -201,7 +176,7 @@ const assertNever = (value: never): never => {
|
||||
throw new Error(`Unexpected view: ${value}`);
|
||||
};
|
||||
|
||||
export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence }: SceneBodyProps) => {
|
||||
export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence, onFocusPathChange, motionDisabled }: SceneBodyProps) => {
|
||||
const sceneId = location.kind === "main" ? location.sceneId : "positioning";
|
||||
const beatId = location.kind === "main" ? location.beatId : "landscape";
|
||||
const scene = findScene(sceneId) ?? findScene("thesis")!;
|
||||
@@ -217,7 +192,14 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
|
||||
case "lifecycle":
|
||||
return <LifecycleScene scene={scene} beat={beat} />;
|
||||
case "architecture":
|
||||
return <ArchitectureScene scene={scene} beat={beat} />;
|
||||
return (
|
||||
<ArchitectureScene
|
||||
focusPath={location.kind === "main" ? location.focusPath : []}
|
||||
activeNodeId={beat.figure?.activeNodeId ?? null}
|
||||
onFocusPathChange={onFocusPathChange}
|
||||
motionDisabled={motionDisabled}
|
||||
/>
|
||||
);
|
||||
case "authoring":
|
||||
return <AuthoringScene scene={scene} beat={beat} />;
|
||||
case "agent":
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveFigureFocus } from "./focus.js";
|
||||
import { architectureCatalog } from "./architecture-catalog.js";
|
||||
|
||||
describe("architectureCatalog", () => {
|
||||
it("contains the conceptual architecture overview", () => {
|
||||
const root = resolveFigureFocus(architectureCatalog, []).figure;
|
||||
expect(root.nodes.map((node) => node.label)).toEqual([
|
||||
"Client operations",
|
||||
"Application lifecycle",
|
||||
"Runtime & providers",
|
||||
"NodeUse",
|
||||
]);
|
||||
});
|
||||
|
||||
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("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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
import { defineFigureCatalog } from "./catalog.js";
|
||||
import type { FigureCatalogDefinition } from "./model.js";
|
||||
|
||||
export const ARCHITECTURE_CATALOG_ID = "system-architecture";
|
||||
|
||||
export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog({
|
||||
rootFigureId: "architecture-overview",
|
||||
figures: [
|
||||
{
|
||||
id: "architecture-overview",
|
||||
title: "Architecture",
|
||||
layout: { kind: "layered" },
|
||||
nodes: [
|
||||
{
|
||||
id: "client-operations",
|
||||
label: "Client operations",
|
||||
summary: "Public lifecycle surface",
|
||||
kind: "actor",
|
||||
evidencePointer: "docs/source_architecture.md",
|
||||
childFigureId: "client-surface-detail",
|
||||
},
|
||||
{
|
||||
id: "application-lifecycle",
|
||||
label: "Application lifecycle",
|
||||
summary: "WorkflowApi + WorkflowServer",
|
||||
kind: "operation",
|
||||
evidencePointer: "src/wf_api/service.py",
|
||||
},
|
||||
{
|
||||
id: "runtime-providers",
|
||||
label: "Runtime & providers",
|
||||
summary: "CapabilitySource projection",
|
||||
kind: "runtime",
|
||||
evidencePointer: "src/wf_core/runtime/ops/state.py",
|
||||
childFigureId: "runtime-provider-detail",
|
||||
},
|
||||
{
|
||||
id: "node-use",
|
||||
label: "NodeUse",
|
||||
summary: "Typed node execution",
|
||||
kind: "operation",
|
||||
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: "client-surface-detail",
|
||||
title: "Client surface",
|
||||
layout: { kind: "flow" },
|
||||
nodes: [
|
||||
{
|
||||
id: "cli",
|
||||
label: "CLI",
|
||||
summary: "wf command entry",
|
||||
kind: "actor",
|
||||
evidencePointer: "docs/project_map.md",
|
||||
},
|
||||
{
|
||||
id: "json-rpc",
|
||||
label: "JSON-RPC HTTP",
|
||||
summary: "wf_transport_rpc_http",
|
||||
kind: "actor",
|
||||
evidencePointer: "src/wf_transport_rpc_http/",
|
||||
},
|
||||
{
|
||||
id: "web-console",
|
||||
label: "Web console",
|
||||
summary: "React SPA",
|
||||
kind: "actor",
|
||||
evidencePointer: "web/apps/console",
|
||||
},
|
||||
],
|
||||
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: "runtime-provider-detail",
|
||||
title: "Runtime and providers",
|
||||
layout: { kind: "layered" },
|
||||
nodes: [
|
||||
{
|
||||
id: "workflow-server",
|
||||
label: "WorkflowServer",
|
||||
summary: "wf_server composition",
|
||||
kind: "runtime",
|
||||
evidencePointer: "src/wf_server/server.py",
|
||||
},
|
||||
{
|
||||
id: "workflow-api",
|
||||
label: "WorkflowApi",
|
||||
summary: "Application operations",
|
||||
kind: "operation",
|
||||
evidencePointer: "src/wf_api/service.py",
|
||||
},
|
||||
{
|
||||
id: "capability-source",
|
||||
label: "CapabilitySource",
|
||||
summary: "Provider-neutral projection",
|
||||
kind: "artifact",
|
||||
evidencePointer: "docs/source_architecture.md",
|
||||
},
|
||||
{
|
||||
id: "configured-providers",
|
||||
label: "Configured providers",
|
||||
summary: "Source families",
|
||||
kind: "runtime",
|
||||
evidencePointer: "docs/source_architecture.md",
|
||||
childFigureId: "configured-provider-detail",
|
||||
},
|
||||
{
|
||||
id: "deterministic-kernel",
|
||||
label: "Deterministic kernel",
|
||||
summary: "Replay-safe execution",
|
||||
kind: "artifact",
|
||||
evidencePointer: "src/wf_core/runtime/step.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: "configured-provider-detail",
|
||||
title: "Configured providers",
|
||||
layout: { kind: "layered" },
|
||||
nodes: [
|
||||
{
|
||||
id: "builtin-sources",
|
||||
label: "Built-in sources",
|
||||
summary: "wf.std / wf.recipes",
|
||||
kind: "runtime",
|
||||
evidencePointer: "src/wf_api/service.py",
|
||||
},
|
||||
{
|
||||
id: "mcp-sources",
|
||||
label: "MCP sources",
|
||||
summary: "wf_sources_mcp",
|
||||
kind: "runtime",
|
||||
evidencePointer: "src/wf_sources_mcp/",
|
||||
},
|
||||
{
|
||||
id: "python-sources",
|
||||
label: "Python sources",
|
||||
summary: "wf_sources_python",
|
||||
kind: "runtime",
|
||||
evidencePointer: "src/wf_sources_python/",
|
||||
},
|
||||
{
|
||||
id: "openapi-future",
|
||||
label: "OpenAPI sources",
|
||||
summary: "Future extension",
|
||||
kind: "boundary",
|
||||
},
|
||||
],
|
||||
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: "node-use-detail",
|
||||
title: "NodeUse execution",
|
||||
layout: { kind: "layered" },
|
||||
nodes: [
|
||||
{
|
||||
id: "resolve-bindings",
|
||||
label: "Resolve input bindings",
|
||||
summary: "NodeSpec inputs",
|
||||
kind: "operation",
|
||||
evidencePointer: "src/wf_core/runtime/ops/nodes.py",
|
||||
},
|
||||
{
|
||||
id: "invoke-handler",
|
||||
label: "Invoke handler",
|
||||
summary: "Capability call",
|
||||
kind: "operation",
|
||||
evidencePointer: "src/wf_core/runtime/step.py",
|
||||
},
|
||||
{
|
||||
id: "normalize-result",
|
||||
label: "Normalize NodeResult",
|
||||
summary: "Typed output",
|
||||
kind: "operation",
|
||||
evidencePointer: "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: "record-trace",
|
||||
label: "Record trace",
|
||||
summary: "Inspection evidence",
|
||||
kind: "evidence",
|
||||
evidencePointer: "src/wf_core/runtime/ops/nodes.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-trace", from: "apply-reducers", to: "record-trace", label: "records" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeAll, afterAll, describe, expect, it, vi } from "vitest";
|
||||
import { ArchitectureScene } from "./ArchitectureScene.js";
|
||||
|
||||
class MockResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
|
||||
globalThis.DOMRect = {
|
||||
fromRect: () => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
toJSON() {},
|
||||
}),
|
||||
} as unknown as typeof DOMRect;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete (globalThis as Record<string, unknown>).ResizeObserver;
|
||||
delete (globalThis as Record<string, unknown>).DOMRect;
|
||||
});
|
||||
|
||||
const renderArchitecture = (overrides: Partial<React.ComponentProps<typeof ArchitectureScene>> = {}) => {
|
||||
const onFocusPathChange = overrides.onFocusPathChange ?? vi.fn();
|
||||
return {
|
||||
onFocusPathChange,
|
||||
...render(
|
||||
<ArchitectureScene
|
||||
focusPath={overrides.focusPath ?? []}
|
||||
activeNodeId={overrides.activeNodeId ?? null}
|
||||
onFocusPathChange={onFocusPathChange}
|
||||
motionDisabled={overrides.motionDisabled ?? false}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe("ArchitectureScene", () => {
|
||||
it("renders the overview and expands Runtime & providers", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onFocusPathChange = vi.fn();
|
||||
renderArchitecture({ focusPath: [], onFocusPathChange });
|
||||
expect(screen.getByRole("heading", { name: /architecture/i })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId("figure-node-runtime-providers"));
|
||||
expect(onFocusPathChange).toHaveBeenCalledWith(["runtime-providers"]);
|
||||
});
|
||||
|
||||
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.getByText("MCP sources")).toBeInTheDocument();
|
||||
expect(screen.getByText("Python sources")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { StageCaption } from "../StageCaption.js";
|
||||
import { InteractiveFigure } from "../figures/InteractiveFigure.js";
|
||||
import { architectureCatalog } from "../figures/architecture-catalog.js";
|
||||
|
||||
type ArchitectureSceneProps = {
|
||||
readonly focusPath: readonly string[];
|
||||
readonly activeNodeId: string | null;
|
||||
readonly onFocusPathChange: (path: readonly string[]) => void;
|
||||
readonly motionDisabled: boolean;
|
||||
};
|
||||
|
||||
export const ArchitectureScene = ({
|
||||
focusPath,
|
||||
activeNodeId,
|
||||
onFocusPathChange,
|
||||
motionDisabled,
|
||||
}: ArchitectureSceneProps) => (
|
||||
<>
|
||||
<StageCaption eyebrow="Act II · implemented" title="Architecture Zoom">
|
||||
<p>The system exposes one public lifecycle surface across all client types.</p>
|
||||
</StageCaption>
|
||||
<InteractiveFigure
|
||||
catalog={architectureCatalog}
|
||||
focusPath={focusPath}
|
||||
activeNodeId={activeNodeId}
|
||||
onFocusPathChange={onFocusPathChange}
|
||||
motionDisabled={motionDisabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -137,10 +137,10 @@ export const mainScenes = defineScenes([
|
||||
stageTheme: "night",
|
||||
view: "architecture",
|
||||
beats: [
|
||||
sceneBeat("client", "Client operations", "Human and agent clients use the same public lifecycle surface."),
|
||||
sceneBeat("api", "Transport and API", "JSON-RPC reaches WorkflowApi without owning domain behavior."),
|
||||
sceneBeat("runtime", "Runtime and providers", "The runtime resolves provider-neutral capabilities and stores lifecycle records."),
|
||||
sceneBeat("node-use", "NodeUse", "One callable node validates input, invokes a capability, and reduces output into state.", { evidenceMode: "peek" }),
|
||||
sceneBeat("client", "Client operations", "Human and agent clients use the same public lifecycle surface.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "client-operations" } }),
|
||||
sceneBeat("api", "Transport and API", "JSON-RPC reaches WorkflowApi without owning domain behavior.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "application-lifecycle" } }),
|
||||
sceneBeat("runtime", "Runtime and providers", "The runtime resolves provider-neutral capabilities and stores lifecycle records.", { figure: { catalogId: "system-architecture", focusPath: ["runtime-providers"], activeNodeId: "configured-providers" } }),
|
||||
sceneBeat("node-use", "NodeUse", "One callable node validates input, invokes a capability, and reduces output into state.", { evidenceMode: "peek", figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "node-use" } }),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user