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:
2026-10-21 20:46:05 +03:00
parent c8645b228b
commit 99ea076571
8 changed files with 237 additions and 42 deletions
+26 -9
View File
@@ -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) {