import { Decision, DecisionSet, effectiveStatus } from './decisions'; import { DecisionAttachment, DiagramSummary, ModelMeta, Registry } from './format'; /** * The model as one graph: every element any diagram draws (and every registry * element, drawn or not), every connection any diagram has between two of * them, each once. Pure: built from the model index, no VS Code imports, so * the webview bundle and the unit tests share it. */ export type GraphNodeKind = 'element' | 'decision' | 'registry'; export type GraphEdgeKind = 'connection' | 'decides' | 'defines' | 'supersedes'; export interface GraphNode { id: string; /** An element of the model, a decision (`decision:`) or a registry file (`registry:`). */ kind: GraphNodeKind; label: string; sublabel?: string; type?: string; brand?: string; /** Defined in a registry (true) or inline in one diagram (false). */ shared: boolean; description?: string; /** Registry file and 0-based line, for shared elements. */ file?: string; line?: number; /** Diagram that explains the element, relative to the model root. */ link?: string; /** Diagrams that draw it, relative paths in path order. */ diagrams: string[]; /** Decisions about it: named by the decision, or attached to its node in a diagram. */ decisions: string[]; /** Decision nodes: the effective status. */ status?: string; /** Registry nodes: ids of the elements the file defines. */ elements?: string[]; } export interface GraphEdge { /** `from>to`, unique in the graph. */ id: string; /** A connection drawn by a diagram, or a link to a decision or registry node. */ kind: GraphEdgeKind; from: string; to: string; /** Distinct labels the diagrams give this connection. */ labels: string[]; diagrams: string[]; decisions: string[]; } export interface GraphDecision { id: string; title?: string; status: string; file: string; } export interface GraphDiagram { path: string; title?: string; kind?: string; } export interface ModelGraph { meta: ModelMeta; nodes: GraphNode[]; edges: GraphEdge[]; decisions: GraphDecision[]; diagrams: GraphDiagram[]; } export interface GraphInput { meta: ModelMeta; registry: Registry; diagrams: DiagramSummary[]; decisions: DecisionSet; decisionUsage: Map; } export const edgeId = (from: string, to: string): string => `${from}>${to}`; export const decisionNodeId = (id: string): string => `decision:${id}`; export const registryNodeId = (file: string): string => `registry:${file}`; function pushUnique(list: string[], value: string): void { if (!list.includes(value)) list.push(value); } export function buildGraph(input: GraphInput): ModelGraph { const nodes = new Map(); const edges = new Map(); // Registry elements first: shared, and present even when no diagram draws them yet. for (const e of input.registry.elements.values()) { nodes.set(e.id, { id: e.id, kind: 'element', label: e.label ?? e.id, sublabel: e.sublabel, type: e.type, brand: e.brand, shared: true, description: e.description?.trim() || undefined, file: e.file, line: e.line, link: e.diagram, diagrams: [], decisions: [], }); } const diagrams = [...input.diagrams].filter((d) => !d.parseError).sort((a, b) => a.relativePath.localeCompare(b.relativePath)); for (const d of diagrams) { for (const n of d.nodes) { let node = nodes.get(n.id); if (!node) { const element = n.ref ? input.registry.elements.get(n.ref) : undefined; node = element ? { ...nodes.get(element.id)!, id: n.id, label: n.label ?? element.label ?? n.id, sublabel: n.sublabel ?? element.sublabel, diagrams: [], decisions: [] } : { id: n.id, kind: 'element', label: n.label ?? n.ref ?? n.id, sublabel: n.sublabel, type: n.type, brand: n.brand, shared: false, diagrams: [], decisions: [] }; nodes.set(n.id, node); } pushUnique(node.diagrams, d.relativePath); } for (const e of d.edges) { const id = edgeId(e.from, e.to); let edge = edges.get(id); if (!edge) { edge = { id, kind: 'connection', from: e.from, to: e.to, labels: [], diagrams: [], decisions: [] }; edges.set(id, edge); } if (e.label) pushUnique(edge.labels, e.label); pushUnique(edge.diagrams, d.relativePath); } } const all = input.decisions.decisions; for (const d of all.values()) for (const id of d.elements) nodes.get(id)?.decisions && pushUnique(nodes.get(id)!.decisions, d.id); for (const [decisionId, uses] of input.decisionUsage) { if (!all.has(decisionId)) continue; for (const use of uses) { if (use.attachedTo.kind === 'node') { const node = nodes.get(use.attachedTo.id); if (node) pushUnique(node.decisions, decisionId); } else if (use.attachedTo.kind === 'edge') { const edge = edges.get(edgeId(use.attachedTo.from, use.attachedTo.to)); if (edge) pushUnique(edge.decisions, decisionId); } } } const decisionRow = (d: Decision): GraphDecision => ({ id: d.id, title: d.title, status: effectiveStatus(d, all).status, file: d.file }); // Registry files as nodes, each linked to the elements it defines. const byFile = new Map(); for (const e of input.registry.elements.values()) byFile.set(e.file, [...(byFile.get(e.file) ?? []), e.id]); for (const file of input.registry.files) if (!byFile.has(file)) byFile.set(file, []); for (const [file, ids] of byFile) { const id = registryNodeId(file); nodes.set(id, { id, kind: 'registry', label: file.replace(/\.archify-registry$/, ''), type: 'registry', shared: true, file, diagrams: [], decisions: [], elements: ids }); for (const element of ids) { const edge = edgeId(id, element); edges.set(edge, { id: edge, kind: 'defines', from: id, to: element, labels: [], diagrams: [], decisions: [] }); } } // Decisions as nodes, linked to the elements and connections they are about, and to what they supersede. for (const d of all.values()) { const id = decisionNodeId(d.id); const row = decisionRow(d); nodes.set(id, { id, kind: 'decision', label: d.title ?? d.id, sublabel: d.id, type: 'decision', shared: true, description: [d.context, d.decision].filter(Boolean).join('\n\n') || undefined, file: d.file, line: 0, diagrams: [...d.diagrams], decisions: [], status: row.status }); } for (const node of [...nodes.values()]) { if (node.kind !== 'element') continue; for (const decision of node.decisions) { if (!all.has(decision)) continue; const edge = edgeId(decisionNodeId(decision), node.id); edges.set(edge, { id: edge, kind: 'decides', from: decisionNodeId(decision), to: node.id, labels: [], diagrams: [], decisions: [] }); } } for (const d of all.values()) { for (const other of d.supersedes) { if (!all.has(other)) continue; const edge = edgeId(decisionNodeId(d.id), decisionNodeId(other)); edges.set(edge, { id: edge, kind: 'supersedes', from: decisionNodeId(d.id), to: decisionNodeId(other), labels: ['supersedes'], diagrams: [], decisions: [] }); } } return { meta: input.meta, nodes: [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)), edges: [...edges.values()].sort((a, b) => a.id.localeCompare(b.id)), decisions: [...all.values()].map(decisionRow).sort((a, b) => a.id.localeCompare(b.id)), diagrams: diagrams.map((d) => ({ path: d.relativePath, title: d.title, kind: d.kind })), }; }