feat: choreograph architecture presentation beats
This commit is contained in:
@@ -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