feat: expose architecture node evidence spotlights

This commit is contained in:
lda
2026-07-13 10:33:22 +07:00 Verified
parent aec2651ae6
commit 78235d1f0e
4 changed files with 128 additions and 40 deletions
@@ -40,7 +40,14 @@ const validCatalog: FigureCatalogDefinition = {
title: "Architecture", title: "Architecture",
layout: { kind: "layered" }, layout: { kind: "layered" },
nodes: [ nodes: [
{ id: "client", label: "Client operations", summary: "CLI callers", kind: "actor" }, {
id: "client",
label: "Client operations",
summary: "CLI callers",
kind: "actor",
details: [{ label: "Surface", value: "CLI / JSON-RPC" }],
evidencePointer: "src/client.ts",
},
{ id: "runtime", label: "Runtime & providers", summary: "WorkflowServer", kind: "runtime", childFigureId: "runtime-detail" }, { id: "runtime", label: "Runtime & providers", summary: "WorkflowServer", kind: "runtime", childFigureId: "runtime-detail" },
{ {
id: "leaf", id: "leaf",
@@ -210,6 +217,18 @@ describe("InteractiveFigure", () => {
expect(figureNode("leaf")).toBeInTheDocument(); expect(figureNode("leaf")).toBeInTheDocument();
}); });
it("projects evidence-backed leaf details into a spotlight instead of the node", () => {
renderFigure({ size: "stage" });
expect(figureNode("client")).not.toHaveTextContent("CLI / JSON-RPC");
fireEvent.click(figureNode("client"));
const evidence = screen.getByRole("region", { name: /client operations evidence/i });
expect(evidence).toHaveTextContent("CLI callers");
expect(evidence).toHaveTextContent("CLI / JSON-RPC");
expect(evidence).toHaveTextContent("src/client.ts");
});
it("enables pan and zoom on the root architecture figure", () => { it("enables pan and zoom on the root architecture figure", () => {
renderFigure({ focusPath: [], size: "stage" }); renderFigure({ focusPath: [], size: "stage" });
@@ -23,6 +23,7 @@ import type {
FigureCatalogDefinition, FigureCatalogDefinition,
FigureLayoutKind, FigureLayoutKind,
FigureNodeDefinition, FigureNodeDefinition,
FigureNodeEvidence,
FigureNodeIcon, FigureNodeIcon,
FigureNodeKind, FigureNodeKind,
FigureNodeShape, FigureNodeShape,
@@ -65,6 +66,22 @@ type FigureNodeData = {
readonly onExpand: (nodeId: string) => void; readonly onExpand: (nodeId: string) => void;
}; };
/**
* Builds the standard inspector payload for leaf nodes that already point to
* repository evidence. Catalog-authored evidence wins when a concept needs a
* richer explanation; otherwise the node's concise summary remains factual.
*/
const evidenceForNode = (node: FigureNodeDefinition): FigureNodeEvidence | undefined =>
node.evidence ?? (node.evidencePointer
? {
label: "Code evidence",
title: node.label,
body: node.summary,
...(node.details ? { facts: node.details } : {}),
codePointer: node.evidencePointer,
}
: undefined);
const iconByName: Record<FigureNodeIcon, LucideIcon> = { const iconByName: Record<FigureNodeIcon, LucideIcon> = {
users: Users, users: Users,
terminal: Terminal, terminal: Terminal,
@@ -285,37 +302,41 @@ const InteractiveFigureInner = ({
focusedNodeIdRef.current = nodeId; focusedNodeIdRef.current = nodeId;
setFocusedNodeId(nodeId); setFocusedNodeId(nodeId);
const node = layout.nodes.find((candidate) => candidate.id === nodeId); const node = layout.nodes.find((candidate) => candidate.id === nodeId);
setSelectedNodeId(node?.evidence || node?.details ? nodeId : null); setSelectedNodeId(node && evidenceForNode(node) ? nodeId : null);
}, [layout.nodes]); }, [layout.nodes]);
const selectedNode = selectedNodeId === null const selectedNode = selectedNodeId === null
? undefined ? undefined
: layout.nodes.find((node) => node.id === selectedNodeId); : layout.nodes.find((node) => node.id === selectedNodeId);
const selectedEvidence = selectedNode ? evidenceForNode(selectedNode) : undefined;
const rfNodes: Node[] = useMemo( const rfNodes: Node[] = useMemo(
() => () =>
layout.nodes.map((node) => ({ layout.nodes.map((node) => {
id: node.id, const evidence = evidenceForNode(node);
type: "figure", return {
position: node.position, id: node.id,
data: { type: "figure",
nodeId: node.id, position: node.position,
label: node.label, data: {
summary: node.summary, nodeId: node.id,
kind: node.kind, label: node.label,
shape: node.shape ?? shapeByKind[node.kind], summary: node.summary,
icon: node.icon ?? null, kind: node.kind,
details: node.details, shape: node.shape ?? shapeByKind[node.kind],
evidence: node.evidence, icon: node.icon ?? null,
orientation: horizontalLayouts.has(layout.definition.layout.kind) ? "horizontal" : "vertical", details: evidence ? undefined : node.details,
isActive: node.id === activeNodeId, evidence,
isFocused: node.id === focusedNodeId, orientation: horizontalLayouts.has(layout.definition.layout.kind) ? "horizontal" : "vertical",
isSelected: node.id === selectedNodeId, isActive: node.id === activeNodeId,
isExpandable: node.childFigureId !== undefined, isFocused: node.id === focusedNodeId,
onActivate: handleActivateNode, isSelected: node.id === selectedNodeId,
onExpand: handleExpand, isExpandable: node.childFigureId !== undefined,
}, onActivate: handleActivateNode,
})), onExpand: handleExpand,
},
};
}),
[layout.definition.layout.kind, layout.nodes, activeNodeId, focusedNodeId, selectedNodeId, handleActivateNode, handleExpand], [layout.definition.layout.kind, layout.nodes, activeNodeId, focusedNodeId, selectedNodeId, handleActivateNode, handleExpand],
); );
@@ -387,10 +408,10 @@ const InteractiveFigureInner = ({
<FitViewOnLayoutChange layoutKey={focus.figure.id} /> <FitViewOnLayoutChange layoutKey={focus.figure.id} />
</ReactFlow> </ReactFlow>
</div> </div>
{selectedNode?.evidence && ( {selectedNode && selectedEvidence && (
<aside className="figure-evidence" role="region" aria-label={`${selectedNode.label} evidence`}> <aside className="figure-evidence" role="region" aria-label={`${selectedNode.label} evidence`}>
<div className="figure-evidence__header"> <div className="figure-evidence__header">
<span className="figure-evidence__label">{selectedNode.evidence.label}</span> <span className="figure-evidence__label">{selectedEvidence.label}</span>
<button <button
type="button" type="button"
className="figure-evidence__close" className="figure-evidence__close"
@@ -400,11 +421,11 @@ const InteractiveFigureInner = ({
Close Close
</button> </button>
</div> </div>
<h3>{selectedNode.evidence.title}</h3> <h3>{selectedEvidence.title}</h3>
<p>{selectedNode.evidence.body}</p> <p>{selectedEvidence.body}</p>
{selectedNode.evidence.facts && selectedNode.evidence.facts.length > 0 && ( {selectedEvidence.facts && selectedEvidence.facts.length > 0 && (
<dl className="figure-evidence__facts"> <dl className="figure-evidence__facts">
{selectedNode.evidence.facts.map((fact) => ( {selectedEvidence.facts.map((fact) => (
<div key={`${fact.label}-${fact.value}`}> <div key={`${fact.label}-${fact.value}`}>
<dt>{fact.label}</dt> <dt>{fact.label}</dt>
<dd>{fact.value}</dd> <dd>{fact.value}</dd>
@@ -412,8 +433,8 @@ const InteractiveFigureInner = ({
))} ))}
</dl> </dl>
)} )}
{selectedNode.evidence.codePointer && ( {selectedEvidence.codePointer && (
<code className="figure-evidence__pointer">{selectedNode.evidence.codePointer}</code> <code className="figure-evidence__pointer">{selectedEvidence.codePointer}</code>
)} )}
</aside> </aside>
)} )}
@@ -74,4 +74,22 @@ describe("architectureCatalog", () => {
]); ]);
expect(sequence.nodes.every((node) => node.evidence !== undefined)).toBe(true); expect(sequence.nodes.every((node) => node.evidence !== undefined)).toBe(true);
}); });
it("keeps WorkflowApi nodes concise and moves operation facts into evidence spotlights", () => {
const api = figure("workflow-api-detail");
const operationIds = [
"capability-operations",
"draft-operations",
"artifact-operations",
"deployment-operations",
"run-operations",
];
for (const nodeId of operationIds) {
const node = api.nodes.find((candidate) => candidate.id === nodeId);
expect(node?.details, nodeId).toBeUndefined();
expect(node?.evidence?.facts?.length, nodeId).toBeGreaterThan(0);
expect(node?.evidence?.codePointer, nodeId).toMatch(/^src\//);
}
});
}); });
@@ -199,11 +199,17 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Discover and call workflow capabilities", summary: "Discover and call workflow capabilities",
kind: "operation", kind: "operation",
icon: "plug", icon: "plug",
details: [
{ label: "Methods", value: "list / inspect / call" },
{ label: "Input", value: "qualified name + payload" },
],
evidencePointer: "src/wf_api/service.py", evidencePointer: "src/wf_api/service.py",
evidence: {
label: "Public operations",
title: "Capability operations",
body: "Clients discover schemas before invoking one qualified capability through the same application facade.",
facts: [
{ label: "Methods", value: "list / inspect / call" },
{ label: "Call input", value: "qualified name + payload" },
],
codePointer: "src/wf_api/service.py",
},
}, },
{ {
id: "draft-operations", id: "draft-operations",
@@ -211,8 +217,14 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Validate, compile, patch authoring state", summary: "Validate, compile, patch authoring state",
kind: "operation", kind: "operation",
icon: "layers", icon: "layers",
details: [{ label: "Methods", value: "validate / compile / patch" }],
evidencePointer: "src/wf_api/service.py", evidencePointer: "src/wf_api/service.py",
evidence: {
label: "Mutable lifecycle",
title: "Draft operations",
body: "Draft operations preserve editable authoring state while exposing validation and compilation as explicit transitions.",
facts: [{ label: "Methods", value: "create / patch / validate / compile" }],
codePointer: "src/wf_api/service.py",
},
}, },
{ {
id: "artifact-operations", id: "artifact-operations",
@@ -220,8 +232,14 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Save immutable workflow versions", summary: "Save immutable workflow versions",
kind: "artifact", kind: "artifact",
icon: "database", icon: "database",
details: [{ label: "Methods", value: "create / inspect / delete" }],
evidencePointer: "src/wf_api/service.py", evidencePointer: "src/wf_api/service.py",
evidence: {
label: "Immutable lifecycle",
title: "Artifact operations",
body: "Artifact operations create and inspect immutable workflow definitions independently of environment bindings.",
facts: [{ label: "Methods", value: "list / create / inspect" }],
codePointer: "src/wf_api/service.py",
},
}, },
{ {
id: "deployment-operations", id: "deployment-operations",
@@ -229,8 +247,14 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Bind artifacts to concrete sources", summary: "Bind artifacts to concrete sources",
kind: "operation", kind: "operation",
icon: "network", icon: "network",
details: [{ label: "Methods", value: "validate / save / inspect" }],
evidencePointer: "src/wf_api/service.py", evidencePointer: "src/wf_api/service.py",
evidence: {
label: "Binding lifecycle",
title: "Deployment operations",
body: "Deployment operations inspect and validate the concrete source bindings used to run an artifact version.",
facts: [{ label: "Methods", value: "list / inspect / validate" }],
codePointer: "src/wf_api/service.py",
},
}, },
{ {
id: "run-operations", id: "run-operations",
@@ -238,8 +262,14 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Start, resume, inspect execution", summary: "Start, resume, inspect execution",
kind: "runtime", kind: "runtime",
icon: "repeat", icon: "repeat",
details: [{ label: "Methods", value: "start / resume / trace" }],
evidencePointer: "src/wf_api/service.py", evidencePointer: "src/wf_api/service.py",
evidence: {
label: "Execution lifecycle",
title: "Run operations",
body: "Run operations start deployment execution, resume declared boundaries, and expose persisted inspection evidence.",
facts: [{ label: "Methods", value: "list / start / resume / inspect / trace" }],
codePointer: "src/wf_api/service.py",
},
}, },
{ {
id: "trace-output", id: "trace-output",