@webpieces/nx-webpieces-rules 0.4.907 → 0.4.909

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuntimeNodeLevels = void 0;
4
+ exports.runtimeAdjacency = runtimeAdjacency;
5
+ const runtime_graph_model_1 = require("./runtime-graph-model");
6
+ const runtime_graph_levels_1 = require("./runtime-graph-levels");
7
+ /** Generator-owned layering. Renderers consume these numbers without deriving topology. */
8
+ class RuntimeNodeLevels {
9
+ apply(graph) {
10
+ this.collectContracts(graph);
11
+ const adjacency = this.adjacency(graph);
12
+ const order = (0, runtime_graph_levels_1.assignLevels)(adjacency); // The shared detector reports queue identities in cycles.
13
+ const levels = {};
14
+ for (const name of Object.keys(order).sort((a, b) => order[a] - order[b] || a.localeCompare(b))) {
15
+ const floor = graph.services[name] === undefined ? 0 : 1;
16
+ levels[name] = Math.max(floor, ...adjacency[name].map((target) => levels[target] + 1));
17
+ }
18
+ for (const [name, service] of Object.entries(graph.services)) {
19
+ service.level = levels[name];
20
+ const highest = [...adjacency[name]].sort((a, b) => levels[b] - levels[a] || a.localeCompare(b))[0];
21
+ service.levelBecause =
22
+ highest === undefined
23
+ ? 'no callees (service starts at L1)'
24
+ : `${graph.services[highest] === undefined ? highest : `rpc:${highest}`} (L${levels[highest]})`;
25
+ }
26
+ for (const entry of this.queues(graph))
27
+ this.queueLevel(entry[0], entry[1], graph, levels);
28
+ for (const system of Object.values(graph.externalSystems ?? {}))
29
+ this.sink(system);
30
+ for (const remote of Object.values(graph.remoteServices ?? {}))
31
+ this.sink(remote);
32
+ for (const use of graph.unresolvedUses)
33
+ this.sink(use);
34
+ const top = 1 +
35
+ Math.max(0, ...Object.values(levels), ...this.queues(graph).map((entry) => entry[1].level));
36
+ for (const trigger of graph.triggers) {
37
+ trigger.level = top;
38
+ trigger.levelBecause = `above every non-trigger node (L${top - 1})`;
39
+ }
40
+ graph.maxLevel = graph.triggers.length > 0 ? top : top - 1;
41
+ }
42
+ /** All runtime dependencies except self-producers and explicitly admitted legacy RPC cuts. */
43
+ adjacency(graph) {
44
+ const adj = {};
45
+ for (const name of Object.keys(graph.services))
46
+ adj[name] = [];
47
+ for (const edge of graph.runtimeEdges) {
48
+ if (edge.type !== 'pubsub' && edge.cutLegacyCycle !== true)
49
+ adj[edge.from].push(edge.to);
50
+ }
51
+ for (const entry of this.queues(graph)) {
52
+ const key = entry[0];
53
+ const queue = entry[1];
54
+ const foreign = queue.producedBy.filter((producer) => queue.consumedBy.some((consumer) => consumer !== producer));
55
+ queue.selfQueue = foreign.length === 0;
56
+ if (queue.selfQueue)
57
+ continue;
58
+ adj[`queue:${key}`] = [...queue.consumedBy];
59
+ for (const producer of foreign)
60
+ adj[producer].push(`queue:${key}`);
61
+ }
62
+ for (const [name, system] of Object.entries(graph.externalSystems ?? {})) {
63
+ adj[`external:${name}`] = [];
64
+ for (const caller of system.usedBy)
65
+ adj[caller].push(`external:${name}`);
66
+ }
67
+ for (const [name, remote] of Object.entries(graph.remoteServices ?? {})) {
68
+ adj[`remote:${name}`] = [];
69
+ for (const call of remote.calledBy)
70
+ adj[call.from].push(`remote:${name}`);
71
+ }
72
+ for (const use of graph.unresolvedUses) {
73
+ const id = `external:${graph.apis[use.api]?.owner ?? use.api}`;
74
+ adj[id] = [];
75
+ adj[use.service].push(id);
76
+ }
77
+ for (const targets of Object.values(adj))
78
+ targets.sort();
79
+ return adj;
80
+ }
81
+ collectContracts(graph) {
82
+ graph.contractQueues = {};
83
+ for (const edge of graph.runtimeEdges) {
84
+ if (edge.type !== 'pubsub' || edge.queue !== undefined)
85
+ continue;
86
+ for (const api of edge.via) {
87
+ const queue = (graph.contractQueues[api] ??= new runtime_graph_model_1.RuntimeQueueFlow());
88
+ if (!queue.producedBy.includes(edge.from))
89
+ queue.producedBy.push(edge.from);
90
+ if (!queue.consumedBy.includes(edge.to))
91
+ queue.consumedBy.push(edge.to);
92
+ }
93
+ }
94
+ for (const queue of Object.values(graph.contractQueues)) {
95
+ queue.producedBy.sort();
96
+ queue.consumedBy.sort();
97
+ }
98
+ }
99
+ queues(graph) {
100
+ return [...Object.entries(graph.queues), ...Object.entries(graph.contractQueues)];
101
+ }
102
+ queueLevel(key, queue, graph, levels) {
103
+ const consumerLevel = Math.max(0, ...queue.consumedBy.map((name) => graph.services[name].level));
104
+ queue.level = queue.selfQueue ? consumerLevel : levels[`queue:${key}`];
105
+ queue.levelBecause = `${queue.selfQueue ? 'self-queue beside' : 'above'} consumer ${queue.consumedBy.join(', ')} (L${consumerLevel})`;
106
+ }
107
+ sink(node) {
108
+ node.level = 0;
109
+ node.levelBecause = 'outbound sink';
110
+ }
111
+ }
112
+ exports.RuntimeNodeLevels = RuntimeNodeLevels;
113
+ /** Complete generated runtime dependencies, including foreign queues and outbound sinks. */
114
+ // webpieces-disable no-function-outside-class -- runtime graph entry point, sibling of deriveRuntimeGraph
115
+ function runtimeAdjacency(graph) {
116
+ return new RuntimeNodeLevels().adjacency(graph);
117
+ }
118
+ //# sourceMappingURL=runtime-node-levels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-node-levels.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-node-levels.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RuntimeGraph, RuntimeLevel } from './runtime-graph-model';\nimport { RuntimeQueueFlow } from './runtime-graph-model';\nimport { assignLevels } from './runtime-graph-levels';\n\ntype QueueEntry = [string, RuntimeQueueFlow];\n\n/** Generator-owned layering. Renderers consume these numbers without deriving topology. */\nexport class RuntimeNodeLevels {\n apply(graph: RuntimeGraph): void {\n this.collectContracts(graph);\n const adjacency = this.adjacency(graph);\n const order = assignLevels(adjacency); // The shared detector reports queue identities in cycles.\n const levels: Record<string, number> = {};\n for (const name of Object.keys(order).sort(\n (a, b) => order[a] - order[b] || a.localeCompare(b),\n )) {\n const floor = graph.services[name] === undefined ? 0 : 1;\n levels[name] = Math.max(floor, ...adjacency[name].map((target) => levels[target] + 1));\n }\n for (const [name, service] of Object.entries(graph.services)) {\n service.level = levels[name];\n const highest = [...adjacency[name]].sort(\n (a, b) => levels[b] - levels[a] || a.localeCompare(b),\n )[0];\n service.levelBecause =\n highest === undefined\n ? 'no callees (service starts at L1)'\n : `${graph.services[highest] === undefined ? highest : `rpc:${highest}`} (L${levels[highest]})`;\n }\n for (const entry of this.queues(graph)) this.queueLevel(entry[0], entry[1], graph, levels);\n for (const system of Object.values(graph.externalSystems ?? {})) this.sink(system);\n for (const remote of Object.values(graph.remoteServices ?? {})) this.sink(remote);\n for (const use of graph.unresolvedUses) this.sink(use);\n const top =\n 1 +\n Math.max(\n 0,\n ...Object.values(levels),\n ...this.queues(graph).map((entry) => entry[1].level),\n );\n for (const trigger of graph.triggers) {\n trigger.level = top;\n trigger.levelBecause = `above every non-trigger node (L${top - 1})`;\n }\n graph.maxLevel = graph.triggers.length > 0 ? top : top - 1;\n }\n\n /** All runtime dependencies except self-producers and explicitly admitted legacy RPC cuts. */\n adjacency(graph: RuntimeGraph): Record<string, string[]> {\n const adj: Record<string, string[]> = {};\n for (const name of Object.keys(graph.services)) adj[name] = [];\n for (const edge of graph.runtimeEdges) {\n if (edge.type !== 'pubsub' && edge.cutLegacyCycle !== true)\n adj[edge.from].push(edge.to);\n }\n for (const entry of this.queues(graph)) {\n const key = entry[0];\n const queue = entry[1];\n const foreign = queue.producedBy.filter((producer) =>\n queue.consumedBy.some((consumer) => consumer !== producer),\n );\n queue.selfQueue = foreign.length === 0;\n if (queue.selfQueue) continue;\n adj[`queue:${key}`] = [...queue.consumedBy];\n for (const producer of foreign) adj[producer].push(`queue:${key}`);\n }\n for (const [name, system] of Object.entries(graph.externalSystems ?? {})) {\n adj[`external:${name}`] = [];\n for (const caller of system.usedBy) adj[caller].push(`external:${name}`);\n }\n for (const [name, remote] of Object.entries(graph.remoteServices ?? {})) {\n adj[`remote:${name}`] = [];\n for (const call of remote.calledBy) adj[call.from].push(`remote:${name}`);\n }\n for (const use of graph.unresolvedUses) {\n const id = `external:${graph.apis[use.api]?.owner ?? use.api}`;\n adj[id] = [];\n adj[use.service].push(id);\n }\n for (const targets of Object.values(adj)) targets.sort();\n return adj;\n }\n\n private collectContracts(graph: RuntimeGraph): void {\n graph.contractQueues = {};\n for (const edge of graph.runtimeEdges) {\n if (edge.type !== 'pubsub' || edge.queue !== undefined) continue;\n for (const api of edge.via) {\n const queue = (graph.contractQueues[api] ??= new RuntimeQueueFlow());\n if (!queue.producedBy.includes(edge.from)) queue.producedBy.push(edge.from);\n if (!queue.consumedBy.includes(edge.to)) queue.consumedBy.push(edge.to);\n }\n }\n for (const queue of Object.values(graph.contractQueues)) {\n queue.producedBy.sort();\n queue.consumedBy.sort();\n }\n }\n\n private queues(graph: RuntimeGraph): QueueEntry[] {\n return [...Object.entries(graph.queues), ...Object.entries(graph.contractQueues)];\n }\n\n private queueLevel(\n key: string,\n queue: RuntimeQueueFlow,\n graph: RuntimeGraph,\n levels: Record<string, number>,\n ): void {\n const consumerLevel = Math.max(\n 0,\n ...queue.consumedBy.map((name) => graph.services[name].level),\n );\n queue.level = queue.selfQueue ? consumerLevel : levels[`queue:${key}`];\n queue.levelBecause = `${queue.selfQueue ? 'self-queue beside' : 'above'} consumer ${queue.consumedBy.join(', ')} (L${consumerLevel})`;\n }\n\n private sink(node: RuntimeLevel): void {\n node.level = 0;\n node.levelBecause = 'outbound sink';\n }\n}\n\n/** Complete generated runtime dependencies, including foreign queues and outbound sinks. */\n// webpieces-disable no-function-outside-class -- runtime graph entry point, sibling of deriveRuntimeGraph\nexport function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]> {\n return new RuntimeNodeLevels().adjacency(graph);\n}\n"]}
@@ -64,6 +64,8 @@ class RemoteCallCollector {
64
64
  for (const name of names) {
65
65
  const decl = this.remotes[name];
66
66
  remoteServices[name] = {
67
+ level: 0,
68
+ levelBecause: 'outbound sink',
67
69
  label: decl.label,
68
70
  implements: decl.implements.map((entry) => `${entry.api} (${entry.type})`),
69
71
  calledBy: this.calledBy(name),
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-remote-calls.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-remote-calls.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Remote service calls in the runtime graph (#1203)\n *\n * A `uses` whose named target (the call-site `targetService`, else the caller's `callsService`) is a\n * REVIEWED remote service — declared in dependencies.json `remoteServices`, outside this workspace —\n * is claimed here instead of being resolved against the workspace's own nodes. Without this the call\n * either decayed into an \"unresolved\" vendor box (nothing in-repo implements the contract) or failed as\n * a name no module answers to (something in-repo does).\n *\n * Workspace names always win: a remote name that collides with a node is reported as a problem and\n * never claims the call, so a stray declaration cannot redirect a call away from a real module.\n */\n\nimport type { ApiRef, ApiTransport } from './api-usage/api-relations';\nimport type { RemoteServiceApi, RemoteServices } from './remote-services';\nimport type { RuntimeGraph, RuntimeRemoteCall, RuntimeRemoteService } from './runtime-graph-model';\n\nexport class RemoteCallCollector {\n /** remote name -> \"caller transport\" -> contracts called there. */\n private readonly calls = new Map<string, Map<string, Set<string>>>();\n\n constructor(\n private readonly remotes: RemoteServices,\n /** True when a workspace runtime node answers to the name. */\n private readonly isWorkspaceName: (name: string) => boolean,\n private readonly problems: string[],\n ) {\n for (const name of Object.keys(remotes).sort())\n if (isWorkspaceName(name))\n problems.push(\n `remoteServices declares '${name}', but a workspace module already answers to that name — a remote ` +\n `service must live OUTSIDE this workspace. Remove the remoteServices entry or rename one of them.`,\n );\n }\n\n /**\n * True when `wanted` names a declared remote service, which then owns this use. A remote that does\n * not declare the contract + transport is a problem; the call is still drawn to it, so the picture\n * shows exactly what the declaration is missing.\n */\n claim(user: string, ref: ApiRef, wanted: string | undefined): boolean {\n if (wanted === undefined) return false;\n const remote = this.remotes[wanted];\n if (remote === undefined || this.isWorkspaceName(wanted)) return false;\n if (!remote.implementsApi(ref.api, ref.type))\n this.problems.push(\n `${user} calls \"${ref.api}\" (${ref.type}) at remote service '${wanted}', but remoteServices.${wanted}.implements ` +\n `does not declare it. Add { \"api\": \"${ref.api}\", \"type\": \"${ref.type}\" } after review, or fix the target.`,\n );\n let byCaller = this.calls.get(wanted);\n if (byCaller === undefined) {\n byCaller = new Map<string, Set<string>>();\n this.calls.set(wanted, byCaller);\n }\n const key = `${user} ${ref.type}`;\n if (!byCaller.has(key)) byCaller.set(key, new Set<string>());\n byCaller.get(key)!.add(ref.api);\n return true;\n }\n\n /** Every DECLARED remote service, called or not, onto the graph. Absent when none are declared. */\n attach(graph: RuntimeGraph): void {\n const names = Object.keys(this.remotes).sort();\n if (names.length === 0) return;\n const remoteServices: Record<string, RuntimeRemoteService> = {};\n for (const name of names) {\n const decl = this.remotes[name];\n remoteServices[name] = {\n label: decl.label,\n implements: decl.implements.map(\n (entry: RemoteServiceApi) => `${entry.api} (${entry.type})`,\n ),\n calledBy: this.calledBy(name),\n };\n }\n graph.remoteServices = remoteServices;\n }\n\n private calledBy(name: string): RuntimeRemoteCall[] {\n const byCaller = this.calls.get(name) ?? new Map<string, Set<string>>();\n return [...byCaller.keys()].sort().map((key: string): RuntimeRemoteCall => {\n const parts = key.split(' ');\n return {\n from: parts[0],\n via: [...byCaller.get(key)!].sort(),\n type: parts[1] as ApiTransport,\n };\n });\n }\n}\n"]}
1
+ {"version":3,"file":"runtime-remote-calls.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-remote-calls.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Remote service calls in the runtime graph (#1203)\n *\n * A `uses` whose named target (the call-site `targetService`, else the caller's `callsService`) is a\n * REVIEWED remote service — declared in dependencies.json `remoteServices`, outside this workspace —\n * is claimed here instead of being resolved against the workspace's own nodes. Without this the call\n * either decayed into an \"unresolved\" vendor box (nothing in-repo implements the contract) or failed as\n * a name no module answers to (something in-repo does).\n *\n * Workspace names always win: a remote name that collides with a node is reported as a problem and\n * never claims the call, so a stray declaration cannot redirect a call away from a real module.\n */\n\nimport type { ApiRef, ApiTransport } from './api-usage/api-relations';\nimport type { RemoteServiceApi, RemoteServices } from './remote-services';\nimport type { RuntimeGraph, RuntimeRemoteCall, RuntimeRemoteService } from './runtime-graph-model';\n\nexport class RemoteCallCollector {\n /** remote name -> \"caller transport\" -> contracts called there. */\n private readonly calls = new Map<string, Map<string, Set<string>>>();\n\n constructor(\n private readonly remotes: RemoteServices,\n /** True when a workspace runtime node answers to the name. */\n private readonly isWorkspaceName: (name: string) => boolean,\n private readonly problems: string[],\n ) {\n for (const name of Object.keys(remotes).sort())\n if (isWorkspaceName(name))\n problems.push(\n `remoteServices declares '${name}', but a workspace module already answers to that name — a remote ` +\n `service must live OUTSIDE this workspace. Remove the remoteServices entry or rename one of them.`,\n );\n }\n\n /**\n * True when `wanted` names a declared remote service, which then owns this use. A remote that does\n * not declare the contract + transport is a problem; the call is still drawn to it, so the picture\n * shows exactly what the declaration is missing.\n */\n claim(user: string, ref: ApiRef, wanted: string | undefined): boolean {\n if (wanted === undefined) return false;\n const remote = this.remotes[wanted];\n if (remote === undefined || this.isWorkspaceName(wanted)) return false;\n if (!remote.implementsApi(ref.api, ref.type))\n this.problems.push(\n `${user} calls \"${ref.api}\" (${ref.type}) at remote service '${wanted}', but remoteServices.${wanted}.implements ` +\n `does not declare it. Add { \"api\": \"${ref.api}\", \"type\": \"${ref.type}\" } after review, or fix the target.`,\n );\n let byCaller = this.calls.get(wanted);\n if (byCaller === undefined) {\n byCaller = new Map<string, Set<string>>();\n this.calls.set(wanted, byCaller);\n }\n const key = `${user} ${ref.type}`;\n if (!byCaller.has(key)) byCaller.set(key, new Set<string>());\n byCaller.get(key)!.add(ref.api);\n return true;\n }\n\n /** Every DECLARED remote service, called or not, onto the graph. Absent when none are declared. */\n attach(graph: RuntimeGraph): void {\n const names = Object.keys(this.remotes).sort();\n if (names.length === 0) return;\n const remoteServices: Record<string, RuntimeRemoteService> = {};\n for (const name of names) {\n const decl = this.remotes[name];\n remoteServices[name] = {\n level: 0,\n levelBecause: 'outbound sink',\n label: decl.label,\n implements: decl.implements.map(\n (entry: RemoteServiceApi) => `${entry.api} (${entry.type})`,\n ),\n calledBy: this.calledBy(name),\n };\n }\n graph.remoteServices = remoteServices;\n }\n\n private calledBy(name: string): RuntimeRemoteCall[] {\n const byCaller = this.calls.get(name) ?? new Map<string, Set<string>>();\n return [...byCaller.keys()].sort().map((key: string): RuntimeRemoteCall => {\n const parts = key.split(' ');\n return {\n from: parts[0],\n via: [...byCaller.get(key)!].sort(),\n type: parts[1] as ApiTransport,\n };\n });\n }\n}\n"]}
@@ -5,6 +5,8 @@ const graph_render_model_1 = require("./graph-render-model");
5
5
  const graph_color_modes_1 = require("./graph-color-modes");
6
6
  const graph_products_1 = require("./graph-products");
7
7
  const graph_level_bands_1 = require("./graph-level-bands");
8
+ const remote_service_node_id_1 = require("./remote-service-node-id");
9
+ const runtime_external_triggers_1 = require("./runtime-external-triggers");
8
10
  /** Saved project facts enrich presentation without changing runtime derivation or topology. */
9
11
  class RuntimeViewerAdapter {
10
12
  graph;
@@ -28,11 +30,44 @@ class RuntimeViewerAdapter {
28
30
  return model.styledNode(facts, this.styler.dots(facts, this.palette));
29
31
  }
30
32
  levelBands(model) {
31
- return [...new Set(model.nodes.map((node) => node.tags.level))]
32
- .sort((a, b) => b - a)
33
- .map((level) => new graph_level_bands_1.LevelBand(level, model.nodes
34
- .filter((node) => node.tags?.level === level)
35
- .map((node) => node.id)));
33
+ const levels = new Map();
34
+ const safe = (name) => name.replace(/[^A-Za-z0-9_]/g, '_');
35
+ for (const [name, service] of Object.entries(this.graph.services))
36
+ levels.set(name, service.level);
37
+ for (const node of model.nodes) {
38
+ if (node.queueKeys.length === 0)
39
+ continue;
40
+ const key = node.queueKeys[0];
41
+ const queue = key.startsWith('queue:')
42
+ ? this.graph.contractQueues[key.slice(6)]
43
+ : this.graph.queues[key];
44
+ levels.set(node.id, queue.level);
45
+ }
46
+ for (const [name, system] of Object.entries(this.graph.externalSystems ?? {}))
47
+ levels.set(`system__${safe(name)}`, system.level);
48
+ for (const [name, remote] of Object.entries(this.graph.remoteServices ?? {}))
49
+ levels.set(new remote_service_node_id_1.RemoteServiceNodeId().of(name), remote.level);
50
+ for (const trigger of this.graph.triggers) {
51
+ if (trigger.kind === 'cron')
52
+ levels.set(`cron__${safe(`${trigger.api}_${trigger.method}`)}`, trigger.level);
53
+ }
54
+ for (const group of new runtime_external_triggers_1.RuntimeExternalTriggers(this.graph.triggers).groups)
55
+ levels.set(group.from, group.triggers[0].level);
56
+ // Undeclared vendor seams are terminal sinks too, independent of their grouping for display.
57
+ for (const use of this.graph.unresolvedUses) {
58
+ const owner = this.graph.apis[use.api]?.owner ?? use.api;
59
+ levels.set(`external__${owner}`, use.level);
60
+ }
61
+ const bands = new Map();
62
+ for (const node of model.nodes) {
63
+ const level = levels.get(node.id);
64
+ if (!bands.has(level))
65
+ bands.set(level, []);
66
+ bands.get(level).push(node.id);
67
+ }
68
+ return [...bands.entries()]
69
+ .sort((a, b) => b[0] - a[0])
70
+ .map((entry) => new graph_level_bands_1.LevelBand(entry[0], entry[1]));
36
71
  }
37
72
  /** Context keeps its shape/kind palette; Impact never paints a queue as a source owner. */
38
73
  contextModes(model) {
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-viewer-adapter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-viewer-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EnhancedGraph } from './graph-sorter';\nimport type { RuntimeGraph, RuntimeService } from './runtime-graph-model';\nimport { GraphRenderModel, RenderNode } from './graph-render-model';\nimport { NodeFacts, NodeModeDots, NodeModeStyler } from './graph-color-modes';\nimport { ProductPalette, PRODUCT_SHARED_FILL, PRODUCT_NONE_FILL } from './graph-products';\nimport { LevelBand } from './graph-level-bands';\n\n/** Saved project facts enrich presentation without changing runtime derivation or topology. */\nexport class RuntimeViewerAdapter {\n private readonly styler = new NodeModeStyler();\n private readonly palette: ProductPalette;\n\n constructor(\n private readonly graph: RuntimeGraph,\n private readonly projects: EnhancedGraph | null,\n ) {\n this.palette = new ProductPalette(\n [\n ...new Set(\n Object.values(graph.services)\n .filter((service) => service.drawOnGraph !== false)\n .flatMap((service) => service.products ?? []),\n ),\n ].sort(),\n );\n }\n\n service(model: GraphRenderModel, name: string, service: RuntimeService): string {\n const project = this.projects?.[name];\n const role = service.role ?? project?.role ?? 'unknown';\n const facts = new NodeFacts(\n name,\n name.split('/').pop() ?? name,\n service.level,\n role,\n project?.framework ?? [],\n service.products ?? [],\n `Implements (${new Set(service.implements).size})${service.serviceName === undefined ? '' : ` · \"${service.serviceName}\"`}`,\n );\n this.styler.record(model.legend, facts, this.palette);\n return model.styledNode(facts, this.styler.dots(facts, this.palette));\n }\n\n levelBands(model: GraphRenderModel): LevelBand[] {\n return [...new Set(model.nodes.map((node: RenderNode) => node.tags!.level))]\n .sort((a: number, b: number) => b - a)\n .map(\n (level: number) =>\n new LevelBand(\n level,\n model.nodes\n .filter((node: RenderNode) => node.tags?.level === level)\n .map((node: RenderNode) => node.id),\n ),\n );\n }\n\n /** Context keeps its shape/kind palette; Impact never paints a queue as a source owner. */\n contextModes(model: GraphRenderModel): void {\n for (let index = 0; index < model.nodes.length; index++) {\n const node = model.nodes[index];\n if (node.tags !== null) continue;\n const colors = node.products.map((product) => this.palette.colorOf(product).color);\n const fill =\n colors.length === 0\n ? PRODUCT_NONE_FILL\n : this.palette.sharedByAll(node.products)\n ? PRODUCT_SHARED_FILL\n : colors.join(':');\n // Graphviz cylinders cannot stripe; a gradient preserves recognizable kind geometry.\n const recolored = node.dot.replace(/fillcolor=\"[^\"]*\"/, `fillcolor=\"${fill}\"`);\n const product =\n colors.length === 0\n ? recolored.replace(/style=\"[^\"]*\"/, 'style=\"filled,dashed\"')\n : recolored;\n const modes = new NodeModeDots(\n node.dot,\n node.dot,\n node.dot,\n node.dot,\n node.dot,\n node.dot,\n product,\n );\n model.nodes[index] = new RenderNode(\n node.id,\n node.dot,\n modes,\n null,\n node.products,\n node.queueKeys,\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"runtime-viewer-adapter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-viewer-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EnhancedGraph } from './graph-sorter';\nimport type { RuntimeGraph, RuntimeService } from './runtime-graph-model';\nimport { GraphRenderModel, RenderNode } from './graph-render-model';\nimport { NodeFacts, NodeModeDots, NodeModeStyler } from './graph-color-modes';\nimport { ProductPalette, PRODUCT_SHARED_FILL, PRODUCT_NONE_FILL } from './graph-products';\nimport { LevelBand } from './graph-level-bands';\nimport { RemoteServiceNodeId } from './remote-service-node-id';\nimport { RuntimeExternalTriggers } from './runtime-external-triggers';\n\n/** Saved project facts enrich presentation without changing runtime derivation or topology. */\nexport class RuntimeViewerAdapter {\n private readonly styler = new NodeModeStyler();\n private readonly palette: ProductPalette;\n\n constructor(\n private readonly graph: RuntimeGraph,\n private readonly projects: EnhancedGraph | null,\n ) {\n this.palette = new ProductPalette(\n [\n ...new Set(\n Object.values(graph.services)\n .filter((service) => service.drawOnGraph !== false)\n .flatMap((service) => service.products ?? []),\n ),\n ].sort(),\n );\n }\n\n service(model: GraphRenderModel, name: string, service: RuntimeService): string {\n const project = this.projects?.[name];\n const role = service.role ?? project?.role ?? 'unknown';\n const facts = new NodeFacts(\n name,\n name.split('/').pop() ?? name,\n service.level,\n role,\n project?.framework ?? [],\n service.products ?? [],\n `Implements (${new Set(service.implements).size})${service.serviceName === undefined ? '' : ` · \"${service.serviceName}\"`}`,\n );\n this.styler.record(model.legend, facts, this.palette);\n return model.styledNode(facts, this.styler.dots(facts, this.palette));\n }\n\n levelBands(model: GraphRenderModel): LevelBand[] {\n const levels = new Map<string, number>();\n const safe = (name: string): string => name.replace(/[^A-Za-z0-9_]/g, '_');\n for (const [name, service] of Object.entries(this.graph.services))\n levels.set(name, service.level);\n for (const node of model.nodes) {\n if (node.queueKeys.length === 0) continue;\n const key = node.queueKeys[0];\n const queue = key.startsWith('queue:')\n ? this.graph.contractQueues[key.slice(6)]\n : this.graph.queues[key];\n levels.set(node.id, queue.level);\n }\n for (const [name, system] of Object.entries(this.graph.externalSystems ?? {}))\n levels.set(`system__${safe(name)}`, system.level);\n for (const [name, remote] of Object.entries(this.graph.remoteServices ?? {}))\n levels.set(new RemoteServiceNodeId().of(name), remote.level);\n for (const trigger of this.graph.triggers) {\n if (trigger.kind === 'cron')\n levels.set(`cron__${safe(`${trigger.api}_${trigger.method}`)}`, trigger.level);\n }\n for (const group of new RuntimeExternalTriggers(this.graph.triggers).groups)\n levels.set(group.from, group.triggers[0].level);\n // Undeclared vendor seams are terminal sinks too, independent of their grouping for display.\n for (const use of this.graph.unresolvedUses) {\n const owner = this.graph.apis[use.api]?.owner ?? use.api;\n levels.set(`external__${owner}`, use.level);\n }\n const bands = new Map<number, string[]>();\n for (const node of model.nodes) {\n const level = levels.get(node.id)!;\n if (!bands.has(level)) bands.set(level, []);\n bands.get(level)!.push(node.id);\n }\n return [...bands.entries()]\n .sort((a, b) => b[0] - a[0])\n .map((entry) => new LevelBand(entry[0], entry[1]));\n }\n\n /** Context keeps its shape/kind palette; Impact never paints a queue as a source owner. */\n contextModes(model: GraphRenderModel): void {\n for (let index = 0; index < model.nodes.length; index++) {\n const node = model.nodes[index];\n if (node.tags !== null) continue;\n const colors = node.products.map((product) => this.palette.colorOf(product).color);\n const fill =\n colors.length === 0\n ? PRODUCT_NONE_FILL\n : this.palette.sharedByAll(node.products)\n ? PRODUCT_SHARED_FILL\n : colors.join(':');\n // Graphviz cylinders cannot stripe; a gradient preserves recognizable kind geometry.\n const recolored = node.dot.replace(/fillcolor=\"[^\"]*\"/, `fillcolor=\"${fill}\"`);\n const product =\n colors.length === 0\n ? recolored.replace(/style=\"[^\"]*\"/, 'style=\"filled,dashed\"')\n : recolored;\n const modes = new NodeModeDots(\n node.dot,\n node.dot,\n node.dot,\n node.dot,\n node.dot,\n node.dot,\n product,\n );\n model.nodes[index] = new RenderNode(\n node.id,\n node.dot,\n modes,\n null,\n node.products,\n node.queueKeys,\n );\n }\n }\n}\n"]}
@@ -38,8 +38,8 @@ const graph_level_bands_1 = require("./graph-level-bands");
38
38
  *
39
39
  * The same is true in the other direction for endpoints nothing in-repo CALLS: a
40
40
  * `cron` method hangs off a clock and an `external` method off a box naming the
41
- * CALLER that posts to it (`twilio`), in the same id space as the outbound
42
- * systems, so a vendor we both call and are called by is ONE box. Those are the
41
+ * CALLER that posts to it (`twilio`), separately from an outbound sink for the same
42
+ * vendor, so callers can sit on the top row and sinks on L0. Those are the
43
43
  * entry points that wake a service up at 3am, and a graph built only from in-repo
44
44
  * callers cannot show them at all.
45
45
  *
@@ -57,6 +57,7 @@ const dot_syntax_1 = require("./dot-syntax");
57
57
  const graph_render_model_1 = require("./graph-render-model");
58
58
  const runtime_viewer_adapter_1 = require("./runtime-viewer-adapter");
59
59
  const runtime_html_page_1 = require("./runtime-html-page");
60
+ const runtime_external_triggers_1 = require("./runtime-external-triggers");
60
61
  const remote_service_node_id_1 = require("./remote-service-node-id");
61
62
  const runtime_viz_theme_1 = require("./runtime-viz-theme");
62
63
  /** Apis per line inside a node label — beyond this the box grows wider than it is readable. */
@@ -105,7 +106,7 @@ function labelList(entries) {
105
106
  function edgeDot(edge) {
106
107
  const from = serviceNodeId(edge.from);
107
108
  const to = serviceNodeId(edge.to);
108
- const viaRaw = edge.via.length > 2
109
+ const viaRaw = edge.via.length > 1
109
110
  ? `Uses (${edge.via.length}) ▾`
110
111
  : edge.via.map((v) => getShortName(v)).join(', ');
111
112
  const cut = edge.cutLegacyCycle === true;
@@ -119,8 +120,8 @@ class QueueEndpoints {
119
120
  }
120
121
  /**
121
122
  * One drawn queue BOX: the queue keys (`Api.method`) it shows, and the endpoints they all share.
122
- * Several keys land in one box only when they agree on all three of contract, producers and
123
- * consumers — see {@link queuesDot}.
123
+ * Several keys land in one box only when they agree on contract, producers, consumers and
124
+ * their persisted level — see {@link queuesDot}.
124
125
  */
125
126
  class QueueGroup {
126
127
  members;
@@ -152,21 +153,22 @@ function queueLine(key, queue) {
152
153
  }
153
154
  /** The node statement + enqueue/deliver arrows for one queue box. Service identities remain raw; only emitted DOT is escaped. */
154
155
  // webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
155
- function queueBoxDot(id, body, producers, consumers, model, contractCount, queueKeys) {
156
+ function queueBoxDot(id, body, producers, consumers, model, queueKeys) {
156
157
  // Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the
157
158
  // empty leading field that draws the cylinder's end cap. Drop it and the node silently
158
159
  // degrades to a plain box.
159
160
  let dot = model.node(id, ` "${id}" [shape=${runtime_viz_theme_1.QUEUE_SHAPE}, style="filled", fillcolor="${runtime_viz_theme_1.QUEUE_FILL}", ` +
160
161
  `class="${runtime_viz_theme_1.QUEUE_CLASS}", label="${runtime_viz_theme_1.QUEUE_LABEL_PREFIX}${body}"];\n`, [], queueKeys);
161
162
  for (const producer of producers)
162
- dot += model.edge(producer, id, ` "${serviceNodeId(producer)}" -> "${id}" [label="Uses (${contractCount}) ▾\\nenqueue", style=dashed];\n`);
163
+ dot += model.edge(producer, id, ` "${serviceNodeId(producer)}" -> "${id}" [label="enqueue", style=dashed];\n`);
163
164
  for (const consumer of consumers)
164
165
  dot += model.edge(id, consumer, ` "${id}" -> "${serviceNodeId(consumer)}" [style=dashed];\n`);
165
166
  return dot;
166
167
  }
167
168
  /**
168
169
  * Group the queued hops into boxes. The key is (contract, producer SET, consumer SET) — NOT the
169
- * (from,to) pair of some edge.
170
+ * (from,to) pair of some edge. Persisted levels must also match: hiding a foreign producer
171
+ * can make a foreign queue look self-only without changing its generated level.
170
172
  *
171
173
  * A queue node converges EVERY producer and consumer of its method onto one box, so keying by a
172
174
  * service PAIR would re-split a two-producer queue into two boxes and invent topology nobody wrote.
@@ -182,7 +184,12 @@ function groupQueues(byQueue, graph) {
182
184
  const producers = [...ends.producers].sort();
183
185
  const consumers = [...ends.consumers].sort();
184
186
  const api = graph.queues[key]?.api ?? key.split('.')[0];
185
- const mergeKey = [api, producers.join(','), consumers.join(',')].join(PAIR_SEP);
187
+ const mergeKey = [
188
+ api,
189
+ graph.queues[key].level,
190
+ producers.join(','),
191
+ consumers.join(','),
192
+ ].join(PAIR_SEP);
186
193
  const existing = groups.get(mergeKey);
187
194
  if (existing === undefined)
188
195
  groups.set(mergeKey, new QueueGroup([key], producers, consumers));
@@ -225,13 +232,13 @@ function queuesDot(graph, hidden, model) {
225
232
  }
226
233
  for (const api of [...byContract.keys()].sort()) {
227
234
  const ends = byContract.get(api);
228
- dot += queueBoxDot(`queue__${dotId(api)}`, `${(0, dot_syntax_1.recordValue)(getShortName(api))} queue`, [...ends.producers].sort(), [...ends.consumers].sort(), model, 1, [`queue:${api}`]);
235
+ dot += queueBoxDot(`queue__${dotId(api)}`, `${(0, dot_syntax_1.recordValue)(getShortName(api))} queue`, [...ends.producers].sort(), [...ends.consumers].sort(), model, [`queue:${api}`]);
229
236
  }
230
237
  for (const group of groupQueues(byQueue, graph)) {
231
238
  const body = group.members
232
239
  .map((key) => queueLine(key, graph.queues[key]))
233
240
  .join('\\n');
234
- dot += queueBoxDot(`queue__${dotId(group.members[0])}`, body, group.producers, group.consumers, model, 1, group.members);
241
+ dot += queueBoxDot(`queue__${dotId(group.members[0])}`, body, group.producers, group.consumers, model, group.members);
235
242
  }
236
243
  return dot;
237
244
  }
@@ -245,9 +252,8 @@ function dotId(raw) {
245
252
  * parenthesised subtitle saying which way it faces.
246
253
  *
247
254
  * Shared by the OUTBOUND systems ({@link externalSystemsDot}) and the INBOUND callers
248
- * ({@link triggerDot}) on purpose. They emit into the SAME `system__<identity>` id space, so a
249
- * vendor this repo both calls and is called by is one box with arrows in both directions — and a
250
- * copy-pasted attribute string would have let the two drift until they stopped being one box.
255
+ * ({@link triggerDot}) on purpose. Callers use a separate identity from outbound sinks
256
+ * because their generated levels differ, while sharing the same shape and color palette.
251
257
  */
252
258
  // webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
253
259
  function externalSystemNodeDot(identity, kind, label, subtitle, model) {
@@ -278,47 +284,42 @@ function externalSystemNodeDot(identity, kind, label, subtitle, model) {
278
284
  * exists to convey — which vendor is posting to us — appeared nowhere. The contract keeps its place
279
285
  * on the EDGE label, where `WhatsAppApi.inbound` reads as "…posts to this method".
280
286
  *
281
- * Identity is the caller's label in the SAME `system__` space {@link externalSystemsDot} uses, which
282
- * buys three things at once: two vendors hitting one contract are two boxes, one vendor hitting three
283
- * methods is one box with three arrows, and a vendor this repo also CALLS is that same single box.
284
- * `options` is passed in for the last of those — when that outbound half will be drawn, this half
285
- * must not restate the node statement.
287
+ * Inbound identities are separate from outbound sinks so each has its own generated level.
288
+ * Several methods called by the same vendor still share one caller box.
286
289
  */
287
290
  // webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
288
- function triggerDot(graph, hidden, options, model) {
291
+ function triggerDot(graph, hidden, model) {
289
292
  const triggers = graph.triggers.filter((t) => !hidden.has(t.service));
290
293
  if (triggers.length === 0)
291
294
  return '';
292
- // Identities externalSystemsDot is about to draw; skipped here so the node is stated ONCE.
293
- const drawnOutbound = new Set(options.showExternalNodes ? Object.keys(graph.externalSystems ?? {}) : []);
294
295
  const emitted = new Set();
295
296
  let dot = '\n // Entry points nothing in this repo calls: a clock, or a system outside the repo.\n';
296
- for (const trigger of triggers) {
297
+ for (const trigger of triggers.filter((trigger) => trigger.kind === 'cron')) {
297
298
  const service = serviceNodeId(trigger.service);
298
299
  const label = (0, dot_syntax_1.dotValue)(`${trigger.api}.${trigger.method}`);
299
- if (trigger.kind === 'cron') {
300
- const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;
301
- const schedule = (0, dot_syntax_1.dotValue)(trigger.queueName ?? `${trigger.api}-${trigger.method}`);
302
- dot +=
303
- model.node(id, ` "${id}" [shape=circle, style="filled", fillcolor="${runtime_viz_theme_1.CRON_FILL}", ` +
304
- `color="${runtime_viz_theme_1.CRON_BORDER}", label="⏰\\ncron"];\n`) +
305
- model.edge(id, trigger.service, ` "${id}" -> "${service}" [label="${label}\\n${schedule}", color="${runtime_viz_theme_1.CRON_BORDER}"];\n`);
306
- continue;
307
- }
300
+ const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;
301
+ const schedule = (0, dot_syntax_1.dotValue)(trigger.queueName ?? `${trigger.api}-${trigger.method}`);
302
+ dot +=
303
+ model.node(id, ` "${id}" [shape=circle, style="filled", fillcolor="${runtime_viz_theme_1.CRON_FILL}", ` +
304
+ `color="${runtime_viz_theme_1.CRON_BORDER}", label="⏰\\ncron"];\n`) +
305
+ model.edge(id, trigger.service, ` "${id}" -> "${service}" [label="${label}\\n${schedule}", color="${runtime_viz_theme_1.CRON_BORDER}"];\n`);
306
+ }
307
+ for (const group of new runtime_external_triggers_1.RuntimeExternalTriggers(triggers).groups) {
308
+ const trigger = group.triggers[0];
309
+ const service = serviceNodeId(group.to);
310
+ const label = (0, dot_syntax_1.dotValue)(group.label());
308
311
  const caller = trigger.caller;
309
312
  // Undeclared: only possible for a graph generated BEFORE the caller was required (generation
310
313
  // now fails instead). DOTTED and question-marked, so "we were never told who this is" cannot
311
314
  // be mistaken for a named vendor.
312
- const id = caller === undefined
313
- ? `inbound__${dotId(trigger.api)}`
314
- : `system__${dotId(caller.label)}`;
315
- if (!emitted.has(id) && !(caller !== undefined && drawnOutbound.has(caller.label))) {
315
+ const id = group.from;
316
+ if (!emitted.has(id)) {
316
317
  emitted.add(id);
317
318
  dot +=
318
319
  caller === undefined
319
320
  ? model.node(id, ` "${id}" [shape=box, style="dotted,filled", fillcolor="${runtime_viz_theme_1.EXTERNAL_FILL}", ` +
320
321
  `color="${runtime_viz_theme_1.EXTERNAL_BORDER}", label="${(0, dot_syntax_1.dotValue)(trigger.api)}\\n? unknown caller"];\n`)
321
- : externalSystemNodeDot(caller.label, caller.kind, caller.label, 'external caller', model);
322
+ : externalSystemNodeDot(`caller__${caller.label}`, caller.kind, caller.label, 'external caller', model);
322
323
  }
323
324
  dot += model.edge(id, trigger.service, ` "${id}" -> "${service}" [label="${label}", style=dashed, color="${runtime_viz_theme_1.EXTERNAL_BORDER}"];\n`);
324
325
  }
@@ -349,7 +350,7 @@ function externalSystemsDot(graph, hidden, model) {
349
350
  const system = systems[id];
350
351
  const via = system.apis.length === 0
351
352
  ? ''
352
- : ` [label="${system.apis.length > 2 ? `Uses (${system.apis.length}) ▾` : labelList([...system.apis].sort())}"]`;
353
+ : ` [label="${system.apis.length > 1 ? `Uses (${system.apis.length}) ▾` : labelList([...system.apis].sort())}"]`;
353
354
  for (const service of [...system.usedBy].sort()) {
354
355
  if (hidden.has(service))
355
356
  continue;
@@ -364,8 +365,8 @@ function externalSystemsDot(graph, hidden, model) {
364
365
  * computes and which was, until now, only ever printed as a warning.
365
366
  *
366
367
  * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts
367
- * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they
368
- * are absent from levels, cycle detection and the transitive implements attribution.
368
+ * draws ONE `lib-firestore (external)` box rather than three. The unresolved uses carry their
369
+ * terminal level in the generated graph; grouping changes presentation only.
369
370
  *
370
371
  * A contract carrying an `@externalSystem` declaration is skipped here — {@link externalSystemsDot}
371
372
  * has already drawn it with a real shape, and rendering it in both places would double the node.
@@ -403,7 +404,7 @@ function externalDot(graph, hidden, model) {
403
404
  const service = parts[0];
404
405
  const external = parts[1];
405
406
  const apis = apisByPair.get(key).sort();
406
- const via = apis.length > 2 ? `Uses (${apis.length}) ▾` : labelList(apis);
407
+ const via = apis.length > 1 ? `Uses (${apis.length}) ▾` : labelList(apis);
407
408
  // SOLID: this is a synchronous call that returns a value. Dashed is reserved for events, and
408
409
  // "outside the repo" is already said by the node's dashed border.
409
410
  dot += model.edge(service, `external__${external}`, ` "${serviceNodeId(service)}" -> "external__${(0, dot_syntax_1.dotValue)(external)}" ` +
@@ -442,7 +443,7 @@ function remoteServicesDot(graph, hidden, model) {
442
443
  for (const call of remotes[name].calledBy) {
443
444
  if (hidden.has(call.from))
444
445
  continue;
445
- const via = call.via.length > 2 ? `Uses (${call.via.length}) ▾` : labelList(call.via);
446
+ const via = call.via.length > 1 ? `Uses (${call.via.length}) ▾` : labelList(call.via);
446
447
  const label = call.type === 'pubsub' ? `${via}\\nenqueue` : via;
447
448
  const style = call.type === 'pubsub' ? ', style=dashed' : '';
448
449
  dot += model.edge(call.from, id, ` "${serviceNodeId(call.from)}" -> "${id}" [label="${label}", color="${runtime_viz_theme_1.REMOTE_SERVICE_BORDER}"${style}];\n`);
@@ -475,8 +476,6 @@ function generateRuntimeRenderModel(graph, title = 'WebPieces Runtime Architectu
475
476
  const svc = graph.services[name];
476
477
  dot += adapter.service(model, name, svc);
477
478
  }
478
- model.bands = adapter.levelBands(model);
479
- dot += new graph_level_bands_1.LevelBandLayout().dot(model.bands);
480
479
  dot += '\n';
481
480
  for (const edge of graph.runtimeEdges) {
482
481
  if (hidden.has(edge.from) || hidden.has(edge.to))
@@ -488,11 +487,13 @@ function generateRuntimeRenderModel(graph, title = 'WebPieces Runtime Architectu
488
487
  }
489
488
  dot += queuesDot(graph, hidden, model);
490
489
  dot += remoteServicesDot(graph, hidden, model);
491
- dot += triggerDot(graph, hidden, options, model);
490
+ dot += triggerDot(graph, hidden, model);
492
491
  if (options.showExternalNodes) {
493
492
  dot += externalSystemsDot(graph, hidden, model);
494
493
  dot += externalDot(graph, hidden, model);
495
494
  }
495
+ model.bands = adapter.levelBands(model);
496
+ dot += new graph_level_bands_1.LevelBandLayout().dot(model.bands);
496
497
  const footerStart = dot.length;
497
498
  dot += '\n labelloc="t";\n';
498
499
  dot += ` label="${(0, dot_syntax_1.dotValue)(title)}\\n(from architecture/runtime-dependencies.json)";\n`;