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