feat: define recursive presentation figures

This commit is contained in:
lda
2026-07-05 17:41:33 +07:00 Verified
parent 905a604a74
commit dc38251f3b
6 changed files with 561 additions and 0 deletions
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { defineFigureCatalog } from "./catalog.js";
import {
cyclicCatalog,
duplicateFigureCatalog,
duplicateNodeCatalog,
unknownChildCatalog,
unknownEdgeCatalog,
unknownRootCatalog,
validCatalog,
} from "./test-fixtures.js";
describe("defineFigureCatalog", () => {
it("accepts a valid recursive catalog", () => {
expect(() => defineFigureCatalog(validCatalog)).not.toThrow();
});
it.each([
["duplicate figure", duplicateFigureCatalog, "duplicate_figure"],
["duplicate node", duplicateNodeCatalog, "duplicate_node"],
["unknown root figure", unknownRootCatalog, "unknown_root_figure"],
["unknown edge endpoint", unknownEdgeCatalog, "unknown_edge_endpoint"],
["unknown child figure", unknownChildCatalog, "unknown_child_figure"],
["recursive child cycle", cyclicCatalog, "child_cycle"],
])("rejects %s", (_label, catalog, code) => {
expect(() => defineFigureCatalog(catalog)).toThrow(code);
});
});
@@ -0,0 +1,91 @@
import type {
FigureCatalogDefinition,
FigureDefinition,
FigureNodeDefinition,
} from "./model.js";
/**
* Validates and returns a figure catalog. Static authored data is validated once
* at module load; user or server payloads are not accepted through this interface.
*
* Throws an aggregated Error with one issue code per invalid reference so
* catalog authors see every problem in a single run.
*/
export const defineFigureCatalog = (
catalog: FigureCatalogDefinition,
): FigureCatalogDefinition => {
const issues: string[] = [];
const figureById = new Map<string, FigureDefinition>();
for (const figure of catalog.figures) {
if (figureById.has(figure.id)) {
issues.push(`duplicate_figure:${figure.id}`);
}
figureById.set(figure.id, figure);
}
if (!figureById.has(catalog.rootFigureId)) {
issues.push(`unknown_root_figure:${catalog.rootFigureId}`);
}
const nodeIdsByFigure = new Map<string, Set<string>>();
for (const figure of catalog.figures) {
const nodeIds = new Set<string>();
for (const node of figure.nodes) {
if (nodeIds.has(node.id)) {
issues.push(`duplicate_node:${figure.id}:${node.id}`);
}
nodeIds.add(node.id);
}
nodeIdsByFigure.set(figure.id, nodeIds);
}
for (const figure of catalog.figures) {
const nodeIds = nodeIdsByFigure.get(figure.id)!;
for (const edge of figure.edges) {
if (!nodeIds.has(edge.from)) {
issues.push(`unknown_edge_endpoint:${figure.id}:${edge.from}`);
}
if (!nodeIds.has(edge.to)) {
issues.push(`unknown_edge_endpoint:${figure.id}:${edge.to}`);
}
}
}
for (const figure of catalog.figures) {
for (const node of figure.nodes) {
if (node.childFigureId !== undefined) {
if (!figureById.has(node.childFigureId)) {
issues.push(`unknown_child_figure:${figure.id}:${node.childFigureId}`);
}
}
}
}
const visited = new Set<string>();
const inStack = new Set<string>();
const visitChildChains = (figureId: string, path: string[]) => {
const figure = figureById.get(figureId);
if (!figure) return;
for (const node of figure.nodes) {
if (node.childFigureId === undefined) continue;
const key = `${figureId}:${node.childFigureId}`;
if (inStack.has(key)) {
issues.push(`child_cycle:${figureId}:${node.childFigureId}`);
continue;
}
if (visited.has(key)) continue;
visited.add(key);
inStack.add(key);
visitChildChains(node.childFigureId, [...path, node.childFigureId]);
inStack.delete(key);
}
};
visitChildChains(catalog.rootFigureId, [catalog.rootFigureId]);
if (issues.length > 0) {
throw new Error(issues.join(";"));
}
return catalog;
};
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { popFigureFocus, pushFigureFocus, resolveFigureFocus } from "./focus.js";
import { validCatalog } from "./test-fixtures.js";
describe("figure focus", () => {
it("resolves a two-level Focus Path with breadcrumbs", () => {
const focus = resolveFigureFocus(validCatalog, ["runtime", "providers"]);
expect(focus.figure.id).toBe("provider-detail");
expect(focus.path).toEqual(["runtime", "providers"]);
expect(focus.breadcrumbs.map((item) => item.label)).toEqual([
"Architecture",
"Runtime & providers",
"Configured providers",
]);
});
it("fails closed to the root for an invalid Focus Path", () => {
expect(resolveFigureFocus(validCatalog, ["missing"]).path).toEqual([]);
expect(resolveFigureFocus(validCatalog, ["runtime", "missing"]).figure.id)
.toBe("architecture-overview");
});
it("pushes only expandable nodes and pops one level", () => {
const root = resolveFigureFocus(validCatalog, []);
const runtime = pushFigureFocus(validCatalog, root, "runtime");
expect(runtime.path).toEqual(["runtime"]);
expect(pushFigureFocus(validCatalog, runtime, "leaf")).toEqual(runtime);
expect(popFigureFocus(validCatalog, runtime).path).toEqual([]);
});
});
@@ -0,0 +1,116 @@
import type {
FigureCatalogDefinition,
FigureDefinition,
FigureNodeDefinition,
} from "./model.js";
export type FigureBreadcrumb = {
readonly label: string;
readonly path: readonly string[];
};
export type FigureFocus = {
readonly figure: FigureDefinition;
readonly path: readonly string[];
readonly breadcrumbs: readonly FigureBreadcrumb[];
};
const findFigure = (
catalog: FigureCatalogDefinition,
id: string,
): FigureDefinition | undefined =>
catalog.figures.find((f) => f.id === id);
const findNode = (
figure: FigureDefinition,
nodeId: string,
): FigureNodeDefinition | undefined =>
figure.nodes.find((n) => n.id === nodeId);
const buildBreadcrumbs = (
catalog: FigureCatalogDefinition,
path: readonly string[],
): readonly FigureBreadcrumb[] => {
const crumbs: FigureBreadcrumb[] = [];
let currentFigure = findFigure(catalog, catalog.rootFigureId);
if (!currentFigure) return crumbs;
crumbs.push({ label: currentFigure.title, path: [] });
for (const segment of path) {
const node = findNode(currentFigure, segment);
if (!node?.childFigureId) break;
const childFigure = findFigure(catalog, node.childFigureId);
if (!childFigure) break;
crumbs.push({ label: node.label, path: [...crumbs[crumbs.length - 1]!.path, segment] });
currentFigure = childFigure;
}
return crumbs;
};
/**
* Resolves a Focus Path from the root figure, walking childFigureId references.
* If any path segment is missing or non-expandable, returns the root focus with
* an empty path.
*/
export const resolveFigureFocus = (
catalog: FigureCatalogDefinition,
path: readonly string[],
): FigureFocus => {
const rootFigure = findFigure(catalog, catalog.rootFigureId);
if (!rootFigure) {
return {
figure: { id: "", title: "", layout: { kind: "layered" }, nodes: [], edges: [] },
path: [],
breadcrumbs: [],
};
}
let currentFigure = rootFigure;
const resolvedPath: string[] = [];
for (const segment of path) {
const node = findNode(currentFigure, segment);
if (!node?.childFigureId) {
return { figure: rootFigure, path: [], breadcrumbs: buildBreadcrumbs(catalog, []) };
}
const childFigure = findFigure(catalog, node.childFigureId);
if (!childFigure) {
return { figure: rootFigure, path: [], breadcrumbs: buildBreadcrumbs(catalog, []) };
}
resolvedPath.push(segment);
currentFigure = childFigure;
}
return {
figure: currentFigure,
path: resolvedPath,
breadcrumbs: buildBreadcrumbs(catalog, resolvedPath),
};
};
/**
* Pushes a node focus if the node is expandable; otherwise returns the current
* focus unchanged.
*/
export const pushFigureFocus = (
catalog: FigureCatalogDefinition,
focus: FigureFocus,
nodeId: string,
): FigureFocus => {
const node = findNode(focus.figure, nodeId);
if (!node?.childFigureId) return focus;
return resolveFigureFocus(catalog, [...focus.path, nodeId]);
};
/**
* Pops one focus level, returning to the parent figure.
*/
export const popFigureFocus = (
catalog: FigureCatalogDefinition,
focus: FigureFocus,
): FigureFocus => {
if (focus.path.length === 0) return focus;
return resolveFigureFocus(catalog, focus.path.slice(0, -1));
};
@@ -0,0 +1,44 @@
export type FigureNodeKind =
| "actor"
| "operation"
| "artifact"
| "runtime"
| "boundary"
| "evidence";
export type FigureLayout =
| { readonly kind: "layered" }
| { readonly kind: "flow" }
| {
readonly kind: "explicit";
readonly positions: Readonly<Record<string, { readonly x: number; readonly y: number }>>;
};
export type FigureNodeDefinition = {
readonly id: string;
readonly label: string;
readonly summary: string;
readonly kind: FigureNodeKind;
readonly evidencePointer?: string;
readonly childFigureId?: string;
};
export type FigureEdgeDefinition = {
readonly id: string;
readonly from: string;
readonly to: string;
readonly label?: string;
};
export type FigureDefinition = {
readonly id: string;
readonly title: string;
readonly layout: FigureLayout;
readonly nodes: readonly FigureNodeDefinition[];
readonly edges: readonly FigureEdgeDefinition[];
};
export type FigureCatalogDefinition = {
readonly rootFigureId: string;
readonly figures: readonly FigureDefinition[];
};
@@ -0,0 +1,252 @@
import type {
FigureCatalogDefinition,
FigureDefinition,
FigureEdgeDefinition,
FigureNodeDefinition,
} from "./model.js";
const runtimeNode: FigureNodeDefinition = {
id: "runtime",
label: "Runtime & providers",
summary: "WorkflowServer and provider composition",
kind: "runtime",
childFigureId: "runtime-detail",
};
const leafNode: FigureNodeDefinition = {
id: "leaf",
label: "Leaf node",
summary: "Non-expandable leaf",
kind: "artifact",
};
const clientNode: FigureNodeDefinition = {
id: "client",
label: "Client operations",
summary: "CLI, JSON-RPC, and web console callers",
kind: "actor",
};
const apiNode: FigureNodeDefinition = {
id: "api",
label: "Application lifecycle",
summary: "Public lifecycle operations",
kind: "operation",
};
const overviewFigure: FigureDefinition = {
id: "architecture-overview",
title: "Architecture",
layout: { kind: "layered" },
nodes: [clientNode, apiNode, runtimeNode, leafNode],
edges: [
{ id: "e-client-api", from: "client", to: "api", label: "calls" },
{ id: "e-api-runtime", from: "api", to: "runtime", label: "uses" },
],
};
const providersNode: FigureNodeDefinition = {
id: "providers",
label: "Configured providers",
summary: "Built-in and external providers",
kind: "runtime",
childFigureId: "provider-detail",
};
const runtimeDetailFigure: FigureDefinition = {
id: "runtime-detail",
title: "Runtime detail",
layout: { kind: "layered" },
nodes: [providersNode, leafNode],
edges: [{ id: "e-providers-leaf", from: "providers", to: "leaf" }],
};
const pythonProviderNode: FigureNodeDefinition = {
id: "python-provider",
label: "Python provider",
summary: "Trusted in-process Python execution",
kind: "runtime",
};
const providerDetailFigure: FigureDefinition = {
id: "provider-detail",
title: "Provider detail",
layout: { kind: "layered" },
nodes: [pythonProviderNode],
edges: [],
};
export const validCatalog: FigureCatalogDefinition = {
rootFigureId: "architecture-overview",
figures: [overviewFigure, runtimeDetailFigure, providerDetailFigure],
};
export const duplicateFigureCatalog: FigureCatalogDefinition = {
rootFigureId: "architecture-overview",
figures: [
overviewFigure,
{ ...overviewFigure, id: "architecture-overview" },
runtimeDetailFigure,
providerDetailFigure,
],
};
export const duplicateNodeCatalog: FigureCatalogDefinition = {
rootFigureId: "architecture-overview",
figures: [
{
...overviewFigure,
nodes: [
...overviewFigure.nodes,
{ id: "client", label: "Dup", summary: "Dup", kind: "actor" as const },
],
},
runtimeDetailFigure,
providerDetailFigure,
],
};
export const unknownRootCatalog: FigureCatalogDefinition = {
rootFigureId: "nonexistent",
figures: [overviewFigure, runtimeDetailFigure, providerDetailFigure],
};
export const unknownEdgeCatalog: FigureCatalogDefinition = {
rootFigureId: "architecture-overview",
figures: [
{
...overviewFigure,
edges: [
{ id: "e-bad", from: "client", to: "nonexistent", label: "bad" },
],
},
runtimeDetailFigure,
providerDetailFigure,
],
};
export const unknownChildCatalog: FigureCatalogDefinition = {
rootFigureId: "architecture-overview",
figures: [
{
...overviewFigure,
nodes: overviewFigure.nodes.map((n) =>
n.id === "runtime" ? { ...n, childFigureId: "nonexistent" } : n,
),
},
runtimeDetailFigure,
providerDetailFigure,
],
};
export const cyclicCatalog: FigureCatalogDefinition = {
rootFigureId: "architecture-overview",
figures: [
{
...overviewFigure,
nodes: overviewFigure.nodes.map((n) =>
n.id === "runtime" ? { ...n, childFigureId: "runtime-detail" } : n,
),
},
{
...runtimeDetailFigure,
nodes: runtimeDetailFigure.nodes.map((n) =>
n.id === "providers" ? { ...n, childFigureId: "architecture-overview" } : n,
),
},
providerDetailFigure,
],
};
const edgeA: FigureEdgeDefinition = { id: "e1", from: "a", to: "b" };
const edgeB: FigureEdgeDefinition = { id: "e2", from: "b", to: "c" };
export const layeredFigure: FigureDefinition = {
id: "layered",
title: "Layered",
layout: { kind: "layered" },
nodes: [
{ id: "client", label: "Client", summary: "caller", kind: "actor" },
{ id: "runtime", label: "Runtime", summary: "server", kind: "runtime" },
],
edges: [edgeA],
};
export const flowFigure: FigureDefinition = {
id: "flow",
title: "Flow",
layout: { kind: "flow" },
nodes: [
{ id: "discover", label: "Discover", summary: "find", kind: "operation" },
{ id: "repair", label: "Repair", summary: "fix", kind: "operation" },
],
edges: [edgeB],
};
export const explicitFigure: FigureDefinition = {
id: "explicit",
title: "Explicit",
layout: {
kind: "explicit",
positions: { runtime: { x: 420, y: 180 }, client: { x: 100, y: 50 } },
},
nodes: [
{ id: "client", label: "Client", summary: "caller", kind: "actor" },
{ id: "runtime", label: "Runtime", summary: "server", kind: "runtime" },
],
edges: [{ id: "e-explicit", from: "client", to: "runtime" }],
};
export const explicitFigureMissingPosition: FigureDefinition = {
id: "explicit-missing",
title: "Explicit Missing",
layout: {
kind: "explicit",
positions: { client: { x: 100, y: 50 } },
},
nodes: [
{ id: "client", label: "Client", summary: "caller", kind: "actor" },
{ id: "runtime", label: "Runtime", summary: "server", kind: "runtime" },
],
edges: [{ id: "e-explicit-missing", from: "client", to: "runtime" }],
};
export const navigationLayout: FigureDefinition = {
id: "navigation",
title: "Navigation",
layout: {
kind: "explicit",
positions: {
left: { x: 0, y: 100 },
right: { x: 400, y: 100 },
top: { x: 200, y: 0 },
bottom: { x: 200, y: 200 },
},
},
nodes: [
{ id: "left", label: "Left", summary: "left node", kind: "actor" },
{ id: "right", label: "Right", summary: "right node", kind: "actor" },
{ id: "top", label: "Top", summary: "top node", kind: "actor" },
{ id: "bottom", label: "Bottom", summary: "bottom node", kind: "actor" },
],
edges: [],
};
export const tiedNavigationLayout: FigureDefinition = {
id: "tied-navigation",
title: "Tied Navigation",
layout: {
kind: "explicit",
positions: {
start: { x: 0, y: 100 },
alpha: { x: 200, y: 0 },
beta: { x: 200, y: 200 },
},
},
nodes: [
{ id: "start", label: "Start", summary: "start node", kind: "actor" },
{ id: "alpha", label: "Alpha", summary: "alpha node", kind: "actor" },
{ id: "beta", label: "Beta", summary: "beta node", kind: "actor" },
],
edges: [],
};