Model graph: the whole model as one graph with queries; 0.9.0
- Model | Graph switch on main.archify (Diagram | Graph on diagram pages, narrowed to that diagram) - union graph of every element and connection, registry and inline, built from the model index - query box: text, type:, diagram:, decision:, uses:, near:, from: to:, unused, shared, -shared - side panel per element and connection with diagrams, registry line and decisions one click away - legend chips, Fit, Layout, Labels, Export PNG; Cytoscape.js bundled as dist/graph.js - command Archify | Open Model Graph; integration check; unit tests for graph and queries
This commit is contained in:
1 parent
265f8c57f6
commit
c8645b228b
20 files changed
+1551
-30
No files matched your search
@@ -0,0 +1,147 @@
|
||||
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 interface GraphNode {
|
||||
id: string;
|
||||
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[];
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
/** `from>to`, unique in the graph. */
|
||||
id: string;
|
||||
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}`;
|
||||
|
||||
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,
|
||||
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, 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, 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 });
|
||||
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 })),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user