Model graph: decisions and registry files as nodes
- 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
This commit is contained in:
1 parent
c8645b228b
commit
99ea076571
8 files changed
+237
-42
No files matched your search
+51
-2
@@ -8,8 +8,13 @@ import { DecisionAttachment, DiagramSummary, ModelMeta, Registry } from './forma
|
||||
* 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;
|
||||
@@ -26,11 +31,17 @@ export interface GraphNode {
|
||||
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. */
|
||||
@@ -69,6 +80,8 @@ export interface GraphInput {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -82,6 +95,7 @@ export function buildGraph(input: GraphInput): ModelGraph {
|
||||
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,
|
||||
@@ -104,7 +118,7 @@ export function buildGraph(input: GraphInput): ModelGraph {
|
||||
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: [] };
|
||||
: { 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);
|
||||
@@ -113,7 +127,7 @@ export function buildGraph(input: GraphInput): ModelGraph {
|
||||
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: [] };
|
||||
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);
|
||||
@@ -137,6 +151,41 @@ export function buildGraph(input: GraphInput): ModelGraph {
|
||||
}
|
||||
|
||||
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)),
|
||||
|
||||
+26
-9
@@ -67,12 +67,16 @@ export function parseQuery(text: string): QueryTerm[] {
|
||||
|
||||
const lower = (s: string | undefined) => (s ?? '').toLowerCase();
|
||||
|
||||
/** An element by id, then by id or label ignoring case. */
|
||||
const isElement = (n: GraphNode) => n.kind === 'element';
|
||||
const isConnection = (e: GraphEdge) => e.kind === 'connection';
|
||||
|
||||
/** An element by id, then by id or label ignoring case. Decision and registry nodes are not walked from. */
|
||||
export function findNode(graph: ModelGraph, ref: string): GraphNode | undefined {
|
||||
const exact = graph.nodes.find((n) => n.id === ref);
|
||||
const elements = graph.nodes.filter(isElement);
|
||||
const exact = elements.find((n) => n.id === ref);
|
||||
if (exact) return exact;
|
||||
const l = ref.toLowerCase();
|
||||
return graph.nodes.find((n) => n.id.toLowerCase() === l) ?? graph.nodes.find((n) => n.label.toLowerCase() === l);
|
||||
return elements.find((n) => n.id.toLowerCase() === l) ?? elements.find((n) => n.label.toLowerCase() === l);
|
||||
}
|
||||
|
||||
function globToRegExp(pattern: string): RegExp {
|
||||
@@ -88,10 +92,11 @@ function diagramMatches(path: string, pattern: string): boolean {
|
||||
return bare.toLowerCase() === l || bare.toLowerCase().startsWith(`${l}/`) || bare.toLowerCase().startsWith(l) || bare.toLowerCase().includes(`/${l}`);
|
||||
}
|
||||
|
||||
/** Element-to-element adjacency over the diagrams' connections; decision and registry links do not count as steps. */
|
||||
function neighbours(graph: ModelGraph): Map<string, Set<string>> {
|
||||
const map = new Map<string, Set<string>>();
|
||||
for (const n of graph.nodes) map.set(n.id, new Set());
|
||||
for (const e of graph.edges) {
|
||||
for (const e of graph.edges.filter(isConnection)) {
|
||||
map.get(e.from)?.add(e.to);
|
||||
map.get(e.to)?.add(e.from);
|
||||
}
|
||||
@@ -116,7 +121,7 @@ export function shortestPath(graph: ModelGraph, from: string, to: string): { nod
|
||||
const attempt = (directed: boolean) => {
|
||||
const out = new Map<string, { to: string; edge: GraphEdge }[]>();
|
||||
for (const n of graph.nodes) out.set(n.id, []);
|
||||
for (const e of graph.edges) {
|
||||
for (const e of graph.edges.filter(isConnection)) {
|
||||
out.get(e.from)?.push({ to: e.to, edge: e });
|
||||
if (!directed) out.get(e.to)?.push({ to: e.from, edge: e });
|
||||
}
|
||||
@@ -174,12 +179,13 @@ export function runQuery(graph: ModelGraph, text: string): QueryResult {
|
||||
const ids = new Set(graph.nodes.filter((n) => n.decisions.includes(v)).map((n) => n.id));
|
||||
for (const e of graph.edges) if (e.decisions.includes(v)) { ids.add(e.from); ids.add(e.to); }
|
||||
if (!graph.decisions.some((d) => d.id === v)) notes.push(`No decision "${v}" in this model.`);
|
||||
else ids.add(`decision:${v}`);
|
||||
return ids;
|
||||
}
|
||||
case 'shared':
|
||||
return new Set(graph.nodes.filter((n) => n.shared).map((n) => n.id));
|
||||
return new Set(graph.nodes.filter((n) => isElement(n) && n.shared).map((n) => n.id));
|
||||
case 'unused':
|
||||
return new Set(graph.nodes.filter((n) => n.shared && n.diagrams.length === 0).map((n) => n.id));
|
||||
return new Set(graph.nodes.filter((n) => isElement(n) && n.shared && n.diagrams.length === 0).map((n) => n.id));
|
||||
case 'uses':
|
||||
case 'near': {
|
||||
const [ref, hopsText] = term.key === 'near' ? v.split(',') : [v, '1'];
|
||||
@@ -198,11 +204,22 @@ export function runQuery(graph: ModelGraph, text: string): QueryResult {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const negated: Set<string>[] = [];
|
||||
for (const term of terms) {
|
||||
const ids = matching(term);
|
||||
if (!ids) continue;
|
||||
if (term.negate) for (const id of ids) result.delete(id);
|
||||
else result = new Set([...result].filter((id) => ids.has(id)));
|
||||
if (term.negate) {
|
||||
negated.push(ids);
|
||||
for (const id of ids) result.delete(id);
|
||||
} else result = new Set([...result].filter((id) => ids.has(id)));
|
||||
}
|
||||
// Decisions and registries follow their elements: a matching element brings the nodes attached to it.
|
||||
if (terms.length) {
|
||||
for (const e of graph.edges) {
|
||||
if (isConnection(e)) continue;
|
||||
if (result.has(e.to) && graph.nodes.some((n) => n.id === e.from && !isElement(n))) result.add(e.from);
|
||||
}
|
||||
for (const ids of negated) for (const id of ids) result.delete(id);
|
||||
}
|
||||
|
||||
if (from || to) {
|
||||
|
||||
Reference in new issue
Block a user