@webpieces/nx-webpieces-rules 0.3.333 → 0.3.335

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 (47) hide show
  1. package/executors.json +5 -0
  2. package/package.json +6 -6
  3. package/src/executors/generate/executor.js +7 -1
  4. package/src/executors/generate/executor.js.map +1 -1
  5. package/src/executors/validate-api-relations/executor.d.ts +18 -0
  6. package/src/executors/validate-api-relations/executor.js +51 -0
  7. package/src/executors/validate-api-relations/executor.js.map +1 -0
  8. package/src/executors/validate-api-relations/schema.json +8 -0
  9. package/src/executors/validate-architecture-unchanged/executor.js +22 -11
  10. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  11. package/src/executors/visualize/executor.js +3 -2
  12. package/src/executors/visualize/executor.js.map +1 -1
  13. package/src/executors/visualize-runtime/executor.js +1 -1
  14. package/src/executors/visualize-runtime/executor.js.map +1 -1
  15. package/src/lib/api-usage/api-relations-validator.d.ts +30 -0
  16. package/src/lib/api-usage/api-relations-validator.js +74 -0
  17. package/src/lib/api-usage/api-relations-validator.js.map +1 -0
  18. package/src/lib/api-usage/api-relations.d.ts +51 -0
  19. package/src/lib/api-usage/api-relations.js +34 -0
  20. package/src/lib/api-usage/api-relations.js.map +1 -0
  21. package/src/lib/api-usage/api-scanner.d.ts +60 -0
  22. package/src/lib/api-usage/api-scanner.js +239 -0
  23. package/src/lib/api-usage/api-scanner.js.map +1 -0
  24. package/src/lib/di-graph/analyzer-strategy.d.ts +1 -0
  25. package/src/lib/di-graph/analyzer-strategy.js +3 -0
  26. package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
  27. package/src/lib/graph-comparator.js +8 -0
  28. package/src/lib/graph-comparator.js.map +1 -1
  29. package/src/lib/graph-loader.js +18 -0
  30. package/src/lib/graph-loader.js.map +1 -1
  31. package/src/lib/graph-names.d.ts +17 -0
  32. package/src/lib/graph-names.js +24 -0
  33. package/src/lib/graph-names.js.map +1 -0
  34. package/src/lib/graph-responsibilities.d.ts +48 -0
  35. package/src/lib/graph-responsibilities.js +152 -0
  36. package/src/lib/graph-responsibilities.js.map +1 -0
  37. package/src/lib/graph-sorter.d.ts +5 -0
  38. package/src/lib/graph-sorter.js.map +1 -1
  39. package/src/lib/graph-visualizer.client.js +127 -0
  40. package/src/lib/graph-visualizer.d.ts +87 -27
  41. package/src/lib/graph-visualizer.js +316 -257
  42. package/src/lib/graph-visualizer.js.map +1 -1
  43. package/src/lib/role-resolver.d.ts +3 -0
  44. package/src/lib/role-resolver.js +4 -1
  45. package/src/lib/role-resolver.js.map +1 -1
  46. package/src/scripts/wp-design-visualize.js +1 -1
  47. package/src/scripts/wp-design-visualize.js.map +1 -1
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * Graph Names
4
+ *
5
+ * Shared helper for turning a scoped project name into the short display name
6
+ * used everywhere in the visualization (graph box titles, the lock-control
7
+ * options, and the responsibilities cards). Kept in its own class so both
8
+ * GraphVisualizer and ResponsibilitiesRenderer can depend on it without a
9
+ * circular dependency between them.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.GraphNames = void 0;
13
+ class GraphNames {
14
+ /**
15
+ * Remove scope from name for display
16
+ * '@scope/name' → 'name'
17
+ * 'name' → 'name'
18
+ */
19
+ getShortName(name) {
20
+ return name.includes('/') ? name.split('/').pop() : name;
21
+ }
22
+ }
23
+ exports.GraphNames = GraphNames;
24
+ //# sourceMappingURL=graph-names.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-names.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-names.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAEH,MAAa,UAAU;IACnB;;;;OAIG;IACH,YAAY,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9D,CAAC;CACJ;AATD,gCASC","sourcesContent":["/**\n * Graph Names\n *\n * Shared helper for turning a scoped project name into the short display name\n * used everywhere in the visualization (graph box titles, the lock-control\n * options, and the responsibilities cards). Kept in its own class so both\n * GraphVisualizer and ResponsibilitiesRenderer can depend on it without a\n * circular dependency between them.\n */\n\nexport class GraphNames {\n /**\n * Remove scope from name for display\n * '@scope/name' → 'name'\n * 'name' → 'name'\n */\n getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n }\n}\n"]}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Responsibilities Section
3
+ *
4
+ * Renders every module's full `responsibilities.md` as a collapsible card below
5
+ * the dependency graph in architecture/dependencies.html. Cards are ordered by
6
+ * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a
7
+ * reader scrolls from the runnable servers/clients down into the libraries.
8
+ *
9
+ * Each card carries `data-node="<shortName>"` matching the graph box's title, so
10
+ * the page script can filter the list to just the locked box's chain (see
11
+ * graph-visualizer.client.js). The summary paragraph is the `shortDescription`
12
+ * already on each graph entry; the expanded body is the rendered markdown file.
13
+ */
14
+ import type { EnhancedGraph } from './graph-sorter';
15
+ export declare class ResponsibilitiesRenderer {
16
+ private readonly names;
17
+ /**
18
+ * Build the responsibilities section HTML: one collapsible card per module,
19
+ * sorted by level descending (tie-break by name ascending). Returned as a
20
+ * <section> that GraphVisualizer injects below the graph.
21
+ */
22
+ generateSection(graph: EnhancedGraph, workspaceRoot: string): string;
23
+ private renderCard;
24
+ /**
25
+ * Read a module's responsibilities.md body. Generation already guarantees the
26
+ * file exists (metadata validation throws otherwise); a fallback line guards
27
+ * the edge case where the graph JSON is stale relative to disk.
28
+ */
29
+ private readBody;
30
+ /**
31
+ * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings
32
+ * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.
33
+ * Everything is HTML-escaped first; only a small inline set is re-marked. This
34
+ * is deliberately tiny — responsibilities.md files are short and structured,
35
+ * and the repo has no markdown dependency.
36
+ */
37
+ private renderMarkdown;
38
+ private renderLine;
39
+ /**
40
+ * Render inline markdown spans within an already HTML-escaped line: `code` →
41
+ * <code>, **bold** → <strong>, *italic* → <em>.
42
+ */
43
+ private renderInline;
44
+ /**
45
+ * Escape a string for safe embedding in HTML text/attributes.
46
+ */
47
+ private escapeHtml;
48
+ }
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ /**
3
+ * Responsibilities Section
4
+ *
5
+ * Renders every module's full `responsibilities.md` as a collapsible card below
6
+ * the dependency graph in architecture/dependencies.html. Cards are ordered by
7
+ * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a
8
+ * reader scrolls from the runnable servers/clients down into the libraries.
9
+ *
10
+ * Each card carries `data-node="<shortName>"` matching the graph box's title, so
11
+ * the page script can filter the list to just the locked box's chain (see
12
+ * graph-visualizer.client.js). The summary paragraph is the `shortDescription`
13
+ * already on each graph entry; the expanded body is the rendered markdown file.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ResponsibilitiesRenderer = void 0;
17
+ const tslib_1 = require("tslib");
18
+ const fs = tslib_1.__importStar(require("fs"));
19
+ const path = tslib_1.__importStar(require("path"));
20
+ const graph_names_1 = require("./graph-names");
21
+ /**
22
+ * Mutable cursor state threaded through the markdown line renderer: whether we
23
+ * are currently inside an open `<ul>` (so the next blank line / heading / non-
24
+ * bullet closes it).
25
+ */
26
+ class MarkdownListState {
27
+ inList = false;
28
+ }
29
+ class ResponsibilitiesRenderer {
30
+ names = new graph_names_1.GraphNames();
31
+ /**
32
+ * Build the responsibilities section HTML: one collapsible card per module,
33
+ * sorted by level descending (tie-break by name ascending). Returned as a
34
+ * <section> that GraphVisualizer injects below the graph.
35
+ */
36
+ generateSection(graph, workspaceRoot) {
37
+ const projects = Object.keys(graph);
38
+ projects.sort((a, b) => {
39
+ const levelDiff = graph[b].level - graph[a].level;
40
+ if (levelDiff !== 0)
41
+ return levelDiff;
42
+ return a.localeCompare(b);
43
+ });
44
+ const cards = [];
45
+ for (const project of projects) {
46
+ cards.push(this.renderCard(project, graph[project], workspaceRoot));
47
+ }
48
+ return (`<section id="wp-responsibilities">` +
49
+ `<h2>Responsibilities (level high → low)</h2>` +
50
+ `<p class="hint">Lock a box above to narrow this list to just that box's dependency chain.</p>` +
51
+ cards.join('\n') +
52
+ `</section>`);
53
+ }
54
+ renderCard(project, entry, workspaceRoot) {
55
+ const shortName = this.names.getShortName(project);
56
+ const summary = entry.shortDescription ?? '';
57
+ const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';
58
+ const body = this.readBody(entry, workspaceRoot);
59
+ return (`<details class="wp-resp-card" data-node="${this.escapeHtml(shortName)}">` +
60
+ `<summary><span class="wp-resp-level">L${entry.level}</span> ` +
61
+ `<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +
62
+ `<div class="wp-resp-body">${body}</div>` +
63
+ `</details>`);
64
+ }
65
+ /**
66
+ * Read a module's responsibilities.md body. Generation already guarantees the
67
+ * file exists (metadata validation throws otherwise); a fallback line guards
68
+ * the edge case where the graph JSON is stale relative to disk.
69
+ */
70
+ readBody(entry, workspaceRoot) {
71
+ const file = entry.responsibilitiesFile;
72
+ if (!file)
73
+ return '<p><em>No responsibilities.md recorded for this module.</em></p>';
74
+ const absolutePath = path.join(workspaceRoot, file);
75
+ if (!fs.existsSync(absolutePath)) {
76
+ return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;
77
+ }
78
+ return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));
79
+ }
80
+ /**
81
+ * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings
82
+ * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.
83
+ * Everything is HTML-escaped first; only a small inline set is re-marked. This
84
+ * is deliberately tiny — responsibilities.md files are short and structured,
85
+ * and the repo has no markdown dependency.
86
+ */
87
+ renderMarkdown(markdown) {
88
+ const out = [];
89
+ const state = new MarkdownListState();
90
+ for (const rawLine of markdown.split('\n')) {
91
+ this.renderLine(rawLine.trim(), out, state);
92
+ }
93
+ if (state.inList)
94
+ out.push('</ul>');
95
+ return out.join('\n');
96
+ }
97
+ renderLine(line, out, state) {
98
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
99
+ const bullet = /^[-*]\s+(.*)$/.exec(line);
100
+ if (line.length === 0) {
101
+ if (state.inList) {
102
+ out.push('</ul>');
103
+ state.inList = false;
104
+ }
105
+ return;
106
+ }
107
+ if (heading) {
108
+ if (state.inList) {
109
+ out.push('</ul>');
110
+ state.inList = false;
111
+ }
112
+ const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6
113
+ out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);
114
+ return;
115
+ }
116
+ if (bullet) {
117
+ if (!state.inList) {
118
+ out.push('<ul>');
119
+ state.inList = true;
120
+ }
121
+ out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);
122
+ return;
123
+ }
124
+ if (state.inList) {
125
+ out.push('</ul>');
126
+ state.inList = false;
127
+ }
128
+ out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);
129
+ }
130
+ /**
131
+ * Render inline markdown spans within an already HTML-escaped line: `code` →
132
+ * <code>, **bold** → <strong>, *italic* → <em>.
133
+ */
134
+ renderInline(escaped) {
135
+ return escaped
136
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
137
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
138
+ .replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>');
139
+ }
140
+ /**
141
+ * Escape a string for safe embedding in HTML text/attributes.
142
+ */
143
+ escapeHtml(text) {
144
+ return text
145
+ .replace(/&/g, '&amp;')
146
+ .replace(/</g, '&lt;')
147
+ .replace(/>/g, '&gt;')
148
+ .replace(/"/g, '&quot;');
149
+ }
150
+ }
151
+ exports.ResponsibilitiesRenderer = ResponsibilitiesRenderer;
152
+ //# sourceMappingURL=graph-responsibilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-responsibilities.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-responsibilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Responsibilities Section\n *\n * Renders every module's full `responsibilities.md` as a collapsible card below\n * the dependency graph in architecture/dependencies.html. Cards are ordered by\n * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a\n * reader scrolls from the runnable servers/clients down into the libraries.\n *\n * Each card carries `data-node=\"<shortName>\"` matching the graph box's title, so\n * the page script can filter the list to just the locked box's chain (see\n * graph-visualizer.client.js). The summary paragraph is the `shortDescription`\n * already on each graph entry; the expanded body is the rendered markdown file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { GraphNames } from './graph-names';\n\n/**\n * Mutable cursor state threaded through the markdown line renderer: whether we\n * are currently inside an open `<ul>` (so the next blank line / heading / non-\n * bullet closes it).\n */\nclass MarkdownListState {\n inList = false;\n}\n\nexport class ResponsibilitiesRenderer {\n private readonly names = new GraphNames();\n\n /**\n * Build the responsibilities section HTML: one collapsible card per module,\n * sorted by level descending (tie-break by name ascending). Returned as a\n * <section> that GraphVisualizer injects below the graph.\n */\n generateSection(graph: EnhancedGraph, workspaceRoot: string): 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\n const cards: string[] = [];\n for (const project of projects) {\n cards.push(this.renderCard(project, graph[project], workspaceRoot));\n }\n\n return (\n `<section id=\"wp-responsibilities\">` +\n `<h2>Responsibilities (level high → low)</h2>` +\n `<p class=\"hint\">Lock a box above to narrow this list to just that box's dependency chain.</p>` +\n cards.join('\\n') +\n `</section>`\n );\n }\n\n private renderCard(project: string, entry: GraphEntry, workspaceRoot: string): string {\n const shortName = this.names.getShortName(project);\n const summary = entry.shortDescription ?? '';\n const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';\n const body = this.readBody(entry, workspaceRoot);\n return (\n `<details class=\"wp-resp-card\" data-node=\"${this.escapeHtml(shortName)}\">` +\n `<summary><span class=\"wp-resp-level\">L${entry.level}</span> ` +\n `<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +\n `<div class=\"wp-resp-body\">${body}</div>` +\n `</details>`\n );\n }\n\n /**\n * Read a module's responsibilities.md body. Generation already guarantees the\n * file exists (metadata validation throws otherwise); a fallback line guards\n * the edge case where the graph JSON is stale relative to disk.\n */\n private readBody(entry: GraphEntry, workspaceRoot: string): string {\n const file = entry.responsibilitiesFile;\n if (!file) return '<p><em>No responsibilities.md recorded for this module.</em></p>';\n const absolutePath = path.join(workspaceRoot, file);\n if (!fs.existsSync(absolutePath)) {\n return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;\n }\n return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));\n }\n\n /**\n * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings\n * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.\n * Everything is HTML-escaped first; only a small inline set is re-marked. This\n * is deliberately tiny — responsibilities.md files are short and structured,\n * and the repo has no markdown dependency.\n */\n private renderMarkdown(markdown: string): string {\n const out: string[] = [];\n const state = new MarkdownListState();\n for (const rawLine of markdown.split('\\n')) {\n this.renderLine(rawLine.trim(), out, state);\n }\n if (state.inList) out.push('</ul>');\n return out.join('\\n');\n }\n\n private renderLine(line: string, out: string[], state: MarkdownListState): void {\n const heading = /^(#{1,6})\\s+(.*)$/.exec(line);\n const bullet = /^[-*]\\s+(.*)$/.exec(line);\n if (line.length === 0) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n return;\n }\n if (heading) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6\n out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);\n return;\n }\n if (bullet) {\n if (!state.inList) { out.push('<ul>'); state.inList = true; }\n out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);\n return;\n }\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);\n }\n\n /**\n * Render inline markdown spans within an already HTML-escaped line: `code` →\n * <code>, **bold** → <strong>, *italic* → <em>.\n */\n private renderInline(escaped: string): string {\n return escaped\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/(^|[^*])\\*([^*]+)\\*/g, '$1<em>$2</em>');\n }\n\n /**\n * Escape a string for safe embedding in HTML text/attributes.\n */\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n}\n"]}
@@ -6,6 +6,7 @@
6
6
  * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)
7
7
  * 3. Group projects into layers for deterministic ordering
8
8
  */
9
+ import type { ProjectApiRelations } from './api-usage/api-relations';
9
10
  /**
10
11
  * Graph entry with level metadata plus AI-oriented metadata filled in by
11
12
  * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:
@@ -17,6 +18,9 @@
17
18
  * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc
18
19
  * - designFile: repo-relative path to the generated DI design.json (only for
19
20
  * project.json projects)
21
+ * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the
22
+ * APIs this project implements (serves) and/or uses (calls), each with its
23
+ * transport (rpc | pubsub). Derived by scanning source (see api-usage/).
20
24
  */
21
25
  export interface GraphEntry {
22
26
  level: number;
@@ -26,6 +30,7 @@ export interface GraphEntry {
26
30
  shortDescription?: string;
27
31
  responsibilitiesFile?: string;
28
32
  designFile?: string;
33
+ apiRelations?: ProjectApiRelations;
29
34
  }
30
35
  /**
31
36
  * Enhanced graph format with level information
@@ -1 +1 @@
1
- {"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
1
+ {"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AA4CH,4DA2CC;AA8CD,wDAgBC;AAlHD;;;;;;;;GAQG;AACH,SAAgB,wBAAwB,CAAC,KAA+B;IACpE,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEvC,OAAO,SAAS,CAAC,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAErC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,uEAAuE;YACvE,MAAM,mBAAmB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAEpE,IAAI,mBAAmB,EAAE,CAAC;gBACtB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,CAAC;QACL,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,kDAAkD;YAClD,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAE/D,4BAA4B;YAC5B,MAAM,SAAS,GAAG,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YAE9C,MAAM,IAAI,KAAK,CACX,uCAAuC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;gBAC3D,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,SAAS,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1C,yDAAyD,CAChE,CAAC;QACN,CAAC;QAED,4DAA4D;QAC5D,YAAY,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAE1B,oBAAoB;QACpB,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,SAAS,SAAS,CAAC,KAA+B,EAAE,SAAmB;IACnE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,SAAS,GAAG,CAAC,IAAY;QACrB,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACtC,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAEnC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEhB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1B,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;gBACxB,IAAI,MAAM;oBAAE,OAAO,MAAM,CAAC;YAC9B,CAAC;QACL,CAAC;QAED,IAAI,CAAC,GAAG,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC5B,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,sBAAsB,CAAC,KAA+B;IAClE,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACjC,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC1B,6CAA6C;YAC7C,MAAM,CAAC,OAAO,CAAC,GAAG;gBACd,KAAK,EAAE,UAAU;gBACjB,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;aAC3C,CAAC;QACN,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
@@ -0,0 +1,127 @@
1
+ /*
2
+ * Browser-side script for architecture/dependencies.html (inlined into a <script>
3
+ * tag by graph-visualizer.ts, which replaces the __DOT__ placeholder with the
4
+ * JSON-encoded Graphviz DOT). Kept as a plain .js asset — NOT a TypeScript
5
+ * template literal — so the dim/highlight/lock logic can define ordinary browser
6
+ * functions without tripping the TypeScript lint rules that scan .ts template
7
+ * strings. Copied into dist by the build's assets glob and read via readFileSync.
8
+ *
9
+ * After viz.js renders the SVG, wireHoverHighlight indexes nodes/edges and:
10
+ * - hovering a box dims the rest and lights its full ancestor+descendant chain;
11
+ * - the #wp-lock dropdown LOCKS one box's chain (dim persists on mouse-leave)
12
+ * and filters the responsibilities cards below the graph to just that chain.
13
+ * "All" (empty value) clears the lock and shows every card.
14
+ */
15
+ (function () {
16
+ var dot = __DOT__;
17
+ var viz = new Viz();
18
+ viz.renderSVGElement(dot)
19
+ .then(function (element) {
20
+ document.getElementById('graph').appendChild(element);
21
+ wireHoverHighlight(element);
22
+ })
23
+ .catch(function (err) {
24
+ console.error(err);
25
+ document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';
26
+ });
27
+
28
+ function wireHoverHighlight(svg) {
29
+ var nodeByName = new Map();
30
+ svg.querySelectorAll('g.node').forEach(function (g) {
31
+ var t = g.querySelector('title');
32
+ if (t) nodeByName.set(t.textContent.trim(), g);
33
+ });
34
+ // Directed adjacency: in* = entering (up/ancestors), out* = leaving (down/deps).
35
+ var inEdges = new Map(), outEdges = new Map(), inNodes = new Map(), outNodes = new Map();
36
+ function ensure(map, key) {
37
+ var v = map.get(key);
38
+ if (!v) { v = new Set(); map.set(key, v); }
39
+ return v;
40
+ }
41
+ svg.querySelectorAll('g.edge').forEach(function (edge) {
42
+ var t = edge.querySelector('title');
43
+ if (!t) return;
44
+ var idx = t.textContent.indexOf('->');
45
+ if (idx < 0) return;
46
+ var from = t.textContent.slice(0, idx).trim();
47
+ var to = t.textContent.slice(idx + 2).trim();
48
+ ensure(outEdges, from).add(edge);
49
+ ensure(inEdges, to).add(edge);
50
+ ensure(outNodes, from).add(to);
51
+ ensure(inNodes, to).add(from);
52
+ });
53
+ function clear() {
54
+ svg.classList.remove('wp-dim');
55
+ svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl').forEach(function (el) {
56
+ el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');
57
+ });
58
+ }
59
+ function highlight(name, focusEl) {
60
+ clear();
61
+ svg.classList.add('wp-dim');
62
+ focusEl.classList.add('wp-focus');
63
+ // Transitively light ancestors (up) then descendants (down): edges
64
+ // reached -> wp-hl, boxes -> wp-neighbor. visited guards cycles.
65
+ [[inNodes, inEdges], [outNodes, outEdges]].forEach(function (dir) {
66
+ var visited = new Set();
67
+ var stack = [name];
68
+ while (stack.length) {
69
+ var cur = stack.pop();
70
+ (dir[1].get(cur) || []).forEach(function (e) { e.classList.add('wp-hl'); });
71
+ (dir[0].get(cur) || []).forEach(function (next) {
72
+ if (visited.has(next)) return;
73
+ visited.add(next);
74
+ stack.push(next);
75
+ var g = nodeByName.get(next);
76
+ if (g) g.classList.add('wp-neighbor');
77
+ });
78
+ }
79
+ });
80
+ }
81
+ // locked = the box the dropdown pinned (or null). Hover still works on top
82
+ // of a lock; leaving a box restores the locked view instead of clearing, so
83
+ // the pinned subgraph stays visible as you scroll to its responsibilities.
84
+ var locked = null;
85
+ function relight() {
86
+ if (locked) {
87
+ var lg = nodeByName.get(locked);
88
+ if (lg) highlight(locked, lg);
89
+ } else {
90
+ clear();
91
+ }
92
+ }
93
+ nodeByName.forEach(function (g, name) {
94
+ g.addEventListener('mouseenter', function () { highlight(name, g); });
95
+ g.addEventListener('mouseleave', relight);
96
+ });
97
+
98
+ // Filter the responsibilities cards to the locked box's chain by reading the
99
+ // .wp-focus/.wp-neighbor classes highlight() just set — no second graph walk.
100
+ var lockSelect = document.getElementById('wp-lock');
101
+ function filterCards() {
102
+ var lit = new Set();
103
+ if (locked) {
104
+ svg.querySelectorAll('.wp-focus, .wp-neighbor').forEach(function (el) {
105
+ var t = el.querySelector('title');
106
+ if (t) lit.add(t.textContent.trim());
107
+ });
108
+ }
109
+ document.querySelectorAll('.wp-resp-card').forEach(function (card) {
110
+ var nm = card.getAttribute('data-node');
111
+ card.classList.toggle('wp-hidden', !(!locked || lit.has(nm)));
112
+ });
113
+ }
114
+ if (lockSelect) {
115
+ lockSelect.addEventListener('change', function () {
116
+ locked = lockSelect.value || null;
117
+ if (locked) {
118
+ var g = nodeByName.get(locked);
119
+ if (g) highlight(locked, g);
120
+ } else {
121
+ clear();
122
+ }
123
+ filterCards();
124
+ });
125
+ }
126
+ }
127
+ })();
@@ -5,34 +5,94 @@
5
5
  * - DOT format (for Graphviz)
6
6
  * - Interactive HTML (using viz.js)
7
7
  *
8
- * Output files go to tmp/webpieces/ for easy viewing without committing.
8
+ * All behavior lives on the injectable GraphVisualizer class so webpieces DI +
9
+ * @DocumentDesign can wire it — module-scope functions are a dead end the DI
10
+ * graph can't reach.
9
11
  */
10
12
  import type { EnhancedGraph } from './graph-sorter';
11
- /**
12
- * Generate Graphviz DOT format from the graph
13
- */
14
- export declare function generateDot(graph: EnhancedGraph, title?: string): string;
15
- /**
16
- * Generate interactive HTML with embedded SVG using viz.js
17
- */
18
- export declare function generateHTML(dot: string, title?: string): string;
19
- interface VisualizationPaths {
13
+ export declare class VisualizationPaths {
20
14
  htmlPath: string;
15
+ constructor(htmlPath: string);
16
+ }
17
+ export declare class GraphVisualizer {
18
+ private readonly names;
19
+ private readonly responsibilities;
20
+ /**
21
+ * Fill color for an env set — the color of the first env in the set that has
22
+ * a known color, else the default.
23
+ */
24
+ private frameworkColor;
25
+ /**
26
+ * Role border styling — fill stays keyed on framework; the border shows a
27
+ * project's ROLE at a glance. Server and client are the top-level runnable
28
+ * nodes, so they get bold, colored borders to stand out:
29
+ * server → thick GREEN border (a runnable server app)
30
+ * client → thick RED border (a client app, e.g. angular)
31
+ * designed-lib → bold border (a library with a generated @DocumentDesign design)
32
+ * lib / other → plain thin border
33
+ */
34
+ private roleBorderAttrs;
35
+ /**
36
+ * Edge styling by API-relation kind (why the edge exists):
37
+ * implements → DASHED (a controller serves this api-lib's contract)
38
+ * uses → SOLID blue, thicker (a generated client calls it)
39
+ * uses-implements → DASHED purple, thicker (does both — implements some
40
+ * contracts of the api-lib, uses others)
41
+ * plain lib (none) → the default thin solid arrow, unchanged.
42
+ * `kind` is undefined for every non-api-lib dependency edge.
43
+ */
44
+ private edgeAttrs;
45
+ /**
46
+ * Click-through href for a node: the project's committed design.html, made
47
+ * relative to architecture/dependencies.html. Returns null when the project
48
+ * has no generated DI design (no design.json → no clickable design page).
49
+ */
50
+ private designHtmlHref;
51
+ /**
52
+ * Generate Graphviz DOT format from the graph
53
+ */
54
+ generateDot(graph: EnhancedGraph, title?: string): string;
55
+ private dotNodes;
56
+ private dotEdges;
57
+ /**
58
+ * Generate interactive HTML with embedded SVG using viz.js
59
+ */
60
+ generateHTML(dot: string, title?: string, lockControl?: string, responsibilitiesHtml?: string): string;
61
+ /**
62
+ * The lock control (a single-select dropdown, rendered above the legend).
63
+ * Picking a module LOCKS the graph into that box's hover view — its full
64
+ * ancestor + descendant chain stays lit while everything else stays dimmed —
65
+ * and narrows the responsibilities list below the graph to just that chain.
66
+ * The first option, "All", is the default and clears the lock. Hover still
67
+ * works on top of a lock; leaving a box returns to the locked view.
68
+ *
69
+ * Options are ordered by level DESCENDING to match the responsibilities cards.
70
+ */
71
+ lockControl(graph: EnhancedGraph): string;
72
+ private styles;
73
+ private componentStyles;
74
+ private legend;
75
+ /**
76
+ * The page script. The browser code lives in graph-visualizer.client.js (a
77
+ * plain .js asset, NOT a TS template literal) so its dim/highlight/lock
78
+ * functions can be ordinary browser functions — the TS lint rules that scan
79
+ * .ts template strings would otherwise forbid them, and browser JS cannot
80
+ * carry TS return annotations. We inline it and substitute the DOT.
81
+ */
82
+ private script;
83
+ /**
84
+ * Write the committed architecture visualization to
85
+ * architecture/dependencies.html, next to dependencies.json.
86
+ *
87
+ * This is a checked-in artifact, regenerated deterministically by
88
+ * architecture:generate so the boxes stay clickable into each project's
89
+ * design.html. The DOT is embedded in the HTML (rendered client-side by
90
+ * viz.js). Output is deterministic (sorted graph in → same bytes out) so git
91
+ * only shows a diff when the architecture actually changed.
92
+ */
93
+ writeVisualization(graph: EnhancedGraph, workspaceRoot: string, title?: string): VisualizationPaths;
94
+ /**
95
+ * Open the HTML visualization in the default browser
96
+ */
97
+ openVisualization(htmlPath: string): boolean;
21
98
  }
22
- /**
23
- * Write the committed architecture visualization to architecture/dependencies.html,
24
- * next to dependencies.json.
25
- *
26
- * This is a checked-in artifact, regenerated deterministically by
27
- * architecture:generate so the boxes stay clickable into each project's
28
- * design.html. The DOT is embedded in the HTML (rendered client-side by
29
- * viz.js), so no separate .dot file is committed — same as design.html. Output
30
- * is deterministic (sorted graph in → same bytes out) so git only shows a diff
31
- * when the architecture actually changed.
32
- */
33
- export declare function writeVisualization(graph: EnhancedGraph, workspaceRoot: string, title?: string): VisualizationPaths;
34
- /**
35
- * Open the HTML visualization in the default browser
36
- */
37
- export declare function openVisualization(htmlPath: string): boolean;
38
- export {};