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:
lda
2026-07-05 18:19:58 +07:00 Verified
parent 196abe4b49
commit 20f99a5034
8 changed files with 379 additions and 34 deletions
@@ -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();
+12 -30
View File
@@ -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" } }),
],
},
{