@webpieces/nx-webpieces-rules 0.3.338 → 0.3.340

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.3.338",
3
+ "version": "0.3.340",
4
4
  "description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -21,11 +21,11 @@
21
21
  "README.md"
22
22
  ],
23
23
  "dependencies": {
24
- "@webpieces/ai-hook-rules": "0.3.338",
25
- "@webpieces/code-rules": "0.3.338",
26
- "@webpieces/eslint-rules": "0.3.338",
27
- "@webpieces/pr-gate": "0.3.338",
28
- "@webpieces/rules-config": "0.3.338",
24
+ "@webpieces/ai-hook-rules": "0.3.340",
25
+ "@webpieces/code-rules": "0.3.340",
26
+ "@webpieces/eslint-rules": "0.3.340",
27
+ "@webpieces/pr-gate": "0.3.340",
28
+ "@webpieces/rules-config": "0.3.340",
29
29
  "madge": "8.0.0"
30
30
  },
31
31
  "peerDependencies": {
@@ -41,6 +41,11 @@ function findUnclassifiedApiDeps(graph, projectInfos, scan) {
41
41
  const role = (0, role_resolver_1.resolveRole)(info).role;
42
42
  if (role === null || !CHECKED_ROLES.includes(role))
43
43
  continue;
44
+ // Only flag projects whose production source was actually scanned. An all-test project
45
+ // (e.g. an e2e harness whose only files are *.spec.ts) is never observed, so we can't
46
+ // conclude its api-lib dependency is unused.
47
+ if (!scan.scannedProjects.has(projectName))
48
+ continue;
44
49
  const entry = graph[projectName];
45
50
  for (const dep of entry.dependsOn) {
46
51
  if (!scan.apiLibProjects.has(dep))
@@ -1 +1 @@
1
- {"version":3,"file":"api-relations-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations-validator.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAkCH,0DAoBC;AAID,gEAgBC;AAtED,oDAA+C;AAG/C,8EAA8E;AAC9E,MAAM,aAAa,GAA0B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAWlE,6EAA6E;AAC7E,qGAAqG;AACrG,SAAS,WAAW,CAAC,IAAmB,EAAE,MAAc;IACpD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QACxC,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,+GAA+G;AAC/G,SAAgB,uBAAuB,CACnC,KAAoB,EACpB,YAAsC,EACtC,IAAmB;IAEnB,MAAM,UAAU,GAAyB,EAAE,CAAC;IAC5C,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,IAAI,GAAG,IAAA,2BAAW,EAAC,IAAI,CAAC,CAAC,IAAI,CAAC;QACpC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,SAAS;QAE7D,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;QACjC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5C,IAAI,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5D,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/F,CAAC;IACL,CAAC;IACD,OAAO,UAAU,CAAC;AACtB,CAAC;AAED,2EAA2E;AAC3E,iGAAiG;AACjG,SAAgB,0BAA0B,CAAC,SAA6B;IACpE,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClF,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC;IAC7C,MAAM,KAAK,GAAG;QACV,QAAQ,SAAS,CAAC,OAAO,WAAW,SAAS,CAAC,IAAI,yBAAyB,SAAS,CAAC,MAAM,IAAI;YAC3F,oDAAoD,OAAO,IAAI;QACnE,iBAAiB;QACjB,0FAA0F;YACtF,qEAAqE;YACrE,2BAA2B,MAAM,mCAAmC;YACpE,8CAA8C,MAAM,YAAY;QACpE,6DAA6D;YACzD,wBAAwB,MAAM,mBAAmB;QACrD,kDAAkD,SAAS,CAAC,MAAM,WAAW,SAAS,CAAC,OAAO,IAAI;KACrG,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * API Relations Validator\n *\n * Enforces that the dependency graph is TRUTHFUL: a runnable project (role server\n * or client) that compiles against an api-lib must actually IMPLEMENT (serve) or\n * USE (call) at least one of its APIs. A dependency that is neither is almost\n * always a mistake — a forgotten client wiring, a dead import, or a controller\n * that was never registered — and it would draw an unexplained edge in the graph.\n *\n * The check reuses the same source scan that produces `apiRelations`, so \"does P\n * relate to api-lib D\" is answered by real code, never a declaration.\n */\n\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult } from './api-scanner';\n\n/** Roles that must justify every api-lib dependency (top-level runnables). */\nconst CHECKED_ROLES: ReadonlyArray<string> = ['server', 'client'];\n\n/** One `role:server`/`role:client` project that depends on an api-lib it neither implements nor uses. */\nexport interface UnclassifiedApiDep {\n project: string;\n role: string;\n apiLib: string;\n /** The API contract class names that api-lib exports (for the fix hint). */\n apis: string[];\n}\n\n/** The API class names owned by `apiLib`, sorted (for a stable fix hint). */\n// webpieces-disable no-function-outside-class -- pure lookup helper, matches the validator-lib style\nfunction apisOwnedBy(scan: ApiScanResult, apiLib: string): string[] {\n const names: string[] = [];\n for (const info of scan.apiIndex.values()) {\n if (info.owner === apiLib) names.push(info.api);\n }\n return names.sort();\n}\n\n/**\n * Every server/client → api-lib edge for which the scan found NO implements and\n * NO uses. `graph` must already carry `apiRelations` (call scanAndAttachApiRelations first).\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors findUnclassified-style validators\nexport function findUnclassifiedApiDeps(\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n): UnclassifiedApiDep[] {\n const violations: UnclassifiedApiDep[] = [];\n for (const projectName of Object.keys(graph)) {\n const info = projectInfos.get(projectName);\n if (!info) continue;\n const role = resolveRole(info).role;\n if (role === null || !CHECKED_ROLES.includes(role)) continue;\n\n const entry = graph[projectName];\n for (const dep of entry.dependsOn) {\n if (!scan.apiLibProjects.has(dep)) continue;\n if (entry.apiRelations && entry.apiRelations[dep]) continue;\n violations.push({ project: projectName, role, apiLib: dep, apis: apisOwnedBy(scan, dep) });\n }\n }\n return violations;\n}\n\n/** Human-readable, fix-oriented report for one unclassified dependency. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style\nexport function describeUnclassifiedApiDep(violation: UnclassifiedApiDep): string {\n const apiHint = violation.apis.length > 0 ? violation.apis.join(', ') : 'the API';\n const theApi = violation.apis[0] ?? 'TheApi';\n const lines = [\n ` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' ` +\n `but neither IMPLEMENTS nor USES any of its APIs (${apiHint}).`,\n ` Do ONE of:`,\n ` 1. USE it as a client: inject ClientHttpFactory (@webpieces/http-client-node) or ` +\n `ClientHttpBrowserFactory (@webpieces/http-client-browser) and call ` +\n `factory.createRpcClient(${theApi}, config); for a @PubSub api use ` +\n `ClientCloudTasksFactory.createPubSubClient(${theApi}, config).`,\n ` 2. IMPLEMENT it: add a controller and register it — ` +\n `apiFactory.addRoutes(${theApi}, TheController).`,\n ` 3. If the dependency is unused, remove '${violation.apiLib}' from '${violation.project}'.`,\n ];\n return lines.join('\\n');\n}\n"]}
1
+ {"version":3,"file":"api-relations-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations-validator.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAkCH,0DAwBC;AAID,gEAgBC;AA1ED,oDAA+C;AAG/C,8EAA8E;AAC9E,MAAM,aAAa,GAA0B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAWlE,6EAA6E;AAC7E,qGAAqG;AACrG,SAAS,WAAW,CAAC,IAAmB,EAAE,MAAc;IACpD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QACxC,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,+GAA+G;AAC/G,SAAgB,uBAAuB,CACnC,KAAoB,EACpB,YAAsC,EACtC,IAAmB;IAEnB,MAAM,UAAU,GAAyB,EAAE,CAAC;IAC5C,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,IAAI,GAAG,IAAA,2BAAW,EAAC,IAAI,CAAC,CAAC,IAAI,CAAC;QACpC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7D,uFAAuF;QACvF,sFAAsF;QACtF,6CAA6C;QAC7C,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,SAAS;QAErD,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;QACjC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5C,IAAI,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5D,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/F,CAAC;IACL,CAAC;IACD,OAAO,UAAU,CAAC;AACtB,CAAC;AAED,2EAA2E;AAC3E,iGAAiG;AACjG,SAAgB,0BAA0B,CAAC,SAA6B;IACpE,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClF,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC;IAC7C,MAAM,KAAK,GAAG;QACV,QAAQ,SAAS,CAAC,OAAO,WAAW,SAAS,CAAC,IAAI,yBAAyB,SAAS,CAAC,MAAM,IAAI;YAC3F,oDAAoD,OAAO,IAAI;QACnE,iBAAiB;QACjB,0FAA0F;YACtF,qEAAqE;YACrE,2BAA2B,MAAM,mCAAmC;YACpE,8CAA8C,MAAM,YAAY;QACpE,6DAA6D;YACzD,wBAAwB,MAAM,mBAAmB;QACrD,kDAAkD,SAAS,CAAC,MAAM,WAAW,SAAS,CAAC,OAAO,IAAI;KACrG,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * API Relations Validator\n *\n * Enforces that the dependency graph is TRUTHFUL: a runnable project (role server\n * or client) that compiles against an api-lib must actually IMPLEMENT (serve) or\n * USE (call) at least one of its APIs. A dependency that is neither is almost\n * always a mistake — a forgotten client wiring, a dead import, or a controller\n * that was never registered — and it would draw an unexplained edge in the graph.\n *\n * The check reuses the same source scan that produces `apiRelations`, so \"does P\n * relate to api-lib D\" is answered by real code, never a declaration.\n */\n\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult } from './api-scanner';\n\n/** Roles that must justify every api-lib dependency (top-level runnables). */\nconst CHECKED_ROLES: ReadonlyArray<string> = ['server', 'client'];\n\n/** One `role:server`/`role:client` project that depends on an api-lib it neither implements nor uses. */\nexport interface UnclassifiedApiDep {\n project: string;\n role: string;\n apiLib: string;\n /** The API contract class names that api-lib exports (for the fix hint). */\n apis: string[];\n}\n\n/** The API class names owned by `apiLib`, sorted (for a stable fix hint). */\n// webpieces-disable no-function-outside-class -- pure lookup helper, matches the validator-lib style\nfunction apisOwnedBy(scan: ApiScanResult, apiLib: string): string[] {\n const names: string[] = [];\n for (const info of scan.apiIndex.values()) {\n if (info.owner === apiLib) names.push(info.api);\n }\n return names.sort();\n}\n\n/**\n * Every server/client → api-lib edge for which the scan found NO implements and\n * NO uses. `graph` must already carry `apiRelations` (call scanAndAttachApiRelations first).\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors findUnclassified-style validators\nexport function findUnclassifiedApiDeps(\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n): UnclassifiedApiDep[] {\n const violations: UnclassifiedApiDep[] = [];\n for (const projectName of Object.keys(graph)) {\n const info = projectInfos.get(projectName);\n if (!info) continue;\n const role = resolveRole(info).role;\n if (role === null || !CHECKED_ROLES.includes(role)) continue;\n // Only flag projects whose production source was actually scanned. An all-test project\n // (e.g. an e2e harness whose only files are *.spec.ts) is never observed, so we can't\n // conclude its api-lib dependency is unused.\n if (!scan.scannedProjects.has(projectName)) continue;\n\n const entry = graph[projectName];\n for (const dep of entry.dependsOn) {\n if (!scan.apiLibProjects.has(dep)) continue;\n if (entry.apiRelations && entry.apiRelations[dep]) continue;\n violations.push({ project: projectName, role, apiLib: dep, apis: apisOwnedBy(scan, dep) });\n }\n }\n return violations;\n}\n\n/** Human-readable, fix-oriented report for one unclassified dependency. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style\nexport function describeUnclassifiedApiDep(violation: UnclassifiedApiDep): string {\n const apiHint = violation.apis.length > 0 ? violation.apis.join(', ') : 'the API';\n const theApi = violation.apis[0] ?? 'TheApi';\n const lines = [\n ` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' ` +\n `but neither IMPLEMENTS nor USES any of its APIs (${apiHint}).`,\n ` Do ONE of:`,\n ` 1. USE it as a client: inject ClientHttpFactory (@webpieces/http-client-node) or ` +\n `ClientHttpBrowserFactory (@webpieces/http-client-browser) and call ` +\n `factory.createRpcClient(${theApi}, config); for a @PubSub api use ` +\n `ClientCloudTasksFactory.createPubSubClient(${theApi}, config).`,\n ` 2. IMPLEMENT it: add a controller and register it — ` +\n `apiFactory.addRoutes(${theApi}, TheController).`,\n ` 3. If the dependency is unused, remove '${violation.apiLib}' from '${violation.project}'.`,\n ];\n return lines.join('\\n');\n}\n"]}
@@ -28,6 +28,12 @@ export interface ApiScanResult {
28
28
  apiLibProjects: Set<string>;
29
29
  /** apiClassName -> where it lives + its transport. */
30
30
  apiIndex: Map<string, ApiClassInfo>;
31
+ /**
32
+ * Projects whose production (non-test) source was actually scanned. A project with only test
33
+ * files (e.g. an e2e harness), or one the compiler couldn't load, is ABSENT — callers must not
34
+ * conclude "no implements/uses" for it, because its behavior was never observed.
35
+ */
36
+ scannedProjects: Set<string>;
31
37
  }
32
38
  /** Statically scans every project for its api-lib implements/uses relationships. */
33
39
  export declare class ApiUsageScanner {
@@ -37,6 +43,7 @@ export declare class ApiUsageScanner {
37
43
  private readonly apiLibProjects;
38
44
  private readonly apiIndex;
39
45
  private readonly relationsByProject;
46
+ private readonly scannedProjects;
40
47
  constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>);
41
48
  scan(): ApiScanResult;
42
49
  private scanProject;
@@ -23,6 +23,7 @@ exports.ApiUsageScanner = void 0;
23
23
  exports.scanAndAttachApiRelations = scanAndAttachApiRelations;
24
24
  const tslib_1 = require("tslib");
25
25
  const ts = tslib_1.__importStar(require("typescript"));
26
+ const fs = tslib_1.__importStar(require("fs"));
26
27
  const path = tslib_1.__importStar(require("path"));
27
28
  const program_1 = require("../di-graph/program");
28
29
  const bindings_1 = require("../di-graph/bindings");
@@ -107,6 +108,7 @@ class ApiUsageScanner {
107
108
  apiLibProjects = new Set();
108
109
  apiIndex = new Map();
109
110
  relationsByProject = new Map();
111
+ scannedProjects = new Set();
110
112
  constructor(workspaceRoot, projectInfos) {
111
113
  this.workspaceRoot = workspaceRoot;
112
114
  this.projectInfos = projectInfos;
@@ -122,14 +124,16 @@ class ApiUsageScanner {
122
124
  relationsByProject: this.relationsByProject,
123
125
  apiLibProjects: this.apiLibProjects,
124
126
  apiIndex: this.apiIndex,
127
+ scannedProjects: this.scannedProjects,
125
128
  };
126
129
  }
127
130
  scanProject(info) {
128
- const program = (0, program_1.createProjectProgram)(path.resolve(this.workspaceRoot, info.root));
131
+ const program = createScanProgram(path.resolve(this.workspaceRoot, info.root));
129
132
  if (!program)
130
133
  return;
131
134
  const checker = program.getTypeChecker();
132
135
  const accumulator = new RelationAccumulator();
136
+ let scannedProductionFile = false;
133
137
  for (const sourceFile of program.getSourceFiles()) {
134
138
  if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/'))
135
139
  continue;
@@ -138,8 +142,13 @@ class ApiUsageScanner {
138
142
  // Only this project's OWN files — imported api-lib source is in the program too.
139
143
  if (this.locator.projectOf(sourceFile.fileName) !== info.name)
140
144
  continue;
145
+ scannedProductionFile = true;
141
146
  this.visit(sourceFile, checker, info.name, accumulator);
142
147
  }
148
+ // Record coverage only when we actually saw production source — an all-test project (e2e)
149
+ // stays absent so the validator won't wrongly flag its api-lib deps as unused.
150
+ if (scannedProductionFile)
151
+ this.scannedProjects.add(info.name);
143
152
  if (!accumulator.isEmpty())
144
153
  this.relationsByProject.set(info.name, accumulator.toRelations());
145
154
  }
@@ -213,6 +222,50 @@ function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos) {
213
222
  }
214
223
  return result;
215
224
  }
225
+ /**
226
+ * Build a program for scanning ONE project. Prefers the project's compile tsconfig; but when that
227
+ * is a solution-style tsconfig (only `references`, no `files`/`include` — e.g. legacy-server), it
228
+ * yields zero files, so we fall back to globbing the project's own `src/**` and reuse the resolved
229
+ * compiler options (which carry tsconfig.base `paths` for cross-package @webpieces resolution).
230
+ */
231
+ // webpieces-disable no-function-outside-class -- ts Program factory, mirrors di-graph/program.ts
232
+ function createScanProgram(projectRootAbs) {
233
+ const configPath = (0, program_1.findProjectTsconfig)(projectRootAbs);
234
+ if (!configPath)
235
+ return buildProgramFromSrc(projectRootAbs, {});
236
+ const host = Object.assign({}, ts.sys, {
237
+ onUnRecoverableConfigFileDiagnostic: () => undefined,
238
+ });
239
+ const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host);
240
+ if (!parsed)
241
+ return null;
242
+ if (parsed.fileNames.length > 0)
243
+ return ts.createProgram(parsed.fileNames, parsed.options);
244
+ return buildProgramFromSrc(projectRootAbs, parsed.options);
245
+ }
246
+ // webpieces-disable no-function-outside-class -- ts Program factory helper, mirrors di-graph/program.ts
247
+ function buildProgramFromSrc(projectRootAbs, options) {
248
+ const srcDir = path.join(projectRootAbs, 'src');
249
+ if (!fs.existsSync(srcDir))
250
+ return null;
251
+ const files = collectTsFiles(srcDir);
252
+ return files.length > 0 ? ts.createProgram(files, options) : null;
253
+ }
254
+ // webpieces-disable no-function-outside-class -- recursive fs walker, matching the AST-helper style here
255
+ function collectTsFiles(dir) {
256
+ const out = [];
257
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
258
+ const full = path.join(dir, entry.name);
259
+ if (entry.isDirectory()) {
260
+ if (entry.name !== 'node_modules')
261
+ out.push(...collectTsFiles(full));
262
+ }
263
+ else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
264
+ out.push(full);
265
+ }
266
+ }
267
+ return out;
268
+ }
216
269
  // webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
217
270
  function isAbstractClass(cls) {
218
271
  return (ts.getModifiers(cls) ?? []).some((m) => m.kind === ts.SyntaxKind.AbstractKeyword);
@@ -1 +1 @@
1
- {"version":3,"file":"api-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Usage Scanner\n *\n * Derives, by scanning real source (not a declaration file), how every project\n * relates to the api-lib projects it depends on. This is the single source of\n * truth for the `apiRelations` field in architecture/dependencies.json AND for\n * the runtime microservice graph.\n *\n * Signals (all resolved through the TypeScript checker, so re-exports resolve):\n * - IMPLEMENTS: `apiFactory.addRoutes(XxxApi, XxxController)` — the registration\n * that actually SERVES the contract over the wire. We deliberately\n * do NOT use `class Ctrl extends XxxApi`: a class can extend an API\n * as an in-process test double / simulator (e.g. Server2Simulator)\n * without ever serving it — only `addRoutes` proves a served route.\n * - USES: `factory.createRpcClient(XxxApi, ...)` → rpc client\n * `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client\n * An api-lib is DETECTED, not tagged: a project exporting an `abstract class`\n * carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.\n */\n\nimport * as ts from 'typescript';\nimport * as path from 'path';\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { createProjectProgram } from '../di-graph/program';\nimport { resolveClassDeclaration, classDecorators, decoratorName } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiRef,\n ApiRelation,\n ProjectApiRelations,\n deriveApiRelationKind,\n sortApiRefs,\n} from './api-relations';\n\nconst RPC_CLIENT_METHOD = 'createRpcClient';\nconst PUBSUB_CLIENT_METHOD = 'createPubSubClient';\nconst ADD_ROUTES_METHOD = 'addRoutes';\n\n/** The whole-workspace result of a scan. */\nexport interface ApiScanResult {\n /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */\n relationsByProject: Map<string, ProjectApiRelations>;\n /** Every project that owns ≥1 API contract class. */\n apiLibProjects: Set<string>;\n /** apiClassName -> where it lives + its transport. */\n apiIndex: Map<string, ApiClassInfo>;\n}\n\n/** Maps an absolute source-file path to the workspace project that owns it (longest-root-prefix). */\nclass ProjectLocator {\n private readonly roots: ProjectRoot[];\n\n constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>) {\n const roots: ProjectRoot[] = [];\n for (const info of projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n roots.push(new ProjectRoot(info.name, path.resolve(workspaceRoot, info.root)));\n }\n // Longest root first so a nested project wins over its parent.\n this.roots = roots.sort((a: ProjectRoot, b: ProjectRoot) => b.abs.length - a.abs.length);\n }\n\n projectOf(absFile: string): string | null {\n const normalized = path.resolve(absFile);\n for (const root of this.roots) {\n if (normalized === root.abs || normalized.startsWith(root.abs + path.sep)) return root.name;\n }\n return null;\n }\n}\n\nclass ProjectRoot {\n constructor(\n public readonly name: string,\n public readonly abs: string,\n ) {}\n}\n\n/** Per-owner accumulator that dedupes API refs while a single project is scanned. */\nclass RelationAccumulator {\n private readonly implementsByOwner = new Map<string, Map<string, ApiRef>>();\n private readonly usesByOwner = new Map<string, Map<string, ApiRef>>();\n\n addImplements(owner: string, ref: ApiRef): void {\n ensureRefMap(this.implementsByOwner, owner).set(ref.api, ref);\n }\n\n addUses(owner: string, ref: ApiRef): void {\n ensureRefMap(this.usesByOwner, owner).set(ref.api, ref);\n }\n\n /** Build the deterministic { owner -> relation } record, owners in sorted order. */\n toRelations(): ProjectApiRelations {\n const owners = new Set<string>([...this.implementsByOwner.keys(), ...this.usesByOwner.keys()]);\n const relations: ProjectApiRelations = {};\n for (const owner of [...owners].sort()) {\n const implementsRefs = sortApiRefs([...(this.implementsByOwner.get(owner)?.values() ?? [])]);\n const usesRefs = sortApiRefs([...(this.usesByOwner.get(owner)?.values() ?? [])]);\n const relation: ApiRelation = {\n kind: deriveApiRelationKind(implementsRefs, usesRefs),\n implements: implementsRefs,\n uses: usesRefs,\n };\n relations[owner] = relation;\n }\n return relations;\n }\n\n isEmpty(): boolean {\n return this.implementsByOwner.size === 0 && this.usesByOwner.size === 0;\n }\n}\n\n// webpieces-disable no-function-outside-class -- tiny map helper, matching the AST-helper style of di-graph/bindings.ts\nfunction ensureRefMap(map: Map<string, Map<string, ApiRef>>, owner: string): Map<string, ApiRef> {\n let inner = map.get(owner);\n if (!inner) {\n inner = new Map<string, ApiRef>();\n map.set(owner, inner);\n }\n return inner;\n}\n\n/** Statically scans every project for its api-lib implements/uses relationships. */\nexport class ApiUsageScanner {\n private readonly locator: ProjectLocator;\n private readonly apiLibProjects = new Set<string>();\n private readonly apiIndex = new Map<string, ApiClassInfo>();\n private readonly relationsByProject = new Map<string, ProjectApiRelations>();\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n ) {\n this.locator = new ProjectLocator(workspaceRoot, projectInfos);\n }\n\n scan(): ApiScanResult {\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.scanProject(info);\n }\n return {\n relationsByProject: this.relationsByProject,\n apiLibProjects: this.apiLibProjects,\n apiIndex: this.apiIndex,\n };\n }\n\n private scanProject(info: ProjectInfo): void {\n const program = createProjectProgram(path.resolve(this.workspaceRoot, info.root));\n if (!program) return;\n const checker = program.getTypeChecker();\n const accumulator = new RelationAccumulator();\n\n for (const sourceFile of program.getSourceFiles()) {\n if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/')) continue;\n if (isTestFile(sourceFile.fileName)) continue; // tests are not production topology\n // Only this project's OWN files — imported api-lib source is in the program too.\n if (this.locator.projectOf(sourceFile.fileName) !== info.name) continue;\n this.visit(sourceFile, checker, info.name, accumulator);\n }\n\n if (!accumulator.isEmpty()) this.relationsByProject.set(info.name, accumulator.toRelations());\n }\n\n private visit(node: ts.Node, checker: ts.TypeChecker, project: string, acc: RelationAccumulator): void {\n if (ts.isClassDeclaration(node)) {\n this.recordApiClass(node, project);\n } else if (ts.isCallExpression(node)) {\n this.recordCall(node, checker, acc);\n }\n ts.forEachChild(node, (child: ts.Node) => this.visit(child, checker, project, acc));\n }\n\n /** Register a project-owned API contract (abstract @ApiPath class) into the index. */\n private recordApiClass(cls: ts.ClassDeclaration, project: string): void {\n const own = this.apiClassInfoFor(cls);\n if (!own) return;\n this.apiLibProjects.add(project);\n this.apiIndex.set(own.api, own);\n }\n\n private recordCall(call: ts.CallExpression, checker: ts.TypeChecker, acc: RelationAccumulator): void {\n const method = calleeMethodName(call);\n if (method === null || call.arguments.length === 0) return;\n if (method === ADD_ROUTES_METHOD) {\n this.addImplementsFromExpr(call.arguments[0], checker, acc);\n return;\n }\n if (method === RPC_CLIENT_METHOD || method === PUBSUB_CLIENT_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker);\n if (info) acc.addUses(info.owner, { api: info.api, type: info.type });\n }\n }\n\n private addImplementsFromExpr(expr: ts.Expression, checker: ts.TypeChecker, acc: RelationAccumulator): void {\n const info = this.apiInfoFromExpr(expr, checker);\n if (info) acc.addImplements(info.owner, { api: info.api, type: info.type });\n }\n\n /** Resolve an expression to the API contract it names, or null if it is not one. */\n private apiInfoFromExpr(expr: ts.Expression, checker: ts.TypeChecker): ApiClassInfo | null {\n const decl = resolveClassDeclaration(expr, checker);\n return decl ? this.apiClassInfoFor(decl) : null;\n }\n\n /** {api, owner, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */\n private apiClassInfoFor(cls: ts.ClassDeclaration): ApiClassInfo | null {\n if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name) return null;\n const owner = this.locator.projectOf(cls.getSourceFile().fileName);\n if (owner === null) return null;\n const type = hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';\n return { api: cls.name.text, owner, type };\n }\n}\n\n/**\n * Run the scan and attach the derived `apiRelations` onto each graph entry in\n * place. Shared by `architecture:generate` (which then saves) and\n * `architecture:validate-architecture-unchanged` (which regenerates in memory\n * and must attach the SAME field, or it would see a phantom diff). Returns the\n * full scan so callers (validators, runtime graph) can reuse the api index.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings\nexport function scanAndAttachApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ApiScanResult {\n const result = new ApiUsageScanner(workspaceRoot, projectInfos).scan();\n for (const projectName of result.relationsByProject.keys()) {\n const entry = graph[projectName];\n if (entry) entry.apiRelations = result.relationsByProject.get(projectName);\n }\n return result;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction isAbstractClass(cls: ts.ClassDeclaration): boolean {\n return (ts.getModifiers(cls) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AbstractKeyword);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean {\n return classDecorators(cls).some((d: ts.Decorator) => decoratorName(d) === name);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nfunction calleeMethodName(call: ts.CallExpression): string | null {\n const callee = call.expression;\n if (ts.isPropertyAccessExpression(callee)) return callee.name.text;\n if (ts.isIdentifier(callee)) return callee.text;\n return null;\n}\n\n// webpieces-disable no-function-outside-class -- pure path predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction isTestFile(fileName: string): boolean {\n return (\n fileName.includes('/__tests__/') ||\n fileName.includes('.spec.') ||\n fileName.includes('.test.')\n );\n}\n"]}
1
+ {"version":3,"file":"api-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Usage Scanner\n *\n * Derives, by scanning real source (not a declaration file), how every project\n * relates to the api-lib projects it depends on. This is the single source of\n * truth for the `apiRelations` field in architecture/dependencies.json AND for\n * the runtime microservice graph.\n *\n * Signals (all resolved through the TypeScript checker, so re-exports resolve):\n * - IMPLEMENTS: `apiFactory.addRoutes(XxxApi, XxxController)` — the registration\n * that actually SERVES the contract over the wire. We deliberately\n * do NOT use `class Ctrl extends XxxApi`: a class can extend an API\n * as an in-process test double / simulator (e.g. Server2Simulator)\n * without ever serving it — only `addRoutes` proves a served route.\n * - USES: `factory.createRpcClient(XxxApi, ...)` → rpc client\n * `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client\n * An api-lib is DETECTED, not tagged: a project exporting an `abstract class`\n * carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.\n */\n\nimport * as ts from 'typescript';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { findProjectTsconfig } from '../di-graph/program';\nimport { resolveClassDeclaration, classDecorators, decoratorName } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiRef,\n ApiRelation,\n ProjectApiRelations,\n deriveApiRelationKind,\n sortApiRefs,\n} from './api-relations';\n\nconst RPC_CLIENT_METHOD = 'createRpcClient';\nconst PUBSUB_CLIENT_METHOD = 'createPubSubClient';\nconst ADD_ROUTES_METHOD = 'addRoutes';\n\n/** The whole-workspace result of a scan. */\nexport interface ApiScanResult {\n /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */\n relationsByProject: Map<string, ProjectApiRelations>;\n /** Every project that owns ≥1 API contract class. */\n apiLibProjects: Set<string>;\n /** apiClassName -> where it lives + its transport. */\n apiIndex: Map<string, ApiClassInfo>;\n /**\n * Projects whose production (non-test) source was actually scanned. A project with only test\n * files (e.g. an e2e harness), or one the compiler couldn't load, is ABSENT — callers must not\n * conclude \"no implements/uses\" for it, because its behavior was never observed.\n */\n scannedProjects: Set<string>;\n}\n\n/** Maps an absolute source-file path to the workspace project that owns it (longest-root-prefix). */\nclass ProjectLocator {\n private readonly roots: ProjectRoot[];\n\n constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>) {\n const roots: ProjectRoot[] = [];\n for (const info of projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n roots.push(new ProjectRoot(info.name, path.resolve(workspaceRoot, info.root)));\n }\n // Longest root first so a nested project wins over its parent.\n this.roots = roots.sort((a: ProjectRoot, b: ProjectRoot) => b.abs.length - a.abs.length);\n }\n\n projectOf(absFile: string): string | null {\n const normalized = path.resolve(absFile);\n for (const root of this.roots) {\n if (normalized === root.abs || normalized.startsWith(root.abs + path.sep)) return root.name;\n }\n return null;\n }\n}\n\nclass ProjectRoot {\n constructor(\n public readonly name: string,\n public readonly abs: string,\n ) {}\n}\n\n/** Per-owner accumulator that dedupes API refs while a single project is scanned. */\nclass RelationAccumulator {\n private readonly implementsByOwner = new Map<string, Map<string, ApiRef>>();\n private readonly usesByOwner = new Map<string, Map<string, ApiRef>>();\n\n addImplements(owner: string, ref: ApiRef): void {\n ensureRefMap(this.implementsByOwner, owner).set(ref.api, ref);\n }\n\n addUses(owner: string, ref: ApiRef): void {\n ensureRefMap(this.usesByOwner, owner).set(ref.api, ref);\n }\n\n /** Build the deterministic { owner -> relation } record, owners in sorted order. */\n toRelations(): ProjectApiRelations {\n const owners = new Set<string>([...this.implementsByOwner.keys(), ...this.usesByOwner.keys()]);\n const relations: ProjectApiRelations = {};\n for (const owner of [...owners].sort()) {\n const implementsRefs = sortApiRefs([...(this.implementsByOwner.get(owner)?.values() ?? [])]);\n const usesRefs = sortApiRefs([...(this.usesByOwner.get(owner)?.values() ?? [])]);\n const relation: ApiRelation = {\n kind: deriveApiRelationKind(implementsRefs, usesRefs),\n implements: implementsRefs,\n uses: usesRefs,\n };\n relations[owner] = relation;\n }\n return relations;\n }\n\n isEmpty(): boolean {\n return this.implementsByOwner.size === 0 && this.usesByOwner.size === 0;\n }\n}\n\n// webpieces-disable no-function-outside-class -- tiny map helper, matching the AST-helper style of di-graph/bindings.ts\nfunction ensureRefMap(map: Map<string, Map<string, ApiRef>>, owner: string): Map<string, ApiRef> {\n let inner = map.get(owner);\n if (!inner) {\n inner = new Map<string, ApiRef>();\n map.set(owner, inner);\n }\n return inner;\n}\n\n/** Statically scans every project for its api-lib implements/uses relationships. */\nexport class ApiUsageScanner {\n private readonly locator: ProjectLocator;\n private readonly apiLibProjects = new Set<string>();\n private readonly apiIndex = new Map<string, ApiClassInfo>();\n private readonly relationsByProject = new Map<string, ProjectApiRelations>();\n private readonly scannedProjects = new Set<string>();\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n ) {\n this.locator = new ProjectLocator(workspaceRoot, projectInfos);\n }\n\n scan(): ApiScanResult {\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.scanProject(info);\n }\n return {\n relationsByProject: this.relationsByProject,\n apiLibProjects: this.apiLibProjects,\n apiIndex: this.apiIndex,\n scannedProjects: this.scannedProjects,\n };\n }\n\n private scanProject(info: ProjectInfo): void {\n const program = createScanProgram(path.resolve(this.workspaceRoot, info.root));\n if (!program) return;\n const checker = program.getTypeChecker();\n const accumulator = new RelationAccumulator();\n let scannedProductionFile = false;\n\n for (const sourceFile of program.getSourceFiles()) {\n if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/')) continue;\n if (isTestFile(sourceFile.fileName)) continue; // tests are not production topology\n // Only this project's OWN files — imported api-lib source is in the program too.\n if (this.locator.projectOf(sourceFile.fileName) !== info.name) continue;\n scannedProductionFile = true;\n this.visit(sourceFile, checker, info.name, accumulator);\n }\n\n // Record coverage only when we actually saw production source — an all-test project (e2e)\n // stays absent so the validator won't wrongly flag its api-lib deps as unused.\n if (scannedProductionFile) this.scannedProjects.add(info.name);\n if (!accumulator.isEmpty()) this.relationsByProject.set(info.name, accumulator.toRelations());\n }\n\n private visit(node: ts.Node, checker: ts.TypeChecker, project: string, acc: RelationAccumulator): void {\n if (ts.isClassDeclaration(node)) {\n this.recordApiClass(node, project);\n } else if (ts.isCallExpression(node)) {\n this.recordCall(node, checker, acc);\n }\n ts.forEachChild(node, (child: ts.Node) => this.visit(child, checker, project, acc));\n }\n\n /** Register a project-owned API contract (abstract @ApiPath class) into the index. */\n private recordApiClass(cls: ts.ClassDeclaration, project: string): void {\n const own = this.apiClassInfoFor(cls);\n if (!own) return;\n this.apiLibProjects.add(project);\n this.apiIndex.set(own.api, own);\n }\n\n private recordCall(call: ts.CallExpression, checker: ts.TypeChecker, acc: RelationAccumulator): void {\n const method = calleeMethodName(call);\n if (method === null || call.arguments.length === 0) return;\n if (method === ADD_ROUTES_METHOD) {\n this.addImplementsFromExpr(call.arguments[0], checker, acc);\n return;\n }\n if (method === RPC_CLIENT_METHOD || method === PUBSUB_CLIENT_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker);\n if (info) acc.addUses(info.owner, { api: info.api, type: info.type });\n }\n }\n\n private addImplementsFromExpr(expr: ts.Expression, checker: ts.TypeChecker, acc: RelationAccumulator): void {\n const info = this.apiInfoFromExpr(expr, checker);\n if (info) acc.addImplements(info.owner, { api: info.api, type: info.type });\n }\n\n /** Resolve an expression to the API contract it names, or null if it is not one. */\n private apiInfoFromExpr(expr: ts.Expression, checker: ts.TypeChecker): ApiClassInfo | null {\n const decl = resolveClassDeclaration(expr, checker);\n return decl ? this.apiClassInfoFor(decl) : null;\n }\n\n /** {api, owner, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */\n private apiClassInfoFor(cls: ts.ClassDeclaration): ApiClassInfo | null {\n if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name) return null;\n const owner = this.locator.projectOf(cls.getSourceFile().fileName);\n if (owner === null) return null;\n const type = hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';\n return { api: cls.name.text, owner, type };\n }\n}\n\n/**\n * Run the scan and attach the derived `apiRelations` onto each graph entry in\n * place. Shared by `architecture:generate` (which then saves) and\n * `architecture:validate-architecture-unchanged` (which regenerates in memory\n * and must attach the SAME field, or it would see a phantom diff). Returns the\n * full scan so callers (validators, runtime graph) can reuse the api index.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings\nexport function scanAndAttachApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ApiScanResult {\n const result = new ApiUsageScanner(workspaceRoot, projectInfos).scan();\n for (const projectName of result.relationsByProject.keys()) {\n const entry = graph[projectName];\n if (entry) entry.apiRelations = result.relationsByProject.get(projectName);\n }\n return result;\n}\n\n/**\n * Build a program for scanning ONE project. Prefers the project's compile tsconfig; but when that\n * is a solution-style tsconfig (only `references`, no `files`/`include` — e.g. legacy-server), it\n * yields zero files, so we fall back to globbing the project's own `src/**` and reuse the resolved\n * compiler options (which carry tsconfig.base `paths` for cross-package @webpieces resolution).\n */\n// webpieces-disable no-function-outside-class -- ts Program factory, mirrors di-graph/program.ts\nfunction createScanProgram(projectRootAbs: string): ts.Program | null {\n const configPath = findProjectTsconfig(projectRootAbs);\n if (!configPath) return buildProgramFromSrc(projectRootAbs, {});\n const host = Object.assign({}, ts.sys, {\n onUnRecoverableConfigFileDiagnostic: (): void => undefined,\n }) as ts.ParseConfigFileHost;\n const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host);\n if (!parsed) return null;\n if (parsed.fileNames.length > 0) return ts.createProgram(parsed.fileNames, parsed.options);\n return buildProgramFromSrc(projectRootAbs, parsed.options);\n}\n\n// webpieces-disable no-function-outside-class -- ts Program factory helper, mirrors di-graph/program.ts\nfunction buildProgramFromSrc(projectRootAbs: string, options: ts.CompilerOptions): ts.Program | null {\n const srcDir = path.join(projectRootAbs, 'src');\n if (!fs.existsSync(srcDir)) return null;\n const files = collectTsFiles(srcDir);\n return files.length > 0 ? ts.createProgram(files, options) : null;\n}\n\n// webpieces-disable no-function-outside-class -- recursive fs walker, matching the AST-helper style here\nfunction collectTsFiles(dir: string): string[] {\n const out: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (entry.name !== 'node_modules') out.push(...collectTsFiles(full));\n } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {\n out.push(full);\n }\n }\n return out;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction isAbstractClass(cls: ts.ClassDeclaration): boolean {\n return (ts.getModifiers(cls) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AbstractKeyword);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean {\n return classDecorators(cls).some((d: ts.Decorator) => decoratorName(d) === name);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nfunction calleeMethodName(call: ts.CallExpression): string | null {\n const callee = call.expression;\n if (ts.isPropertyAccessExpression(callee)) return callee.name.text;\n if (ts.isIdentifier(callee)) return callee.text;\n return null;\n}\n\n// webpieces-disable no-function-outside-class -- pure path predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction isTestFile(fileName: string): boolean {\n return (\n fileName.includes('/__tests__/') ||\n fileName.includes('.spec.') ||\n fileName.includes('.test.')\n );\n}\n"]}
@@ -59,7 +59,7 @@ export declare class GraphVisualizer {
59
59
  */
60
60
  generateHTML(dot: string, title?: string, lockControl?: string, responsibilitiesHtml?: string): string;
61
61
  /**
62
- * The lock control (a single-select dropdown, rendered above the legend).
62
+ * The lock control (a single-select dropdown, rendered below the legend).
63
63
  * Picking a module LOCKS the graph into that box's hover view — its full
64
64
  * ancestor + descendant chain stays lit while everything else stays dimmed —
65
65
  * and narrows the responsibilities list below the graph to just that chain.
@@ -72,6 +72,9 @@ export declare class GraphVisualizer {
72
72
  private styles;
73
73
  private componentStyles;
74
74
  private legend;
75
+ private fillItems;
76
+ private borderItems;
77
+ private edgeItems;
75
78
  /**
76
79
  * The page script. The browser code lives in graph-visualizer.client.js (a
77
80
  * plain .js asset, NOT a TS template literal) so its dim/highlight/lock
@@ -199,8 +199,8 @@ class GraphVisualizer {
199
199
  <h1>${title}</h1>
200
200
  <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>
201
201
  <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>
202
- ${lockControl}
203
202
  ${legend}
203
+ ${lockControl}
204
204
  <div id="graph"></div>
205
205
  ${responsibilitiesHtml}
206
206
  <script>${script}</script>
@@ -208,7 +208,7 @@ class GraphVisualizer {
208
208
  </html>`;
209
209
  }
210
210
  /**
211
- * The lock control (a single-select dropdown, rendered above the legend).
211
+ * The lock control (a single-select dropdown, rendered below the legend).
212
212
  * Picking a module LOCKS the graph into that box's hover view — its full
213
213
  * ancestor + descendant chain stays lit while everything else stays dimmed —
214
214
  * and narrows the responsibilities list below the graph to just that chain.
@@ -276,7 +276,7 @@ class GraphVisualizer {
276
276
  }
277
277
  .legend {
278
278
  margin: 20px auto;
279
- max-width: 600px;
279
+ max-width: 1100px;
280
280
  padding: 15px;
281
281
  background: white;
282
282
  border-radius: 8px;
@@ -299,6 +299,18 @@ class GraphVisualizer {
299
299
  // graph. Split out of styles() to keep each method within the line limit.
300
300
  componentStyles() {
301
301
  return `
302
+ /* The architecture graph is very wide, so lay the legend out as three
303
+ * side-by-side columns (fill / border / edge) instead of one tall
304
+ * column — it keeps the legend short next to the wide graph, and
305
+ * collapses back to a single column on narrow viewports. */
306
+ .legend-columns {
307
+ display: grid;
308
+ grid-template-columns: repeat(3, 1fr);
309
+ gap: 28px;
310
+ align-items: start;
311
+ }
312
+ .legend-col h3 { margin: 0 0 8px; color: #333; font-size: 15px; }
313
+ @media (max-width: 800px) { .legend-columns { grid-template-columns: 1fr; } }
302
314
  .wp-lock-control {
303
315
  max-width: 600px;
304
316
  margin: 0 auto 16px;
@@ -340,10 +352,35 @@ class GraphVisualizer {
340
352
  }
341
353
  .wp-hidden { display: none; }`;
342
354
  }
355
+ // The legend is laid out in three side-by-side columns (fill / border /
356
+ // edge) so it stays short next to the very wide architecture graph. Each
357
+ // column's rows come from a helper below to keep this method within the line
358
+ // limit; the footnote spans the full width beneath the columns.
343
359
  legend() {
344
360
  return `<div class="legend">
345
- <h2>Legend — fill = framework (libType), border = role</h2>
346
- <div class="legend-item">
361
+ <h2>Legend</h2>
362
+ <div class="legend-columns">
363
+ <div class="legend-col">
364
+ <h3>Fill = framework (libType)</h3>
365
+ ${this.fillItems()}
366
+ </div>
367
+ <div class="legend-col">
368
+ <h3>Border = role</h3>
369
+ ${this.borderItems()}
370
+ </div>
371
+ <div class="legend-col">
372
+ <h3>Edge lines — <em>why</em> a project depends on an api-lib</h3>
373
+ ${this.edgeItems()}
374
+ </div>
375
+ </div>
376
+ <div class="legend-item" style="margin-top: 15px;">
377
+ <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>
378
+ </div>
379
+ </div>`;
380
+ }
381
+ // Column 1 — fill color keyed on the project's framework (libType) env set.
382
+ fillItems() {
383
+ return `<div class="legend-item">
347
384
  <span class="legend-box" style="background: #FCE4EC;"></span>
348
385
  <strong>angular:</strong> Angular front-end
349
386
  </div>
@@ -362,8 +399,11 @@ class GraphVisualizer {
362
399
  <div class="legend-item">
363
400
  <span class="legend-box" style="background: #FFF9C4;"></span>
364
401
  <strong>node:</strong> node server base env
365
- </div>
366
- <div class="legend-item" style="margin-top: 12px;">
402
+ </div>`;
403
+ }
404
+ // Column 2 — border style keyed on the project's role.
405
+ borderItems() {
406
+ return `<div class="legend-item">
367
407
  <span class="legend-box" style="border: 3px solid green;"></span>
368
408
  <strong>server:</strong> runnable server app (thick green border)
369
409
  </div>
@@ -382,11 +422,13 @@ class GraphVisualizer {
382
422
  <div class="legend-item">
383
423
  <span class="legend-box" style="border: 2px solid #EF6C00;"></span>
384
424
  <strong>api-lib:</strong> API-contract library (defines <code>@ApiPath</code>/<code>@Rpc</code>/<code>@PubSub</code> <code>*Api</code> classes)
385
- </div>
386
- <h2 style="margin-top: 18px;">Edge lines — <em>why</em> a project depends on an api-lib</h2>
387
- <div class="legend-item">
425
+ </div>`;
426
+ }
427
+ // Column 3 — edge line style keyed on WHY a project depends on an api-lib.
428
+ edgeItems() {
429
+ return `<div class="legend-item">
388
430
  <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>
389
- <strong>implements:</strong> serves the API (a controller is registered via <code>addRoutes</code>)
431
+ <strong>implements:</strong> serves the API 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.
390
432
  </div>
391
433
  <div class="legend-item">
392
434
  <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"/></svg>
@@ -399,11 +441,7 @@ class GraphVisualizer {
399
441
  <div class="legend-item">
400
442
  <svg width="42" height="12" style="vertical-align: middle; margin-right: 10px;"><line x1="0" y1="6" x2="42" y2="6" stroke="#999" stroke-width="1.5"/></svg>
401
443
  <strong>plain dependency:</strong> a normal library import (no API relationship)
402
- </div>
403
- <div class="legend-item" style="margin-top: 15px;">
404
- <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>
405
- </div>
406
- </div>`;
444
+ </div>`;
407
445
  }
408
446
  /**
409
447
  * The page script. The browser code lives in graph-visualizer.client.js (a
@@ -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 { 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\nexport class VisualizationPaths {\n htmlPath: string;\n\n constructor(htmlPath: string) {\n this.htmlPath = htmlPath;\n }\n}\n\nexport class GraphVisualizer {\n private readonly names = new GraphNames();\n private readonly responsibilities = new ResponsibilitiesRenderer();\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 * 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 === '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 → DASHED (a controller serves this api-lib's contract)\n * uses → SOLID blue, thicker (a generated client calls it)\n * uses-implements → DASHED purple, thicker (does both — implements some\n * contracts of the api-lib, uses others)\n * plain lib (none) → the default thin 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') return ' [color=\"#1976d2\", penwidth=2]';\n if (kind === 'uses-implements') return ' [style=dashed, color=\"#8e24aa\", penwidth=2]';\n return '';\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\n const levels: Record<number, string[]> = {};\n for (const project of Object.keys(graph)) {\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 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 shortName = this.names.getShortName(project);\n const info = graph[project];\n for (const dep of info.dependsOn || []) {\n const attrs = this.edgeAttrs(info.apiRelations?.[dep]?.kind);\n dot += ` \"${shortName}\" -> \"${this.names.getShortName(dep)}\"${attrs};\\n`;\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@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.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 ${lockControl}\n ${legend}\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 above 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 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: 600px;\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 .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 private legend(): string {\n return `<div class=\"legend\">\n <h2>Legend — fill = framework (libType), border = role</h2>\n <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 <div class=\"legend-item\" style=\"margin-top: 12px;\">\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 <h2 style=\"margin-top: 18px;\">Edge lines — <em>why</em> a project depends on an api-lib</h2>\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 (a controller is registered via <code>addRoutes</code>)\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\"/></svg>\n <strong>uses:</strong> calls the API (generates an rpc/pubsub client via <code>createRpcClient</code>/<code>createPubSubClient</code>)\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=\"#8e24aa\" 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 <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=\"#999\" stroke-width=\"1.5\"/></svg>\n <strong>plain dependency:</strong> a normal library import (no API relationship)\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 /**\n * The page script. The browser code lives in graph-visualizer.client.js (a\n * plain .js asset, NOT a TS template literal) so its dim/highlight/lock\n * functions can be ordinary browser functions — the TS lint rules that scan\n * .ts template strings would otherwise forbid them, and browser JS cannot\n * carry TS return annotations. We inline it and substitute the DOT.\n */\n private script(dot: string): string {\n const clientJs = fs.readFileSync(path.join(__dirname, 'graph-visualizer.client.js'), 'utf-8');\n return clientJs.split('__DOT__').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 { 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\nexport class VisualizationPaths {\n htmlPath: string;\n\n constructor(htmlPath: string) {\n this.htmlPath = htmlPath;\n }\n}\n\nexport class GraphVisualizer {\n private readonly names = new GraphNames();\n private readonly responsibilities = new ResponsibilitiesRenderer();\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 * 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 === '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 → DASHED (a controller serves this api-lib's contract)\n * uses → SOLID blue, thicker (a generated client calls it)\n * uses-implements → DASHED purple, thicker (does both — implements some\n * contracts of the api-lib, uses others)\n * plain lib (none) → the default thin 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') return ' [color=\"#1976d2\", penwidth=2]';\n if (kind === 'uses-implements') return ' [style=dashed, color=\"#8e24aa\", penwidth=2]';\n return '';\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\n const levels: Record<number, string[]> = {};\n for (const project of Object.keys(graph)) {\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 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 shortName = this.names.getShortName(project);\n const info = graph[project];\n for (const dep of info.dependsOn || []) {\n const attrs = this.edgeAttrs(info.apiRelations?.[dep]?.kind);\n dot += ` \"${shortName}\" -> \"${this.names.getShortName(dep)}\"${attrs};\\n`;\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@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.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 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\" stroke-dasharray=\"5,3\"/></svg>\n <strong>implements:</strong> serves the API — 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\"/></svg>\n <strong>uses:</strong> calls the API (generates an rpc/pubsub client via <code>createRpcClient</code>/<code>createPubSubClient</code>)\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=\"#8e24aa\" 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 <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=\"#999\" stroke-width=\"1.5\"/></svg>\n <strong>plain dependency:</strong> a normal library import (no API relationship)\n </div>`;\n }\n\n /**\n * The page script. The browser code lives in graph-visualizer.client.js (a\n * plain .js asset, NOT a TS template literal) so its dim/highlight/lock\n * functions can be ordinary browser functions — the TS lint rules that scan\n * .ts template strings would otherwise forbid them, and browser JS cannot\n * carry TS return annotations. We inline it and substitute the DOT.\n */\n private script(dot: string): string {\n const clientJs = fs.readFileSync(path.join(__dirname, 'graph-visualizer.client.js'), 'utf-8');\n return clientJs.split('__DOT__').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"]}