@webpieces/nx-webpieces-rules 0.4.651 → 0.4.653

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.
Files changed (33) hide show
  1. package/README.md +4 -4
  2. package/package.json +6 -6
  3. package/src/executors/validate-catch-error-pattern/schema.json +1 -1
  4. package/src/executors/validate-code/schema.json +12 -12
  5. package/src/executors/validate-dtos/schema.json +1 -1
  6. package/src/executors/validate-no-any-unknown/schema.json +1 -1
  7. package/src/executors/validate-no-destructure/schema.json +1 -1
  8. package/src/executors/validate-no-direct-api-resolver/schema.json +1 -1
  9. package/src/executors/validate-no-file-import-cycles/executor.d.ts +4 -2
  10. package/src/executors/validate-no-file-import-cycles/executor.js +14 -8
  11. package/src/executors/validate-no-file-import-cycles/executor.js.map +1 -1
  12. package/src/executors/validate-no-implicit-any/schema.json +1 -1
  13. package/src/executors/validate-no-inline-types/schema.json +1 -1
  14. package/src/executors/validate-no-unmanaged-exceptions/schema.json +1 -1
  15. package/src/executors/validate-prisma-converters/schema.json +1 -1
  16. package/src/executors/validate-return-types/schema.json +1 -1
  17. package/src/executors/validate-ts-in-src/executor.d.ts +5 -2
  18. package/src/executors/validate-ts-in-src/executor.js +8 -3
  19. package/src/executors/validate-ts-in-src/executor.js.map +1 -1
  20. package/src/lib/graph-level-bands.d.ts +45 -0
  21. package/src/lib/graph-level-bands.js +97 -0
  22. package/src/lib/graph-level-bands.js.map +1 -0
  23. package/src/lib/graph-visualizer.client.d.ts +12 -0
  24. package/src/lib/graph-visualizer.client.js +16 -0
  25. package/src/lib/graph-visualizer.client.js.map +1 -1
  26. package/src/lib/graph-visualizer.d.ts +9 -0
  27. package/src/lib/graph-visualizer.js +27 -20
  28. package/src/lib/graph-visualizer.js.map +1 -1
  29. package/src/lib/runtime-config.d.ts +6 -5
  30. package/src/lib/runtime-config.js +9 -6
  31. package/src/lib/runtime-config.js.map +1 -1
  32. package/src/plugin.js +1 -1
  33. package/src/plugin.js.map +1 -1
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Level bands for the architecture graph.
3
+ *
4
+ * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,
5
+ * descending as you read down, L0 — the foundation everything is built on — always last.
6
+ *
7
+ * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a
8
+ * level's boxes to one row but says nothing about where that row goes, so graphviz derived each
9
+ * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an
10
+ * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and
11
+ * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in
12
+ * the top row, and what parked an L0 lib in the same row as the L6 servers).
13
+ *
14
+ * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in
15
+ * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of
16
+ * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake
17
+ * dependency is ever drawn between two projects.
18
+ */
19
+ /**
20
+ * One horizontal band of the graph: every visible project sharing a dependency level, plus the
21
+ * invisible anchor that pins the band's vertical position.
22
+ */
23
+ export declare class LevelBand {
24
+ level: number;
25
+ /** Short names of the visible projects on this level, sorted for deterministic output. */
26
+ nodeNames: string[];
27
+ constructor(level: number, nodeNames: string[]);
28
+ anchorName(): string;
29
+ }
30
+ /** Emits the DOT that pins each band to its own rank, in descending level order. */
31
+ export declare class LevelBandLayout {
32
+ /**
33
+ * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be
34
+ * contiguous — only the levels actually present get a band, and the chain links
35
+ * whichever ones exist.
36
+ */
37
+ dot(bands: LevelBand[]): string;
38
+ /**
39
+ * The ordering chain between two adjacent bands. When either band is crowded
40
+ * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a
41
+ * wide row then have a whole rank of vertical room to fan out in, instead of being drawn
42
+ * straight through the row below.
43
+ */
44
+ private bandGap;
45
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Level bands for the architecture graph.
4
+ *
5
+ * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,
6
+ * descending as you read down, L0 — the foundation everything is built on — always last.
7
+ *
8
+ * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a
9
+ * level's boxes to one row but says nothing about where that row goes, so graphviz derived each
10
+ * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an
11
+ * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and
12
+ * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in
13
+ * the top row, and what parked an L0 lib in the same row as the L6 servers).
14
+ *
15
+ * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in
16
+ * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of
17
+ * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake
18
+ * dependency is ever drawn between two projects.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.LevelBandLayout = exports.LevelBand = void 0;
22
+ /**
23
+ * A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a
24
+ * crowded row have vertical room to fan out instead of cutting straight through the row below.
25
+ * Eleven-plus boxes in a row is where that became unreadable in practice.
26
+ */
27
+ const SPACER_MIN_BAND_SIZE = 10;
28
+ /**
29
+ * The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops
30
+ * `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page
31
+ * today; the class is how the DOT tells the browser client which elements are scaffolding, and the
32
+ * client skips them when indexing so an anchor can never be walked as a dependency.
33
+ */
34
+ const LAYOUT_CLASS = 'wp-layout';
35
+ /** Attributes shared by every invisible layout node (rank anchors and spacer bands). */
36
+ const LAYOUT_NODE_ATTRS = `[style=invis, shape=point, width=0.01, height=0.01, label="", class="${LAYOUT_CLASS}"]`;
37
+ /** Attributes for the invisible edges that force one band strictly above the next. */
38
+ const LAYOUT_EDGE_ATTRS = `[style=invis, class="${LAYOUT_CLASS}"]`;
39
+ /**
40
+ * One horizontal band of the graph: every visible project sharing a dependency level, plus the
41
+ * invisible anchor that pins the band's vertical position.
42
+ */
43
+ class LevelBand {
44
+ level;
45
+ /** Short names of the visible projects on this level, sorted for deterministic output. */
46
+ nodeNames;
47
+ constructor(level, nodeNames) {
48
+ this.level = level;
49
+ this.nodeNames = nodeNames;
50
+ }
51
+ anchorName() {
52
+ return `__wp_layout_L${this.level}`;
53
+ }
54
+ }
55
+ exports.LevelBand = LevelBand;
56
+ /** Emits the DOT that pins each band to its own rank, in descending level order. */
57
+ class LevelBandLayout {
58
+ /**
59
+ * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be
60
+ * contiguous — only the levels actually present get a band, and the chain links
61
+ * whichever ones exist.
62
+ */
63
+ dot(bands) {
64
+ let dot = '';
65
+ for (const band of bands) {
66
+ dot += ` "${band.anchorName()}" ${LAYOUT_NODE_ATTRS};\n`;
67
+ dot += ` { rank=same; "${band.anchorName()}"; `;
68
+ for (const name of band.nodeNames)
69
+ dot += `"${name}"; `;
70
+ dot += '}\n';
71
+ }
72
+ for (let i = 0; i + 1 < bands.length; i++) {
73
+ dot += this.bandGap(bands[i], bands[i + 1]);
74
+ }
75
+ return dot;
76
+ }
77
+ /**
78
+ * The ordering chain between two adjacent bands. When either band is crowded
79
+ * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a
80
+ * wide row then have a whole rank of vertical room to fan out in, instead of being drawn
81
+ * straight through the row below.
82
+ */
83
+ bandGap(upper, lower) {
84
+ const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE
85
+ || lower.nodeNames.length > SPACER_MIN_BAND_SIZE;
86
+ if (!crowded) {
87
+ return ` "${upper.anchorName()}" -> "${lower.anchorName()}" ${LAYOUT_EDGE_ATTRS};\n`;
88
+ }
89
+ const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;
90
+ return ` "${spacer}" ${LAYOUT_NODE_ATTRS};\n`
91
+ + ` { rank=same; "${spacer}"; }\n`
92
+ + ` "${upper.anchorName()}" -> "${spacer}" ${LAYOUT_EDGE_ATTRS};\n`
93
+ + ` "${spacer}" -> "${lower.anchorName()}" ${LAYOUT_EDGE_ATTRS};\n`;
94
+ }
95
+ }
96
+ exports.LevelBandLayout = LevelBandLayout;
97
+ //# sourceMappingURL=graph-level-bands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-level-bands.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-level-bands.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAEH;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,YAAY,GAAG,WAAW,CAAC;AAEjC,wFAAwF;AACxF,MAAM,iBAAiB,GACnB,wEAAwE,YAAY,IAAI,CAAC;AAE7F,sFAAsF;AACtF,MAAM,iBAAiB,GAAG,wBAAwB,YAAY,IAAI,CAAC;AAEnE;;;GAGG;AACH,MAAa,SAAS;IAClB,KAAK,CAAS;IACd,0FAA0F;IAC1F,SAAS,CAAW;IAEpB,YAAY,KAAa,EAAE,SAAmB;QAC1C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,UAAU;QACN,OAAO,gBAAgB,IAAI,CAAC,KAAK,EAAE,CAAC;IACxC,CAAC;CACJ;AAbD,8BAaC;AAED,oFAAoF;AACpF,MAAa,eAAe;IACxB;;;;OAIG;IACH,GAAG,CAAC,KAAkB;QAClB,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,GAAG,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;YAC1D,GAAG,IAAI,mBAAmB,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;YACjD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS;gBAAE,GAAG,IAAI,IAAI,IAAI,KAAK,CAAC;YACxD,GAAG,IAAI,KAAK,CAAC;QACjB,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACK,OAAO,CAAC,KAAgB,EAAE,KAAgB;QAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB;eACtD,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB,CAAC;QACrD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;QAC1F,CAAC;QACD,MAAM,MAAM,GAAG,uBAAuB,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;QACpE,OAAO,MAAM,MAAM,KAAK,iBAAiB,KAAK;cACxC,mBAAmB,MAAM,QAAQ;cACjC,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,MAAM,KAAK,iBAAiB,KAAK;cAClE,MAAM,MAAM,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;IAC7E,CAAC;CACJ;AAtCD,0CAsCC","sourcesContent":["/**\n * Level bands for the architecture graph.\n *\n * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,\n * descending as you read down, L0 — the foundation everything is built on — always last.\n *\n * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a\n * level's boxes to one row but says nothing about where that row goes, so graphviz derived each\n * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an\n * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and\n * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in\n * the top row, and what parked an L0 lib in the same row as the L6 servers).\n *\n * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in\n * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of\n * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake\n * dependency is ever drawn between two projects.\n */\n\n/**\n * A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a\n * crowded row have vertical room to fan out instead of cutting straight through the row below.\n * Eleven-plus boxes in a row is where that became unreadable in practice.\n */\nconst SPACER_MIN_BAND_SIZE = 10;\n\n/**\n * The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops\n * `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page\n * today; the class is how the DOT tells the browser client which elements are scaffolding, and the\n * client skips them when indexing so an anchor can never be walked as a dependency.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** Attributes shared by every invisible layout node (rank anchors and spacer bands). */\nconst LAYOUT_NODE_ATTRS =\n `[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"${LAYOUT_CLASS}\"]`;\n\n/** Attributes for the invisible edges that force one band strictly above the next. */\nconst LAYOUT_EDGE_ATTRS = `[style=invis, class=\"${LAYOUT_CLASS}\"]`;\n\n/**\n * One horizontal band of the graph: every visible project sharing a dependency level, plus the\n * invisible anchor that pins the band's vertical position.\n */\nexport class LevelBand {\n level: number;\n /** Short names of the visible projects on this level, sorted for deterministic output. */\n nodeNames: string[];\n\n constructor(level: number, nodeNames: string[]) {\n this.level = level;\n this.nodeNames = nodeNames;\n }\n\n anchorName(): string {\n return `__wp_layout_L${this.level}`;\n }\n}\n\n/** Emits the DOT that pins each band to its own rank, in descending level order. */\nexport class LevelBandLayout {\n /**\n * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be\n * contiguous — only the levels actually present get a band, and the chain links\n * whichever ones exist.\n */\n dot(bands: LevelBand[]): string {\n let dot = '';\n for (const band of bands) {\n dot += ` \"${band.anchorName()}\" ${LAYOUT_NODE_ATTRS};\\n`;\n dot += ` { rank=same; \"${band.anchorName()}\"; `;\n for (const name of band.nodeNames) dot += `\"${name}\"; `;\n dot += '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n dot += this.bandGap(bands[i], bands[i + 1]);\n }\n return dot;\n }\n\n /**\n * The ordering chain between two adjacent bands. When either band is crowded\n * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a\n * wide row then have a whole rank of vertical room to fan out in, instead of being drawn\n * straight through the row below.\n */\n private bandGap(upper: LevelBand, lower: LevelBand): string {\n const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE\n || lower.nodeNames.length > SPACER_MIN_BAND_SIZE;\n if (!crowded) {\n return ` \"${upper.anchorName()}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n return ` \"${spacer}\" ${LAYOUT_NODE_ATTRS};\\n`\n + ` { rank=same; \"${spacer}\"; }\\n`\n + ` \"${upper.anchorName()}\" -> \"${spacer}\" ${LAYOUT_EDGE_ATTRS};\\n`\n + ` \"${spacer}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n}\n"]}
@@ -1,3 +1,15 @@
1
+ /**
2
+ * The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering
3
+ * edges that pin each level to its own row. They are layout scaffolding, not architecture, so the
4
+ * indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box
5
+ * would light boxes it has nothing to do with.
6
+ *
7
+ * Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),
8
+ * so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the
9
+ * scaffolding stays inert if that ever stops being true — the DOT is the only place that decides
10
+ * what is scaffolding, and it says so with this class.
11
+ */
12
+ declare const LAYOUT_CLASS = "wp-layout";
1
13
  /** One traversal direction: the edges to light, and the nodes to walk on to. */
2
14
  declare class Direction {
3
15
  readonly nodes: Map<string, Set<string>>;
@@ -20,6 +20,18 @@
20
20
  // The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both
21
21
  // scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token
22
22
  // in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.
23
+ /**
24
+ * The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering
25
+ * edges that pin each level to its own row. They are layout scaffolding, not architecture, so the
26
+ * indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box
27
+ * would light boxes it has nothing to do with.
28
+ *
29
+ * Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),
30
+ * so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the
31
+ * scaffolding stays inert if that ever stops being true — the DOT is the only place that decides
32
+ * what is scaffolding, and it says so with this class.
33
+ */
34
+ const LAYOUT_CLASS = 'wp-layout';
23
35
  /** One traversal direction: the edges to light, and the nodes to walk on to. */
24
36
  class Direction {
25
37
  nodes;
@@ -61,6 +73,8 @@ class GraphHighlighter {
61
73
  }
62
74
  indexNodes() {
63
75
  this.svg.querySelectorAll('g.node').forEach((g) => {
76
+ if (g.classList.contains(LAYOUT_CLASS))
77
+ return;
64
78
  const title = g.querySelector('title');
65
79
  if (title !== null && title.textContent !== null) {
66
80
  this.nodeByName.set(title.textContent.trim(), g);
@@ -69,6 +83,8 @@ class GraphHighlighter {
69
83
  }
70
84
  indexEdges() {
71
85
  this.svg.querySelectorAll('g.edge').forEach((edge) => {
86
+ if (edge.classList.contains(LAYOUT_CLASS))
87
+ return;
72
88
  const title = edge.querySelector('title');
73
89
  const text = title === null ? null : title.textContent;
74
90
  if (text === null)
@@ -1 +1 @@
1
- {"version":3,"file":"graph-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Browser-side script for architecture/dependencies.html.\n *\n * graph-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 Graphviz DOT, and inlines the result into a <script> tag. So it\n * runs in a browser, never in node — but it is ordinary TypeScript, compiled in place by tsc exactly\n * like the package's bin entry points, and linted like every other file here.\n *\n * IT USED TO BE A COMMITTED .js ASSET, exempted from `no-js-files`, and its own header said why: so the\n * dim/highlight/lock logic \"can define ordinary browser functions without tripping the TypeScript lint\n * rules\". That is a file existing to dodge the rules — the same anti-pattern as the bin shims this repo\n * deleted, and the same cure applies. The logic is a CLASS now, which is what `no-function-outside-class`\n * was asking for all along; nothing needed a disable.\n *\n * Behaviour, unchanged: after Viz renders the SVG, hovering a box dims the rest and lights its full\n * ancestor+descendant chain; the #wp-lock dropdown PINS one box's chain (the dim survives mouse-leave)\n * and filters the responsibilities cards below the graph to just that chain. \"All\" clears the lock.\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/** One traversal direction: the edges to light, and the nodes to walk on to. */\nclass Direction {\n constructor(\n readonly nodes: Map<string, Set<string>>,\n readonly edges: Map<string, Set<Element>>,\n ) {}\n}\n\n/**\n * Indexes the rendered SVG and drives the dim/highlight/lock interaction.\n *\n * The adjacency is DIRECTED and kept as two maps per axis: `in*` is what points AT a node (ancestors,\n * walked upward) and `out*` is what it points to (dependencies, walked downward). Highlighting walks\n * both, so a hovered box lights its entire chain in both directions rather than just its neighbours.\n */\nclass GraphHighlighter {\n private readonly nodeByName = new Map<string, SVGGElement>();\n private readonly inEdges = new Map<string, Set<Element>>();\n private readonly outEdges = new Map<string, Set<Element>>();\n private readonly inNodes = new Map<string, Set<string>>();\n private readonly outNodes = new Map<string, Set<string>>();\n\n /**\n * The box the dropdown pinned, or null. Hover still works on top of a lock — leaving a box\n * restores the LOCKED view instead of clearing, so the pinned subgraph stays visible while you\n * scroll down to its responsibility cards.\n */\n private locked: string | null = null;\n\n constructor(private readonly svg: SVGSVGElement) {}\n\n /** Index the SVG, then wire hover and the lock dropdown. */\n wire(): void {\n this.indexNodes();\n this.indexEdges();\n this.wireHover();\n this.wireLock();\n }\n\n private indexNodes(): void {\n this.svg.querySelectorAll('g.node').forEach((g: Element): void => {\n const title = g.querySelector('title');\n if (title !== null && title.textContent !== null) {\n this.nodeByName.set(title.textContent.trim(), g as SVGGElement);\n }\n });\n }\n\n private indexEdges(): void {\n this.svg.querySelectorAll('g.edge').forEach((edge: Element): void => {\n const title = edge.querySelector('title');\n const text = title === null ? null : title.textContent;\n if (text === null) return;\n const idx = text.indexOf('->');\n if (idx < 0) return;\n const from = text.slice(0, idx).trim();\n const to = text.slice(idx + 2).trim();\n this.ensureEdges(this.outEdges, from).add(edge);\n this.ensureEdges(this.inEdges, to).add(edge);\n this.ensureNodes(this.outNodes, from).add(to);\n this.ensureNodes(this.inNodes, to).add(from);\n });\n }\n\n private ensureEdges(map: Map<string, Set<Element>>, key: string): Set<Element> {\n const existing = map.get(key);\n if (existing !== undefined) return existing;\n const created = new Set<Element>();\n map.set(key, created);\n return created;\n }\n\n private ensureNodes(map: Map<string, Set<string>>, key: string): Set<string> {\n const existing = map.get(key);\n if (existing !== undefined) return existing;\n const created = new Set<string>();\n map.set(key, created);\n return created;\n }\n\n private clear(): void {\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl').forEach((el: Element): void => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');\n });\n }\n\n /**\n * Dim everything, then transitively light ancestors and descendants of `name`: edges reached get\n * `wp-hl`, boxes get `wp-neighbor`. The `visited` set is what makes a cyclic graph terminate.\n */\n private highlight(name: string, focusEl: SVGGElement): void {\n this.clear();\n this.svg.classList.add('wp-dim');\n focusEl.classList.add('wp-focus');\n const directions = [\n new Direction(this.inNodes, this.inEdges),\n new Direction(this.outNodes, this.outEdges),\n ];\n for (const dir of directions) this.walk(name, dir);\n }\n\n private walk(start: string, dir: Direction): void {\n const visited = new Set<string>();\n const stack = [start];\n while (stack.length > 0) {\n const cur = stack.pop() as string;\n const edges = dir.edges.get(cur);\n if (edges !== undefined) {\n edges.forEach((e: Element): void => { e.classList.add('wp-hl'); });\n }\n const next = dir.nodes.get(cur);\n if (next === undefined) continue;\n next.forEach((name: string): void => {\n if (visited.has(name)) return;\n visited.add(name);\n stack.push(name);\n const g = this.nodeByName.get(name);\n if (g !== undefined) g.classList.add('wp-neighbor');\n });\n }\n }\n\n /** Leaving a box restores the locked view rather than clearing it outright. */\n private relight(): void {\n if (this.locked === null) {\n this.clear();\n return;\n }\n const g = this.nodeByName.get(this.locked);\n if (g !== undefined) this.highlight(this.locked, g);\n }\n\n private wireHover(): void {\n this.nodeByName.forEach((g: SVGGElement, name: string): void => {\n g.addEventListener('mouseenter', (): void => { this.highlight(name, g); });\n g.addEventListener('mouseleave', (): void => { this.relight(); });\n });\n }\n\n private wireLock(): void {\n const lockSelect = document.getElementById('wp-lock') as HTMLSelectElement | null;\n if (lockSelect === null) return;\n lockSelect.addEventListener('change', (): void => {\n this.locked = lockSelect.value === '' ? null : lockSelect.value;\n if (this.locked === null) {\n this.clear();\n } else {\n const g = this.nodeByName.get(this.locked);\n if (g !== undefined) this.highlight(this.locked, g);\n }\n this.filterCards();\n });\n }\n\n /**\n * Filter the responsibility cards to the locked box's chain by READING BACK the\n * `.wp-focus`/`.wp-neighbor` classes highlight() just set — so there is no second graph walk and\n * the cards can never disagree with the picture.\n */\n private filterCards(): void {\n const lit = new Set<string>();\n if (this.locked !== null) {\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor').forEach((el: Element): void => {\n const title = el.querySelector('title');\n if (title !== null && title.textContent !== null) lit.add(title.textContent.trim());\n });\n }\n document.querySelectorAll('.wp-resp-card').forEach((card: Element): void => {\n const name = card.getAttribute('data-node');\n const visible = this.locked === null || (name !== null && lit.has(name));\n card.classList.toggle('wp-hidden', !visible);\n });\n }\n}\n\n/** Renders the DOT and hands the SVG to the highlighter. Reports a failure into the page, not just the console. */\nclass GraphPage {\n render(): void {\n Viz.instance()\n .then((viz: VizInstance): void => {\n const element = viz.renderSVGElement(__DOT__);\n const host = document.getElementById('graph');\n if (host === null) return;\n host.appendChild(element);\n new GraphHighlighter(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 GraphPage().render();\n"]}
1
+ {"version":3,"file":"graph-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Browser-side script for architecture/dependencies.html.\n *\n * graph-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 Graphviz DOT, and inlines the result into a <script> tag. So it\n * runs in a browser, never in node — but it is ordinary TypeScript, compiled in place by tsc exactly\n * like the package's bin entry points, and linted like every other file here.\n *\n * IT USED TO BE A COMMITTED .js ASSET, exempted from `no-js-files`, and its own header said why: so the\n * dim/highlight/lock logic \"can define ordinary browser functions without tripping the TypeScript lint\n * rules\". That is a file existing to dodge the rules — the same anti-pattern as the bin shims this repo\n * deleted, and the same cure applies. The logic is a CLASS now, which is what `no-function-outside-class`\n * was asking for all along; nothing needed a disable.\n *\n * Behaviour, unchanged: after Viz renders the SVG, hovering a box dims the rest and lights its full\n * ancestor+descendant chain; the #wp-lock dropdown PINS one box's chain (the dim survives mouse-leave)\n * and filters the responsibilities cards below the graph to just that chain. \"All\" clears the lock.\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/**\n * The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering\n * edges that pin each level to its own row. They are layout scaffolding, not architecture, so the\n * indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box\n * would light boxes it has nothing to do with.\n *\n * Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),\n * so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the\n * scaffolding stays inert if that ever stops being true — the DOT is the only place that decides\n * what is scaffolding, and it says so with this class.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** One traversal direction: the edges to light, and the nodes to walk on to. */\nclass Direction {\n constructor(\n readonly nodes: Map<string, Set<string>>,\n readonly edges: Map<string, Set<Element>>,\n ) {}\n}\n\n/**\n * Indexes the rendered SVG and drives the dim/highlight/lock interaction.\n *\n * The adjacency is DIRECTED and kept as two maps per axis: `in*` is what points AT a node (ancestors,\n * walked upward) and `out*` is what it points to (dependencies, walked downward). Highlighting walks\n * both, so a hovered box lights its entire chain in both directions rather than just its neighbours.\n */\nclass GraphHighlighter {\n private readonly nodeByName = new Map<string, SVGGElement>();\n private readonly inEdges = new Map<string, Set<Element>>();\n private readonly outEdges = new Map<string, Set<Element>>();\n private readonly inNodes = new Map<string, Set<string>>();\n private readonly outNodes = new Map<string, Set<string>>();\n\n /**\n * The box the dropdown pinned, or null. Hover still works on top of a lock — leaving a box\n * restores the LOCKED view instead of clearing, so the pinned subgraph stays visible while you\n * scroll down to its responsibility cards.\n */\n private locked: string | null = null;\n\n constructor(private readonly svg: SVGSVGElement) {}\n\n /** Index the SVG, then wire hover and the lock dropdown. */\n wire(): void {\n this.indexNodes();\n this.indexEdges();\n this.wireHover();\n this.wireLock();\n }\n\n private indexNodes(): void {\n this.svg.querySelectorAll('g.node').forEach((g: Element): void => {\n if (g.classList.contains(LAYOUT_CLASS)) return;\n const title = g.querySelector('title');\n if (title !== null && title.textContent !== null) {\n this.nodeByName.set(title.textContent.trim(), g as SVGGElement);\n }\n });\n }\n\n private indexEdges(): void {\n this.svg.querySelectorAll('g.edge').forEach((edge: Element): void => {\n if (edge.classList.contains(LAYOUT_CLASS)) return;\n const title = edge.querySelector('title');\n const text = title === null ? null : title.textContent;\n if (text === null) return;\n const idx = text.indexOf('->');\n if (idx < 0) return;\n const from = text.slice(0, idx).trim();\n const to = text.slice(idx + 2).trim();\n this.ensureEdges(this.outEdges, from).add(edge);\n this.ensureEdges(this.inEdges, to).add(edge);\n this.ensureNodes(this.outNodes, from).add(to);\n this.ensureNodes(this.inNodes, to).add(from);\n });\n }\n\n private ensureEdges(map: Map<string, Set<Element>>, key: string): Set<Element> {\n const existing = map.get(key);\n if (existing !== undefined) return existing;\n const created = new Set<Element>();\n map.set(key, created);\n return created;\n }\n\n private ensureNodes(map: Map<string, Set<string>>, key: string): Set<string> {\n const existing = map.get(key);\n if (existing !== undefined) return existing;\n const created = new Set<string>();\n map.set(key, created);\n return created;\n }\n\n private clear(): void {\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl').forEach((el: Element): void => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');\n });\n }\n\n /**\n * Dim everything, then transitively light ancestors and descendants of `name`: edges reached get\n * `wp-hl`, boxes get `wp-neighbor`. The `visited` set is what makes a cyclic graph terminate.\n */\n private highlight(name: string, focusEl: SVGGElement): void {\n this.clear();\n this.svg.classList.add('wp-dim');\n focusEl.classList.add('wp-focus');\n const directions = [\n new Direction(this.inNodes, this.inEdges),\n new Direction(this.outNodes, this.outEdges),\n ];\n for (const dir of directions) this.walk(name, dir);\n }\n\n private walk(start: string, dir: Direction): void {\n const visited = new Set<string>();\n const stack = [start];\n while (stack.length > 0) {\n const cur = stack.pop() as string;\n const edges = dir.edges.get(cur);\n if (edges !== undefined) {\n edges.forEach((e: Element): void => { e.classList.add('wp-hl'); });\n }\n const next = dir.nodes.get(cur);\n if (next === undefined) continue;\n next.forEach((name: string): void => {\n if (visited.has(name)) return;\n visited.add(name);\n stack.push(name);\n const g = this.nodeByName.get(name);\n if (g !== undefined) g.classList.add('wp-neighbor');\n });\n }\n }\n\n /** Leaving a box restores the locked view rather than clearing it outright. */\n private relight(): void {\n if (this.locked === null) {\n this.clear();\n return;\n }\n const g = this.nodeByName.get(this.locked);\n if (g !== undefined) this.highlight(this.locked, g);\n }\n\n private wireHover(): void {\n this.nodeByName.forEach((g: SVGGElement, name: string): void => {\n g.addEventListener('mouseenter', (): void => { this.highlight(name, g); });\n g.addEventListener('mouseleave', (): void => { this.relight(); });\n });\n }\n\n private wireLock(): void {\n const lockSelect = document.getElementById('wp-lock') as HTMLSelectElement | null;\n if (lockSelect === null) return;\n lockSelect.addEventListener('change', (): void => {\n this.locked = lockSelect.value === '' ? null : lockSelect.value;\n if (this.locked === null) {\n this.clear();\n } else {\n const g = this.nodeByName.get(this.locked);\n if (g !== undefined) this.highlight(this.locked, g);\n }\n this.filterCards();\n });\n }\n\n /**\n * Filter the responsibility cards to the locked box's chain by READING BACK the\n * `.wp-focus`/`.wp-neighbor` classes highlight() just set — so there is no second graph walk and\n * the cards can never disagree with the picture.\n */\n private filterCards(): void {\n const lit = new Set<string>();\n if (this.locked !== null) {\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor').forEach((el: Element): void => {\n const title = el.querySelector('title');\n if (title !== null && title.textContent !== null) lit.add(title.textContent.trim());\n });\n }\n document.querySelectorAll('.wp-resp-card').forEach((card: Element): void => {\n const name = card.getAttribute('data-node');\n const visible = this.locked === null || (name !== null && lit.has(name));\n card.classList.toggle('wp-hidden', !visible);\n });\n }\n}\n\n/** Renders the DOT and hands the SVG to the highlighter. Reports a failure into the page, not just the console. */\nclass GraphPage {\n render(): void {\n Viz.instance()\n .then((viz: VizInstance): void => {\n const element = viz.renderSVGElement(__DOT__);\n const host = document.getElementById('graph');\n if (host === null) return;\n host.appendChild(element);\n new GraphHighlighter(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 GraphPage().render();\n"]}
@@ -32,6 +32,7 @@ export declare class GraphVisualizer {
32
32
  private readonly clientJs;
33
33
  private readonly names;
34
34
  private readonly responsibilities;
35
+ private readonly bandLayout;
35
36
  /**
36
37
  * How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD
37
38
  * ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in
@@ -98,6 +99,14 @@ export declare class GraphVisualizer {
98
99
  * Generate Graphviz DOT format from the graph
99
100
  */
100
101
  generateDot(graph: EnhancedGraph, title?: string): string;
102
+ /**
103
+ * The visible projects grouped into one band per dependency level, ordered HIGHEST LEVEL FIRST
104
+ * so the emitted bands read top-to-bottom with L0 last (hidden projects are omitted, so no
105
+ * stray rank=same name is emitted for an absent node). LevelBandLayout turns these into the
106
+ * rank sets and the invisible chain that pins them — see graph-level-bands.ts for why the
107
+ * chain is required at all.
108
+ */
109
+ private levelBands;
101
110
  private dotNodes;
102
111
  private dotEdges;
103
112
  /**
@@ -18,6 +18,7 @@ const fs = tslib_1.__importStar(require("fs"));
18
18
  const path = tslib_1.__importStar(require("path"));
19
19
  const child_process_1 = require("child_process");
20
20
  const graph_names_1 = require("./graph-names");
21
+ const graph_level_bands_1 = require("./graph-level-bands");
21
22
  const graph_responsibilities_1 = require("./graph-responsibilities");
22
23
  const toError_1 = require("../toError");
23
24
  /**
@@ -74,6 +75,7 @@ class GraphVisualizer {
74
75
  clientJs;
75
76
  names = new graph_names_1.GraphNames();
76
77
  responsibilities = new graph_responsibilities_1.ResponsibilitiesRenderer();
78
+ bandLayout = new graph_level_bands_1.LevelBandLayout();
77
79
  /**
78
80
  * How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD
79
81
  * ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in
@@ -196,27 +198,10 @@ class GraphVisualizer {
196
198
  dot += ' rankdir=TB;\n';
197
199
  dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
198
200
  dot += ' edge [fontname="Arial"];\n\n';
199
- // Group projects by level (hidden projects are omitted from the ranks so
200
- // no stray rank=same name is emitted for an absent node).
201
- const levels = {};
202
- for (const project of Object.keys(graph)) {
203
- if (this.isHidden(graph[project]))
204
- continue;
205
- const level = graph[project].level;
206
- if (!levels[level])
207
- levels[level] = [];
208
- levels[level].push(project);
209
- }
201
+ const bands = this.levelBands(graph);
210
202
  dot += this.dotNodes(graph);
211
203
  dot += '\n';
212
- // Create same-rank subgraphs for each level
213
- for (const projects of Object.values(levels)) {
214
- dot += ` { rank=same; `;
215
- for (const p of projects) {
216
- dot += `"${this.names.getShortName(p)}"; `;
217
- }
218
- dot += '}\n';
219
- }
204
+ dot += this.bandLayout.dot(bands);
220
205
  dot += '\n';
221
206
  dot += this.dotEdges(graph);
222
207
  dot += '\n labelloc="t";\n';
@@ -225,6 +210,28 @@ class GraphVisualizer {
225
210
  dot += '}\n';
226
211
  return dot;
227
212
  }
213
+ /**
214
+ * The visible projects grouped into one band per dependency level, ordered HIGHEST LEVEL FIRST
215
+ * so the emitted bands read top-to-bottom with L0 last (hidden projects are omitted, so no
216
+ * stray rank=same name is emitted for an absent node). LevelBandLayout turns these into the
217
+ * rank sets and the invisible chain that pins them — see graph-level-bands.ts for why the
218
+ * chain is required at all.
219
+ */
220
+ levelBands(graph) {
221
+ const byLevel = new Map();
222
+ for (const project of Object.keys(graph)) {
223
+ if (this.isHidden(graph[project]))
224
+ continue;
225
+ const level = graph[project].level;
226
+ const names = byLevel.get(level);
227
+ if (names === undefined)
228
+ byLevel.set(level, [this.names.getShortName(project)]);
229
+ else
230
+ names.push(this.names.getShortName(project));
231
+ }
232
+ const levels = [...byLevel.keys()].sort((a, b) => b - a);
233
+ return levels.map((level) => new graph_level_bands_1.LevelBand(level, [...byLevel.get(level)].sort()));
234
+ }
228
235
  // Node lines: fill colored by framework env set (libType), border shaped by
229
236
  // role; the label shows the env set + role (e.g. [browser, node] · server). A
230
237
  // node with a generated DI design also gets a URL so the rendered SVG box is
@@ -464,7 +471,7 @@ class GraphVisualizer {
464
471
  </div>
465
472
  </div>
466
473
  <div class="legend-item" style="margin-top: 15px;">
467
- <em>Each node label shows its dependency level (L#), its framework env set (e.g. [browser, node]), and its role. Rows are laid out by level (top = no dependencies), with the deepest libraries at the bottom. Transitive dependencies are allowed but not shown.</em>
474
+ <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>
468
475
  </div>
469
476
  </div>`;
470
477
  }
@@ -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 } from './graph-names';\nimport { ResponsibilitiesRenderer } from './graph-responsibilities';\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\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/**\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\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 * Click-through href for a node: 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 clickable design page).\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 * Generate Graphviz DOT format from the graph\n */\n generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\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 // Group projects by level (hidden projects are omitted from the ranks so\n // no stray rank=same name is emitted for an absent node).\n const levels: Record<number, string[]> = {};\n for (const project of Object.keys(graph)) {\n if (this.isHidden(graph[project])) continue;\n const level = graph[project].level;\n if (!levels[level]) levels[level] = [];\n levels[level].push(project);\n }\n\n dot += this.dotNodes(graph);\n dot += '\\n';\n\n // Create same-rank subgraphs for each level\n for (const projects of Object.values(levels)) {\n dot += ` { rank=same; `;\n for (const p of projects) {\n dot += `\"${this.names.getShortName(p)}\"; `;\n }\n dot += '}\\n';\n }\n\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 // 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). A\n // node with a generated DI design also gets a URL so the rendered SVG box is\n // clickable — it opens that project's committed design.html in a new tab.\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 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 href = this.designHtmlHref(info.designFile);\n const link = href ? `, URL=\"${href}\", target=\"_blank\"` : '';\n const envSet = `[${frameworks.join(', ')}]`;\n const labelMeta = `L${info.level} · ${envSet} · ${role}`;\n dot += ` \"${shortName}\" [fillcolor=\"${color}\"${border}${link}, 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 shortName = this.names.getShortName(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(shortName, this.names.getShortName(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 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);\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\">💡 Click any box with a generated DI design to open its <strong>design.html</strong> (what the AI sees inside that project).</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>${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 const shortName = this.names.getShortName(project);\n options += `<option value=\"${shortName}\">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 /* viz.js renders node URLs as <a> — only boxes with a design.html get\n * one, so a:hover is a clickable-only signal. Make it pop clearly: a\n * thicker border PLUS a blue glow lift the box off the page, so it is\n * obvious which boxes you can click into vs. which you cannot. (A plain\n * stroke bump is invisible on server/client boxes, whose resting border\n * is already thick.) */\n #graph a { cursor: pointer; }\n #graph a polygon,\n #graph a ellipse { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n #graph a:hover polygon,\n #graph a:hover ellipse {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }\n /* Hover-highlight (wired up in JS after viz.js renders). Hovering a node\n * adds .wp-dim to the <svg> and .wp-focus/.wp-neighbor/.wp-hl to the\n * connected box, its neighbors, and its edges. We ONLY dim: the connected\n * subgraph keeps its exact normal look (full opacity), the rest recedes.\n * The un-dim rules repeat \"svg.wp-dim\" so they out-specify the dim rule\n * (which has an extra type selector) — else the subgraph stays dimmed. */\n #graph .node, #graph .edge { transition: opacity 0.12s ease; }\n #graph svg.wp-dim .node,\n #graph svg.wp-dim .edge { opacity: 0.15; }\n #graph svg.wp-dim .node.wp-focus,\n #graph svg.wp-dim .node.wp-neighbor,\n #graph svg.wp-dim .edge.wp-hl { opacity: 1; }\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. Rows are laid out by level (top = no dependencies), with the deepest libraries at the bottom. 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): string {\n return this.clientJs().split(CLIENT_DOT_PLACEHOLDER).join(JSON.stringify(dot));\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 the boxes stay clickable into each project's\n * design.html. 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(this.generateDot(graph, title), 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 } from './graph-names';\nimport { LevelBand, LevelBandLayout } from './graph-level-bands';\nimport { ResponsibilitiesRenderer } from './graph-responsibilities';\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\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/**\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\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 * Click-through href for a node: 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 clickable design page).\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 * Generate Graphviz DOT format from the graph\n */\n generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\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 * 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 names = byLevel.get(level);\n if (names === undefined) byLevel.set(level, [this.names.getShortName(project)]);\n else names.push(this.names.getShortName(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). A\n // node with a generated DI design also gets a URL so the rendered SVG box is\n // clickable — it opens that project's committed design.html in a new tab.\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 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 href = this.designHtmlHref(info.designFile);\n const link = href ? `, URL=\"${href}\", target=\"_blank\"` : '';\n const envSet = `[${frameworks.join(', ')}]`;\n const labelMeta = `L${info.level} · ${envSet} · ${role}`;\n dot += ` \"${shortName}\" [fillcolor=\"${color}\"${border}${link}, 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 shortName = this.names.getShortName(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(shortName, this.names.getShortName(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 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);\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\">💡 Click any box with a generated DI design to open its <strong>design.html</strong> (what the AI sees inside that project).</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>${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 const shortName = this.names.getShortName(project);\n options += `<option value=\"${shortName}\">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 /* viz.js renders node URLs as <a> — only boxes with a design.html get\n * one, so a:hover is a clickable-only signal. Make it pop clearly: a\n * thicker border PLUS a blue glow lift the box off the page, so it is\n * obvious which boxes you can click into vs. which you cannot. (A plain\n * stroke bump is invisible on server/client boxes, whose resting border\n * is already thick.) */\n #graph a { cursor: pointer; }\n #graph a polygon,\n #graph a ellipse { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n #graph a:hover polygon,\n #graph a:hover ellipse {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }\n /* Hover-highlight (wired up in JS after viz.js renders). Hovering a node\n * adds .wp-dim to the <svg> and .wp-focus/.wp-neighbor/.wp-hl to the\n * connected box, its neighbors, and its edges. We ONLY dim: the connected\n * subgraph keeps its exact normal look (full opacity), the rest recedes.\n * The un-dim rules repeat \"svg.wp-dim\" so they out-specify the dim rule\n * (which has an extra type selector) — else the subgraph stays dimmed. */\n #graph .node, #graph .edge { transition: opacity 0.12s ease; }\n #graph svg.wp-dim .node,\n #graph svg.wp-dim .edge { opacity: 0.15; }\n #graph svg.wp-dim .node.wp-focus,\n #graph svg.wp-dim .node.wp-neighbor,\n #graph svg.wp-dim .edge.wp-hl { opacity: 1; }\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): string {\n return this.clientJs().split(CLIENT_DOT_PLACEHOLDER).join(JSON.stringify(dot));\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 the boxes stay clickable into each project's\n * design.html. 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(this.generateDot(graph, title), 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"]}