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:
+37
-10
@@ -37,8 +37,9 @@ test('diagram summaries carry nodes and edges, shared and inline', () => {
|
||||
|
||||
test('the model graph unites every diagram: elements once, connections once, labels merged', () => {
|
||||
const graph = graphOfModel();
|
||||
// 11 registry elements + 3 inline components of the API diagram.
|
||||
assert.equal(graph.nodes.length, 14);
|
||||
// 11 registry elements + 3 inline components of the API diagram, plus 3 decision and 3 registry nodes.
|
||||
assert.equal(graph.nodes.filter((n) => n.kind === 'element').length, 14);
|
||||
assert.equal(graph.nodes.length, 20);
|
||||
const db = graph.nodes.find((n) => n.id === 'db')!;
|
||||
assert.equal(db.shared, true);
|
||||
assert.equal(db.type, 'database');
|
||||
@@ -49,7 +50,7 @@ test('the model graph unites every diagram: elements once, connections once, lab
|
||||
assert.equal(orders.shared, false);
|
||||
assert.deepEqual(orders.diagrams, ['0300-components/api.archify']);
|
||||
// api → db is drawn once in the containers diagram; orders → db and catalogue → db are separate connections.
|
||||
assert.equal(graph.edges.length, 4 + 7 + 6);
|
||||
assert.equal(graph.edges.filter((e) => e.kind === 'connection').length, 4 + 7 + 6);
|
||||
const apiDb = graph.edges.find((e) => e.id === 'api>db')!;
|
||||
assert.deepEqual(apiDb.labels, ['SQL']);
|
||||
assert.deepEqual(apiDb.diagrams, ['0200-containers/online-shop.archify']);
|
||||
@@ -67,12 +68,36 @@ test('decisions reach the elements they name and the nodes and edges they are at
|
||||
assert.equal(graph.decisions.find((d) => d.id === 'postgres-as-source-of-truth')!.status, 'accepted');
|
||||
});
|
||||
|
||||
test('decisions and registries are nodes of their own, linked to the elements they are about or define', () => {
|
||||
const graph = graphOfModel();
|
||||
const decision = graph.nodes.find((n) => n.id === 'decision:queue-between-api-and-worker')!;
|
||||
assert.equal(decision.kind, 'decision');
|
||||
assert.equal(decision.type, 'decision');
|
||||
assert.equal(decision.status, 'accepted');
|
||||
assert.equal(decision.sublabel, 'queue-between-api-and-worker');
|
||||
assert.deepEqual(graph.edges.filter((e) => e.kind === 'decides' && e.from === decision.id).map((e) => e.to).sort(), ['api', 'queue', 'worker']);
|
||||
const registry = graph.nodes.find((n) => n.id === 'registry:registry/containers.archify-registry')!;
|
||||
assert.equal(registry.kind, 'registry');
|
||||
assert.equal(registry.label, 'registry/containers');
|
||||
assert.deepEqual(registry.elements, ['web', 'api', 'worker', 'queue', 'db', 'cache']);
|
||||
assert.equal(graph.edges.filter((e) => e.kind === 'defines' && e.from === registry.id).length, 6);
|
||||
// Walks and paths do not go through them: a registry is not a shortcut between its elements.
|
||||
assert.deepEqual(shortestPath(graph, 'customer', 'cache')?.nodes, ['customer', 'web', 'api', 'cache']);
|
||||
const ids = (q: string) => [...runQuery(graph, q).nodes].sort();
|
||||
assert.ok(!ids('uses:api').some((id) => id.startsWith('registry:') && id !== 'registry:registry/containers.archify-registry'));
|
||||
// They follow their elements into a result, and a negative term removes them again.
|
||||
assert.deepEqual(ids('type:database').filter((id) => id.includes(':')), ['decision:opensearch-for-product-search', 'decision:postgres-as-source-of-truth', 'registry:registry/containers.archify-registry']);
|
||||
assert.deepEqual(ids('type:database -type:decision -type:registry'), ['cache', 'db']);
|
||||
assert.deepEqual(ids('type:registry'), ['registry:registry/containers.archify-registry', 'registry:registry/people.archify-registry', 'registry:registry/systems.archify-registry']);
|
||||
assert.ok(ids('decision:queue-between-api-and-worker').includes('decision:queue-between-api-and-worker'));
|
||||
});
|
||||
|
||||
test('an unused registry element is in the graph with no diagrams', () => {
|
||||
const registry = mergeRegistries([{ file: 'r', ...parseRegistry('elements:\n api: { type: backend, label: API }\n ghost: { type: database, label: Ghost }\n', 'r') }]);
|
||||
const diagrams = [summarizeDiagram('diagram_type: architecture\ncomponents:\n - ref: api\n', 'a.archify')];
|
||||
const graph = buildGraph({ meta: {}, registry, diagrams, decisions: mergeDecisions([]), decisionUsage: new Map() });
|
||||
assert.deepEqual(graph.nodes.map((n) => [n.id, n.diagrams.length]), [['api', 1], ['ghost', 0]]);
|
||||
assert.deepEqual([...runQuery(graph, 'unused').nodes], ['ghost']);
|
||||
assert.deepEqual(graph.nodes.filter((n) => n.kind === 'element').map((n) => [n.id, n.diagrams.length]), [['api', 1], ['ghost', 0]]);
|
||||
assert.deepEqual([...runQuery(graph, 'unused').nodes], ['ghost', 'registry:r']);
|
||||
});
|
||||
|
||||
test('queries parse into terms; unknown keys are plain text', () => {
|
||||
@@ -87,10 +112,12 @@ test('queries parse into terms; unknown keys are plain text', () => {
|
||||
assert.deepEqual(parseQuery(' '), []);
|
||||
});
|
||||
|
||||
const elementIds = (graph: ModelGraph, q: string) => [...runQuery(graph, q).nodes].filter((id) => !id.includes(':')).sort();
|
||||
|
||||
test('queries narrow the graph: text, type, diagram, decision, shared', () => {
|
||||
const graph = graphOfModel();
|
||||
const ids = (q: string) => [...runQuery(graph, q).nodes].sort();
|
||||
assert.equal(runQuery(graph, '').nodes.size, 14);
|
||||
const ids = (q: string) => elementIds(graph, q);
|
||||
assert.equal(runQuery(graph, '').nodes.size, 20);
|
||||
assert.deepEqual(ids('type:database'), ['cache', 'db']);
|
||||
assert.deepEqual(ids('type:external'), ['customer', 'email', 'psp', 'support']);
|
||||
assert.deepEqual(ids('worker'), ['worker']);
|
||||
@@ -107,14 +134,14 @@ test('queries narrow the graph: text, type, diagram, decision, shared', () => {
|
||||
|
||||
test('queries walk the graph: uses, near, from/to', () => {
|
||||
const graph = graphOfModel();
|
||||
const ids = (q: string) => [...runQuery(graph, q).nodes].sort();
|
||||
const ids = (q: string) => elementIds(graph, q);
|
||||
assert.deepEqual(ids('uses:api'), ['api', 'cache', 'db', 'queue', 'web']);
|
||||
assert.deepEqual(ids('uses:API'), ['api', 'cache', 'db', 'queue', 'web']);
|
||||
assert.deepEqual(ids('near:worker,2'), ['api', 'catalogue', 'db', 'orders', 'queue', 'worker']);
|
||||
assert.deepEqual(ids('near:worker 2 type:database'), ['db']);
|
||||
const result = runQuery(graph, 'from:customer to:db');
|
||||
assert.deepEqual(result.path, ['customer', 'web', 'api', 'db']);
|
||||
assert.deepEqual([...result.edges].sort(), ['api>db', 'customer>web', 'web>api']);
|
||||
assert.deepEqual([...result.edges].filter((id) => !id.includes(':')).sort(), ['api>db', 'customer>web', 'web>api']);
|
||||
// Direction is tried first; against the arrows the search still finds a way.
|
||||
assert.deepEqual(shortestPath(graph, 'db', 'customer')?.nodes, ['db', 'api', 'web', 'customer']);
|
||||
assert.equal(shortestPath(graph, 'psp', 'auth')?.nodes.length, 5);
|
||||
@@ -131,5 +158,5 @@ test('connections are shown only when both ends match', () => {
|
||||
assert.ok(result.nodes.has('api'));
|
||||
assert.ok(!result.edges.has('api>db'));
|
||||
assert.ok(result.edges.has('orders>queue') === false);
|
||||
assert.ok([...result.edges].every((id) => id.split('>').every((end) => result.nodes.has(end))));
|
||||
assert.ok([...result.edges].every((id) => { const at = id.indexOf('>', id.startsWith('decision:') || id.startsWith('registry:') ? id.indexOf(':') : 0); return result.nodes.has(id.slice(0, at)) && result.nodes.has(id.slice(at + 1)); }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user