feat: expose architecture node evidence spotlights
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user