@webpieces/nx-webpieces-rules 0.4.658 → 0.4.659

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.658",
3
+ "version": "0.4.659",
4
4
  "description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -18,11 +18,11 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@webpieces/ai-hook-rules": "0.4.658",
22
- "@webpieces/code-rules": "0.4.658",
23
- "@webpieces/eslint-rules": "0.4.658",
24
- "@webpieces/pr-gate": "0.4.658",
25
- "@webpieces/rules-config": "0.4.658",
21
+ "@webpieces/ai-hook-rules": "0.4.659",
22
+ "@webpieces/code-rules": "0.4.659",
23
+ "@webpieces/eslint-rules": "0.4.659",
24
+ "@webpieces/pr-gate": "0.4.659",
25
+ "@webpieces/rules-config": "0.4.659",
26
26
  "madge": "8.0.0"
27
27
  },
28
28
  "peerDependencies": {
@@ -1,23 +1,26 @@
1
1
  /**
2
- * The floating per-node menu, shared by BOTH generated graphs.
2
+ * The floating per-node menu, shared by ALL THREE generated graphs.
3
3
  *
4
4
  * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the
5
- * click wiring that opens it — and both `architecture/dependencies.html` (graph-visualizer.ts) and
6
- * every project's `design.html` (di-graph/design-visualizer.ts) inline these same bytes. Duplicating
7
- * it into the two emitters is what this module exists to prevent.
5
+ * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every
6
+ * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`
7
+ * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what
8
+ * this module exists to prevent.
8
9
  *
9
10
  * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`
10
11
  * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the
11
12
  * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a
12
13
  * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one
13
- * copy reachable by both emitters with no build-order coupling.
14
+ * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled
15
+ * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as
16
+ * ambient globals — see viz-client-globals.d.ts.
14
17
  *
15
- * What the menu does NOT own is the LOCK BEHAVIOUR, because the two pages genuinely differ:
18
+ * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:
16
19
  * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain
17
20
  * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;
18
- * - a design page has neither dropdown nor responsibilities, so it uses `WpNodeLock` below —
19
- * dim every other box in that graph, light the locked one.
20
- * Both spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.
21
+ * - a design page and the runtime page have neither dropdown nor responsibilities, so they use
22
+ * `WpNodeLock` below — dim every other box in that graph, light the locked one.
23
+ * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.
21
24
  */
22
25
  export declare class GraphNodeMenu {
23
26
  /**
@@ -43,8 +46,8 @@ export declare class GraphNodeMenu {
43
46
  */
44
47
  private menuScript;
45
48
  /**
46
- * The lock a DESIGN page uses: no dropdown and no responsibilities exist there, so locking a class
47
- * box dims every other box in that design's graph and lights the locked one.
49
+ * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities
50
+ * list, so locking a box dims every other box in that graph and lights the locked one.
48
51
  */
49
52
  private lockScript;
50
53
  /** Outside click and Escape both dismiss — wired once, on the document. */
@@ -2,25 +2,28 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GraphNodeMenu = void 0;
4
4
  /**
5
- * The floating per-node menu, shared by BOTH generated graphs.
5
+ * The floating per-node menu, shared by ALL THREE generated graphs.
6
6
  *
7
7
  * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the
8
- * click wiring that opens it — and both `architecture/dependencies.html` (graph-visualizer.ts) and
9
- * every project's `design.html` (di-graph/design-visualizer.ts) inline these same bytes. Duplicating
10
- * it into the two emitters is what this module exists to prevent.
8
+ * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every
9
+ * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`
10
+ * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what
11
+ * this module exists to prevent.
11
12
  *
12
13
  * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`
13
14
  * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the
14
15
  * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a
15
16
  * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one
16
- * copy reachable by both emitters with no build-order coupling.
17
+ * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled
18
+ * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as
19
+ * ambient globals — see viz-client-globals.d.ts.
17
20
  *
18
- * What the menu does NOT own is the LOCK BEHAVIOUR, because the two pages genuinely differ:
21
+ * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:
19
22
  * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain
20
23
  * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;
21
- * - a design page has neither dropdown nor responsibilities, so it uses `WpNodeLock` below —
22
- * dim every other box in that graph, light the locked one.
23
- * Both spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.
24
+ * - a design page and the runtime page have neither dropdown nor responsibilities, so they use
25
+ * `WpNodeLock` below — dim every other box in that graph, light the locked one.
26
+ * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.
24
27
  */
25
28
  class GraphNodeMenu {
26
29
  /**
@@ -63,10 +66,15 @@ class GraphNodeMenu {
63
66
  }
64
67
  .wp-node-menu-item:hover { background: #E3F2FD; }
65
68
  g.wp-node-clickable { cursor: pointer; }
69
+ /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node
70
+ * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A
71
+ * shape left out of this list would be a box that is clickable but never looks it. */
66
72
  g.wp-node-clickable polygon,
67
- g.wp-node-clickable ellipse { transition: stroke-width 0.12s ease, filter 0.12s ease; }
73
+ g.wp-node-clickable ellipse,
74
+ g.wp-node-clickable path { transition: stroke-width 0.12s ease, filter 0.12s ease; }
68
75
  g.wp-node-clickable:hover polygon,
69
- g.wp-node-clickable:hover ellipse {
76
+ g.wp-node-clickable:hover ellipse,
77
+ g.wp-node-clickable:hover path {
70
78
  stroke: #1976d2;
71
79
  stroke-width: 5;
72
80
  filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));
@@ -160,8 +168,8 @@ class GraphNodeMenu {
160
168
  }`;
161
169
  }
162
170
  /**
163
- * The lock a DESIGN page uses: no dropdown and no responsibilities exist there, so locking a class
164
- * box dims every other box in that design's graph and lights the locked one.
171
+ * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities
172
+ * list, so locking a box dims every other box in that graph and lights the locked one.
165
173
  */
166
174
  lockScript() {
167
175
  return `
@@ -1 +1 @@
1
- {"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAyCL,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAa;QACnB,OAAO;UACL,KAAK,WAAW,KAAK;UACrB,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK,yCAAyC,CAAC;IACrD,CAAC;IAED,+FAA+F;IAC/F,MAAM;QACF,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA2DL,CAAC;IACP,CAAC;IAED;;;OAGG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;UAqBL,CAAC;IACP,CAAC;IAED,2EAA2E;IACnE,aAAa;QACjB,OAAO;;;;YAIH,CAAC;IACT,CAAC;CACJ;AAlLD,sCAkLC","sourcesContent":["/**\n * The floating per-node menu, shared by BOTH generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and both `architecture/dependencies.html` (graph-visualizer.ts) and\n * every project's `design.html` (di-graph/design-visualizer.ts) inline these same bytes. Duplicating\n * it into the two emitters is what this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by both emitters with no build-order coupling.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the two pages genuinely differ:\n * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page has neither dropdown nor responsibilities, so it uses `WpNodeLock` below —\n * dim every other box in that graph, light the locked one.\n * Both spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n g.wp-node-clickable { cursor: pointer; }\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n static close() {\n const open = document.getElementById('wp-node-menu');\n if (open !== null) open.remove();\n }\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n }\n static button(item) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close();\n item.onSelect();\n });\n return button;\n }\n static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n node.classList.add('wp-node-clickable');\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n /**\n * The lock a DESIGN page uses: no dropdown and no responsibilities exist there, so locking a class\n * box dims every other box in that design's graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.key === 'Escape') WpNodeMenu.close();\n });`;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8CL,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAa;QACnB,OAAO;UACL,KAAK,WAAW,KAAK;UACrB,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK,yCAAyC,CAAC;IACrD,CAAC;IAED,+FAA+F;IAC/F,MAAM;QACF,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA2DL,CAAC;IACP,CAAC;IAED;;;OAGG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;UAqBL,CAAC;IACP,CAAC;IAED,2EAA2E;IACnE,aAAa;QACjB,OAAO;;;;YAIH,CAAC;IACT,CAAC;CACJ;AAvLD,sCAuLC","sourcesContent":["/**\n * The floating per-node menu, shared by ALL THREE generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every\n * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`\n * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what\n * this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled\n * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as\n * ambient globals — see viz-client-globals.d.ts.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:\n * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither dropdown nor responsibilities, so they use\n * `WpNodeLock` below — dim every other box in that graph, light the locked one.\n * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n g.wp-node-clickable { cursor: pointer; }\n /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node\n * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A\n * shape left out of this list would be a box that is clickable but never looks it. */\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse,\n g.wp-node-clickable path { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse,\n g.wp-node-clickable:hover path {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n static close() {\n const open = document.getElementById('wp-node-menu');\n if (open !== null) open.remove();\n }\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n }\n static button(item) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close();\n item.onSelect();\n });\n return button;\n }\n static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n node.classList.add('wp-node-clickable');\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n /**\n * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities\n * list, so locking a box dims every other box in that graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.key === 'Escape') WpNodeMenu.close();\n });`;\n }\n}\n"]}
@@ -82,7 +82,7 @@ exports.CLIENT_DESIGN_LINKS_PLACEHOLDER = '__' + 'DESIGN_LINKS' + '__';
82
82
  * tarball but NOT in a source checkout. The error says that outright rather than surfacing a bare
83
83
  * ENOENT, because "the build has not run" and "the file is missing" look identical otherwise.
84
84
  */
85
- // webpieces-disable no-function-outside-class -- module-level resolver for the default injected into GraphVisualizer/generateRuntimeHtml; making it a class would need a container in a module both of them load eagerly
85
+ // webpieces-disable no-function-outside-class -- module-level resolver for the default injected into GraphVisualizer/RuntimeHtmlPage; making it a class would need a container in a module both of them load eagerly
86
86
  function readCompiledClient(name) {
87
87
  const file = path.join(__dirname, name);
88
88
  if (!fs.existsSync(file)) {
@@ -1 +1 @@
1
- {"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * All behavior lives on the injectable GraphVisualizer class so webpieces DI +\n * @DocumentDesign can wire it — module-scope functions are a dead end the DI\n * graph can't reach.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { ApiRef, ApiRelation, ApiRelationKind } from './api-usage/api-relations';\nimport { GraphNames, NodeIdOwner } from './graph-names';\nimport { LevelBand, LevelBandLayout } from './graph-level-bands';\nimport { ProjectAdjacency, ProjectCycleDetector } from './graph-cycles';\nimport { ResponsibilitiesRenderer } from './graph-responsibilities';\nimport { GraphNodeMenu } from './graph-node-menu';\nimport { toError } from '../toError';\n\n/**\n * Framework (libType) colors for visualization — nodes are filled by the FIRST\n * env in their set that has a color, so it is obvious at a glance which side a\n * project targets. A project's full env set is shown in the label.\n */\nconst FRAMEWORK_COLORS: Record<string, string> = {\n angular: '#FCE4EC', // pink - Angular front-end\n react: '#E3F2FD', // blue - React front-end\n browser: '#EDE7F6', // purple - browser (front-end base env)\n express: '#E8F5E9', // green - Express / server side\n node: '#FFF9C4', // yellow - node (server base env)\n};\n\nconst DEFAULT_FRAMEWORK_COLOR = '#F5F5F5'; // grey - unknown/empty env set\n\n/**\n * Directory (repo-relative) that the committed architecture HTML lives in.\n * Node click-through links are computed relative to this so they resolve when\n * the file is opened straight from the checkout.\n */\nconst ARCH_OUTPUT_DIR = 'architecture';\n\n/** Contracts named on one `implements` edge label before it truncates to \"+N more\". */\nconst MAX_EDGE_LABEL_APIS = 4;\n\n\n/**\n * One node's design page, handed to the browser so the node menu can offer \"View Design\" for the\n * boxes that HAVE one and omit the item entirely for the boxes that do not.\n *\n * The link is data on the PAGE now, not a `URL=` attribute in the DOT: a URL made viz.js wrap the\n * box in an `<a>` and clicking navigated straight out, which the menu replaces.\n */\nexport class DesignLink {\n constructor(\n public readonly nodeId: string,\n public readonly href: string\n ) {}\n}\n\nexport class VisualizationPaths {\n htmlPath: string;\n\n constructor(htmlPath: string) {\n this.htmlPath = htmlPath;\n }\n}\n\n/**\n * The token the client scripts carry where the DOT belongs. It must appear EXACTLY ONCE in a compiled\n * client — the inliner is a blind split/join, so a second occurrence (in a comment, say) would be\n * replaced by the entire DOT too, bloating every generated page.\n */\nexport const CLIENT_DOT_PLACEHOLDER = '__' + 'DOT' + '__';\n\n/** Same contract as CLIENT_DOT_PLACEHOLDER, for the node → design.html links the menu offers. */\nexport const CLIENT_DESIGN_LINKS_PLACEHOLDER = '__' + 'DESIGN_LINKS' + '__';\n\n/**\n * Read a compiled browser client sitting beside this file.\n *\n * It is generated from the matching `.client.ts` by tsc, so it exists in `dist` and in the published\n * tarball but NOT in a source checkout. The error says that outright rather than surfacing a bare\n * ENOENT, because \"the build has not run\" and \"the file is missing\" look identical otherwise.\n */\n// webpieces-disable no-function-outside-class -- module-level resolver for the default injected into GraphVisualizer/generateRuntimeHtml; making it a class would need a container in a module both of them load eagerly\nexport function readCompiledClient(name: string): string {\n const file = path.join(__dirname, name);\n if (!fs.existsSync(file)) {\n throw new Error(\n `${name} not found beside ${__dirname}. It is COMPILED from ${name.replace(/\\.js$/, '.ts')} `\n + 'by tsc, so it only exists after a build — run the package build, or inject the text.');\n }\n return fs.readFileSync(file, 'utf-8');\n}\n\nexport class GraphVisualizer {\n private readonly names = new GraphNames();\n private readonly responsibilities = new ResponsibilitiesRenderer();\n private readonly bandLayout = new LevelBandLayout();\n private readonly cycles = new ProjectCycleDetector();\n /** The ONE floating-node-menu implementation, shared with the per-project design pages. */\n private readonly nodeMenu = new GraphNodeMenu();\n\n /**\n * How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD\n * ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in\n * a source checkout (the source there is .client.ts). A unit test running from source supplies the\n * text itself rather than requiring the package to have been built first.\n */\n constructor(\n private readonly clientJs: () => string =\n (): string => readCompiledClient('graph-visualizer.client.js'),\n ) {}\n\n /**\n * A project tagged `drawOnGraph:false` is hidden from the rendered graph —\n * its node, its rank placement, its dropdown option, its responsibilities\n * card, and every edge touching it are all omitted. It stays in the JSON.\n */\n private isHidden(entry: EnhancedGraph[string]): boolean {\n return entry.drawOnGraph === false;\n }\n\n /**\n * Fill color for an env set — the color of the first env in the set that has\n * a known color, else the default.\n */\n private frameworkColor(frameworks: string[]): string {\n for (const env of frameworks) {\n const color = FRAMEWORK_COLORS[env];\n if (color !== undefined) return color;\n }\n return DEFAULT_FRAMEWORK_COLOR;\n }\n\n /**\n * Role border styling — fill stays keyed on framework; the border shows a\n * project's ROLE at a glance. Server and client are the top-level runnable\n * nodes, so they get bold, colored borders to stand out:\n * server → thick GREEN border (a runnable server app)\n * app → thick BLUE border (a runnable non-HTTP app, e.g. a tooling app)\n * bundle → thick PURPLE border (an aggregator that bundles several apps)\n * client → thick RED border (a client app, e.g. angular)\n * designed-lib → bold border (a library with a generated @DocumentDesign design)\n * lib / other → plain thin border\n */\n private roleBorderAttrs(role: string): string {\n if (role === 'server') return ', color=\"green\", penwidth=3';\n if (role === 'app') return ', color=\"#1976d2\", penwidth=3';\n if (role === 'bundle') return ', color=\"#6A1B9A\", penwidth=3';\n if (role === 'client') return ', color=\"red\", penwidth=3';\n if (role === 'api-lib') return ', color=\"#EF6C00\", penwidth=2';\n if (role === 'designed-lib') return ', penwidth=2';\n return '';\n }\n\n /**\n * Edge styling by API-relation kind (why the edge exists):\n * implements → BLACK dashed (a controller serves this api-lib's contract)\n * uses → BLACK solid (a generated client calls it) — same as a\n * plain library import, since a plain dependency IS a use.\n * uses-implements → BLUE dashed, thicker (does both — implements some\n * contracts of the api-lib, uses others)\n * plain lib (none) → the default thin black solid arrow, unchanged.\n * `kind` is undefined for every non-api-lib dependency edge.\n */\n private edgeAttrs(kind: ApiRelationKind | undefined): string {\n if (kind === 'implements') return 'style=dashed';\n if (kind === 'uses-implements') return 'style=dashed, color=\"#1976d2\", penwidth=2';\n return '';\n }\n\n /**\n * The DOT attribute list for ONE dependency edge. An edge that IMPLEMENTS contracts is also\n * LABELED with them: the dashed line alone says \"something here is implemented\", which reads as\n * \"the tool did not detect anything\" to everyone who has not memorized the legend. Naming the\n * contracts makes `auth-store-api` visibly resolve to the server that serves it — the single\n * most important relationship in a microservice architecture, and the one the diagram was\n * silent about.\n */\n private edgeDot(from: string, to: string, relation: ApiRelation | undefined): string {\n const attrs: string[] = [];\n const styling = this.edgeAttrs(relation?.kind);\n if (styling !== '') attrs.push(styling);\n const served = (relation?.implements ?? []).map((ref: ApiRef) => ref.api);\n if (served.length > 0) attrs.push(`label=\"implements: ${this.labelledApis(served)}\", fontsize=9`);\n const suffix = attrs.length === 0 ? '' : ` [${attrs.join(', ')}]`;\n return ` \"${from}\" -> \"${to}\"${suffix};\\n`;\n }\n\n /**\n * The contract names for an edge label, sorted and capped so a shared api-lib serving a dozen\n * contracts cannot blow the edge label up into a wall of text. The truncation is stated in the\n * label (\"+N more\") rather than silent — the full list is in dependencies.json.\n */\n private labelledApis(apis: string[]): string {\n const sorted = [...apis].sort();\n if (sorted.length <= MAX_EDGE_LABEL_APIS) return sorted.join(', ');\n const shown = sorted.slice(0, MAX_EDGE_LABEL_APIS).join(', ');\n return `${shown} +${sorted.length - MAX_EDGE_LABEL_APIS} more`;\n }\n\n /**\n * Href for a node's design page: the project's committed design.html, made\n * relative to architecture/dependencies.html. Returns null when the project\n * has no generated DI design (no design.json → no design page exists).\n */\n private designHtmlHref(designFile: string | undefined): string | null {\n if (!designFile) return null;\n const designHtml = designFile.replace(/design\\.json$/, 'design.html');\n return path.posix.relative(ARCH_OUTPUT_DIR, designHtml);\n }\n\n /**\n * The design pages that EXIST, one entry per visible node that has one. A node absent from this\n * list gets no \"View Design\" item at all — the item is never rendered dead or greyed out.\n */\n designLinks(graph: EnhancedGraph): DesignLink[] {\n const links: DesignLink[] = [];\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const href = this.designHtmlHref(info.designFile);\n if (href === null) continue;\n links.push(new DesignLink(this.names.getNodeId(project), href));\n }\n return links;\n }\n\n /**\n * Generate Graphviz DOT format from the graph\n */\n generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\n this.assertDrawable(graph);\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n const bands = this.levelBands(graph);\n\n dot += this.dotNodes(graph);\n dot += '\\n';\n dot += this.bandLayout.dot(bands);\n dot += '\\n';\n dot += this.dotEdges(graph);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n return dot;\n }\n\n /**\n * Refuse to draw a graph that would render something false.\n *\n * Two conditions, both of which produced a confidently-wrong picture with no warning at all:\n * - two projects sharing a node id fuse into one box, which UNIONS their two rank sets and\n * collapses two whole dependency levels onto one row (see graph-names.ts);\n * - a cycle makes the level numbers every row is keyed on meaningless (see graph-cycles.ts).\n */\n private assertDrawable(graph: EnhancedGraph): void {\n const owners: NodeIdOwner[] = [];\n const adjacency: ProjectAdjacency = {};\n for (const project of Object.keys(graph)) {\n owners.push(new NodeIdOwner(project, graph[project].level));\n adjacency[project] = graph[project].dependsOn ?? [];\n }\n this.names.assertUniqueNodeIds(owners);\n this.cycles.assertAcyclic(adjacency, 'architecture/dependencies.json');\n }\n\n /**\n * The visible projects grouped into one band per dependency level, ordered HIGHEST LEVEL FIRST\n * so the emitted bands read top-to-bottom with L0 last (hidden projects are omitted, so no\n * stray rank=same name is emitted for an absent node). LevelBandLayout turns these into the\n * rank sets and the invisible chain that pins them — see graph-level-bands.ts for why the\n * chain is required at all.\n */\n private levelBands(graph: EnhancedGraph): LevelBand[] {\n const byLevel = new Map<number, string[]>();\n for (const project of Object.keys(graph)) {\n if (this.isHidden(graph[project])) continue;\n const level = graph[project].level;\n const nodeIds = byLevel.get(level);\n if (nodeIds === undefined) byLevel.set(level, [this.names.getNodeId(project)]);\n else nodeIds.push(this.names.getNodeId(project));\n }\n const levels = [...byLevel.keys()].sort((a: number, b: number): number => b - a);\n return levels.map((level: number): LevelBand =>\n new LevelBand(level, [...(byLevel.get(level) as string[])].sort()));\n }\n\n // Node lines: fill colored by framework env set (libType), border shaped by\n // role; the label shows the env set + role (e.g. [browser, node] · server).\n // No node carries a URL: EVERY box is clickable and opens the floating node\n // menu instead, which is where a design page is reached (see designLinks).\n private dotNodes(graph: EnhancedGraph): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n const frameworks = info.framework ?? [];\n const role = info.role ?? 'lib';\n const color = this.frameworkColor(frameworks);\n const border = this.roleBorderAttrs(role);\n const envSet = `[${frameworks.join(', ')}]`;\n const labelMeta = `L${info.level} · ${envSet} · ${role}`;\n // Identity is the project key; the LABEL is the pretty short name.\n dot += ` \"${nodeId}\" [fillcolor=\"${color}\"${border}, label=\"${shortName}\\\\n(${labelMeta})\"];\\n`;\n }\n return dot;\n }\n\n // Edge lines (dependencies). An edge to an api-lib is styled by WHY it exists\n // (implements/uses/uses-implements, from apiRelations); every other dependency\n // keeps the default plain arrow.\n private dotEdges(graph: EnhancedGraph): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const nodeId = this.names.getNodeId(project);\n for (const dep of info.dependsOn || []) {\n // Both endpoints must be visible — an edge to/from a hidden box\n // is dropped so no connection dangles into empty space.\n if (graph[dep] !== undefined && this.isHidden(graph[dep])) continue;\n dot += this.edgeDot(nodeId, this.names.getNodeId(dep), info.apiRelations?.[dep]);\n }\n }\n return dot;\n }\n\n /**\n * Generate interactive HTML with embedded SVG using viz.js\n */\n generateHTML(\n dot: string,\n links: DesignLink[],\n title: string = 'Monorepo Dependency Architecture',\n lockControl: string = '',\n responsibilitiesHtml: string = ''\n ): string {\n const styles = this.styles();\n const legend = this.legend();\n const script = this.script(dot, links);\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>${styles}</style>\n</head>\n<body>\n <h1>${title}</h1>\n <p class=\"hint\">💡 <strong>Click any box</strong> for its menu — <strong>View Design</strong> (only where that project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it) and <strong>Lock/Unlock</strong>, which is the same lock as the dropdown below.</p>\n <p class=\"hint\">🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it (all the way up) <em>and</em> every dependency below it (all the way down), with all the boxes and lines between — while the rest of the graph dims so you can follow one box at a glance.</p>\n ${legend}\n ${lockControl}\n <div id=\"graph\"></div>\n ${responsibilitiesHtml}\n <script>${this.nodeMenu.script()}</script>\n <script>${script}</script>\n</body>\n</html>`;\n }\n\n /**\n * The lock control (a single-select dropdown, rendered below the legend).\n * Picking a module LOCKS the graph into that box's hover view — its full\n * ancestor + descendant chain stays lit while everything else stays dimmed —\n * and narrows the responsibilities list below the graph to just that chain.\n * The first option, \"All\", is the default and clears the lock. Hover still\n * works on top of a lock; leaving a box returns to the locked view.\n *\n * Options are ordered by level DESCENDING to match the responsibilities cards.\n */\n lockControl(graph: EnhancedGraph): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n let options = '';\n for (const project of projects) {\n if (this.isHidden(graph[project])) continue;\n // The VALUE is the node id the SVG is keyed on; the TEXT stays the pretty short name.\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n options += `<option value=\"${nodeId}\">L${graph[project].level} · ${shortName}</option>`;\n }\n return `<div class=\"wp-lock-control\">\n <label for=\"wp-lock\">🔒 Lock a box (dim the rest &amp; filter responsibilities):</label>\n <select id=\"wp-lock\"><option value=\"\">All (no lock)</option>${options}</select>\n </div>`;\n }\n\n private styles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .hint { text-align: center; color: #555; margin: 0 0 16px; }\n /* Every box is clickable (the floating node menu replaced direct\n * navigation), so the menu's own stylesheet carries the cursor + blue\n * glow, and the dim/undim rules a hover or a lock toggles. Both are\n * shared verbatim with the per-project design pages. */\n ${this.nodeMenu.styles()}\n ${this.nodeMenu.dimStyles('#graph')}\n #graph {\n text-align: center;\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; }\n .legend-item { margin: 8px 0; }\n .legend-box {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 1px solid #ccc;\n margin-right: 10px;\n vertical-align: middle;\n }\n ${this.componentStyles()}\n `;\n }\n\n // Styles for the lock dropdown and the responsibilities card list below the\n // graph. Split out of styles() to keep each method within the line limit.\n private componentStyles(): string {\n return `\n /* The architecture graph is very wide, so lay the legend out as three\n * side-by-side columns (fill / border / edge) instead of one tall\n * column — it keeps the legend short next to the wide graph, and\n * collapses back to a single column on narrow viewports. */\n .legend-columns {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 28px;\n align-items: start;\n }\n .legend-col h3 { margin: 0 0 8px; color: #333; font-size: 15px; }\n @media (max-width: 800px) { .legend-columns { grid-template-columns: 1fr; } }\n .wp-lock-control {\n max-width: 600px;\n margin: 0 auto 16px;\n padding: 12px 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n text-align: center;\n }\n .wp-lock-control label { font-weight: bold; color: #333; margin-right: 8px; }\n .wp-lock-control select { font-size: 14px; padding: 4px 8px; }\n #wp-responsibilities { max-width: 900px; margin: 24px auto 0; }\n #wp-responsibilities h2 { color: #333; }\n .wp-resp-card {\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n margin: 10px 0;\n padding: 10px 15px;\n }\n .wp-resp-card > summary { cursor: pointer; color: #333; }\n .wp-resp-level {\n display: inline-block;\n min-width: 26px;\n padding: 1px 6px;\n margin-right: 6px;\n border-radius: 4px;\n background: #eef;\n font-size: 12px;\n font-weight: bold;\n text-align: center;\n }\n .wp-resp-body { margin-top: 8px; color: #444; }\n .wp-resp-body code {\n background: #f2f2f2;\n padding: 1px 4px;\n border-radius: 3px;\n font-family: monospace;\n }\n .wp-hidden { display: none; }`;\n }\n\n // The legend is laid out in three side-by-side columns (fill / border /\n // edge) so it stays short next to the very wide architecture graph. Each\n // column's rows come from a helper below to keep this method within the line\n // limit; the footnote spans the full width beneath the columns.\n private legend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-columns\">\n <div class=\"legend-col\">\n <h3>Fill = framework (libType)</h3>\n ${this.fillItems()}\n </div>\n <div class=\"legend-col\">\n <h3>Border = role</h3>\n ${this.borderItems()}\n </div>\n <div class=\"legend-col\">\n <h3>Edge lines — <em>why</em> a project depends on an api-lib</h3>\n ${this.edgeItems()}\n </div>\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Each node label shows its dependency level (L#), its framework env set (e.g. [browser, node]), and its role. Every row is one dependency level and nothing else: the HIGHEST level is the top row, levels descend as you read down, and L0 — the foundation libraries everything else is built on — is always the bottom row. Transitive dependencies are allowed but not shown.</em>\n </div>\n </div>`;\n }\n\n // Column 1 — fill color keyed on the project's framework (libType) env set.\n private fillItems(): string {\n return `<div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FCE4EC;\"></span>\n <strong>angular:</strong> Angular front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>react:</strong> React front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #EDE7F6;\"></span>\n <strong>browser:</strong> browser front-end base env\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>express:</strong> Express / server side\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FFF9C4;\"></span>\n <strong>node:</strong> node server base env\n </div>`;\n }\n\n // Column 2 — border style keyed on the project's role.\n private borderItems(): string {\n return `<div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 3px solid green;\"></span>\n <strong>server:</strong> runnable server app (thick green border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 3px solid red;\"></span>\n <strong>client:</strong> client app, e.g. angular (thick red border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 2px solid #333;\"></span>\n <strong>designed-lib:</strong> library with a generated @DocumentDesign design (bold border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 1px solid #ccc;\"></span>\n <strong>lib:</strong> plain library, no generated design (thin border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 2px solid #EF6C00;\"></span>\n <strong>api-lib:</strong> API-contract library (defines <code>@ApiPath</code>/<code>@Rpc</code>/<code>@PubSub</code> <code>*Api</code> classes)\n </div>`;\n }\n\n // Column 3 — edge line style keyed on WHY a project depends on an api-lib.\n private edgeItems(): string {\n return `<div class=\"legend-item\">\n <svg width=\"42\" height=\"12\" style=\"vertical-align: middle; margin-right: 10px;\"><line x1=\"0\" y1=\"6\" x2=\"42\" y2=\"6\" stroke=\"#333\" stroke-width=\"2\"/></svg>\n <strong>uses:</strong> calls the API (generates an rpc/pubsub client via <code>createRpcClient</code>/<code>createPubSubClient</code>) — also covers a plain library import, since a plain dependency is just a use.\n </div>\n <div class=\"legend-item\">\n <svg width=\"42\" height=\"12\" style=\"vertical-align: middle; margin-right: 10px;\"><line x1=\"0\" y1=\"6\" x2=\"42\" y2=\"6\" stroke=\"#333\" stroke-width=\"2\" stroke-dasharray=\"5,3\"/></svg>\n <strong>implements:</strong> serves the API — labeled <code>implements: &lt;contracts&gt;</code> so you can see WHICH contracts resolve to this server without opening the JSON. NOTE: this is a build-dependency diagram, so a UML <em>implements</em> arrow can't be used; we use a dashed line to signal a build dep, because this server implements the api and the api is built first, then this server after.\n </div>\n <div class=\"legend-item\">\n <svg width=\"42\" height=\"12\" style=\"vertical-align: middle; margin-right: 10px;\"><line x1=\"0\" y1=\"6\" x2=\"42\" y2=\"6\" stroke=\"#1976d2\" stroke-width=\"2\" stroke-dasharray=\"5,3\"/></svg>\n <strong>uses/implements:</strong> both — implements some of the api-lib's contracts, uses others\n </div>`;\n }\n\n /**\n * The page script. The browser code lives in graph-visualizer.client.ts — real, linted TypeScript\n * that tsc compiles in place, so what this inlines is its COMPILED .js sitting beside this file.\n *\n * The substitution is a blind split/join, which is why the placeholder token must appear EXACTLY\n * ONCE in the client (never in one of its comments): every literal occurrence would otherwise be\n * replaced by the whole DOT.\n */\n private script(dot: string, links: DesignLink[]): string {\n return this.clientJs()\n .split(CLIENT_DOT_PLACEHOLDER).join(JSON.stringify(dot))\n .split(CLIENT_DESIGN_LINKS_PLACEHOLDER).join(JSON.stringify(links));\n }\n\n /**\n * Write the committed architecture visualization to\n * architecture/dependencies.html, next to dependencies.json.\n *\n * This is a checked-in artifact, regenerated deterministically by\n * architecture:generate so every box keeps its menu, and the \"View Design\"\n * item keeps pointing at each project's committed design.html as designs\n * come and go. The DOT is embedded in the HTML (rendered client-side by\n * viz.js). Output is deterministic (sorted graph in → same bytes out) so git\n * only shows a diff when the architecture actually changed.\n */\n writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'Monorepo Dependency Architecture'\n ): VisualizationPaths {\n const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const lockControl = this.lockControl(graph);\n const responsibilities = this.responsibilities.generateSection(graph, workspaceRoot);\n const html = this.generateHTML(\n this.generateDot(graph, title), this.designLinks(graph), title, lockControl, responsibilities);\n const htmlPath = path.join(outputDir, 'dependencies.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return new VisualizationPaths(htmlPath);\n }\n\n /**\n * Open the HTML visualization in the default browser\n */\n openVisualization(htmlPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not open browser: ${error.message}`);\n return false;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * All behavior lives on the injectable GraphVisualizer class so webpieces DI +\n * @DocumentDesign can wire it — module-scope functions are a dead end the DI\n * graph can't reach.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { ApiRef, ApiRelation, ApiRelationKind } from './api-usage/api-relations';\nimport { GraphNames, NodeIdOwner } from './graph-names';\nimport { LevelBand, LevelBandLayout } from './graph-level-bands';\nimport { ProjectAdjacency, ProjectCycleDetector } from './graph-cycles';\nimport { ResponsibilitiesRenderer } from './graph-responsibilities';\nimport { GraphNodeMenu } from './graph-node-menu';\nimport { toError } from '../toError';\n\n/**\n * Framework (libType) colors for visualization — nodes are filled by the FIRST\n * env in their set that has a color, so it is obvious at a glance which side a\n * project targets. A project's full env set is shown in the label.\n */\nconst FRAMEWORK_COLORS: Record<string, string> = {\n angular: '#FCE4EC', // pink - Angular front-end\n react: '#E3F2FD', // blue - React front-end\n browser: '#EDE7F6', // purple - browser (front-end base env)\n express: '#E8F5E9', // green - Express / server side\n node: '#FFF9C4', // yellow - node (server base env)\n};\n\nconst DEFAULT_FRAMEWORK_COLOR = '#F5F5F5'; // grey - unknown/empty env set\n\n/**\n * Directory (repo-relative) that the committed architecture HTML lives in.\n * Node click-through links are computed relative to this so they resolve when\n * the file is opened straight from the checkout.\n */\nconst ARCH_OUTPUT_DIR = 'architecture';\n\n/** Contracts named on one `implements` edge label before it truncates to \"+N more\". */\nconst MAX_EDGE_LABEL_APIS = 4;\n\n\n/**\n * One node's design page, handed to the browser so the node menu can offer \"View Design\" for the\n * boxes that HAVE one and omit the item entirely for the boxes that do not.\n *\n * The link is data on the PAGE now, not a `URL=` attribute in the DOT: a URL made viz.js wrap the\n * box in an `<a>` and clicking navigated straight out, which the menu replaces.\n */\nexport class DesignLink {\n constructor(\n public readonly nodeId: string,\n public readonly href: string\n ) {}\n}\n\nexport class VisualizationPaths {\n htmlPath: string;\n\n constructor(htmlPath: string) {\n this.htmlPath = htmlPath;\n }\n}\n\n/**\n * The token the client scripts carry where the DOT belongs. It must appear EXACTLY ONCE in a compiled\n * client — the inliner is a blind split/join, so a second occurrence (in a comment, say) would be\n * replaced by the entire DOT too, bloating every generated page.\n */\nexport const CLIENT_DOT_PLACEHOLDER = '__' + 'DOT' + '__';\n\n/** Same contract as CLIENT_DOT_PLACEHOLDER, for the node → design.html links the menu offers. */\nexport const CLIENT_DESIGN_LINKS_PLACEHOLDER = '__' + 'DESIGN_LINKS' + '__';\n\n/**\n * Read a compiled browser client sitting beside this file.\n *\n * It is generated from the matching `.client.ts` by tsc, so it exists in `dist` and in the published\n * tarball but NOT in a source checkout. The error says that outright rather than surfacing a bare\n * ENOENT, because \"the build has not run\" and \"the file is missing\" look identical otherwise.\n */\n// webpieces-disable no-function-outside-class -- module-level resolver for the default injected into GraphVisualizer/RuntimeHtmlPage; making it a class would need a container in a module both of them load eagerly\nexport function readCompiledClient(name: string): string {\n const file = path.join(__dirname, name);\n if (!fs.existsSync(file)) {\n throw new Error(\n `${name} not found beside ${__dirname}. It is COMPILED from ${name.replace(/\\.js$/, '.ts')} `\n + 'by tsc, so it only exists after a build — run the package build, or inject the text.');\n }\n return fs.readFileSync(file, 'utf-8');\n}\n\nexport class GraphVisualizer {\n private readonly names = new GraphNames();\n private readonly responsibilities = new ResponsibilitiesRenderer();\n private readonly bandLayout = new LevelBandLayout();\n private readonly cycles = new ProjectCycleDetector();\n /** The ONE floating-node-menu implementation, shared with the per-project design pages. */\n private readonly nodeMenu = new GraphNodeMenu();\n\n /**\n * How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD\n * ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in\n * a source checkout (the source there is .client.ts). A unit test running from source supplies the\n * text itself rather than requiring the package to have been built first.\n */\n constructor(\n private readonly clientJs: () => string =\n (): string => readCompiledClient('graph-visualizer.client.js'),\n ) {}\n\n /**\n * A project tagged `drawOnGraph:false` is hidden from the rendered graph —\n * its node, its rank placement, its dropdown option, its responsibilities\n * card, and every edge touching it are all omitted. It stays in the JSON.\n */\n private isHidden(entry: EnhancedGraph[string]): boolean {\n return entry.drawOnGraph === false;\n }\n\n /**\n * Fill color for an env set — the color of the first env in the set that has\n * a known color, else the default.\n */\n private frameworkColor(frameworks: string[]): string {\n for (const env of frameworks) {\n const color = FRAMEWORK_COLORS[env];\n if (color !== undefined) return color;\n }\n return DEFAULT_FRAMEWORK_COLOR;\n }\n\n /**\n * Role border styling — fill stays keyed on framework; the border shows a\n * project's ROLE at a glance. Server and client are the top-level runnable\n * nodes, so they get bold, colored borders to stand out:\n * server → thick GREEN border (a runnable server app)\n * app → thick BLUE border (a runnable non-HTTP app, e.g. a tooling app)\n * bundle → thick PURPLE border (an aggregator that bundles several apps)\n * client → thick RED border (a client app, e.g. angular)\n * designed-lib → bold border (a library with a generated @DocumentDesign design)\n * lib / other → plain thin border\n */\n private roleBorderAttrs(role: string): string {\n if (role === 'server') return ', color=\"green\", penwidth=3';\n if (role === 'app') return ', color=\"#1976d2\", penwidth=3';\n if (role === 'bundle') return ', color=\"#6A1B9A\", penwidth=3';\n if (role === 'client') return ', color=\"red\", penwidth=3';\n if (role === 'api-lib') return ', color=\"#EF6C00\", penwidth=2';\n if (role === 'designed-lib') return ', penwidth=2';\n return '';\n }\n\n /**\n * Edge styling by API-relation kind (why the edge exists):\n * implements → BLACK dashed (a controller serves this api-lib's contract)\n * uses → BLACK solid (a generated client calls it) — same as a\n * plain library import, since a plain dependency IS a use.\n * uses-implements → BLUE dashed, thicker (does both — implements some\n * contracts of the api-lib, uses others)\n * plain lib (none) → the default thin black solid arrow, unchanged.\n * `kind` is undefined for every non-api-lib dependency edge.\n */\n private edgeAttrs(kind: ApiRelationKind | undefined): string {\n if (kind === 'implements') return 'style=dashed';\n if (kind === 'uses-implements') return 'style=dashed, color=\"#1976d2\", penwidth=2';\n return '';\n }\n\n /**\n * The DOT attribute list for ONE dependency edge. An edge that IMPLEMENTS contracts is also\n * LABELED with them: the dashed line alone says \"something here is implemented\", which reads as\n * \"the tool did not detect anything\" to everyone who has not memorized the legend. Naming the\n * contracts makes `auth-store-api` visibly resolve to the server that serves it — the single\n * most important relationship in a microservice architecture, and the one the diagram was\n * silent about.\n */\n private edgeDot(from: string, to: string, relation: ApiRelation | undefined): string {\n const attrs: string[] = [];\n const styling = this.edgeAttrs(relation?.kind);\n if (styling !== '') attrs.push(styling);\n const served = (relation?.implements ?? []).map((ref: ApiRef) => ref.api);\n if (served.length > 0) attrs.push(`label=\"implements: ${this.labelledApis(served)}\", fontsize=9`);\n const suffix = attrs.length === 0 ? '' : ` [${attrs.join(', ')}]`;\n return ` \"${from}\" -> \"${to}\"${suffix};\\n`;\n }\n\n /**\n * The contract names for an edge label, sorted and capped so a shared api-lib serving a dozen\n * contracts cannot blow the edge label up into a wall of text. The truncation is stated in the\n * label (\"+N more\") rather than silent — the full list is in dependencies.json.\n */\n private labelledApis(apis: string[]): string {\n const sorted = [...apis].sort();\n if (sorted.length <= MAX_EDGE_LABEL_APIS) return sorted.join(', ');\n const shown = sorted.slice(0, MAX_EDGE_LABEL_APIS).join(', ');\n return `${shown} +${sorted.length - MAX_EDGE_LABEL_APIS} more`;\n }\n\n /**\n * Href for a node's design page: the project's committed design.html, made\n * relative to architecture/dependencies.html. Returns null when the project\n * has no generated DI design (no design.json → no design page exists).\n */\n private designHtmlHref(designFile: string | undefined): string | null {\n if (!designFile) return null;\n const designHtml = designFile.replace(/design\\.json$/, 'design.html');\n return path.posix.relative(ARCH_OUTPUT_DIR, designHtml);\n }\n\n /**\n * The design pages that EXIST, one entry per visible node that has one. A node absent from this\n * list gets no \"View Design\" item at all — the item is never rendered dead or greyed out.\n */\n designLinks(graph: EnhancedGraph): DesignLink[] {\n const links: DesignLink[] = [];\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const href = this.designHtmlHref(info.designFile);\n if (href === null) continue;\n links.push(new DesignLink(this.names.getNodeId(project), href));\n }\n return links;\n }\n\n /**\n * Generate Graphviz DOT format from the graph\n */\n generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\n this.assertDrawable(graph);\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n const bands = this.levelBands(graph);\n\n dot += this.dotNodes(graph);\n dot += '\\n';\n dot += this.bandLayout.dot(bands);\n dot += '\\n';\n dot += this.dotEdges(graph);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n return dot;\n }\n\n /**\n * Refuse to draw a graph that would render something false.\n *\n * Two conditions, both of which produced a confidently-wrong picture with no warning at all:\n * - two projects sharing a node id fuse into one box, which UNIONS their two rank sets and\n * collapses two whole dependency levels onto one row (see graph-names.ts);\n * - a cycle makes the level numbers every row is keyed on meaningless (see graph-cycles.ts).\n */\n private assertDrawable(graph: EnhancedGraph): void {\n const owners: NodeIdOwner[] = [];\n const adjacency: ProjectAdjacency = {};\n for (const project of Object.keys(graph)) {\n owners.push(new NodeIdOwner(project, graph[project].level));\n adjacency[project] = graph[project].dependsOn ?? [];\n }\n this.names.assertUniqueNodeIds(owners);\n this.cycles.assertAcyclic(adjacency, 'architecture/dependencies.json');\n }\n\n /**\n * The visible projects grouped into one band per dependency level, ordered HIGHEST LEVEL FIRST\n * so the emitted bands read top-to-bottom with L0 last (hidden projects are omitted, so no\n * stray rank=same name is emitted for an absent node). LevelBandLayout turns these into the\n * rank sets and the invisible chain that pins them — see graph-level-bands.ts for why the\n * chain is required at all.\n */\n private levelBands(graph: EnhancedGraph): LevelBand[] {\n const byLevel = new Map<number, string[]>();\n for (const project of Object.keys(graph)) {\n if (this.isHidden(graph[project])) continue;\n const level = graph[project].level;\n const nodeIds = byLevel.get(level);\n if (nodeIds === undefined) byLevel.set(level, [this.names.getNodeId(project)]);\n else nodeIds.push(this.names.getNodeId(project));\n }\n const levels = [...byLevel.keys()].sort((a: number, b: number): number => b - a);\n return levels.map((level: number): LevelBand =>\n new LevelBand(level, [...(byLevel.get(level) as string[])].sort()));\n }\n\n // Node lines: fill colored by framework env set (libType), border shaped by\n // role; the label shows the env set + role (e.g. [browser, node] · server).\n // No node carries a URL: EVERY box is clickable and opens the floating node\n // menu instead, which is where a design page is reached (see designLinks).\n private dotNodes(graph: EnhancedGraph): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n const frameworks = info.framework ?? [];\n const role = info.role ?? 'lib';\n const color = this.frameworkColor(frameworks);\n const border = this.roleBorderAttrs(role);\n const envSet = `[${frameworks.join(', ')}]`;\n const labelMeta = `L${info.level} · ${envSet} · ${role}`;\n // Identity is the project key; the LABEL is the pretty short name.\n dot += ` \"${nodeId}\" [fillcolor=\"${color}\"${border}, label=\"${shortName}\\\\n(${labelMeta})\"];\\n`;\n }\n return dot;\n }\n\n // Edge lines (dependencies). An edge to an api-lib is styled by WHY it exists\n // (implements/uses/uses-implements, from apiRelations); every other dependency\n // keeps the default plain arrow.\n private dotEdges(graph: EnhancedGraph): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const nodeId = this.names.getNodeId(project);\n for (const dep of info.dependsOn || []) {\n // Both endpoints must be visible — an edge to/from a hidden box\n // is dropped so no connection dangles into empty space.\n if (graph[dep] !== undefined && this.isHidden(graph[dep])) continue;\n dot += this.edgeDot(nodeId, this.names.getNodeId(dep), info.apiRelations?.[dep]);\n }\n }\n return dot;\n }\n\n /**\n * Generate interactive HTML with embedded SVG using viz.js\n */\n generateHTML(\n dot: string,\n links: DesignLink[],\n title: string = 'Monorepo Dependency Architecture',\n lockControl: string = '',\n responsibilitiesHtml: string = ''\n ): string {\n const styles = this.styles();\n const legend = this.legend();\n const script = this.script(dot, links);\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>${styles}</style>\n</head>\n<body>\n <h1>${title}</h1>\n <p class=\"hint\">💡 <strong>Click any box</strong> for its menu — <strong>View Design</strong> (only where that project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it) and <strong>Lock/Unlock</strong>, which is the same lock as the dropdown below.</p>\n <p class=\"hint\">🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it (all the way up) <em>and</em> every dependency below it (all the way down), with all the boxes and lines between — while the rest of the graph dims so you can follow one box at a glance.</p>\n ${legend}\n ${lockControl}\n <div id=\"graph\"></div>\n ${responsibilitiesHtml}\n <script>${this.nodeMenu.script()}</script>\n <script>${script}</script>\n</body>\n</html>`;\n }\n\n /**\n * The lock control (a single-select dropdown, rendered below the legend).\n * Picking a module LOCKS the graph into that box's hover view — its full\n * ancestor + descendant chain stays lit while everything else stays dimmed —\n * and narrows the responsibilities list below the graph to just that chain.\n * The first option, \"All\", is the default and clears the lock. Hover still\n * works on top of a lock; leaving a box returns to the locked view.\n *\n * Options are ordered by level DESCENDING to match the responsibilities cards.\n */\n lockControl(graph: EnhancedGraph): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n let options = '';\n for (const project of projects) {\n if (this.isHidden(graph[project])) continue;\n // The VALUE is the node id the SVG is keyed on; the TEXT stays the pretty short name.\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n options += `<option value=\"${nodeId}\">L${graph[project].level} · ${shortName}</option>`;\n }\n return `<div class=\"wp-lock-control\">\n <label for=\"wp-lock\">🔒 Lock a box (dim the rest &amp; filter responsibilities):</label>\n <select id=\"wp-lock\"><option value=\"\">All (no lock)</option>${options}</select>\n </div>`;\n }\n\n private styles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .hint { text-align: center; color: #555; margin: 0 0 16px; }\n /* Every box is clickable (the floating node menu replaced direct\n * navigation), so the menu's own stylesheet carries the cursor + blue\n * glow, and the dim/undim rules a hover or a lock toggles. Both are\n * shared verbatim with the per-project design pages. */\n ${this.nodeMenu.styles()}\n ${this.nodeMenu.dimStyles('#graph')}\n #graph {\n text-align: center;\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; }\n .legend-item { margin: 8px 0; }\n .legend-box {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 1px solid #ccc;\n margin-right: 10px;\n vertical-align: middle;\n }\n ${this.componentStyles()}\n `;\n }\n\n // Styles for the lock dropdown and the responsibilities card list below the\n // graph. Split out of styles() to keep each method within the line limit.\n private componentStyles(): string {\n return `\n /* The architecture graph is very wide, so lay the legend out as three\n * side-by-side columns (fill / border / edge) instead of one tall\n * column — it keeps the legend short next to the wide graph, and\n * collapses back to a single column on narrow viewports. */\n .legend-columns {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 28px;\n align-items: start;\n }\n .legend-col h3 { margin: 0 0 8px; color: #333; font-size: 15px; }\n @media (max-width: 800px) { .legend-columns { grid-template-columns: 1fr; } }\n .wp-lock-control {\n max-width: 600px;\n margin: 0 auto 16px;\n padding: 12px 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n text-align: center;\n }\n .wp-lock-control label { font-weight: bold; color: #333; margin-right: 8px; }\n .wp-lock-control select { font-size: 14px; padding: 4px 8px; }\n #wp-responsibilities { max-width: 900px; margin: 24px auto 0; }\n #wp-responsibilities h2 { color: #333; }\n .wp-resp-card {\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n margin: 10px 0;\n padding: 10px 15px;\n }\n .wp-resp-card > summary { cursor: pointer; color: #333; }\n .wp-resp-level {\n display: inline-block;\n min-width: 26px;\n padding: 1px 6px;\n margin-right: 6px;\n border-radius: 4px;\n background: #eef;\n font-size: 12px;\n font-weight: bold;\n text-align: center;\n }\n .wp-resp-body { margin-top: 8px; color: #444; }\n .wp-resp-body code {\n background: #f2f2f2;\n padding: 1px 4px;\n border-radius: 3px;\n font-family: monospace;\n }\n .wp-hidden { display: none; }`;\n }\n\n // The legend is laid out in three side-by-side columns (fill / border /\n // edge) so it stays short next to the very wide architecture graph. Each\n // column's rows come from a helper below to keep this method within the line\n // limit; the footnote spans the full width beneath the columns.\n private legend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-columns\">\n <div class=\"legend-col\">\n <h3>Fill = framework (libType)</h3>\n ${this.fillItems()}\n </div>\n <div class=\"legend-col\">\n <h3>Border = role</h3>\n ${this.borderItems()}\n </div>\n <div class=\"legend-col\">\n <h3>Edge lines — <em>why</em> a project depends on an api-lib</h3>\n ${this.edgeItems()}\n </div>\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Each node label shows its dependency level (L#), its framework env set (e.g. [browser, node]), and its role. Every row is one dependency level and nothing else: the HIGHEST level is the top row, levels descend as you read down, and L0 — the foundation libraries everything else is built on — is always the bottom row. Transitive dependencies are allowed but not shown.</em>\n </div>\n </div>`;\n }\n\n // Column 1 — fill color keyed on the project's framework (libType) env set.\n private fillItems(): string {\n return `<div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FCE4EC;\"></span>\n <strong>angular:</strong> Angular front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>react:</strong> React front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #EDE7F6;\"></span>\n <strong>browser:</strong> browser front-end base env\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>express:</strong> Express / server side\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FFF9C4;\"></span>\n <strong>node:</strong> node server base env\n </div>`;\n }\n\n // Column 2 — border style keyed on the project's role.\n private borderItems(): string {\n return `<div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 3px solid green;\"></span>\n <strong>server:</strong> runnable server app (thick green border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 3px solid red;\"></span>\n <strong>client:</strong> client app, e.g. angular (thick red border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 2px solid #333;\"></span>\n <strong>designed-lib:</strong> library with a generated @DocumentDesign design (bold border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 1px solid #ccc;\"></span>\n <strong>lib:</strong> plain library, no generated design (thin border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 2px solid #EF6C00;\"></span>\n <strong>api-lib:</strong> API-contract library (defines <code>@ApiPath</code>/<code>@Rpc</code>/<code>@PubSub</code> <code>*Api</code> classes)\n </div>`;\n }\n\n // Column 3 — edge line style keyed on WHY a project depends on an api-lib.\n private edgeItems(): string {\n return `<div class=\"legend-item\">\n <svg width=\"42\" height=\"12\" style=\"vertical-align: middle; margin-right: 10px;\"><line x1=\"0\" y1=\"6\" x2=\"42\" y2=\"6\" stroke=\"#333\" stroke-width=\"2\"/></svg>\n <strong>uses:</strong> calls the API (generates an rpc/pubsub client via <code>createRpcClient</code>/<code>createPubSubClient</code>) — also covers a plain library import, since a plain dependency is just a use.\n </div>\n <div class=\"legend-item\">\n <svg width=\"42\" height=\"12\" style=\"vertical-align: middle; margin-right: 10px;\"><line x1=\"0\" y1=\"6\" x2=\"42\" y2=\"6\" stroke=\"#333\" stroke-width=\"2\" stroke-dasharray=\"5,3\"/></svg>\n <strong>implements:</strong> serves the API — labeled <code>implements: &lt;contracts&gt;</code> so you can see WHICH contracts resolve to this server without opening the JSON. NOTE: this is a build-dependency diagram, so a UML <em>implements</em> arrow can't be used; we use a dashed line to signal a build dep, because this server implements the api and the api is built first, then this server after.\n </div>\n <div class=\"legend-item\">\n <svg width=\"42\" height=\"12\" style=\"vertical-align: middle; margin-right: 10px;\"><line x1=\"0\" y1=\"6\" x2=\"42\" y2=\"6\" stroke=\"#1976d2\" stroke-width=\"2\" stroke-dasharray=\"5,3\"/></svg>\n <strong>uses/implements:</strong> both — implements some of the api-lib's contracts, uses others\n </div>`;\n }\n\n /**\n * The page script. The browser code lives in graph-visualizer.client.ts — real, linted TypeScript\n * that tsc compiles in place, so what this inlines is its COMPILED .js sitting beside this file.\n *\n * The substitution is a blind split/join, which is why the placeholder token must appear EXACTLY\n * ONCE in the client (never in one of its comments): every literal occurrence would otherwise be\n * replaced by the whole DOT.\n */\n private script(dot: string, links: DesignLink[]): string {\n return this.clientJs()\n .split(CLIENT_DOT_PLACEHOLDER).join(JSON.stringify(dot))\n .split(CLIENT_DESIGN_LINKS_PLACEHOLDER).join(JSON.stringify(links));\n }\n\n /**\n * Write the committed architecture visualization to\n * architecture/dependencies.html, next to dependencies.json.\n *\n * This is a checked-in artifact, regenerated deterministically by\n * architecture:generate so every box keeps its menu, and the \"View Design\"\n * item keeps pointing at each project's committed design.html as designs\n * come and go. The DOT is embedded in the HTML (rendered client-side by\n * viz.js). Output is deterministic (sorted graph in → same bytes out) so git\n * only shows a diff when the architecture actually changed.\n */\n writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'Monorepo Dependency Architecture'\n ): VisualizationPaths {\n const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const lockControl = this.lockControl(graph);\n const responsibilities = this.responsibilities.generateSection(graph, workspaceRoot);\n const html = this.generateHTML(\n this.generateDot(graph, title), this.designLinks(graph), title, lockControl, responsibilities);\n const htmlPath = path.join(outputDir, 'dependencies.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return new VisualizationPaths(htmlPath);\n }\n\n /**\n * Open the HTML visualization in the default browser\n */\n openVisualization(htmlPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not open browser: ${error.message}`);\n return false;\n }\n }\n}\n"]}
@@ -45,7 +45,28 @@ declare class QueueCylinders {
45
45
  private boundsOf;
46
46
  private pathEl;
47
47
  }
48
- /** Renders the DOT, reshapes the queues, and reports a failure into the page rather than only the console. */
48
+ /**
49
+ * Every node of the runtime graph opens the shared floating menu.
50
+ *
51
+ * There is exactly ONE item, and it is the lock. NO "View Design": a node here is a running SERVICE,
52
+ * a queue, a datastore or a third-party system, not an nx project, so there is no design.html to point
53
+ * at and the item is ABSENT rather than present-and-dead.
54
+ *
55
+ * LOCK means the literal thing the box shows: dim every other node and every edge, light the locked
56
+ * box alone. That is `WpNodeLock`, the same lock a design page uses — this page has no lock dropdown
57
+ * and no responsibilities list, so there is no second control for it to fall out of step with, and
58
+ * the menu label is derived from the lock's own state on every open.
59
+ */
60
+ declare class RuntimeNodeMenu {
61
+ private readonly svg;
62
+ private readonly lock;
63
+ constructor(svg: SVGSVGElement);
64
+ wire(): void;
65
+ }
66
+ /**
67
+ * Renders the DOT, reshapes the queues, wires the node menu, and reports a failure into the page
68
+ * rather than only the console.
69
+ */
49
70
  declare class RuntimePage {
50
71
  render(): void;
51
72
  }
@@ -8,10 +8,12 @@
8
8
  * bin entry points, and linted like every other file here. (It used to be a committed .js asset
9
9
  * exempted from `no-js-files`; see graph-visualizer.client.ts for why that exemption is gone.)
10
10
  *
11
- * Two jobs:
11
+ * Three jobs:
12
12
  * 1. render the DOT with @viz-js/viz v3 (instance() resolves the WASM renderer, and
13
13
  * renderSVGElement is then SYNCHRONOUS — v2's returned a promise);
14
- * 2. upgrade every queue node into a TRUE horizontal cylinder.
14
+ * 2. upgrade every queue node into a TRUE horizontal cylinder;
15
+ * 3. make every node clickable, opening the SHARED floating menu (graph-node-menu.ts — the same one
16
+ * architecture/dependencies.html and every design.html use) whose only item here is Lock/Unlock.
15
17
  *
16
18
  * Why (2) is post-processing rather than a shape:
17
19
  *
@@ -158,7 +160,36 @@ class QueueCylinders {
158
160
  return el;
159
161
  }
160
162
  }
161
- /** Renders the DOT, reshapes the queues, and reports a failure into the page rather than only the console. */
163
+ /**
164
+ * Every node of the runtime graph opens the shared floating menu.
165
+ *
166
+ * There is exactly ONE item, and it is the lock. NO "View Design": a node here is a running SERVICE,
167
+ * a queue, a datastore or a third-party system, not an nx project, so there is no design.html to point
168
+ * at and the item is ABSENT rather than present-and-dead.
169
+ *
170
+ * LOCK means the literal thing the box shows: dim every other node and every edge, light the locked
171
+ * box alone. That is `WpNodeLock`, the same lock a design page uses — this page has no lock dropdown
172
+ * and no responsibilities list, so there is no second control for it to fall out of step with, and
173
+ * the menu label is derived from the lock's own state on every open.
174
+ */
175
+ class RuntimeNodeMenu {
176
+ svg;
177
+ lock;
178
+ constructor(svg) {
179
+ this.svg = svg;
180
+ this.lock = new WpNodeLock(svg);
181
+ }
182
+ wire() {
183
+ WpNodeMenu.wire(this.svg, (name, node) => {
184
+ const label = this.lock.isLocked(name) ? 'Unlock' : 'Lock';
185
+ return [new WpNodeMenuItem(label, () => { this.lock.toggle(name, node); })];
186
+ });
187
+ }
188
+ }
189
+ /**
190
+ * Renders the DOT, reshapes the queues, wires the node menu, and reports a failure into the page
191
+ * rather than only the console.
192
+ */
162
193
  class RuntimePage {
163
194
  render() {
164
195
  Viz.instance()
@@ -166,8 +197,12 @@ class RuntimePage {
166
197
  const element = viz.renderSVGElement(__DOT__);
167
198
  new QueueCylinders().applyTo(element);
168
199
  const host = document.getElementById('graph');
169
- if (host !== null)
170
- host.appendChild(element);
200
+ if (host === null)
201
+ return;
202
+ host.appendChild(element);
203
+ // AFTER the cylinders: wiring reads each node's <title>, which the redraw leaves
204
+ // alone, but the clickable class belongs on the shape that is finally there.
205
+ new RuntimeNodeMenu(element).wire();
171
206
  })
172
207
  // webpieces-disable no-any-unknown -- a promise rejection reason is untyped BY THE LANGUAGE (any value can be thrown), and this browser script cannot import the repo's toError helper; it is stringified, never dereferenced
173
208
  .catch((err) => {
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Browser-side script for tmp/webpieces/runtime-architecture.html.\n *\n * runtime-visualizer.ts reads this file's COMPILED output with readFileSync, substitutes the DOT\n * placeholder (see viz-client-globals.d.ts) with the JSON-encoded DOT, and inlines the result into a <script> tag. It runs in a\n * browser, never in node — but it is ordinary TypeScript, compiled in place by tsc like the package's\n * bin entry points, and linted like every other file here. (It used to be a committed .js asset\n * exempted from `no-js-files`; see graph-visualizer.client.ts for why that exemption is gone.)\n *\n * Two jobs:\n * 1. render the DOT with @viz-js/viz v3 (instance() resolves the WASM renderer, and\n * renderSVGElement is then SYNCHRONOUS — v2's returned a promise);\n * 2. upgrade every queue node into a TRUE horizontal cylinder.\n *\n * Why (2) is post-processing rather than a shape:\n *\n * Graphviz has exactly one `cylinder` and it is upright-only. `orientation=` is documented as rotating\n * POLYGON shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute — graphviz\n * issue #2244, open since 2022 and still reproducible on Graphviz 15, the version this page's renderer\n * carries. All 54 native shapes were compared at the real label size; none is a horizontal cylinder. So\n * the DOT emits `Mrecord` (which renders sensibly on its own for anyone running `dot` over the\n * committed .dot file) and this script redraws it in the browser, where the geometry is computed from\n * each node's actual bounding box and therefore fits any label width — which a fixed image asset never\n * can.\n */\n\n// The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both\n// scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token\n// in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.\n\n/** Cap width as a fraction of the node's half-height. Below ~0.6 it reads as a rounded box. */\nconst CAP_RATIO = 0.75;\n/** A node narrower than this is not a queue box worth reshaping. */\nconst MIN_WIDTH = 40;\n\n/** The union bounding box of a node's shape elements, in user space. Data-only → a class. */\nclass Bounds {\n constructor(\n readonly x0: number,\n readonly x1: number,\n readonly y0: number,\n readonly y1: number,\n ) {}\n\n width(): number {\n return this.x1 - this.x0;\n }\n\n halfHeight(): number {\n return (this.y1 - this.y0) / 2;\n }\n}\n\n/**\n * Redraws the nodes the DOT marked `wp_queue` as cylinders lying on their side.\n *\n * Selected by CLASS, not by id prefix: a queue-kind external system shares the `system__` id space with\n * databases, and a database is an UPRIGHT cylinder that must be left alone.\n */\nclass QueueCylinders {\n private static readonly SVG_NS = 'http://www.w3.org/2000/svg';\n\n applyTo(svg: SVGSVGElement): void {\n svg.querySelectorAll('g.wp_queue').forEach((node: Element): void => { this.reshape(node); });\n }\n\n /**\n * Replace one node's shape elements with a horizontal cylinder sized to its own bounding box.\n *\n * The bbox is measured from the shape elements BEFORE they are removed, never from getBBox() on the\n * whole group — the group includes the text, which is inset, so using it would shrink the cylinder\n * inside the very label it is meant to contain.\n */\n private reshape(node: Element): void {\n const shapes = node.querySelectorAll('polygon, path, polyline');\n if (shapes.length === 0) return;\n const box = this.boundsOf(shapes);\n if (box === null || box.width() < MIN_WIDTH) return;\n\n const first = shapes[0];\n const fill = first.getAttribute('fill') ?? 'none';\n const stroke = first.getAttribute('stroke') ?? 'black';\n // Measured BEFORE the shapes go: the record separator is where the label text starts.\n const separator = this.separatorX(shapes);\n shapes.forEach((shape: Element): void => { shape.remove(); });\n\n const ry = box.halfHeight();\n const rx = this.capRadius(box, ry, separator);\n const body =\n 'M' + (box.x0 + rx) + ',' + box.y0 +\n ' L' + (box.x1 - rx) + ',' + box.y0 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x1 - rx) + ',' + box.y1 +\n ' L' + (box.x0 + rx) + ',' + box.y1 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x0 + rx) + ',' + box.y0 + ' Z';\n // Only the NEAR end cap is drawn: that single arc is what reads as \"tube\" rather than\n // \"stadium\", and a real cylinder hides the far one behind the body.\n const cap =\n 'M' + (box.x0 + rx) + ',' + box.y0 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x0 + rx) + ',' + box.y1;\n\n const anchor = node.firstChild;\n if (anchor === null || anchor.nextSibling === null) return;\n node.insertBefore(this.pathEl(fill, stroke, body), anchor.nextSibling);\n node.insertBefore(this.pathEl('none', stroke, cap), anchor.nextSibling.nextSibling);\n\n // The label is deliberately NOT nudged. QUEUE_LABEL_PREFIX already gives the node an empty\n // leading record field, so Graphviz has centred the text in the space to the RIGHT of where the\n // cap lands. Shifting it again double-counts that offset and pushes the longest line out\n // through the far end of the cylinder.\n }\n\n /**\n * A box listing several queues is TALL, and a cap scaled off half-height alone would then reach\n * past the empty leading field and sit on top of the first characters of the label. Graphviz sizes\n * that field from the font, not from the label's height, so the separator is the only honest bound\n * on how far right the cap may go.\n */\n private capRadius(box: Bounds, ry: number, separator: number | null): number {\n const fromHeight = Math.max(6, ry * CAP_RATIO);\n if (separator === null) return fromHeight;\n return Math.max(6, Math.min(fromHeight, separator - box.x0));\n }\n\n /**\n * The x of the record's field separator — the VERTICAL polyline Graphviz draws between the empty\n * leading field and the label — or null if this node has none.\n */\n private separatorX(shapes: NodeListOf<Element>): number | null {\n for (const shape of Array.from(shapes)) {\n if (shape.tagName !== 'polyline') continue;\n const nums = (shape.getAttribute('points') ?? '').match(/-?[\\d.]+/g);\n if (nums === null || nums.length < 4) continue;\n const x = parseFloat(nums[0]);\n if (Math.abs(x - parseFloat(nums[2])) < 0.5) return x;\n }\n return null;\n }\n\n /** The union bounding box of some SVG shape elements, from their raw geometry attributes. */\n private boundsOf(shapes: NodeListOf<Element>): Bounds | null {\n const xs: number[] = [];\n const ys: number[] = [];\n for (const shape of Array.from(shapes)) {\n const pts = shape.getAttribute('points');\n const source = pts ?? shape.getAttribute('d') ?? '';\n const nums = source.match(/-?[\\d.]+/g);\n if (nums === null) continue;\n for (let n = 0; n + 1 < nums.length; n += 2) {\n xs.push(parseFloat(nums[n]));\n ys.push(parseFloat(nums[n + 1]));\n }\n }\n if (xs.length === 0) return null;\n return new Bounds(Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys));\n }\n\n private pathEl(fill: string, stroke: string, d: string): SVGPathElement {\n const el = document.createElementNS(QueueCylinders.SVG_NS, 'path');\n el.setAttribute('fill', fill);\n el.setAttribute('stroke', stroke);\n el.setAttribute('d', d);\n return el;\n }\n}\n\n/** Renders the DOT, reshapes the queues, and reports a failure into the page rather than only the console. */\nclass RuntimePage {\n render(): void {\n Viz.instance()\n .then((viz: VizInstance): void => {\n const element = viz.renderSVGElement(__DOT__);\n new QueueCylinders().applyTo(element);\n const host = document.getElementById('graph');\n if (host !== null) host.appendChild(element);\n })\n // webpieces-disable no-any-unknown -- a promise rejection reason is untyped BY THE LANGUAGE (any value can be thrown), and this browser script cannot import the repo's toError helper; it is stringified, never dereferenced\n .catch((err: unknown): void => {\n console.error(err);\n const host = document.getElementById('graph');\n if (host !== null) host.innerHTML = '<pre>' + String(err) + '</pre>';\n });\n }\n}\n\nnew RuntimePage().render();\n"]}
1
+ {"version":3,"file":"runtime-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Browser-side script for tmp/webpieces/runtime-architecture.html.\n *\n * runtime-visualizer.ts reads this file's COMPILED output with readFileSync, substitutes the DOT\n * placeholder (see viz-client-globals.d.ts) with the JSON-encoded DOT, and inlines the result into a <script> tag. It runs in a\n * browser, never in node — but it is ordinary TypeScript, compiled in place by tsc like the package's\n * bin entry points, and linted like every other file here. (It used to be a committed .js asset\n * exempted from `no-js-files`; see graph-visualizer.client.ts for why that exemption is gone.)\n *\n * Three jobs:\n * 1. render the DOT with @viz-js/viz v3 (instance() resolves the WASM renderer, and\n * renderSVGElement is then SYNCHRONOUS — v2's returned a promise);\n * 2. upgrade every queue node into a TRUE horizontal cylinder;\n * 3. make every node clickable, opening the SHARED floating menu (graph-node-menu.ts — the same one\n * architecture/dependencies.html and every design.html use) whose only item here is Lock/Unlock.\n *\n * Why (2) is post-processing rather than a shape:\n *\n * Graphviz has exactly one `cylinder` and it is upright-only. `orientation=` is documented as rotating\n * POLYGON shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute — graphviz\n * issue #2244, open since 2022 and still reproducible on Graphviz 15, the version this page's renderer\n * carries. All 54 native shapes were compared at the real label size; none is a horizontal cylinder. So\n * the DOT emits `Mrecord` (which renders sensibly on its own for anyone running `dot` over the\n * committed .dot file) and this script redraws it in the browser, where the geometry is computed from\n * each node's actual bounding box and therefore fits any label width — which a fixed image asset never\n * can.\n */\n\n// The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both\n// scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token\n// in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.\n\n/** Cap width as a fraction of the node's half-height. Below ~0.6 it reads as a rounded box. */\nconst CAP_RATIO = 0.75;\n/** A node narrower than this is not a queue box worth reshaping. */\nconst MIN_WIDTH = 40;\n\n/** The union bounding box of a node's shape elements, in user space. Data-only → a class. */\nclass Bounds {\n constructor(\n readonly x0: number,\n readonly x1: number,\n readonly y0: number,\n readonly y1: number,\n ) {}\n\n width(): number {\n return this.x1 - this.x0;\n }\n\n halfHeight(): number {\n return (this.y1 - this.y0) / 2;\n }\n}\n\n/**\n * Redraws the nodes the DOT marked `wp_queue` as cylinders lying on their side.\n *\n * Selected by CLASS, not by id prefix: a queue-kind external system shares the `system__` id space with\n * databases, and a database is an UPRIGHT cylinder that must be left alone.\n */\nclass QueueCylinders {\n private static readonly SVG_NS = 'http://www.w3.org/2000/svg';\n\n applyTo(svg: SVGSVGElement): void {\n svg.querySelectorAll('g.wp_queue').forEach((node: Element): void => { this.reshape(node); });\n }\n\n /**\n * Replace one node's shape elements with a horizontal cylinder sized to its own bounding box.\n *\n * The bbox is measured from the shape elements BEFORE they are removed, never from getBBox() on the\n * whole group — the group includes the text, which is inset, so using it would shrink the cylinder\n * inside the very label it is meant to contain.\n */\n private reshape(node: Element): void {\n const shapes = node.querySelectorAll('polygon, path, polyline');\n if (shapes.length === 0) return;\n const box = this.boundsOf(shapes);\n if (box === null || box.width() < MIN_WIDTH) return;\n\n const first = shapes[0];\n const fill = first.getAttribute('fill') ?? 'none';\n const stroke = first.getAttribute('stroke') ?? 'black';\n // Measured BEFORE the shapes go: the record separator is where the label text starts.\n const separator = this.separatorX(shapes);\n shapes.forEach((shape: Element): void => { shape.remove(); });\n\n const ry = box.halfHeight();\n const rx = this.capRadius(box, ry, separator);\n const body =\n 'M' + (box.x0 + rx) + ',' + box.y0 +\n ' L' + (box.x1 - rx) + ',' + box.y0 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x1 - rx) + ',' + box.y1 +\n ' L' + (box.x0 + rx) + ',' + box.y1 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x0 + rx) + ',' + box.y0 + ' Z';\n // Only the NEAR end cap is drawn: that single arc is what reads as \"tube\" rather than\n // \"stadium\", and a real cylinder hides the far one behind the body.\n const cap =\n 'M' + (box.x0 + rx) + ',' + box.y0 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x0 + rx) + ',' + box.y1;\n\n const anchor = node.firstChild;\n if (anchor === null || anchor.nextSibling === null) return;\n node.insertBefore(this.pathEl(fill, stroke, body), anchor.nextSibling);\n node.insertBefore(this.pathEl('none', stroke, cap), anchor.nextSibling.nextSibling);\n\n // The label is deliberately NOT nudged. QUEUE_LABEL_PREFIX already gives the node an empty\n // leading record field, so Graphviz has centred the text in the space to the RIGHT of where the\n // cap lands. Shifting it again double-counts that offset and pushes the longest line out\n // through the far end of the cylinder.\n }\n\n /**\n * A box listing several queues is TALL, and a cap scaled off half-height alone would then reach\n * past the empty leading field and sit on top of the first characters of the label. Graphviz sizes\n * that field from the font, not from the label's height, so the separator is the only honest bound\n * on how far right the cap may go.\n */\n private capRadius(box: Bounds, ry: number, separator: number | null): number {\n const fromHeight = Math.max(6, ry * CAP_RATIO);\n if (separator === null) return fromHeight;\n return Math.max(6, Math.min(fromHeight, separator - box.x0));\n }\n\n /**\n * The x of the record's field separator — the VERTICAL polyline Graphviz draws between the empty\n * leading field and the label — or null if this node has none.\n */\n private separatorX(shapes: NodeListOf<Element>): number | null {\n for (const shape of Array.from(shapes)) {\n if (shape.tagName !== 'polyline') continue;\n const nums = (shape.getAttribute('points') ?? '').match(/-?[\\d.]+/g);\n if (nums === null || nums.length < 4) continue;\n const x = parseFloat(nums[0]);\n if (Math.abs(x - parseFloat(nums[2])) < 0.5) return x;\n }\n return null;\n }\n\n /** The union bounding box of some SVG shape elements, from their raw geometry attributes. */\n private boundsOf(shapes: NodeListOf<Element>): Bounds | null {\n const xs: number[] = [];\n const ys: number[] = [];\n for (const shape of Array.from(shapes)) {\n const pts = shape.getAttribute('points');\n const source = pts ?? shape.getAttribute('d') ?? '';\n const nums = source.match(/-?[\\d.]+/g);\n if (nums === null) continue;\n for (let n = 0; n + 1 < nums.length; n += 2) {\n xs.push(parseFloat(nums[n]));\n ys.push(parseFloat(nums[n + 1]));\n }\n }\n if (xs.length === 0) return null;\n return new Bounds(Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys));\n }\n\n private pathEl(fill: string, stroke: string, d: string): SVGPathElement {\n const el = document.createElementNS(QueueCylinders.SVG_NS, 'path');\n el.setAttribute('fill', fill);\n el.setAttribute('stroke', stroke);\n el.setAttribute('d', d);\n return el;\n }\n}\n\n/**\n * Every node of the runtime graph opens the shared floating menu.\n *\n * There is exactly ONE item, and it is the lock. NO \"View Design\": a node here is a running SERVICE,\n * a queue, a datastore or a third-party system, not an nx project, so there is no design.html to point\n * at and the item is ABSENT rather than present-and-dead.\n *\n * LOCK means the literal thing the box shows: dim every other node and every edge, light the locked\n * box alone. That is `WpNodeLock`, the same lock a design page uses — this page has no lock dropdown\n * and no responsibilities list, so there is no second control for it to fall out of step with, and\n * the menu label is derived from the lock's own state on every open.\n */\nclass RuntimeNodeMenu {\n private readonly lock: WpNodeLock;\n\n constructor(private readonly svg: SVGSVGElement) {\n this.lock = new WpNodeLock(svg);\n }\n\n wire(): void {\n WpNodeMenu.wire(this.svg, (name: string, node: SVGGElement): WpNodeMenuItem[] => {\n const label = this.lock.isLocked(name) ? 'Unlock' : 'Lock';\n return [new WpNodeMenuItem(label, (): void => { this.lock.toggle(name, node); })];\n });\n }\n}\n\n/**\n * Renders the DOT, reshapes the queues, wires the node menu, and reports a failure into the page\n * rather than only the console.\n */\nclass RuntimePage {\n render(): void {\n Viz.instance()\n .then((viz: VizInstance): void => {\n const element = viz.renderSVGElement(__DOT__);\n new QueueCylinders().applyTo(element);\n const host = document.getElementById('graph');\n if (host === null) return;\n host.appendChild(element);\n // AFTER the cylinders: wiring reads each node's <title>, which the redraw leaves\n // alone, but the clickable class belongs on the shape that is finally there.\n new RuntimeNodeMenu(element).wire();\n })\n // webpieces-disable no-any-unknown -- a promise rejection reason is untyped BY THE LANGUAGE (any value can be thrown), and this browser script cannot import the repo's toError helper; it is stringified, never dereferenced\n .catch((err: unknown): void => {\n console.error(err);\n const host = document.getElementById('graph');\n if (host !== null) host.innerHTML = '<pre>' + String(err) + '</pre>';\n });\n }\n}\n\nnew RuntimePage().render();\n"]}
@@ -34,6 +34,14 @@
34
34
  * systems, so a vendor we both call and are called by is ONE box. Those are the
35
35
  * entry points that wake a service up at 3am, and a graph built only from in-repo
36
36
  * callers cannot show them at all.
37
+ *
38
+ * EVERY node on the rendered page is clickable and opens the SHARED floating menu
39
+ * (graph-node-menu.ts — one implementation, also used by architecture/dependencies.html
40
+ * and every project's design.html). Its only item is Lock/Unlock: locking dims every
41
+ * other node and every edge and lights the locked box alone. There is no "View Design"
42
+ * item here — a node is a running service, queue, datastore or third-party system, not
43
+ * an nx project, so no design.html exists to point at and the item is absent rather than
44
+ * dead. This page has no lock dropdown, so the menu is the one and only lock control.
37
45
  */
38
46
  import type { RuntimeGraph } from './runtime-graph';
39
47
  /** Render options for the runtime graph. */
@@ -54,6 +62,40 @@ export declare class RuntimeVizOptions {
54
62
  }
55
63
  /** Build the Graphviz DOT for the runtime service graph. */
56
64
  export declare function generateRuntimeDot(graph: RuntimeGraph, title?: string, options?: RuntimeVizOptions): string;
65
+ /**
66
+ * The runtime-architecture HTML page: its styles, the shared floating node menu, the graph host
67
+ * and the legend.
68
+ *
69
+ * A CLASS rather than a bag of module functions so the client-text seam can be a constructor
70
+ * parameter the way GraphVisualizer's is: the default reads the COMPILED sibling, which exists in
71
+ * dist and in the published tarball but NOT in a source checkout, and a unit test running from
72
+ * source hands in the text itself instead of requiring the package to have been built first.
73
+ */
74
+ export declare class RuntimeHtmlPage {
75
+ private readonly clientJs;
76
+ /** The ONE floating-node-menu implementation, shared with dependencies.html and every design.html. */
77
+ private readonly nodeMenu;
78
+ constructor(clientJs?: () => string);
79
+ render(dot: string, title: string): string;
80
+ /**
81
+ * The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts)
82
+ * rather than in a template literal here: it renders with @viz-js/viz v3, redraws every queue
83
+ * node as a true horizontal cylinder, and wires the shared node menu onto every box — more
84
+ * logic than belongs inline in a .ts string. The substitution is a blind split/join, so the
85
+ * placeholder must appear EXACTLY ONCE in the client.
86
+ */
87
+ private script;
88
+ /**
89
+ * Page styles, including the shared menu stylesheet: the clickable cursor + blue glow on every
90
+ * box, and the dim/undim rules the lock toggles, scoped to this page's `#graph` host. Those two
91
+ * blocks are shared verbatim with architecture/dependencies.html and every design.html.
92
+ *
93
+ * The legend swatches are hand-drawn inline SVG on purpose: the alternative is shelling out to
94
+ * Graphviz at generate time, which would make writing the HTML depend on a `dot` binary being
95
+ * installed — a dependency this tool does not otherwise have, since rendering is client-side.
96
+ */
97
+ private styles;
98
+ }
57
99
  export interface RuntimeVisualizationPaths {
58
100
  dotPath: string;
59
101
  htmlPath: string;
@@ -35,9 +35,17 @@
35
35
  * systems, so a vendor we both call and are called by is ONE box. Those are the
36
36
  * entry points that wake a service up at 3am, and a graph built only from in-repo
37
37
  * callers cannot show them at all.
38
+ *
39
+ * EVERY node on the rendered page is clickable and opens the SHARED floating menu
40
+ * (graph-node-menu.ts — one implementation, also used by architecture/dependencies.html
41
+ * and every project's design.html). Its only item is Lock/Unlock: locking dims every
42
+ * other node and every edge and lights the locked box alone. There is no "View Design"
43
+ * item here — a node is a running service, queue, datastore or third-party system, not
44
+ * an nx project, so no design.html exists to point at and the item is absent rather than
45
+ * dead. This page has no lock dropdown, so the menu is the one and only lock control.
38
46
  */
39
47
  Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.RuntimeVizOptions = void 0;
48
+ exports.RuntimeHtmlPage = exports.RuntimeVizOptions = void 0;
41
49
  exports.generateRuntimeDot = generateRuntimeDot;
42
50
  exports.writeRuntimeVisualization = writeRuntimeVisualization;
43
51
  const tslib_1 = require("tslib");
@@ -45,6 +53,7 @@ const fs = tslib_1.__importStar(require("fs"));
45
53
  const path = tslib_1.__importStar(require("path"));
46
54
  const dot_syntax_1 = require("./dot-syntax");
47
55
  const graph_visualizer_1 = require("./graph-visualizer");
56
+ const graph_node_menu_1 = require("./graph-node-menu");
48
57
  const runtime_viz_theme_1 = require("./runtime-viz-theme");
49
58
  /** Apis per line inside a node label — beyond this the box grows wider than it is readable. */
50
59
  const APIS_PER_LABEL_LINE = 3;
@@ -481,19 +490,23 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
481
490
  return dot;
482
491
  }
483
492
  /**
484
- * Inline SVG swatches for the legend, hand-drawn to match what Graphviz emits for each shape.
493
+ * The runtime-architecture HTML page: its styles, the shared floating node menu, the graph host
494
+ * and the legend.
485
495
  *
486
- * Hand-drawn on purpose: the alternative is shelling out to Graphviz at generate time, which would
487
- * make writing the HTML depend on a `dot` binary being installed — a dependency this tool does not
488
- * otherwise have, since rendering happens in the browser.
496
+ * A CLASS rather than a bag of module functions so the client-text seam can be a constructor
497
+ * parameter the way GraphVisualizer's is: the default reads the COMPILED sibling, which exists in
498
+ * dist and in the published tarball but NOT in a source checkout, and a unit test running from
499
+ * source hands in the text itself instead of requiring the package to have been built first.
489
500
  */
490
- function generateRuntimeHtml(dot, title) {
491
- // The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts) rather than
492
- // in a template literal here: it renders with @viz-js/viz v3 AND redraws every queue node as a
493
- // true horizontal cylinder, which is more logic than belongs inline in a .ts string.
494
- const script = (0, graph_visualizer_1.readCompiledClient)('runtime-visualizer.client.js')
495
- .split(graph_visualizer_1.CLIENT_DOT_PLACEHOLDER).join(JSON.stringify(dot));
496
- return `<!DOCTYPE html>
501
+ class RuntimeHtmlPage {
502
+ clientJs;
503
+ /** The ONE floating-node-menu implementation, shared with dependencies.html and every design.html. */
504
+ nodeMenu = new graph_node_menu_1.GraphNodeMenu();
505
+ constructor(clientJs = () => (0, graph_visualizer_1.readCompiledClient)('runtime-visualizer.client.js')) {
506
+ this.clientJs = clientJs;
507
+ }
508
+ render(dot, title) {
509
+ return `<!DOCTYPE html>
497
510
  <html>
498
511
  <head>
499
512
  <!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With
@@ -502,9 +515,47 @@ function generateRuntimeHtml(dot, title) {
502
515
  <meta charset="utf-8">
503
516
  <title>${title}</title>
504
517
  <script src="https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js"></script>
505
- <style>
518
+ <style>${this.styles()}</style>
519
+ </head>
520
+ <body>
521
+ <h1>${title}</h1>
522
+ <p class="hint">💡 <strong>Click any box</strong> for its menu — <strong>Lock</strong> dims every other box and every arrow so one service, queue, datastore or external system stands alone; <strong>Unlock</strong> restores the whole picture.</p>
523
+ <div id="graph"></div>
524
+ ${(0, runtime_viz_theme_1.legendHtml)()}
525
+ <script>${this.nodeMenu.script()}</script>
526
+ <script>${this.script(dot)}</script>
527
+ </body>
528
+ </html>`;
529
+ }
530
+ /**
531
+ * The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts)
532
+ * rather than in a template literal here: it renders with @viz-js/viz v3, redraws every queue
533
+ * node as a true horizontal cylinder, and wires the shared node menu onto every box — more
534
+ * logic than belongs inline in a .ts string. The substitution is a blind split/join, so the
535
+ * placeholder must appear EXACTLY ONCE in the client.
536
+ */
537
+ script(dot) {
538
+ return this.clientJs().split(graph_visualizer_1.CLIENT_DOT_PLACEHOLDER).join(JSON.stringify(dot));
539
+ }
540
+ /**
541
+ * Page styles, including the shared menu stylesheet: the clickable cursor + blue glow on every
542
+ * box, and the dim/undim rules the lock toggles, scoped to this page's `#graph` host. Those two
543
+ * blocks are shared verbatim with architecture/dependencies.html and every design.html.
544
+ *
545
+ * The legend swatches are hand-drawn inline SVG on purpose: the alternative is shelling out to
546
+ * Graphviz at generate time, which would make writing the HTML depend on a `dot` binary being
547
+ * installed — a dependency this tool does not otherwise have, since rendering is client-side.
548
+ */
549
+ styles() {
550
+ return `
506
551
  body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }
507
552
  h1 { text-align: center; color: #333; }
553
+ .hint { text-align: center; color: #555; margin: 0 0 16px; }
554
+ /* Every box is clickable and opens the shared floating menu, so the menu's own stylesheet
555
+ * carries the cursor + blue glow and the dim/undim rules the lock toggles. Shared verbatim
556
+ * with architecture/dependencies.html and every project's design.html. */
557
+ ${this.nodeMenu.styles()}
558
+ ${this.nodeMenu.dimStyles('#graph')}
508
559
  #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }
509
560
  #graph svg { max-width: 100%; height: auto; }
510
561
  .legend {
@@ -536,17 +587,10 @@ function generateRuntimeHtml(dot, title) {
536
587
  }
537
588
  .sw { flex: 0 0 auto; display: inline-flex; }
538
589
  code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }
539
- @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }
540
- </style>
541
- </head>
542
- <body>
543
- <h1>${title}</h1>
544
- <div id="graph"></div>
545
- ${(0, runtime_viz_theme_1.legendHtml)()}
546
- <script>${script}</script>
547
- </body>
548
- </html>`;
590
+ @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }`;
591
+ }
549
592
  }
593
+ exports.RuntimeHtmlPage = RuntimeHtmlPage;
550
594
  /** Write the DOT + HTML renderings to tmp/webpieces/. */
551
595
  function writeRuntimeVisualization(graph, workspaceRoot, title = 'WebPieces Runtime Architecture', options = new RuntimeVizOptions()) {
552
596
  const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');
@@ -556,7 +600,7 @@ function writeRuntimeVisualization(graph, workspaceRoot, title = 'WebPieces Runt
556
600
  const dotPath = path.join(outputDir, 'runtime-architecture.dot');
557
601
  fs.writeFileSync(dotPath, dot, 'utf-8');
558
602
  const htmlPath = path.join(outputDir, 'runtime-architecture.html');
559
- fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');
603
+ fs.writeFileSync(htmlPath, new RuntimeHtmlPage().render(dot, title), 'utf-8');
560
604
  return { dotPath, htmlPath };
561
605
  }
562
606
  //# sourceMappingURL=runtime-visualizer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS. That list used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure. What a service USES is NOT\n * listed: every use already draws an outgoing arrow labeled with the same\n * contract, so repeating it in the box only made every box wider.\n *\n * Shape says what a node IS and line style says what a call IS. Solid = rpc: the\n * request follows the arrow and the response flows back. Dashed = event: it flows\n * in the arrow's direction and returns once it is queued. A queue is a sideways\n * cylinder, a datastore an upright one. A queue box lists ONE LINE PER QUEUE: the\n * unit underneath is still the METHOD (what Terraform creates, and what\n * runtime-dependencies.json records), but queues of one contract sharing the same\n * producers and consumers are drawn together instead of as adjacent near-identical\n * boxes — every queue is still named, only the node count drops.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as terminal nodes, so the vendor systems that actually\n * page you at 3am stop being missing from the picture. One that DECLARES what it\n * is, via an `@externalSystem` JSDoc tag or an `external:` nx tag, gets the shape\n * of the thing it is; the rest stay generic dashed boxes. They are RENDER-ONLY:\n * derivation, levels and cycle detection never see them.\n *\n * The same is true in the other direction for endpoints nothing in-repo CALLS: a\n * `cron` method hangs off a clock and an `external` method off a box naming the\n * CALLER that posts to it (`twilio`), in the same id space as the outbound\n * systems, so a vendor we both call and are called by is ONE box. Those are the\n * entry points that wake a service up at 3am, and a graph built only from in-repo\n * callers cannot show them at all.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type {\n RuntimeGraph,\n RuntimeEdge,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n} from './runtime-graph';\nimport { dotValue, recordValue, assertValidDot } from './dot-syntax';\nimport { CLIENT_DOT_PLACEHOLDER, readCompiledClient } from './graph-visualizer';\nimport {\n LEVEL_COLORS,\n QUEUE_FILL,\n QUEUE_SHAPE,\n QUEUE_LABEL_PREFIX,\n QUEUE_CLASS,\n EXTERNAL_SHAPES,\n EXTERNAL_FILLS,\n EXTERNAL_FILL,\n EXTERNAL_BORDER,\n CRON_FILL,\n CRON_BORDER,\n legendHtml,\n} from './runtime-viz-theme';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\n/**\n * The DISPLAY name of a project/service: scope stripped. LABELS ONLY.\n *\n * Never an identity. `@scope/public-api` and `public-api` are two distinct projects that strip to\n * the same string, and keying nodes or edges on that fuses them into one box (and draws the edge\n * between them as a self-loop). See {@link serviceNodeId} and lib/graph-names.ts.\n */\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** The DOT identity of a runtime service: its project key, escaped — never the display name. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction serviceNodeId(name: string): string {\n return dotValue(name);\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n const safe = entries.map((entry: string) => dotValue(entry));\n for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {\n lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it SERVES.\n *\n * What a service USES is deliberately absent. Every `uses` entry already draws an outgoing arrow —\n * to the implementing service, to a queue, or to an external node — and the arrow carries the same\n * contract name as its label, so listing them in the box restated the picture and made every box\n * wider than it needed to be. `implements` stays because it has no such arrow: an api a service\n * serves but nothing in-repo calls is invisible otherwise.\n *\n * The one case this loses information is `showExternalNodes:false`, where an outbound call draws no\n * node and therefore no arrow. That is precisely what that opt-out asks for, and the legend says so.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n // The DECLARED role when the graph carries one; the old inference only as a fallback for a\n // runtime-dependencies.json committed before `role` existed. Inferring it labelled every server\n // with no implements as a \"client\", which is exactly wrong for a queue-driven service.\n const role = svc.role ?? (svc.implements.length > 0 ? 'server' : 'client');\n // The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end\n // the label string and the whole graph stops parsing.\n const declared = svc.serviceName === undefined ? '' : `, \\\\\"${dotValue(svc.serviceName)}\\\\\"`;\n let label = `${dotValue(getShortName(name))}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n return label;\n}\n\n/**\n * DOT for ONE NON-QUEUED runtime edge: a direct labeled SOLID arrow (producer calls consumer, the\n * response comes back). Queued (pubsub) hops are drawn by {@link queuesDot} instead, because they\n * are merged across methods and therefore cannot be emitted one edge at a time.\n *\n * Solid vs dashed is the graph's one line-level distinction: solid is a call that returns a\n * response, dashed is an event that returns as soon as it is queued.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = serviceNodeId(edge.from);\n const to = serviceNodeId(edge.to);\n const viaRaw = edge.via.map((v: string) => getShortName(v)).join(', ');\n return ` \"${from}\" -> \"${to}\" [label=\"${dotValue(viaRaw)}\"];\\n`;\n}\n\n/** The VISIBLE producers and consumers of one queue, gathered from the edges that survived hiding. */\nclass QueueEndpoints {\n readonly producers = new Set<string>();\n readonly consumers = new Set<string>();\n}\n\n/**\n * One drawn queue BOX: the queue keys (`Api.method`) it shows, and the endpoints they all share.\n * Several keys land in one box only when they agree on all three of contract, producers and\n * consumers — see {@link queuesDot}.\n */\nclass QueueGroup {\n constructor(\n public readonly members: string[],\n public readonly producers: string[],\n public readonly consumers: string[],\n ) {}\n}\n\n/**\n * The label line for ONE queue inside a box: `Api.method`, and the Terraform queue name ONLY when it\n * is not the derived `${Api}-${method}`.\n *\n * Printing a derived name was pure restatement — the line above it already said `Api.method`, so\n * every queue box carried a second line that added nothing and doubled its height. A `@Queue(...)`\n * OVERRIDE is the opposite: that string appears nowhere else on the graph, and it is the one string\n * Terraform must match, so it stays.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction queueLine(key: string, queue: RuntimeQueue | undefined): string {\n const line = recordValue(key);\n if (queue === undefined) return line;\n if (queue.queueName === `${queue.api}-${queue.method}`) return line;\n return `${line}\\\\nqueue: ${recordValue(queue.queueName)}`;\n}\n\n/** The node statement + enqueue/deliver arrows for one queue box. Names arrive pre-escaped. */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction queueBoxDot(id: string, body: string, producers: string[], consumers: string[]): string {\n // Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the\n // empty leading field that draws the cylinder's end cap. Drop it and the node silently\n // degrades to a plain box.\n let dot =\n ` \"${id}\" [shape=${QUEUE_SHAPE}, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", ` +\n `class=\"${QUEUE_CLASS}\", label=\"${QUEUE_LABEL_PREFIX}${body}\"];\\n`;\n for (const producer of producers)\n dot += ` \"${producer}\" -> \"${id}\" [label=\"enqueue\", style=dashed];\\n`;\n for (const consumer of consumers)\n dot += ` \"${id}\" -> \"${consumer}\" [label=\"deliver\", style=dashed];\\n`;\n return dot;\n}\n\n/**\n * Group the queued hops into boxes. The key is (contract, producer SET, consumer SET) — NOT the\n * (from,to) pair of some edge.\n *\n * A queue node converges EVERY producer and consumer of its method onto one box, so keying by a\n * service PAIR would re-split a two-producer queue into two boxes and invent topology nobody wrote.\n * Only queues of the same contract whose producer and consumer sets are IDENTICAL may share a box;\n * anything else stays separate, because merging it would assert a routing that does not exist.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction groupQueues(byQueue: Map<string, QueueEndpoints>, graph: RuntimeGraph): QueueGroup[] {\n const groups = new Map<string, QueueGroup>();\n // Sorted keys → the first member of every group, and therefore its node id, is deterministic.\n for (const key of [...byQueue.keys()].sort()) {\n const ends = byQueue.get(key)!;\n const producers = [...ends.producers].sort();\n const consumers = [...ends.consumers].sort();\n const api = graph.queues[key]?.api ?? key.split('.')[0];\n const mergeKey = [api, producers.join(','), consumers.join(',')].join(PAIR_SEP);\n const existing = groups.get(mergeKey);\n if (existing === undefined)\n groups.set(mergeKey, new QueueGroup([key], producers, consumers));\n else existing.members.push(key);\n }\n return [...groups.values()];\n}\n\n/**\n * The queued half of the graph: producer → QUEUE → consumer, drawn with a sideways-cylinder node and\n * DASHED arrows (an event returns as soon as it is queued).\n *\n * One BOX may list several queues, one per line. The unit Cloud Tasks and Terraform create is still\n * the METHOD — that is what runtime-dependencies.json records and this render never changes it — but\n * two methods of one contract flowing between exactly the same producers and consumers drew two\n * adjacent boxes whose only difference was the method name, and each carried a `queue:` line that\n * merely restated it. Listing them inside ONE box keeps every queue named (you can still see which\n * one is stuck) at a fraction of the node count.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction queuesDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const queued = graph.runtimeEdges.filter(\n (e: RuntimeEdge) => e.type === 'pubsub' && !hidden.has(e.from) && !hidden.has(e.to),\n );\n if (queued.length === 0) return '';\n\n let dot =\n '\\n // Queued hops. Each LINE in a box is one Cloud Tasks queue; queues of one contract\\n' +\n ' // sharing the same producers AND consumers are drawn in a single box.\\n';\n\n // A contract with no committed method table (a dependencies.json predating apiContracts) has no\n // per-method queue at all, so it keeps the historical unnamed per-pair box.\n const byQueue = new Map<string, QueueEndpoints>();\n for (const edge of queued) {\n const from = serviceNodeId(edge.from);\n const to = serviceNodeId(edge.to);\n if (edge.queue === undefined) {\n // Kept RAW: recordValue already applies dotValue, so escaping here would double it.\n const viaRaw = edge.via.map((v: string) => getShortName(v)).join(', ');\n dot += queueBoxDot(\n `queue__${dotId(edge.from)}__${dotId(edge.to)}`,\n `${recordValue(viaRaw)}\\\\nqueue`,\n [from],\n [to],\n );\n continue;\n }\n if (!byQueue.has(edge.queue)) byQueue.set(edge.queue, new QueueEndpoints());\n byQueue.get(edge.queue)!.producers.add(from);\n byQueue.get(edge.queue)!.consumers.add(to);\n }\n\n for (const group of groupQueues(byQueue, graph)) {\n const body = group.members\n .map((key: string) => queueLine(key, graph.queues[key]))\n .join('\\\\n');\n dot += queueBoxDot(\n `queue__${dotId(group.members[0])}`,\n body,\n group.producers,\n group.consumers,\n );\n }\n return dot;\n}\n\n/** A DOT-safe node-id fragment: anything but letters, digits and `_` becomes `_`. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction dotId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/**\n * ONE external-system node statement: the shape/fill of what the system IS, its label, and a\n * parenthesised subtitle saying which way it faces.\n *\n * Shared by the OUTBOUND systems ({@link externalSystemsDot}) and the INBOUND callers\n * ({@link triggerDot}) on purpose. They emit into the SAME `system__<identity>` id space, so a\n * vendor this repo both calls and is called by is one box with arrows in both directions — and a\n * copy-pasted attribute string would have let the two drift until they stopped being one box.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemNodeDot(\n identity: string,\n kind: string,\n label: string,\n subtitle: string,\n): string {\n const shape = EXTERNAL_SHAPES[kind] ?? 'box';\n const fill = EXTERNAL_FILLS[kind] ?? EXTERNAL_FILL;\n // An Mrecord-shaped system needs the same empty leading field as a queue node, or it renders as\n // a plain box and silently loses the sideways-cylinder read.\n const isQueue = shape === QUEUE_SHAPE;\n const prefix = isQueue ? QUEUE_LABEL_PREFIX : '';\n const text = isQueue ? recordValue(label) : dotValue(label);\n // Only a queue-kind system is marked: a database here is an UPRIGHT cylinder and must not be\n // caught by the browser-side reshaping that lays queues on their side.\n const marker = isQueue ? `class=\"${QUEUE_CLASS}\", ` : '';\n return (\n ` \"system__${dotId(identity)}\" [shape=${shape}, style=\"filled\", fillcolor=\"${fill}\", ` +\n `${marker}label=\"${prefix}${text}\\\\n(${subtitle})\"];\\n`\n );\n}\n\n/**\n * The clock and outside-system nodes for endpoints NOTHING in-repo calls.\n *\n * A cron sweep and a GCP push subscription are real runtime entry points with real Terraform behind\n * them, but they produce no runtime EDGE (there is no in-repo caller), so until now they were simply\n * absent — a server's most operationally interesting endpoint could be invisible on its own graph.\n * Both are drawn pointing INTO the service that serves them, the opposite direction from\n * {@link externalDot}'s outbound vendor calls.\n *\n * The inbound box names the CALLER (`twilio`), never the contract. Naming the contract was the whole\n * bug: it restated what the service box directly below already says, while the one fact the box\n * exists to convey — which vendor is posting to us — appeared nowhere. The contract keeps its place\n * on the EDGE label, where `WhatsAppApi.inbound` reads as \"…posts to this method\".\n *\n * Identity is the caller's label in the SAME `system__` space {@link externalSystemsDot} uses, which\n * buys three things at once: two vendors hitting one contract are two boxes, one vendor hitting three\n * methods is one box with three arrows, and a vendor this repo also CALLS is that same single box.\n * `options` is passed in for the last of those — when that outbound half will be drawn, this half\n * must not restate the node statement.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction triggerDot(graph: RuntimeGraph, hidden: Set<string>, options: RuntimeVizOptions): string {\n const triggers = graph.triggers.filter((t: RuntimeTrigger) => !hidden.has(t.service));\n if (triggers.length === 0) return '';\n\n // Identities externalSystemsDot is about to draw; skipped here so the node is stated ONCE.\n const drawnOutbound = new Set(\n options.showExternalNodes ? Object.keys(graph.externalSystems ?? {}) : [],\n );\n const emitted = new Set<string>();\n let dot =\n '\\n // Entry points nothing in this repo calls: a clock, or a system outside the repo.\\n';\n for (const trigger of triggers) {\n const service = serviceNodeId(trigger.service);\n const label = dotValue(`${trigger.api}.${trigger.method}`);\n if (trigger.kind === 'cron') {\n const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;\n const schedule = dotValue(trigger.queueName ?? `${trigger.api}-${trigger.method}`);\n dot +=\n ` \"${id}\" [shape=circle, style=\"filled\", fillcolor=\"${CRON_FILL}\", ` +\n `color=\"${CRON_BORDER}\", label=\"⏰\\\\ncron\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\\\\n${schedule}\", color=\"${CRON_BORDER}\"];\\n`;\n continue;\n }\n const caller = trigger.caller;\n // Undeclared: only possible for a graph generated BEFORE the caller was required (generation\n // now fails instead). DOTTED and question-marked, so \"we were never told who this is\" cannot\n // be mistaken for a named vendor.\n const id =\n caller === undefined\n ? `inbound__${dotId(trigger.api)}`\n : `system__${dotId(caller.label)}`;\n if (!emitted.has(id) && !(caller !== undefined && drawnOutbound.has(caller.label))) {\n emitted.add(id);\n dot +=\n caller === undefined\n ? ` \"${id}\" [shape=box, style=\"dotted,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(trigger.api)}\\\\n? unknown caller\"];\\n`\n : externalSystemNodeDot(\n caller.label,\n caller.kind,\n caller.label,\n 'external caller',\n );\n }\n dot += ` \"${id}\" -> \"${service}\" [label=\"${label}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/**\n * The DECLARED external systems — the ones that said what they are, so they get a shape that says\n * it: a cylinder for a database, a folder for a bucket. Everything undeclared falls through to\n * {@link externalDot}'s generic grey box, which is why adding this broke nothing existing.\n *\n * The arrows are SOLID. A call to firestore or postgres is synchronous — it returns a value — and\n * the graph's rule is that solid means \"response comes back\". Being outside the repo is carried by\n * the node's shape, never by the line style; conflating the two is what made a blocking database\n * read look like an event.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemsDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const systems = graph.externalSystems ?? {};\n const ids = Object.keys(systems).sort();\n if (ids.length === 0) return '';\n\n let dot =\n '\\n // Declared external systems — drawn with the shape of what they actually are.\\n';\n for (const id of ids) {\n const system = systems[id];\n dot += externalSystemNodeDot(\n id,\n system.kind,\n system.label,\n `external ${dotValue(system.kind)}`,\n );\n }\n for (const id of ids) {\n const system = systems[id];\n const via =\n system.apis.length === 0 ? '' : ` [label=\"${labelList([...system.apis].sort())}\"]`;\n for (const service of [...system.usedBy].sort()) {\n if (hidden.has(service)) continue;\n dot += ` \"${serviceNodeId(service)}\" -> \"system__${dotId(id)}\"${via};\\n`;\n }\n }\n return dot;\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n *\n * A contract carrying an `@externalSystem` declaration is skipped here — {@link externalSystemsDot}\n * has already drawn it with a real shape, and rendering it in both places would double the node.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n if (graph.apis[use.api]?.externalSystem !== undefined) continue;\n // Keyed on the OWNER's full name, never its short name: two scoped api-libs whose short\n // names match are two external systems, and fusing them draws one box for both.\n const external = graph.apis[use.api]?.owner ?? use.api;\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service, and built\n // from the owner's FULL name so two same-short-named owners stay two boxes; only the label\n // carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${dotValue(external)}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(getShortName(external))}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n // SOLID: this is a synchronous call that returns a value. Dashed is reserved for events, and\n // \"outside the repo\" is already said by the node's dashed border.\n dot +=\n ` \"${serviceNodeId(service)}\" -> \"external__${dotValue(external)}\" ` +\n `[label=\"${via}\", color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter(\n (name: string) => graph.services[name].drawOnGraph === false,\n ),\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${serviceNodeId(name)}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n // Queued hops are merged across methods, so they cannot be emitted one edge at a time.\n if (edge.type === 'pubsub') continue;\n dot += edgeDot(edge);\n }\n\n dot += queuesDot(graph, hidden);\n dot += triggerDot(graph, hidden, options);\n\n if (options.showExternalNodes) {\n dot += externalSystemsDot(graph, hidden);\n dot += externalDot(graph, hidden);\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked\n // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.\n assertValidDot(dot, 'runtime-architecture.dot');\n return dot;\n}\n\n/**\n * Inline SVG swatches for the legend, hand-drawn to match what Graphviz emits for each shape.\n *\n * Hand-drawn on purpose: the alternative is shelling out to Graphviz at generate time, which would\n * make writing the HTML depend on a `dot` binary being installed — a dependency this tool does not\n * otherwise have, since rendering happens in the browser.\n */\nfunction generateRuntimeHtml(dot: string, title: string): string {\n // The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts) rather than\n // in a template literal here: it renders with @viz-js/viz v3 AND redraws every queue node as a\n // true horizontal cylinder, which is more logic than belongs inline in a .ts string.\n const script = readCompiledClient('runtime-visualizer.client.js')\n .split(CLIENT_DOT_PLACEHOLDER).join(JSON.stringify(dot));\n return `<!DOCTYPE html>\n<html>\n<head>\n <!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With\n no declared charset the browser falls back to a locale guess and renders it as mojibake\n (\"â °\") whenever the page is served without a charset header. -->\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }\n #graph svg { max-width: 100%; height: auto; }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px 20px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; color: #333; }\n .legend-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; align-items: start; }\n .legend-col h3 { margin: 0 0 10px; color: #333; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px; }\n .legend-item { margin: 9px 0; display: flex; align-items: center; gap: 10px; line-height: 1.4; color: #444; }\n /* Prose rows carry no swatch, so they must NOT be flex containers: flex would promote every\n * inline <strong>/<em>/<code> to a flex item and shred the sentence into columns. */\n .legend-note { margin: 9px 0; line-height: 1.5; color: #444; }\n .legend-box-anatomy {\n margin: 0 0 12px;\n padding: 8px 10px;\n background: #f7f7f7;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #333;\n white-space: pre;\n overflow-x: auto;\n }\n .sw { flex: 0 0 auto; display: inline-flex; }\n code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div id=\"graph\"></div>\n ${legendHtml()}\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
1
+ {"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS. That list used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure. What a service USES is NOT\n * listed: every use already draws an outgoing arrow labeled with the same\n * contract, so repeating it in the box only made every box wider.\n *\n * Shape says what a node IS and line style says what a call IS. Solid = rpc: the\n * request follows the arrow and the response flows back. Dashed = event: it flows\n * in the arrow's direction and returns once it is queued. A queue is a sideways\n * cylinder, a datastore an upright one. A queue box lists ONE LINE PER QUEUE: the\n * unit underneath is still the METHOD (what Terraform creates, and what\n * runtime-dependencies.json records), but queues of one contract sharing the same\n * producers and consumers are drawn together instead of as adjacent near-identical\n * boxes — every queue is still named, only the node count drops.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as terminal nodes, so the vendor systems that actually\n * page you at 3am stop being missing from the picture. One that DECLARES what it\n * is, via an `@externalSystem` JSDoc tag or an `external:` nx tag, gets the shape\n * of the thing it is; the rest stay generic dashed boxes. They are RENDER-ONLY:\n * derivation, levels and cycle detection never see them.\n *\n * The same is true in the other direction for endpoints nothing in-repo CALLS: a\n * `cron` method hangs off a clock and an `external` method off a box naming the\n * CALLER that posts to it (`twilio`), in the same id space as the outbound\n * systems, so a vendor we both call and are called by is ONE box. Those are the\n * entry points that wake a service up at 3am, and a graph built only from in-repo\n * callers cannot show them at all.\n *\n * EVERY node on the rendered page is clickable and opens the SHARED floating menu\n * (graph-node-menu.ts — one implementation, also used by architecture/dependencies.html\n * and every project's design.html). Its only item is Lock/Unlock: locking dims every\n * other node and every edge and lights the locked box alone. There is no \"View Design\"\n * item here — a node is a running service, queue, datastore or third-party system, not\n * an nx project, so no design.html exists to point at and the item is absent rather than\n * dead. This page has no lock dropdown, so the menu is the one and only lock control.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type {\n RuntimeGraph,\n RuntimeEdge,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n} from './runtime-graph';\nimport { dotValue, recordValue, assertValidDot } from './dot-syntax';\nimport { CLIENT_DOT_PLACEHOLDER, readCompiledClient } from './graph-visualizer';\nimport { GraphNodeMenu } from './graph-node-menu';\nimport {\n LEVEL_COLORS,\n QUEUE_FILL,\n QUEUE_SHAPE,\n QUEUE_LABEL_PREFIX,\n QUEUE_CLASS,\n EXTERNAL_SHAPES,\n EXTERNAL_FILLS,\n EXTERNAL_FILL,\n EXTERNAL_BORDER,\n CRON_FILL,\n CRON_BORDER,\n legendHtml,\n} from './runtime-viz-theme';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\n/**\n * The DISPLAY name of a project/service: scope stripped. LABELS ONLY.\n *\n * Never an identity. `@scope/public-api` and `public-api` are two distinct projects that strip to\n * the same string, and keying nodes or edges on that fuses them into one box (and draws the edge\n * between them as a self-loop). See {@link serviceNodeId} and lib/graph-names.ts.\n */\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** The DOT identity of a runtime service: its project key, escaped — never the display name. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction serviceNodeId(name: string): string {\n return dotValue(name);\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n const safe = entries.map((entry: string) => dotValue(entry));\n for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {\n lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it SERVES.\n *\n * What a service USES is deliberately absent. Every `uses` entry already draws an outgoing arrow —\n * to the implementing service, to a queue, or to an external node — and the arrow carries the same\n * contract name as its label, so listing them in the box restated the picture and made every box\n * wider than it needed to be. `implements` stays because it has no such arrow: an api a service\n * serves but nothing in-repo calls is invisible otherwise.\n *\n * The one case this loses information is `showExternalNodes:false`, where an outbound call draws no\n * node and therefore no arrow. That is precisely what that opt-out asks for, and the legend says so.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n // The DECLARED role when the graph carries one; the old inference only as a fallback for a\n // runtime-dependencies.json committed before `role` existed. Inferring it labelled every server\n // with no implements as a \"client\", which is exactly wrong for a queue-driven service.\n const role = svc.role ?? (svc.implements.length > 0 ? 'server' : 'client');\n // The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end\n // the label string and the whole graph stops parsing.\n const declared = svc.serviceName === undefined ? '' : `, \\\\\"${dotValue(svc.serviceName)}\\\\\"`;\n let label = `${dotValue(getShortName(name))}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n return label;\n}\n\n/**\n * DOT for ONE NON-QUEUED runtime edge: a direct labeled SOLID arrow (producer calls consumer, the\n * response comes back). Queued (pubsub) hops are drawn by {@link queuesDot} instead, because they\n * are merged across methods and therefore cannot be emitted one edge at a time.\n *\n * Solid vs dashed is the graph's one line-level distinction: solid is a call that returns a\n * response, dashed is an event that returns as soon as it is queued.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = serviceNodeId(edge.from);\n const to = serviceNodeId(edge.to);\n const viaRaw = edge.via.map((v: string) => getShortName(v)).join(', ');\n return ` \"${from}\" -> \"${to}\" [label=\"${dotValue(viaRaw)}\"];\\n`;\n}\n\n/** The VISIBLE producers and consumers of one queue, gathered from the edges that survived hiding. */\nclass QueueEndpoints {\n readonly producers = new Set<string>();\n readonly consumers = new Set<string>();\n}\n\n/**\n * One drawn queue BOX: the queue keys (`Api.method`) it shows, and the endpoints they all share.\n * Several keys land in one box only when they agree on all three of contract, producers and\n * consumers — see {@link queuesDot}.\n */\nclass QueueGroup {\n constructor(\n public readonly members: string[],\n public readonly producers: string[],\n public readonly consumers: string[],\n ) {}\n}\n\n/**\n * The label line for ONE queue inside a box: `Api.method`, and the Terraform queue name ONLY when it\n * is not the derived `${Api}-${method}`.\n *\n * Printing a derived name was pure restatement — the line above it already said `Api.method`, so\n * every queue box carried a second line that added nothing and doubled its height. A `@Queue(...)`\n * OVERRIDE is the opposite: that string appears nowhere else on the graph, and it is the one string\n * Terraform must match, so it stays.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction queueLine(key: string, queue: RuntimeQueue | undefined): string {\n const line = recordValue(key);\n if (queue === undefined) return line;\n if (queue.queueName === `${queue.api}-${queue.method}`) return line;\n return `${line}\\\\nqueue: ${recordValue(queue.queueName)}`;\n}\n\n/** The node statement + enqueue/deliver arrows for one queue box. Names arrive pre-escaped. */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction queueBoxDot(id: string, body: string, producers: string[], consumers: string[]): string {\n // Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the\n // empty leading field that draws the cylinder's end cap. Drop it and the node silently\n // degrades to a plain box.\n let dot =\n ` \"${id}\" [shape=${QUEUE_SHAPE}, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", ` +\n `class=\"${QUEUE_CLASS}\", label=\"${QUEUE_LABEL_PREFIX}${body}\"];\\n`;\n for (const producer of producers)\n dot += ` \"${producer}\" -> \"${id}\" [label=\"enqueue\", style=dashed];\\n`;\n for (const consumer of consumers)\n dot += ` \"${id}\" -> \"${consumer}\" [label=\"deliver\", style=dashed];\\n`;\n return dot;\n}\n\n/**\n * Group the queued hops into boxes. The key is (contract, producer SET, consumer SET) — NOT the\n * (from,to) pair of some edge.\n *\n * A queue node converges EVERY producer and consumer of its method onto one box, so keying by a\n * service PAIR would re-split a two-producer queue into two boxes and invent topology nobody wrote.\n * Only queues of the same contract whose producer and consumer sets are IDENTICAL may share a box;\n * anything else stays separate, because merging it would assert a routing that does not exist.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction groupQueues(byQueue: Map<string, QueueEndpoints>, graph: RuntimeGraph): QueueGroup[] {\n const groups = new Map<string, QueueGroup>();\n // Sorted keys → the first member of every group, and therefore its node id, is deterministic.\n for (const key of [...byQueue.keys()].sort()) {\n const ends = byQueue.get(key)!;\n const producers = [...ends.producers].sort();\n const consumers = [...ends.consumers].sort();\n const api = graph.queues[key]?.api ?? key.split('.')[0];\n const mergeKey = [api, producers.join(','), consumers.join(',')].join(PAIR_SEP);\n const existing = groups.get(mergeKey);\n if (existing === undefined)\n groups.set(mergeKey, new QueueGroup([key], producers, consumers));\n else existing.members.push(key);\n }\n return [...groups.values()];\n}\n\n/**\n * The queued half of the graph: producer → QUEUE → consumer, drawn with a sideways-cylinder node and\n * DASHED arrows (an event returns as soon as it is queued).\n *\n * One BOX may list several queues, one per line. The unit Cloud Tasks and Terraform create is still\n * the METHOD — that is what runtime-dependencies.json records and this render never changes it — but\n * two methods of one contract flowing between exactly the same producers and consumers drew two\n * adjacent boxes whose only difference was the method name, and each carried a `queue:` line that\n * merely restated it. Listing them inside ONE box keeps every queue named (you can still see which\n * one is stuck) at a fraction of the node count.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction queuesDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const queued = graph.runtimeEdges.filter(\n (e: RuntimeEdge) => e.type === 'pubsub' && !hidden.has(e.from) && !hidden.has(e.to),\n );\n if (queued.length === 0) return '';\n\n let dot =\n '\\n // Queued hops. Each LINE in a box is one Cloud Tasks queue; queues of one contract\\n' +\n ' // sharing the same producers AND consumers are drawn in a single box.\\n';\n\n // A contract with no committed method table (a dependencies.json predating apiContracts) has no\n // per-method queue at all, so it keeps the historical unnamed per-pair box.\n const byQueue = new Map<string, QueueEndpoints>();\n for (const edge of queued) {\n const from = serviceNodeId(edge.from);\n const to = serviceNodeId(edge.to);\n if (edge.queue === undefined) {\n // Kept RAW: recordValue already applies dotValue, so escaping here would double it.\n const viaRaw = edge.via.map((v: string) => getShortName(v)).join(', ');\n dot += queueBoxDot(\n `queue__${dotId(edge.from)}__${dotId(edge.to)}`,\n `${recordValue(viaRaw)}\\\\nqueue`,\n [from],\n [to],\n );\n continue;\n }\n if (!byQueue.has(edge.queue)) byQueue.set(edge.queue, new QueueEndpoints());\n byQueue.get(edge.queue)!.producers.add(from);\n byQueue.get(edge.queue)!.consumers.add(to);\n }\n\n for (const group of groupQueues(byQueue, graph)) {\n const body = group.members\n .map((key: string) => queueLine(key, graph.queues[key]))\n .join('\\\\n');\n dot += queueBoxDot(\n `queue__${dotId(group.members[0])}`,\n body,\n group.producers,\n group.consumers,\n );\n }\n return dot;\n}\n\n/** A DOT-safe node-id fragment: anything but letters, digits and `_` becomes `_`. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction dotId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/**\n * ONE external-system node statement: the shape/fill of what the system IS, its label, and a\n * parenthesised subtitle saying which way it faces.\n *\n * Shared by the OUTBOUND systems ({@link externalSystemsDot}) and the INBOUND callers\n * ({@link triggerDot}) on purpose. They emit into the SAME `system__<identity>` id space, so a\n * vendor this repo both calls and is called by is one box with arrows in both directions — and a\n * copy-pasted attribute string would have let the two drift until they stopped being one box.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemNodeDot(\n identity: string,\n kind: string,\n label: string,\n subtitle: string,\n): string {\n const shape = EXTERNAL_SHAPES[kind] ?? 'box';\n const fill = EXTERNAL_FILLS[kind] ?? EXTERNAL_FILL;\n // An Mrecord-shaped system needs the same empty leading field as a queue node, or it renders as\n // a plain box and silently loses the sideways-cylinder read.\n const isQueue = shape === QUEUE_SHAPE;\n const prefix = isQueue ? QUEUE_LABEL_PREFIX : '';\n const text = isQueue ? recordValue(label) : dotValue(label);\n // Only a queue-kind system is marked: a database here is an UPRIGHT cylinder and must not be\n // caught by the browser-side reshaping that lays queues on their side.\n const marker = isQueue ? `class=\"${QUEUE_CLASS}\", ` : '';\n return (\n ` \"system__${dotId(identity)}\" [shape=${shape}, style=\"filled\", fillcolor=\"${fill}\", ` +\n `${marker}label=\"${prefix}${text}\\\\n(${subtitle})\"];\\n`\n );\n}\n\n/**\n * The clock and outside-system nodes for endpoints NOTHING in-repo calls.\n *\n * A cron sweep and a GCP push subscription are real runtime entry points with real Terraform behind\n * them, but they produce no runtime EDGE (there is no in-repo caller), so until now they were simply\n * absent — a server's most operationally interesting endpoint could be invisible on its own graph.\n * Both are drawn pointing INTO the service that serves them, the opposite direction from\n * {@link externalDot}'s outbound vendor calls.\n *\n * The inbound box names the CALLER (`twilio`), never the contract. Naming the contract was the whole\n * bug: it restated what the service box directly below already says, while the one fact the box\n * exists to convey — which vendor is posting to us — appeared nowhere. The contract keeps its place\n * on the EDGE label, where `WhatsAppApi.inbound` reads as \"…posts to this method\".\n *\n * Identity is the caller's label in the SAME `system__` space {@link externalSystemsDot} uses, which\n * buys three things at once: two vendors hitting one contract are two boxes, one vendor hitting three\n * methods is one box with three arrows, and a vendor this repo also CALLS is that same single box.\n * `options` is passed in for the last of those — when that outbound half will be drawn, this half\n * must not restate the node statement.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction triggerDot(graph: RuntimeGraph, hidden: Set<string>, options: RuntimeVizOptions): string {\n const triggers = graph.triggers.filter((t: RuntimeTrigger) => !hidden.has(t.service));\n if (triggers.length === 0) return '';\n\n // Identities externalSystemsDot is about to draw; skipped here so the node is stated ONCE.\n const drawnOutbound = new Set(\n options.showExternalNodes ? Object.keys(graph.externalSystems ?? {}) : [],\n );\n const emitted = new Set<string>();\n let dot =\n '\\n // Entry points nothing in this repo calls: a clock, or a system outside the repo.\\n';\n for (const trigger of triggers) {\n const service = serviceNodeId(trigger.service);\n const label = dotValue(`${trigger.api}.${trigger.method}`);\n if (trigger.kind === 'cron') {\n const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;\n const schedule = dotValue(trigger.queueName ?? `${trigger.api}-${trigger.method}`);\n dot +=\n ` \"${id}\" [shape=circle, style=\"filled\", fillcolor=\"${CRON_FILL}\", ` +\n `color=\"${CRON_BORDER}\", label=\"⏰\\\\ncron\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\\\\n${schedule}\", color=\"${CRON_BORDER}\"];\\n`;\n continue;\n }\n const caller = trigger.caller;\n // Undeclared: only possible for a graph generated BEFORE the caller was required (generation\n // now fails instead). DOTTED and question-marked, so \"we were never told who this is\" cannot\n // be mistaken for a named vendor.\n const id =\n caller === undefined\n ? `inbound__${dotId(trigger.api)}`\n : `system__${dotId(caller.label)}`;\n if (!emitted.has(id) && !(caller !== undefined && drawnOutbound.has(caller.label))) {\n emitted.add(id);\n dot +=\n caller === undefined\n ? ` \"${id}\" [shape=box, style=\"dotted,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(trigger.api)}\\\\n? unknown caller\"];\\n`\n : externalSystemNodeDot(\n caller.label,\n caller.kind,\n caller.label,\n 'external caller',\n );\n }\n dot += ` \"${id}\" -> \"${service}\" [label=\"${label}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/**\n * The DECLARED external systems — the ones that said what they are, so they get a shape that says\n * it: a cylinder for a database, a folder for a bucket. Everything undeclared falls through to\n * {@link externalDot}'s generic grey box, which is why adding this broke nothing existing.\n *\n * The arrows are SOLID. A call to firestore or postgres is synchronous — it returns a value — and\n * the graph's rule is that solid means \"response comes back\". Being outside the repo is carried by\n * the node's shape, never by the line style; conflating the two is what made a blocking database\n * read look like an event.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemsDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const systems = graph.externalSystems ?? {};\n const ids = Object.keys(systems).sort();\n if (ids.length === 0) return '';\n\n let dot =\n '\\n // Declared external systems — drawn with the shape of what they actually are.\\n';\n for (const id of ids) {\n const system = systems[id];\n dot += externalSystemNodeDot(\n id,\n system.kind,\n system.label,\n `external ${dotValue(system.kind)}`,\n );\n }\n for (const id of ids) {\n const system = systems[id];\n const via =\n system.apis.length === 0 ? '' : ` [label=\"${labelList([...system.apis].sort())}\"]`;\n for (const service of [...system.usedBy].sort()) {\n if (hidden.has(service)) continue;\n dot += ` \"${serviceNodeId(service)}\" -> \"system__${dotId(id)}\"${via};\\n`;\n }\n }\n return dot;\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n *\n * A contract carrying an `@externalSystem` declaration is skipped here — {@link externalSystemsDot}\n * has already drawn it with a real shape, and rendering it in both places would double the node.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n if (graph.apis[use.api]?.externalSystem !== undefined) continue;\n // Keyed on the OWNER's full name, never its short name: two scoped api-libs whose short\n // names match are two external systems, and fusing them draws one box for both.\n const external = graph.apis[use.api]?.owner ?? use.api;\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service, and built\n // from the owner's FULL name so two same-short-named owners stay two boxes; only the label\n // carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${dotValue(external)}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(getShortName(external))}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n // SOLID: this is a synchronous call that returns a value. Dashed is reserved for events, and\n // \"outside the repo\" is already said by the node's dashed border.\n dot +=\n ` \"${serviceNodeId(service)}\" -> \"external__${dotValue(external)}\" ` +\n `[label=\"${via}\", color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter(\n (name: string) => graph.services[name].drawOnGraph === false,\n ),\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${serviceNodeId(name)}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n // Queued hops are merged across methods, so they cannot be emitted one edge at a time.\n if (edge.type === 'pubsub') continue;\n dot += edgeDot(edge);\n }\n\n dot += queuesDot(graph, hidden);\n dot += triggerDot(graph, hidden, options);\n\n if (options.showExternalNodes) {\n dot += externalSystemsDot(graph, hidden);\n dot += externalDot(graph, hidden);\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked\n // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.\n assertValidDot(dot, 'runtime-architecture.dot');\n return dot;\n}\n\n/**\n * The runtime-architecture HTML page: its styles, the shared floating node menu, the graph host\n * and the legend.\n *\n * A CLASS rather than a bag of module functions so the client-text seam can be a constructor\n * parameter the way GraphVisualizer's is: the default reads the COMPILED sibling, which exists in\n * dist and in the published tarball but NOT in a source checkout, and a unit test running from\n * source hands in the text itself instead of requiring the package to have been built first.\n */\nexport class RuntimeHtmlPage {\n /** The ONE floating-node-menu implementation, shared with dependencies.html and every design.html. */\n private readonly nodeMenu = new GraphNodeMenu();\n\n constructor(\n private readonly clientJs: () => string =\n (): string => readCompiledClient('runtime-visualizer.client.js'),\n ) {}\n\n render(dot: string, title: string): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With\n no declared charset the browser falls back to a locale guess and renders it as mojibake\n (\"â °\") whenever the page is served without a charset header. -->\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>${this.styles()}</style>\n</head>\n<body>\n <h1>${title}</h1>\n <p class=\"hint\">💡 <strong>Click any box</strong> for its menu — <strong>Lock</strong> dims every other box and every arrow so one service, queue, datastore or external system stands alone; <strong>Unlock</strong> restores the whole picture.</p>\n <div id=\"graph\"></div>\n ${legendHtml()}\n <script>${this.nodeMenu.script()}</script>\n <script>${this.script(dot)}</script>\n</body>\n</html>`;\n }\n\n /**\n * The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts)\n * rather than in a template literal here: it renders with @viz-js/viz v3, redraws every queue\n * node as a true horizontal cylinder, and wires the shared node menu onto every box — more\n * logic than belongs inline in a .ts string. The substitution is a blind split/join, so the\n * placeholder must appear EXACTLY ONCE in the client.\n */\n private script(dot: string): string {\n return this.clientJs().split(CLIENT_DOT_PLACEHOLDER).join(JSON.stringify(dot));\n }\n\n /**\n * Page styles, including the shared menu stylesheet: the clickable cursor + blue glow on every\n * box, and the dim/undim rules the lock toggles, scoped to this page's `#graph` host. Those two\n * blocks are shared verbatim with architecture/dependencies.html and every design.html.\n *\n * The legend swatches are hand-drawn inline SVG on purpose: the alternative is shelling out to\n * Graphviz at generate time, which would make writing the HTML depend on a `dot` binary being\n * installed — a dependency this tool does not otherwise have, since rendering is client-side.\n */\n private styles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .hint { text-align: center; color: #555; margin: 0 0 16px; }\n /* Every box is clickable and opens the shared floating menu, so the menu's own stylesheet\n * carries the cursor + blue glow and the dim/undim rules the lock toggles. Shared verbatim\n * with architecture/dependencies.html and every project's design.html. */\n ${this.nodeMenu.styles()}\n ${this.nodeMenu.dimStyles('#graph')}\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }\n #graph svg { max-width: 100%; height: auto; }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px 20px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; color: #333; }\n .legend-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; align-items: start; }\n .legend-col h3 { margin: 0 0 10px; color: #333; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px; }\n .legend-item { margin: 9px 0; display: flex; align-items: center; gap: 10px; line-height: 1.4; color: #444; }\n /* Prose rows carry no swatch, so they must NOT be flex containers: flex would promote every\n * inline <strong>/<em>/<code> to a flex item and shred the sentence into columns. */\n .legend-note { margin: 9px 0; line-height: 1.5; color: #444; }\n .legend-box-anatomy {\n margin: 0 0 12px;\n padding: 8px 10px;\n background: #f7f7f7;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #333;\n white-space: pre;\n overflow-x: auto;\n }\n .sw { flex: 0 0 auto; display: inline-flex; }\n code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }`;\n }\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, new RuntimeHtmlPage().render(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}