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",
layout: { kind: "layered" },
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: "leaf",
@@ -210,6 +217,18 @@ describe("InteractiveFigure", () => {
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", () => {
renderFigure({ focusPath: [], size: "stage" });
@@ -23,6 +23,7 @@ import type {
FigureCatalogDefinition,
FigureLayoutKind,
FigureNodeDefinition,
FigureNodeEvidence,
FigureNodeIcon,
FigureNodeKind,
FigureNodeShape,
@@ -65,6 +66,22 @@ type FigureNodeData = {
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> = {
users: Users,
terminal: Terminal,
@@ -285,37 +302,41 @@ const InteractiveFigureInner = ({
focusedNodeIdRef.current = nodeId;
setFocusedNodeId(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]);
const selectedNode = selectedNodeId === null
? undefined
: layout.nodes.find((node) => node.id === selectedNodeId);
const selectedEvidence = selectedNode ? evidenceForNode(selectedNode) : undefined;
const rfNodes: Node[] = useMemo(
() =>
layout.nodes.map((node) => ({
id: node.id,
type: "figure",
position: node.position,
data: {
nodeId: node.id,
label: node.label,
summary: node.summary,
kind: node.kind,
shape: node.shape ?? shapeByKind[node.kind],
icon: node.icon ?? null,
details: node.details,
evidence: node.evidence,
orientation: horizontalLayouts.has(layout.definition.layout.kind) ? "horizontal" : "vertical",
isActive: node.id === activeNodeId,
isFocused: node.id === focusedNodeId,
isSelected: node.id === selectedNodeId,
isExpandable: node.childFigureId !== undefined,
onActivate: handleActivateNode,
onExpand: handleExpand,
},
})),
layout.nodes.map((node) => {
const evidence = evidenceForNode(node);
return {
id: node.id,
type: "figure",
position: node.position,
data: {
nodeId: node.id,
label: node.label,
summary: node.summary,
kind: node.kind,
shape: node.shape ?? shapeByKind[node.kind],
icon: node.icon ?? null,
details: evidence ? undefined : node.details,
evidence,
orientation: horizontalLayouts.has(layout.definition.layout.kind) ? "horizontal" : "vertical",
isActive: node.id === activeNodeId,
isFocused: node.id === focusedNodeId,
isSelected: node.id === selectedNodeId,
isExpandable: node.childFigureId !== undefined,
onActivate: handleActivateNode,
onExpand: handleExpand,
},
};
}),
[layout.definition.layout.kind, layout.nodes, activeNodeId, focusedNodeId, selectedNodeId, handleActivateNode, handleExpand],
);
@@ -387,10 +408,10 @@ const InteractiveFigureInner = ({
<FitViewOnLayoutChange layoutKey={focus.figure.id} />
</ReactFlow>
</div>
{selectedNode?.evidence && (
{selectedNode && selectedEvidence && (
<aside className="figure-evidence" role="region" aria-label={`${selectedNode.label} evidence`}>
<div className="figure-evidence__header">
<span className="figure-evidence__label">{selectedNode.evidence.label}</span>
<span className="figure-evidence__label">{selectedEvidence.label}</span>
<button
type="button"
className="figure-evidence__close"
@@ -400,11 +421,11 @@ const InteractiveFigureInner = ({
Close
</button>
</div>
<h3>{selectedNode.evidence.title}</h3>
<p>{selectedNode.evidence.body}</p>
{selectedNode.evidence.facts && selectedNode.evidence.facts.length > 0 && (
<h3>{selectedEvidence.title}</h3>
<p>{selectedEvidence.body}</p>
{selectedEvidence.facts && selectedEvidence.facts.length > 0 && (
<dl className="figure-evidence__facts">
{selectedNode.evidence.facts.map((fact) => (
{selectedEvidence.facts.map((fact) => (
<div key={`${fact.label}-${fact.value}`}>
<dt>{fact.label}</dt>
<dd>{fact.value}</dd>
@@ -412,8 +433,8 @@ const InteractiveFigureInner = ({
))}
</dl>
)}
{selectedNode.evidence.codePointer && (
<code className="figure-evidence__pointer">{selectedNode.evidence.codePointer}</code>
{selectedEvidence.codePointer && (
<code className="figure-evidence__pointer">{selectedEvidence.codePointer}</code>
)}
</aside>
)}
@@ -74,4 +74,22 @@ describe("architectureCatalog", () => {
]);
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",
kind: "operation",
icon: "plug",
details: [
{ label: "Methods", value: "list / inspect / call" },
{ label: "Input", value: "qualified name + payload" },
],
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",
@@ -211,8 +217,14 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Validate, compile, patch authoring state",
kind: "operation",
icon: "layers",
details: [{ label: "Methods", value: "validate / compile / patch" }],
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",
@@ -220,8 +232,14 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Save immutable workflow versions",
kind: "artifact",
icon: "database",
details: [{ label: "Methods", value: "create / inspect / delete" }],
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",
@@ -229,8 +247,14 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Bind artifacts to concrete sources",
kind: "operation",
icon: "network",
details: [{ label: "Methods", value: "validate / save / inspect" }],
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",
@@ -238,8 +262,14 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
summary: "Start, resume, inspect execution",
kind: "runtime",
icon: "repeat",
details: [{ label: "Methods", value: "start / resume / trace" }],
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",