feat: choreograph architecture presentation beats
This commit is contained in:
@@ -98,7 +98,7 @@ Keep Scene 5 conceptual. Scene 9 applies this vocabulary to the prepared example
|
||||
|
||||
Say:
|
||||
|
||||
> Human and agent clients use the same public lifecycle operations. JSON-RPC handles transport concerns and delegates to WorkflowApi rather than owning domain behavior. Server composition supplies stores, provider projections, and the runtime while the core remains independent of MCP and Python behavior. A NodeUse validates input, invokes a projected capability, checks its declared outcome, reduces output into state, appends a trace frame, and routes to the next edge.
|
||||
> Human operators and external agents enter through the same public lifecycle surface. WorkflowApi owns lifecycle operations; JSON-RPC adapts requests without owning domain behavior. WorkflowServer composes records, provider-neutral capabilities, WorkflowApi, and the execution kernel; provider behavior remains outside the core. A NodeUse resolves bindings, invokes its NodeDef handler, reduces output into state, appends a trace frame, and routes the outcome.
|
||||
|
||||
The NodeUse sequence is documented in the thesis runtime diagram and narrative ([lines 706-750](../thesis/system-design-implementation.md#workflow-core-model)).
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
|
||||
{
|
||||
id: "configured-providers",
|
||||
label: "Capability inventory",
|
||||
summary: "Provider-neutral CapabilitySource map",
|
||||
summary: "Provider-neutral source map",
|
||||
kind: "provider",
|
||||
icon: "layers",
|
||||
evidencePointer: "src/wf_platform/sources.py",
|
||||
|
||||
@@ -359,6 +359,27 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.architecture-scene .figure-node {
|
||||
transition:
|
||||
opacity 180ms ease-out,
|
||||
border-color 150ms ease-out,
|
||||
background-color 150ms ease-out;
|
||||
}
|
||||
|
||||
.architecture-scene .interactive-figure:has(.figure-node[data-active="true"])
|
||||
.figure-node:not([data-active="true"]) {
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.architecture-scene .figure-node[data-active="true"] {
|
||||
z-index: 1;
|
||||
background: oklch(0.995 0.004 82);
|
||||
}
|
||||
|
||||
.architecture-scene .figure-node__current-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scene-body__evidence {
|
||||
flex-shrink: 0;
|
||||
margin: 0.55rem 0 0;
|
||||
|
||||
@@ -158,14 +158,14 @@ export const presenterNotes = [
|
||||
"architecture",
|
||||
"client",
|
||||
13,
|
||||
"Human and agent clients use the same public lifecycle operations.",
|
||||
"Human operators and external agents enter through **the same public lifecycle surface**.",
|
||||
["Thesis System Architecture", "docs/project_map.md"],
|
||||
),
|
||||
beatNote(
|
||||
"architecture",
|
||||
"api",
|
||||
13,
|
||||
"JSON-RPC handles transport concerns and delegates to WorkflowApi rather than owning domain behavior.",
|
||||
"**WorkflowApi owns lifecycle operations**; JSON-RPC adapts requests without owning domain behavior.",
|
||||
["Thesis System Architecture", "docs/source_architecture.md"],
|
||||
{ qnaBranchIds: ["not-just-cli"] },
|
||||
),
|
||||
@@ -173,7 +173,7 @@ export const presenterNotes = [
|
||||
"architecture",
|
||||
"runtime",
|
||||
14,
|
||||
"Server composition supplies stores, provider projections, and the runtime while the core remains independent of MCP and Python behavior.",
|
||||
"**WorkflowServer composes records, provider-neutral capabilities, WorkflowApi, and the execution kernel**; provider behavior remains outside the core.",
|
||||
["Thesis System Architecture", "docs/source_architecture.md"],
|
||||
{ qnaBranchIds: ["provider-security"] },
|
||||
),
|
||||
@@ -181,7 +181,7 @@ export const presenterNotes = [
|
||||
"architecture",
|
||||
"node-use",
|
||||
15,
|
||||
"A NodeUse validates input, invokes a projected capability, checks its declared outcome, reduces output into state, appends a trace frame, and routes to the next edge.",
|
||||
"A NodeUse resolves bindings, **invokes its NodeDef handler**, reduces output into state, appends a trace frame, and routes the outcome.",
|
||||
["Thesis Workflow Core Model"],
|
||||
),
|
||||
beatNote(
|
||||
|
||||
@@ -117,6 +117,27 @@ describe("ArchitectureScene", () => {
|
||||
expect(screen.getByRole("group", { name: /configured provider boundary/i })).toHaveAttribute("data-figure-focus-level", "2");
|
||||
});
|
||||
|
||||
it("marks the current beat as a guided camera focus", () => {
|
||||
renderArchitecture({
|
||||
beat: { ...mockBeat, id: "api" },
|
||||
focusPath: ["application-lifecycle"],
|
||||
activeNodeId: "workflow-api-boundary",
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("architecture-scene")).toHaveAttribute("data-architecture-beat", "api");
|
||||
expect(screen.getByTestId("figure-node-workflow-api-boundary")).toHaveAttribute("data-active", "true");
|
||||
});
|
||||
|
||||
it("highlights the actual NodeDef handler during the NodeUse beat", () => {
|
||||
renderArchitecture({
|
||||
beat: { ...mockBeat, id: "node-use" },
|
||||
focusPath: ["node-use"],
|
||||
activeNodeId: "node-def-handler",
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("figure-node-node-def-handler")).toHaveAttribute("data-active", "true");
|
||||
});
|
||||
|
||||
it("shows the kernel loop and clickable NodeUse sequence as nested figures", () => {
|
||||
const onFocusPathChange = vi.fn();
|
||||
renderArchitecture({ focusPath: ["core-runtime"], onFocusPathChange });
|
||||
|
||||
@@ -39,6 +39,7 @@ export const ArchitectureScene = ({
|
||||
data-motion={motionDisabled ? "disabled" : "enabled"}
|
||||
data-focus-level={focusPath.length}
|
||||
data-architecture-focus={focusPath.length === 0 ? "system" : "nested"}
|
||||
data-architecture-beat={beat.id}
|
||||
>
|
||||
<StageCaption eyebrow={`Act II · ${scene.claimClass}`} title={scene.title}>
|
||||
<p>{beat.caption}</p>
|
||||
|
||||
@@ -111,6 +111,30 @@ describe("storyboard navigation", () => {
|
||||
expect(locationFromHash(hashForLocation(location))).toEqual(location);
|
||||
});
|
||||
|
||||
it("uses each architecture beat's authored focus for a plain direct hash", () => {
|
||||
expect(locationFromHash("#scene/architecture/api")).toMatchObject({
|
||||
focusPath: ["application-lifecycle"],
|
||||
});
|
||||
expect(locationFromHash("#scene/architecture/runtime")).toMatchObject({
|
||||
focusPath: ["runtime-providers"],
|
||||
});
|
||||
expect(locationFromHash("#scene/architecture/node-use")).toMatchObject({
|
||||
focusPath: ["node-use"],
|
||||
});
|
||||
});
|
||||
|
||||
it("round-trips an explicit root view for a beat with an authored nested focus", () => {
|
||||
const rootView: MainLocation = {
|
||||
kind: "main",
|
||||
sceneId: "architecture",
|
||||
beatId: "runtime",
|
||||
focusPath: [],
|
||||
};
|
||||
|
||||
expect(hashForLocation(rootView)).toBe("#scene/architecture/runtime/focus/~");
|
||||
expect(locationFromHash(hashForLocation(rootView))).toEqual(rootView);
|
||||
});
|
||||
|
||||
it("decodes escaped focus segments and rejects malformed encoding", () => {
|
||||
expect(locationFromHash("#scene/architecture/runtime/focus/runtime%20providers"))
|
||||
.toMatchObject({ focusPath: ["runtime providers"] });
|
||||
|
||||
@@ -18,12 +18,23 @@ const flattenMainLocations = (): readonly MainLocation[] =>
|
||||
})),
|
||||
);
|
||||
|
||||
const ROOT_FOCUS_SENTINEL = "~";
|
||||
|
||||
const authoredFocusPath = (sceneId: string, beatId: string): readonly string[] =>
|
||||
findScene(sceneId)?.beats.find((beat) => beat.id === beatId)?.figure?.focusPath ?? [];
|
||||
|
||||
export const hashForLocation = (location: PresentationLocation): string => {
|
||||
if (location.kind === "discussion") {
|
||||
return `#discuss/${encodeURIComponent(location.branchId)}`;
|
||||
}
|
||||
const base = `#scene/${encodeURIComponent(location.sceneId)}/${encodeURIComponent(location.beatId)}`;
|
||||
if (location.focusPath.length === 0) return base;
|
||||
if (location.focusPath.length === 0) {
|
||||
// Beats may open on an authored nested figure. The sentinel preserves an
|
||||
// explicitly selected root view without making plain deep links ambiguous.
|
||||
return authoredFocusPath(location.sceneId, location.beatId).length > 0
|
||||
? `${base}/focus/${ROOT_FOCUS_SENTINEL}`
|
||||
: base;
|
||||
}
|
||||
const segments = location.focusPath.map(encodeURIComponent).join("/");
|
||||
return `${base}/focus/${segments}`;
|
||||
};
|
||||
@@ -42,7 +53,9 @@ export const locationFromHash = (hash: string): PresentationLocation => {
|
||||
try {
|
||||
sceneId = decodeURIComponent(sceneSegment);
|
||||
beatId = decodeURIComponent(beatSegment);
|
||||
if (focusSegment !== undefined && focusSegment !== "") {
|
||||
if (focusSegment === ROOT_FOCUS_SENTINEL) {
|
||||
focusPath = [];
|
||||
} else if (focusSegment !== undefined && focusSegment !== "") {
|
||||
focusPath = focusSegment.split("/").map(decodeURIComponent);
|
||||
}
|
||||
} catch {
|
||||
@@ -50,6 +63,9 @@ export const locationFromHash = (hash: string): PresentationLocation => {
|
||||
}
|
||||
const scene = findScene(sceneId);
|
||||
if (scene && scene.beats.some((b) => b.id === beatId)) {
|
||||
if (focusSegment === undefined) {
|
||||
focusPath = [...authoredFocusPath(scene.id, beatId)];
|
||||
}
|
||||
return { kind: "main", sceneId: scene.id as MainLocation["sceneId"], beatId, focusPath };
|
||||
}
|
||||
return defaultMainLocation;
|
||||
|
||||
@@ -133,9 +133,9 @@ export const mainScenes = defineScenes([
|
||||
view: "architecture",
|
||||
beats: [
|
||||
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("api", "Transport and API", "JSON-RPC reaches WorkflowApi without owning domain behavior.", { figure: { catalogId: "system-architecture", focusPath: ["application-lifecycle"], activeNodeId: "workflow-api-boundary" } }),
|
||||
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.", { evidencePresentation: "receipt", figure: { catalogId: "system-architecture", focusPath: ["node-use"], activeNodeId: "invoke-handler" } }),
|
||||
sceneBeat("node-use", "NodeUse", "One callable node validates input, invokes a capability, and reduces output into state.", { evidencePresentation: "receipt", figure: { catalogId: "system-architecture", focusPath: ["node-use"], activeNodeId: "node-def-handler" } }),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user