- one node per decision (tag coloured by status) with decides links to its elements and supersedes links - one node per registry file with defines links to its elements - own legend chips, side panels (Open decision, Open registry), type:decision and type:registry - walks and paths follow the diagrams' connections only; attached nodes follow their elements into a result
197 lines
7.5 KiB
TypeScript
197 lines
7.5 KiB
TypeScript
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:<id>`) or a registry file (`registry:<file>`). */
|
|
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<string, { path: string; attachedTo: DecisionAttachment }[]>;
|
|
}
|
|
|
|
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<string, GraphNode>();
|
|
const edges = new Map<string, GraphEdge>();
|
|
|
|
// 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<string, string[]>();
|
|
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 })),
|
|
};
|
|
}
|