@webpieces/nx-webpieces-rules 0.3.334 → 0.3.335

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/executors.json +5 -0
  2. package/package.json +6 -6
  3. package/src/executors/generate/executor.js +6 -0
  4. package/src/executors/generate/executor.js.map +1 -1
  5. package/src/executors/validate-api-relations/executor.d.ts +18 -0
  6. package/src/executors/validate-api-relations/executor.js +51 -0
  7. package/src/executors/validate-api-relations/executor.js.map +1 -0
  8. package/src/executors/validate-api-relations/schema.json +8 -0
  9. package/src/executors/validate-architecture-unchanged/executor.js +22 -11
  10. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  11. package/src/lib/api-usage/api-relations-validator.d.ts +30 -0
  12. package/src/lib/api-usage/api-relations-validator.js +74 -0
  13. package/src/lib/api-usage/api-relations-validator.js.map +1 -0
  14. package/src/lib/api-usage/api-relations.d.ts +51 -0
  15. package/src/lib/api-usage/api-relations.js +34 -0
  16. package/src/lib/api-usage/api-relations.js.map +1 -0
  17. package/src/lib/api-usage/api-scanner.d.ts +60 -0
  18. package/src/lib/api-usage/api-scanner.js +239 -0
  19. package/src/lib/api-usage/api-scanner.js.map +1 -0
  20. package/src/lib/di-graph/analyzer-strategy.d.ts +1 -0
  21. package/src/lib/di-graph/analyzer-strategy.js +3 -0
  22. package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
  23. package/src/lib/graph-comparator.js +8 -0
  24. package/src/lib/graph-comparator.js.map +1 -1
  25. package/src/lib/graph-loader.js +18 -0
  26. package/src/lib/graph-loader.js.map +1 -1
  27. package/src/lib/graph-sorter.d.ts +5 -0
  28. package/src/lib/graph-sorter.js.map +1 -1
  29. package/src/lib/graph-visualizer.d.ts +10 -0
  30. package/src/lib/graph-visualizer.js +48 -3
  31. package/src/lib/graph-visualizer.js.map +1 -1
  32. package/src/lib/role-resolver.d.ts +3 -0
  33. package/src/lib/role-resolver.js +4 -1
  34. package/src/lib/role-resolver.js.map +1 -1
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ /**
3
+ * API Usage Scanner
4
+ *
5
+ * Derives, by scanning real source (not a declaration file), how every project
6
+ * relates to the api-lib projects it depends on. This is the single source of
7
+ * truth for the `apiRelations` field in architecture/dependencies.json AND for
8
+ * the runtime microservice graph.
9
+ *
10
+ * Signals (all resolved through the TypeScript checker, so re-exports resolve):
11
+ * - IMPLEMENTS: `apiFactory.addRoutes(XxxApi, XxxController)` — the registration
12
+ * that actually SERVES the contract over the wire. We deliberately
13
+ * do NOT use `class Ctrl extends XxxApi`: a class can extend an API
14
+ * as an in-process test double / simulator (e.g. Server2Simulator)
15
+ * without ever serving it — only `addRoutes` proves a served route.
16
+ * - USES: `factory.createRpcClient(XxxApi, ...)` → rpc client
17
+ * `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client
18
+ * An api-lib is DETECTED, not tagged: a project exporting an `abstract class`
19
+ * carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.ApiUsageScanner = void 0;
23
+ exports.scanAndAttachApiRelations = scanAndAttachApiRelations;
24
+ const tslib_1 = require("tslib");
25
+ const ts = tslib_1.__importStar(require("typescript"));
26
+ const path = tslib_1.__importStar(require("path"));
27
+ const program_1 = require("../di-graph/program");
28
+ const bindings_1 = require("../di-graph/bindings");
29
+ const api_relations_1 = require("./api-relations");
30
+ const RPC_CLIENT_METHOD = 'createRpcClient';
31
+ const PUBSUB_CLIENT_METHOD = 'createPubSubClient';
32
+ const ADD_ROUTES_METHOD = 'addRoutes';
33
+ /** Maps an absolute source-file path to the workspace project that owns it (longest-root-prefix). */
34
+ class ProjectLocator {
35
+ roots;
36
+ constructor(workspaceRoot, projectInfos) {
37
+ const roots = [];
38
+ for (const info of projectInfos.values()) {
39
+ if (info.root === '' || info.root === '.')
40
+ continue;
41
+ roots.push(new ProjectRoot(info.name, path.resolve(workspaceRoot, info.root)));
42
+ }
43
+ // Longest root first so a nested project wins over its parent.
44
+ this.roots = roots.sort((a, b) => b.abs.length - a.abs.length);
45
+ }
46
+ projectOf(absFile) {
47
+ const normalized = path.resolve(absFile);
48
+ for (const root of this.roots) {
49
+ if (normalized === root.abs || normalized.startsWith(root.abs + path.sep))
50
+ return root.name;
51
+ }
52
+ return null;
53
+ }
54
+ }
55
+ class ProjectRoot {
56
+ name;
57
+ abs;
58
+ constructor(name, abs) {
59
+ this.name = name;
60
+ this.abs = abs;
61
+ }
62
+ }
63
+ /** Per-owner accumulator that dedupes API refs while a single project is scanned. */
64
+ class RelationAccumulator {
65
+ implementsByOwner = new Map();
66
+ usesByOwner = new Map();
67
+ addImplements(owner, ref) {
68
+ ensureRefMap(this.implementsByOwner, owner).set(ref.api, ref);
69
+ }
70
+ addUses(owner, ref) {
71
+ ensureRefMap(this.usesByOwner, owner).set(ref.api, ref);
72
+ }
73
+ /** Build the deterministic { owner -> relation } record, owners in sorted order. */
74
+ toRelations() {
75
+ const owners = new Set([...this.implementsByOwner.keys(), ...this.usesByOwner.keys()]);
76
+ const relations = {};
77
+ for (const owner of [...owners].sort()) {
78
+ const implementsRefs = (0, api_relations_1.sortApiRefs)([...(this.implementsByOwner.get(owner)?.values() ?? [])]);
79
+ const usesRefs = (0, api_relations_1.sortApiRefs)([...(this.usesByOwner.get(owner)?.values() ?? [])]);
80
+ const relation = {
81
+ kind: (0, api_relations_1.deriveApiRelationKind)(implementsRefs, usesRefs),
82
+ implements: implementsRefs,
83
+ uses: usesRefs,
84
+ };
85
+ relations[owner] = relation;
86
+ }
87
+ return relations;
88
+ }
89
+ isEmpty() {
90
+ return this.implementsByOwner.size === 0 && this.usesByOwner.size === 0;
91
+ }
92
+ }
93
+ // webpieces-disable no-function-outside-class -- tiny map helper, matching the AST-helper style of di-graph/bindings.ts
94
+ function ensureRefMap(map, owner) {
95
+ let inner = map.get(owner);
96
+ if (!inner) {
97
+ inner = new Map();
98
+ map.set(owner, inner);
99
+ }
100
+ return inner;
101
+ }
102
+ /** Statically scans every project for its api-lib implements/uses relationships. */
103
+ class ApiUsageScanner {
104
+ workspaceRoot;
105
+ projectInfos;
106
+ locator;
107
+ apiLibProjects = new Set();
108
+ apiIndex = new Map();
109
+ relationsByProject = new Map();
110
+ constructor(workspaceRoot, projectInfos) {
111
+ this.workspaceRoot = workspaceRoot;
112
+ this.projectInfos = projectInfos;
113
+ this.locator = new ProjectLocator(workspaceRoot, projectInfos);
114
+ }
115
+ scan() {
116
+ for (const info of this.projectInfos.values()) {
117
+ if (info.root === '' || info.root === '.')
118
+ continue;
119
+ this.scanProject(info);
120
+ }
121
+ return {
122
+ relationsByProject: this.relationsByProject,
123
+ apiLibProjects: this.apiLibProjects,
124
+ apiIndex: this.apiIndex,
125
+ };
126
+ }
127
+ scanProject(info) {
128
+ const program = (0, program_1.createProjectProgram)(path.resolve(this.workspaceRoot, info.root));
129
+ if (!program)
130
+ return;
131
+ const checker = program.getTypeChecker();
132
+ const accumulator = new RelationAccumulator();
133
+ for (const sourceFile of program.getSourceFiles()) {
134
+ if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/'))
135
+ continue;
136
+ if (isTestFile(sourceFile.fileName))
137
+ continue; // tests are not production topology
138
+ // Only this project's OWN files — imported api-lib source is in the program too.
139
+ if (this.locator.projectOf(sourceFile.fileName) !== info.name)
140
+ continue;
141
+ this.visit(sourceFile, checker, info.name, accumulator);
142
+ }
143
+ if (!accumulator.isEmpty())
144
+ this.relationsByProject.set(info.name, accumulator.toRelations());
145
+ }
146
+ visit(node, checker, project, acc) {
147
+ if (ts.isClassDeclaration(node)) {
148
+ this.recordApiClass(node, project);
149
+ }
150
+ else if (ts.isCallExpression(node)) {
151
+ this.recordCall(node, checker, acc);
152
+ }
153
+ ts.forEachChild(node, (child) => this.visit(child, checker, project, acc));
154
+ }
155
+ /** Register a project-owned API contract (abstract @ApiPath class) into the index. */
156
+ recordApiClass(cls, project) {
157
+ const own = this.apiClassInfoFor(cls);
158
+ if (!own)
159
+ return;
160
+ this.apiLibProjects.add(project);
161
+ this.apiIndex.set(own.api, own);
162
+ }
163
+ recordCall(call, checker, acc) {
164
+ const method = calleeMethodName(call);
165
+ if (method === null || call.arguments.length === 0)
166
+ return;
167
+ if (method === ADD_ROUTES_METHOD) {
168
+ this.addImplementsFromExpr(call.arguments[0], checker, acc);
169
+ return;
170
+ }
171
+ if (method === RPC_CLIENT_METHOD || method === PUBSUB_CLIENT_METHOD) {
172
+ const info = this.apiInfoFromExpr(call.arguments[0], checker);
173
+ if (info)
174
+ acc.addUses(info.owner, { api: info.api, type: info.type });
175
+ }
176
+ }
177
+ addImplementsFromExpr(expr, checker, acc) {
178
+ const info = this.apiInfoFromExpr(expr, checker);
179
+ if (info)
180
+ acc.addImplements(info.owner, { api: info.api, type: info.type });
181
+ }
182
+ /** Resolve an expression to the API contract it names, or null if it is not one. */
183
+ apiInfoFromExpr(expr, checker) {
184
+ const decl = (0, bindings_1.resolveClassDeclaration)(expr, checker);
185
+ return decl ? this.apiClassInfoFor(decl) : null;
186
+ }
187
+ /** {api, owner, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */
188
+ apiClassInfoFor(cls) {
189
+ if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name)
190
+ return null;
191
+ const owner = this.locator.projectOf(cls.getSourceFile().fileName);
192
+ if (owner === null)
193
+ return null;
194
+ const type = hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';
195
+ return { api: cls.name.text, owner, type };
196
+ }
197
+ }
198
+ exports.ApiUsageScanner = ApiUsageScanner;
199
+ /**
200
+ * Run the scan and attach the derived `apiRelations` onto each graph entry in
201
+ * place. Shared by `architecture:generate` (which then saves) and
202
+ * `architecture:validate-architecture-unchanged` (which regenerates in memory
203
+ * and must attach the SAME field, or it would see a phantom diff). Returns the
204
+ * full scan so callers (validators, runtime graph) can reuse the api index.
205
+ */
206
+ // webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings
207
+ function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos) {
208
+ const result = new ApiUsageScanner(workspaceRoot, projectInfos).scan();
209
+ for (const projectName of result.relationsByProject.keys()) {
210
+ const entry = graph[projectName];
211
+ if (entry)
212
+ entry.apiRelations = result.relationsByProject.get(projectName);
213
+ }
214
+ return result;
215
+ }
216
+ // webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
217
+ function isAbstractClass(cls) {
218
+ return (ts.getModifiers(cls) ?? []).some((m) => m.kind === ts.SyntaxKind.AbstractKeyword);
219
+ }
220
+ // webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
221
+ function hasClassDecorator(cls, name) {
222
+ return (0, bindings_1.classDecorators)(cls).some((d) => (0, bindings_1.decoratorName)(d) === name);
223
+ }
224
+ // webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
225
+ function calleeMethodName(call) {
226
+ const callee = call.expression;
227
+ if (ts.isPropertyAccessExpression(callee))
228
+ return callee.name.text;
229
+ if (ts.isIdentifier(callee))
230
+ return callee.text;
231
+ return null;
232
+ }
233
+ // webpieces-disable no-function-outside-class -- pure path predicate, matching the sibling helpers in di-graph/bindings.ts
234
+ function isTestFile(fileName) {
235
+ return (fileName.includes('/__tests__/') ||
236
+ fileName.includes('.spec.') ||
237
+ fileName.includes('.test.'));
238
+ }
239
+ //# sourceMappingURL=api-scanner.js.map
@@ -0,0 +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"]}
@@ -63,6 +63,7 @@ export declare function explicitRoleTag(tags: readonly string[]): string | null;
63
63
  * - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)
64
64
  * - `client` → Angular design for angular apps; otherwise skip
65
65
  * - `lib` → skip (plain libraries get no design)
66
+ * - `api-lib` → skip (contract-only libraries get no design)
66
67
  * - role absent → legacy framework/marker selection
67
68
  */
68
69
  export declare function selectAnalyzer(role: string | null, frameworks: string[], markers: FrameworkMarkers): DiAnalyzer;
@@ -108,6 +108,7 @@ function explicitRoleTag(tags) {
108
108
  * - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)
109
109
  * - `client` → Angular design for angular apps; otherwise skip
110
110
  * - `lib` → skip (plain libraries get no design)
111
+ * - `api-lib` → skip (contract-only libraries get no design)
111
112
  * - role absent → legacy framework/marker selection
112
113
  */
113
114
  function selectAnalyzer(role, frameworks, markers) {
@@ -119,6 +120,8 @@ function selectAnalyzer(role, frameworks, markers) {
119
120
  return frameworks.includes('angular') ? new AngularAnalyzer() : new EmptyAnalyzer();
120
121
  if (role === 'lib')
121
122
  return new EmptyAnalyzer();
123
+ if (role === 'api-lib')
124
+ return new EmptyAnalyzer();
122
125
  // Role tag absent — fall back to the legacy framework/marker selection so
123
126
  // designs stay identical until a project is retagged.
124
127
  if (frameworks.includes('express'))
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer-strategy.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer-strategy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Analyzer Strategy\n *\n * Picks which DI analyzer generates a project's design graph. The primary\n * driver is the project's `role` nx tag; the `framework` env set (its libType)\n * only distinguishes the client runtime by set membership:\n *\n * - env set includes `express` → {@link InversifyAnalyzer} (roots on `@DocumentDesign`)\n * - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)\n * - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)\n *\n * The explicit tags are the source of truth. When the role tag is ABSENT (e.g.\n * before retag), a cheap marker pre-scan corroborates: `@Component(` /\n * `bootstrapApplication` → angular; `@DocumentDesign(` → Inversify. This makes the\n * committed design.* identical whether selection is tag- or marker-driven.\n */\n\nimport type * as ts from 'typescript';\nimport { DiGraph } from './model';\nimport { buildDiGraph, DiRootMode } from './analyzer';\nimport { buildAngularDiGraph } from './angular-analyzer';\n\nconst FRAMEWORK_TAG_PREFIX = 'framework:';\nconst ROLE_TAG_PREFIX = 'role:';\n\n/** Which framework a source tree's decorators point at (marker pre-scan fallback). */\nexport class FrameworkMarkers {\n angular: boolean;\n controller: boolean;\n\n constructor(angular: boolean, controller: boolean) {\n this.angular = angular;\n this.controller = controller;\n }\n}\n\n/** Statically analyzes one project's DI dependency DAG into a `DiGraph`. */\nexport interface DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;\n}\n\n/**\n * Inversify analyzer: one design per `@DocumentDesign` root. The `rootMode` only\n * sets the root box kind:\n * - `'controller'` (server projects) → `controller`.\n * - `'apiImplementation'` (role:designed-lib) → `apiImplementation`.\n */\nexport class InversifyAnalyzer implements DiAnalyzer {\n private readonly rootMode: DiRootMode;\n\n constructor(rootMode: DiRootMode = 'controller') {\n this.rootMode = rootMode;\n }\n\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildDiGraph(program, workspaceRoot, projectRoot, projectName, false, this.rootMode);\n }\n}\n\n/** Angular: one design per entry component (bootstrap + routed). */\nexport class AngularAnalyzer implements DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildAngularDiGraph(program, workspaceRoot, projectRoot, projectName);\n }\n}\n\n/** Non-angular/non-express projects: an empty graph (skip). */\nexport class EmptyAnalyzer implements DiAnalyzer {\n analyzeProject(\n _program: ts.Program,\n _workspaceRoot: string,\n _projectRoot: string,\n projectName: string,\n ): DiGraph {\n return new DiGraph(projectName);\n }\n}\n\n/** Extract the explicit `<prefix><value>` nx tag, or null when the project has none. */\nfunction explicitTag(tags: readonly string[], prefix: string): string | null {\n for (const tag of tags) {\n if (tag.startsWith(prefix)) {\n const value = tag.slice(prefix.length).trim();\n if (value.length > 0) return value;\n }\n }\n return null;\n}\n\n/** Extract the FIRST explicit `framework:<value>` nx tag, or null when the project has none. */\nexport function explicitFrameworkTag(tags: readonly string[]): string | null {\n return explicitTag(tags, FRAMEWORK_TAG_PREFIX);\n}\n\n/** Extract every explicit `framework:<value>` nx tag as an env set (the project's libType). */\nexport function frameworkTags(tags: readonly string[]): string[] {\n const values: string[] = [];\n for (const tag of tags) {\n if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {\n const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();\n if (value.length > 0) values.push(value);\n }\n }\n return values;\n}\n\n/** Extract the explicit `role:<value>` nx tag, or null when the project has none. */\nexport function explicitRoleTag(tags: readonly string[]): string | null {\n return explicitTag(tags, ROLE_TAG_PREFIX);\n}\n\n/**\n * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is\n * the source of truth for WHAT to root on; `frameworks` is the project's env set\n * (its libType) and distinguishes the client runtime (angular vs other) by set\n * membership; `markers` corroborate only when the role tag is ABSENT (rollout\n * fallback keeps pre-retag designs identical).\n *\n * - `server` → Inversify, roots on `@DocumentDesign` (controller kind)\n * - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)\n * - `client` → Angular design for angular apps; otherwise skip\n * - `lib` → skip (plain libraries get no design)\n * - role absent → legacy framework/marker selection\n */\nexport function selectAnalyzer(\n role: string | null,\n frameworks: string[],\n markers: FrameworkMarkers,\n): DiAnalyzer {\n if (role === 'server') return new InversifyAnalyzer('controller');\n if (role === 'designed-lib') return new InversifyAnalyzer('apiImplementation');\n if (role === 'client') return frameworks.includes('angular') ? new AngularAnalyzer() : new EmptyAnalyzer();\n if (role === 'lib') return new EmptyAnalyzer();\n\n // Role tag absent — fall back to the legacy framework/marker selection so\n // designs stay identical until a project is retagged.\n if (frameworks.includes('express')) return new InversifyAnalyzer('controller');\n if (frameworks.includes('angular')) return new AngularAnalyzer();\n if (frameworks.length > 0) return new EmptyAnalyzer();\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer('controller');\n return new EmptyAnalyzer();\n}\n"]}
1
+ {"version":3,"file":"analyzer-strategy.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer-strategy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Analyzer Strategy\n *\n * Picks which DI analyzer generates a project's design graph. The primary\n * driver is the project's `role` nx tag; the `framework` env set (its libType)\n * only distinguishes the client runtime by set membership:\n *\n * - env set includes `express` → {@link InversifyAnalyzer} (roots on `@DocumentDesign`)\n * - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)\n * - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)\n *\n * The explicit tags are the source of truth. When the role tag is ABSENT (e.g.\n * before retag), a cheap marker pre-scan corroborates: `@Component(` /\n * `bootstrapApplication` → angular; `@DocumentDesign(` → Inversify. This makes the\n * committed design.* identical whether selection is tag- or marker-driven.\n */\n\nimport type * as ts from 'typescript';\nimport { DiGraph } from './model';\nimport { buildDiGraph, DiRootMode } from './analyzer';\nimport { buildAngularDiGraph } from './angular-analyzer';\n\nconst FRAMEWORK_TAG_PREFIX = 'framework:';\nconst ROLE_TAG_PREFIX = 'role:';\n\n/** Which framework a source tree's decorators point at (marker pre-scan fallback). */\nexport class FrameworkMarkers {\n angular: boolean;\n controller: boolean;\n\n constructor(angular: boolean, controller: boolean) {\n this.angular = angular;\n this.controller = controller;\n }\n}\n\n/** Statically analyzes one project's DI dependency DAG into a `DiGraph`. */\nexport interface DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;\n}\n\n/**\n * Inversify analyzer: one design per `@DocumentDesign` root. The `rootMode` only\n * sets the root box kind:\n * - `'controller'` (server projects) → `controller`.\n * - `'apiImplementation'` (role:designed-lib) → `apiImplementation`.\n */\nexport class InversifyAnalyzer implements DiAnalyzer {\n private readonly rootMode: DiRootMode;\n\n constructor(rootMode: DiRootMode = 'controller') {\n this.rootMode = rootMode;\n }\n\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildDiGraph(program, workspaceRoot, projectRoot, projectName, false, this.rootMode);\n }\n}\n\n/** Angular: one design per entry component (bootstrap + routed). */\nexport class AngularAnalyzer implements DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildAngularDiGraph(program, workspaceRoot, projectRoot, projectName);\n }\n}\n\n/** Non-angular/non-express projects: an empty graph (skip). */\nexport class EmptyAnalyzer implements DiAnalyzer {\n analyzeProject(\n _program: ts.Program,\n _workspaceRoot: string,\n _projectRoot: string,\n projectName: string,\n ): DiGraph {\n return new DiGraph(projectName);\n }\n}\n\n/** Extract the explicit `<prefix><value>` nx tag, or null when the project has none. */\nfunction explicitTag(tags: readonly string[], prefix: string): string | null {\n for (const tag of tags) {\n if (tag.startsWith(prefix)) {\n const value = tag.slice(prefix.length).trim();\n if (value.length > 0) return value;\n }\n }\n return null;\n}\n\n/** Extract the FIRST explicit `framework:<value>` nx tag, or null when the project has none. */\nexport function explicitFrameworkTag(tags: readonly string[]): string | null {\n return explicitTag(tags, FRAMEWORK_TAG_PREFIX);\n}\n\n/** Extract every explicit `framework:<value>` nx tag as an env set (the project's libType). */\nexport function frameworkTags(tags: readonly string[]): string[] {\n const values: string[] = [];\n for (const tag of tags) {\n if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {\n const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();\n if (value.length > 0) values.push(value);\n }\n }\n return values;\n}\n\n/** Extract the explicit `role:<value>` nx tag, or null when the project has none. */\nexport function explicitRoleTag(tags: readonly string[]): string | null {\n return explicitTag(tags, ROLE_TAG_PREFIX);\n}\n\n/**\n * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is\n * the source of truth for WHAT to root on; `frameworks` is the project's env set\n * (its libType) and distinguishes the client runtime (angular vs other) by set\n * membership; `markers` corroborate only when the role tag is ABSENT (rollout\n * fallback keeps pre-retag designs identical).\n *\n * - `server` → Inversify, roots on `@DocumentDesign` (controller kind)\n * - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)\n * - `client` → Angular design for angular apps; otherwise skip\n * - `lib` → skip (plain libraries get no design)\n * - `api-lib` → skip (contract-only libraries get no design)\n * - role absent → legacy framework/marker selection\n */\nexport function selectAnalyzer(\n role: string | null,\n frameworks: string[],\n markers: FrameworkMarkers,\n): DiAnalyzer {\n if (role === 'server') return new InversifyAnalyzer('controller');\n if (role === 'designed-lib') return new InversifyAnalyzer('apiImplementation');\n if (role === 'client') return frameworks.includes('angular') ? new AngularAnalyzer() : new EmptyAnalyzer();\n if (role === 'lib') return new EmptyAnalyzer();\n if (role === 'api-lib') return new EmptyAnalyzer();\n\n // Role tag absent — fall back to the legacy framework/marker selection so\n // designs stay identical until a project is retagged.\n if (frameworks.includes('express')) return new InversifyAnalyzer('controller');\n if (frameworks.includes('angular')) return new AngularAnalyzer();\n if (frameworks.length > 0) return new EmptyAnalyzer();\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer('controller');\n return new EmptyAnalyzer();\n}\n"]}
@@ -107,6 +107,14 @@ function findChangedFields(currentEntry, savedEntry) {
107
107
  changes.push({ field, from, to });
108
108
  }
109
109
  }
110
+ // apiRelations is a nested object — compare by canonical JSON. Both sides are
111
+ // built with sorted owners + refs (scanner) / preserved key order (loader), so
112
+ // string equality is a faithful deep-equality here.
113
+ const fromRelations = JSON.stringify(savedEntry.apiRelations ?? {});
114
+ const toRelations = JSON.stringify(currentEntry.apiRelations ?? {});
115
+ if (fromRelations !== toRelations) {
116
+ changes.push({ field: 'apiRelations', from: fromRelations, to: toRelations });
117
+ }
110
118
  return changes;
111
119
  }
112
120
  function formatFieldValue(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"graph-comparator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-comparator.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAsDH,sCAqCC;AA/DD;;GAEG;AACH,MAAM,eAAe,GAA6C;IAC9D,WAAW;IACX,kBAAkB;IAClB,sBAAsB;IACtB,YAAY;CACf,CAAC;AAWF;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,OAAsB,EAAE,KAAoB;IACtE,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACtD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAElD,MAAM,IAAI,GAAc;QACpB,KAAK,EAAE,EAAE;QACT,OAAO,EAAE,EAAE;QACX,QAAQ,EAAE,EAAE;KACf,CAAC;IAEF,sBAAsB;IACtB,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACpC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;IACL,CAAC;IAED,wBAAwB;IACxB,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;QAClC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;IACL,CAAC;IAED,yBAAyB;IACzB,oBAAoB,CAAC,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC;IAE3E,MAAM,SAAS,GACX,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC;IAEvF,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAExE,OAAO;QACH,SAAS;QACT,IAAI;QACJ,OAAO;KACV,CAAC;AACN,CAAC;AAED,SAAS,oBAAoB,CACzB,OAAsB,EACtB,KAAoB,EACpB,eAA4B,EAC5B,aAA0B,EAC1B,IAAe;IAEf,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACpC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,SAAS;QAE1C,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAElC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QACpD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAEhD,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,MAAM,WAAW,GAAa,EAAE,CAAC;QAEjC,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;YAC5B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBACtB,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACxB,CAAC;QACL,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC1B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxB,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1B,CAAC;QACL,CAAC;QAED,MAAM,YAAY,GACd,YAAY,CAAC,KAAK,KAAK,UAAU,CAAC,KAAK;YACnC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,YAAY,CAAC,KAAK,EAAE;YACpD,CAAC,CAAC,IAAI,CAAC;QAEf,MAAM,aAAa,GAAG,iBAAiB,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QAElE,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7F,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;gBACf,OAAO;gBACP,SAAS;gBACT,WAAW;gBACX,YAAY;gBACZ,aAAa;aAChB,CAAC,CAAC;QACP,CAAC;IACL,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAS,mBAAmB,CAAC,KAAoC;IAC7D,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAC3D,CAAC;AAED,SAAS,iBAAiB,CAAC,YAAwB,EAAE,UAAsB;IACvE,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,mBAAmB,CAAC,UAAU,CAAC,KAAK,CAAkC,CAAC,CAAC;QACrF,MAAM,EAAE,GAAG,mBAAmB,CAAC,YAAY,CAAC,KAAK,CAAkC,CAAC,CAAC;QACrF,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACtC,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAyB;IAC/C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACzC,MAAM,uBAAuB,GAAG,EAAE,CAAC;IACnC,OAAO,KAAK,CAAC,MAAM,GAAG,uBAAuB;QACzC,CAAC,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,uBAAuB,CAAC,MAAM;QACnD,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC;AACvB,CAAC;AAED,SAAS,YAAY,CAAC,IAAe;IACjC,MAAM,YAAY,GAAa,EAAE,CAAC;IAElC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,YAAY,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,YAAY,CAAC,IAAI,CAAC,qBAAqB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;QACD,IAAI,GAAG,CAAC,YAAY,EAAE,CAAC;YACnB,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,YAAY,CAAC,IAAI,OAAO,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5E,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,aAAa,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CACN,GAAG,MAAM,CAAC,KAAK,KAAK,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CACxF,CAAC;QACN,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnB,YAAY,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,OAAO,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC7D,CAAC;IACL,CAAC;IAED,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/**\n * Graph Comparator\n *\n * Compares the current generated graph with the saved (blessed) graph.\n * Used in validate mode to ensure developers have updated the graph file.\n */\n\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\n\n/**\n * A changed metadata field on a project (framework, shortDescription, ...)\n */\nexport interface FieldChange {\n field: string;\n from: string | undefined;\n to: string | undefined;\n}\n\n/**\n * Difference between two graphs\n */\nexport interface GraphDiff {\n added: string[];\n removed: string[];\n modified: {\n project: string;\n addedDeps: string[];\n removedDeps: string[];\n levelChanged: { from: number; to: number } | null;\n changedFields: FieldChange[];\n }[];\n}\n\n/**\n * Metadata fields compared per project (beyond level + dependsOn)\n */\nconst METADATA_FIELDS: ReadonlyArray<keyof GraphEntry & string> = [\n 'framework',\n 'shortDescription',\n 'responsibilitiesFile',\n 'designFile',\n];\n\n/**\n * Comparison result\n */\nexport interface ComparisonResult {\n identical: boolean;\n diff: GraphDiff;\n summary: string;\n}\n\n/**\n * Compare two graphs and return the differences\n *\n * @param current - Currently generated graph\n * @param saved - Previously saved (blessed) graph\n * @returns Comparison result with detailed diff\n */\nexport function compareGraphs(current: EnhancedGraph, saved: EnhancedGraph): ComparisonResult {\n const currentProjects = new Set(Object.keys(current));\n const savedProjects = new Set(Object.keys(saved));\n\n const diff: GraphDiff = {\n added: [],\n removed: [],\n modified: [],\n };\n\n // Find added projects\n for (const project of currentProjects) {\n if (!savedProjects.has(project)) {\n diff.added.push(project);\n }\n }\n\n // Find removed projects\n for (const project of savedProjects) {\n if (!currentProjects.has(project)) {\n diff.removed.push(project);\n }\n }\n\n // Find modified projects\n findModifiedProjects(current, saved, currentProjects, savedProjects, diff);\n\n const identical =\n diff.added.length === 0 && diff.removed.length === 0 && diff.modified.length === 0;\n\n const summary = identical ? 'Graphs are identical' : buildSummary(diff);\n\n return {\n identical,\n diff,\n summary,\n };\n}\n\nfunction findModifiedProjects(\n current: EnhancedGraph,\n saved: EnhancedGraph,\n currentProjects: Set<string>,\n savedProjects: Set<string>,\n diff: GraphDiff\n): void {\n for (const project of currentProjects) {\n if (!savedProjects.has(project)) continue;\n\n const currentEntry = current[project];\n const savedEntry = saved[project];\n\n const currentDeps = new Set(currentEntry.dependsOn);\n const savedDeps = new Set(savedEntry.dependsOn);\n\n const addedDeps: string[] = [];\n const removedDeps: string[] = [];\n\n for (const dep of currentDeps) {\n if (!savedDeps.has(dep)) {\n addedDeps.push(dep);\n }\n }\n\n for (const dep of savedDeps) {\n if (!currentDeps.has(dep)) {\n removedDeps.push(dep);\n }\n }\n\n const levelChanged =\n currentEntry.level !== savedEntry.level\n ? { from: savedEntry.level, to: currentEntry.level }\n : null;\n\n const changedFields = findChangedFields(currentEntry, savedEntry);\n\n if (addedDeps.length > 0 || removedDeps.length > 0 || levelChanged || changedFields.length > 0) {\n diff.modified.push({\n project,\n addedDeps,\n removedDeps,\n levelChanged,\n changedFields,\n });\n }\n }\n}\n\n/**\n * Normalize a metadata field value to a comparable/displayable string. The\n * `framework` field is a string[] env set (compared by value, joined for\n * display); every other field is already a plain string.\n */\nfunction normalizeFieldValue(value: string | string[] | undefined): string | undefined {\n if (value === undefined) return undefined;\n return Array.isArray(value) ? value.join(', ') : value;\n}\n\nfunction findChangedFields(currentEntry: GraphEntry, savedEntry: GraphEntry): FieldChange[] {\n const changes: FieldChange[] = [];\n for (const field of METADATA_FIELDS) {\n const from = normalizeFieldValue(savedEntry[field] as string | string[] | undefined);\n const to = normalizeFieldValue(currentEntry[field] as string | string[] | undefined);\n if (from !== to) {\n changes.push({ field, from, to });\n }\n }\n return changes;\n}\n\nfunction formatFieldValue(value: string | undefined): string {\n if (value === undefined) return '(none)';\n const MAX_SUMMARY_VALUE_CHARS = 60;\n return value.length > MAX_SUMMARY_VALUE_CHARS\n ? `\"${value.slice(0, MAX_SUMMARY_VALUE_CHARS)}...\"`\n : `\"${value}\"`;\n}\n\nfunction buildSummary(diff: GraphDiff): string {\n const summaryParts: string[] = [];\n\n if (diff.added.length > 0) {\n summaryParts.push(`Added projects: ${diff.added.join(', ')}`);\n }\n\n if (diff.removed.length > 0) {\n summaryParts.push(`Removed projects: ${diff.removed.join(', ')}`);\n }\n\n for (const mod of diff.modified) {\n const parts: string[] = [];\n if (mod.addedDeps.length > 0) {\n parts.push(`+deps: ${mod.addedDeps.join(', ')}`);\n }\n if (mod.removedDeps.length > 0) {\n parts.push(`-deps: ${mod.removedDeps.join(', ')}`);\n }\n if (mod.levelChanged) {\n parts.push(`level: ${mod.levelChanged.from} -> ${mod.levelChanged.to}`);\n }\n for (const change of mod.changedFields) {\n parts.push(\n `${change.field}: ${formatFieldValue(change.from)} -> ${formatFieldValue(change.to)}`\n );\n }\n if (parts.length > 0) {\n summaryParts.push(`${mod.project}: ${parts.join('; ')}`);\n }\n }\n\n return summaryParts.join('\\n');\n}\n"]}
1
+ {"version":3,"file":"graph-comparator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-comparator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Comparator\n *\n * Compares the current generated graph with the saved (blessed) graph.\n * Used in validate mode to ensure developers have updated the graph file.\n */\n\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\n\n/**\n * A changed metadata field on a project (framework, shortDescription, ...)\n */\nexport interface FieldChange {\n field: string;\n from: string | undefined;\n to: string | undefined;\n}\n\n/**\n * Difference between two graphs\n */\nexport interface GraphDiff {\n added: string[];\n removed: string[];\n modified: {\n project: string;\n addedDeps: string[];\n removedDeps: string[];\n levelChanged: { from: number; to: number } | null;\n changedFields: FieldChange[];\n }[];\n}\n\n/**\n * Metadata fields compared per project (beyond level + dependsOn)\n */\nconst METADATA_FIELDS: ReadonlyArray<keyof GraphEntry & string> = [\n 'framework',\n 'shortDescription',\n 'responsibilitiesFile',\n 'designFile',\n];\n\n/**\n * Comparison result\n */\nexport interface ComparisonResult {\n identical: boolean;\n diff: GraphDiff;\n summary: string;\n}\n\n/**\n * Compare two graphs and return the differences\n *\n * @param current - Currently generated graph\n * @param saved - Previously saved (blessed) graph\n * @returns Comparison result with detailed diff\n */\nexport function compareGraphs(current: EnhancedGraph, saved: EnhancedGraph): ComparisonResult {\n const currentProjects = new Set(Object.keys(current));\n const savedProjects = new Set(Object.keys(saved));\n\n const diff: GraphDiff = {\n added: [],\n removed: [],\n modified: [],\n };\n\n // Find added projects\n for (const project of currentProjects) {\n if (!savedProjects.has(project)) {\n diff.added.push(project);\n }\n }\n\n // Find removed projects\n for (const project of savedProjects) {\n if (!currentProjects.has(project)) {\n diff.removed.push(project);\n }\n }\n\n // Find modified projects\n findModifiedProjects(current, saved, currentProjects, savedProjects, diff);\n\n const identical =\n diff.added.length === 0 && diff.removed.length === 0 && diff.modified.length === 0;\n\n const summary = identical ? 'Graphs are identical' : buildSummary(diff);\n\n return {\n identical,\n diff,\n summary,\n };\n}\n\nfunction findModifiedProjects(\n current: EnhancedGraph,\n saved: EnhancedGraph,\n currentProjects: Set<string>,\n savedProjects: Set<string>,\n diff: GraphDiff\n): void {\n for (const project of currentProjects) {\n if (!savedProjects.has(project)) continue;\n\n const currentEntry = current[project];\n const savedEntry = saved[project];\n\n const currentDeps = new Set(currentEntry.dependsOn);\n const savedDeps = new Set(savedEntry.dependsOn);\n\n const addedDeps: string[] = [];\n const removedDeps: string[] = [];\n\n for (const dep of currentDeps) {\n if (!savedDeps.has(dep)) {\n addedDeps.push(dep);\n }\n }\n\n for (const dep of savedDeps) {\n if (!currentDeps.has(dep)) {\n removedDeps.push(dep);\n }\n }\n\n const levelChanged =\n currentEntry.level !== savedEntry.level\n ? { from: savedEntry.level, to: currentEntry.level }\n : null;\n\n const changedFields = findChangedFields(currentEntry, savedEntry);\n\n if (addedDeps.length > 0 || removedDeps.length > 0 || levelChanged || changedFields.length > 0) {\n diff.modified.push({\n project,\n addedDeps,\n removedDeps,\n levelChanged,\n changedFields,\n });\n }\n }\n}\n\n/**\n * Normalize a metadata field value to a comparable/displayable string. The\n * `framework` field is a string[] env set (compared by value, joined for\n * display); every other field is already a plain string.\n */\nfunction normalizeFieldValue(value: string | string[] | undefined): string | undefined {\n if (value === undefined) return undefined;\n return Array.isArray(value) ? value.join(', ') : value;\n}\n\nfunction findChangedFields(currentEntry: GraphEntry, savedEntry: GraphEntry): FieldChange[] {\n const changes: FieldChange[] = [];\n for (const field of METADATA_FIELDS) {\n const from = normalizeFieldValue(savedEntry[field] as string | string[] | undefined);\n const to = normalizeFieldValue(currentEntry[field] as string | string[] | undefined);\n if (from !== to) {\n changes.push({ field, from, to });\n }\n }\n // apiRelations is a nested object — compare by canonical JSON. Both sides are\n // built with sorted owners + refs (scanner) / preserved key order (loader), so\n // string equality is a faithful deep-equality here.\n const fromRelations = JSON.stringify(savedEntry.apiRelations ?? {});\n const toRelations = JSON.stringify(currentEntry.apiRelations ?? {});\n if (fromRelations !== toRelations) {\n changes.push({ field: 'apiRelations', from: fromRelations, to: toRelations });\n }\n return changes;\n}\n\nfunction formatFieldValue(value: string | undefined): string {\n if (value === undefined) return '(none)';\n const MAX_SUMMARY_VALUE_CHARS = 60;\n return value.length > MAX_SUMMARY_VALUE_CHARS\n ? `\"${value.slice(0, MAX_SUMMARY_VALUE_CHARS)}...\"`\n : `\"${value}\"`;\n}\n\nfunction buildSummary(diff: GraphDiff): string {\n const summaryParts: string[] = [];\n\n if (diff.added.length > 0) {\n summaryParts.push(`Added projects: ${diff.added.join(', ')}`);\n }\n\n if (diff.removed.length > 0) {\n summaryParts.push(`Removed projects: ${diff.removed.join(', ')}`);\n }\n\n for (const mod of diff.modified) {\n const parts: string[] = [];\n if (mod.addedDeps.length > 0) {\n parts.push(`+deps: ${mod.addedDeps.join(', ')}`);\n }\n if (mod.removedDeps.length > 0) {\n parts.push(`-deps: ${mod.removedDeps.join(', ')}`);\n }\n if (mod.levelChanged) {\n parts.push(`level: ${mod.levelChanged.from} -> ${mod.levelChanged.to}`);\n }\n for (const change of mod.changedFields) {\n parts.push(\n `${change.field}: ${formatFieldValue(change.from)} -> ${formatFieldValue(change.to)}`\n );\n }\n if (parts.length > 0) {\n summaryParts.push(`${mod.project}: ${parts.join('; ')}`);\n }\n }\n\n return summaryParts.join('\\n');\n}\n"]}
@@ -143,6 +143,7 @@ function formatEntryLines(entry) {
143
143
  pushOptionalField(lines, 'shortDescription', entry.shortDescription);
144
144
  pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);
145
145
  pushOptionalField(lines, 'designFile', entry.designFile);
146
+ pushApiRelationsField(lines, entry.apiRelations);
146
147
  if (entry.dependsOn.length === 0) {
147
148
  lines.push(` "dependsOn": []`);
148
149
  }
@@ -173,6 +174,23 @@ function pushOptionalArrayField(lines, field, value) {
173
174
  lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);
174
175
  }
175
176
  }
177
+ /**
178
+ * Emit the optional `apiRelations` object (pretty, multi-line, reindented under
179
+ * the 12-space entry block) with a trailing comma, since `dependsOn` always
180
+ * follows it. Skipped when absent/empty so plain-lib-only projects stay compact.
181
+ * The scanner already sorts owners + refs, so the JSON is deterministic.
182
+ */
183
+ // webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here
184
+ function pushApiRelationsField(lines, value) {
185
+ if (value === undefined || Object.keys(value).length === 0)
186
+ return;
187
+ const pretty = JSON.stringify(value, null, 4).split('\n');
188
+ pretty.forEach((line, index) => {
189
+ const prefix = index === 0 ? '"apiRelations": ' : '';
190
+ const suffix = index === pretty.length - 1 ? ',' : '';
191
+ lines.push(` ${prefix}${line}${suffix}`);
192
+ });
193
+ }
176
194
  /**
177
195
  * Save the graph to disk in the wrapper format with the standard aiInstructions.
178
196
  *
@@ -1 +1 @@
1
- {"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n *\n * File format (schema aimed at AI consumers):\n * {\n * \"aiInstructions\": \"...how AI should use the per-project fields...\",\n * \"projects\": {\n * \"<project>\": { level, framework, shortDescription,\n * responsibilitiesFile, designFile, dependsOn }\n * }\n * }\n *\n * `framework` is the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]). It comes from the project's `framework:` nx tags\n * and is enforced across edges by the `library-types-match-client` rule.\n *\n * The legacy format (flat { \"<project>\": { level, dependsOn } } map) is still\n * readable so validation against a pre-upgrade file produces a clean\n * \"re-run architecture:generate\" diff instead of a parse failure.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { toError } from '../toError';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Top-level instructions embedded in dependencies.json telling AI how to use\n * the per-project metadata fields.\n */\nexport const AI_INSTRUCTIONS =\n \"Each project's shortDescription is only a summary. BEFORE adding code to a project, \" +\n 'read its responsibilitiesFile for the full responsibilities (what belongs in that ' +\n 'project and what does not), and read its designFile to understand the DI design ' +\n 'before reading the code. Use the entries in `commands` to regenerate these files ' +\n 'or display any of the graphs in a browser.';\n\n/**\n * Named command → \"command — what it does\" map embedded in dependencies.json.\n */\nexport type CommandMap = Record<string, string>;\n\n/**\n * Commands embedded in dependencies.json so AI (and humans) know how to\n * regenerate and DISPLAY the architecture + design graphs. These work in any\n * repo consuming @webpieces/nx-webpieces-rules.\n */\nexport const GRAPH_COMMANDS: CommandMap = {\n regenerateArchitecture:\n 'pnpm nx run architecture:generate — rewrites architecture/dependencies.json and ' +\n 'architecture/runtime-dependencies.json; run after adding/removing project dependencies',\n visualizeArchitecture:\n 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +\n 'as HTML in a browser',\n visualizeRuntimeArchitecture:\n 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +\n 'as HTML in a browser',\n regenerateDesigns:\n \"pnpm nx run-many --target=di-graph-generate — rewrites every project's \" +\n 'design.json/design.md (also runs automatically on every build)',\n visualizeDesign:\n \"pnpm wp-design-visualize <project> — opens a project's DI designs (its designFile) as \" +\n 'HTML, one graph per controller with the controller at the top; no args = interactive picker',\n};\n\n/**\n * The full contents of architecture/dependencies.json.\n */\nexport class DependenciesFile {\n constructor(\n public readonly aiInstructions: string,\n public readonly commands: CommandMap,\n public readonly projects: EnhancedGraph\n ) {}\n}\n\n/**\n * Load the blessed graph from disk. Understands both the current wrapper\n * format and the legacy flat map (which loads with empty aiInstructions).\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n * @returns The blessed graph file, or null if it doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): DependenciesFile | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n const parsed = JSON.parse(content);\n if (parsed !== null && typeof parsed === 'object' && 'projects' in parsed) {\n return new DependenciesFile(\n typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '',\n parsed.commands !== null && typeof parsed.commands === 'object' ? (parsed.commands as CommandMap) : {},\n parsed.projects as EnhancedGraph\n );\n }\n // Legacy flat format: the whole object is the project map\n return new DependenciesFile('', {}, parsed as EnhancedGraph);\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load graph from ${fullPath}`, { cause: error });\n }\n}\n\n/**\n * Format the dependencies file as JSON with multi-line arrays for readability\n */\nfunction formatGraphJson(file: DependenciesFile): string {\n const lines: string[] = ['{'];\n lines.push(` \"aiInstructions\": ${JSON.stringify(file.aiInstructions)},`);\n lines.push(` \"commands\": {`);\n const commandNames = Object.keys(file.commands);\n commandNames.forEach((name: string, index: number) => {\n const comma = index === commandNames.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`);\n });\n lines.push(` },`);\n lines.push(` \"projects\": {`);\n\n const keys = Object.keys(file.projects).sort();\n keys.forEach((key: string, index: number) => {\n const entry = file.projects[key];\n const isLast = index === keys.length - 1;\n const comma = isLast ? '' : ',';\n\n lines.push(` ${JSON.stringify(key)}: {`);\n lines.push(...formatEntryLines(entry));\n lines.push(` }${comma}`);\n });\n\n lines.push(' }');\n lines.push('}');\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * Format one project entry's fields (12-space indent). Optional metadata\n * fields are only emitted when present.\n */\nfunction formatEntryLines(entry: GraphEntry): string[] {\n const lines: string[] = [];\n lines.push(` \"level\": ${entry.level},`);\n\n pushOptionalArrayField(lines, 'framework', entry.framework);\n pushOptionalField(lines, 'role', entry.role);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\n\n if (entry.dependsOn.length === 0) {\n lines.push(` \"dependsOn\": []`);\n } else {\n lines.push(` \"dependsOn\": [`);\n entry.dependsOn.forEach((dep: string, depIndex: number) => {\n const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(dep)}${depComma}`);\n });\n lines.push(` ]`);\n }\n return lines;\n}\n\n/**\n * Emit one optional string field (12-space indent), skipped when undefined.\n */\nfunction pushOptionalField(lines: string[], field: string, value: string | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional string-array field (12-space indent) as a compact inline\n * JSON array (e.g. `\"framework\": [\"browser\",\"node\"],`), skipped when undefined.\n */\nfunction pushOptionalArrayField(lines: string[], field: string, value: string[] | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Save the graph to disk in the wrapper format with the standard aiInstructions.\n *\n * @param graph - The enriched project graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = formatGraphJson(new DependenciesFile(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph));\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
1
+ {"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n *\n * File format (schema aimed at AI consumers):\n * {\n * \"aiInstructions\": \"...how AI should use the per-project fields...\",\n * \"projects\": {\n * \"<project>\": { level, framework, shortDescription,\n * responsibilitiesFile, designFile, dependsOn }\n * }\n * }\n *\n * `framework` is the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]). It comes from the project's `framework:` nx tags\n * and is enforced across edges by the `library-types-match-client` rule.\n *\n * The legacy format (flat { \"<project>\": { level, dependsOn } } map) is still\n * readable so validation against a pre-upgrade file produces a clean\n * \"re-run architecture:generate\" diff instead of a parse failure.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport type { ProjectApiRelations } from './api-usage/api-relations';\nimport { toError } from '../toError';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Top-level instructions embedded in dependencies.json telling AI how to use\n * the per-project metadata fields.\n */\nexport const AI_INSTRUCTIONS =\n \"Each project's shortDescription is only a summary. BEFORE adding code to a project, \" +\n 'read its responsibilitiesFile for the full responsibilities (what belongs in that ' +\n 'project and what does not), and read its designFile to understand the DI design ' +\n 'before reading the code. Use the entries in `commands` to regenerate these files ' +\n 'or display any of the graphs in a browser.';\n\n/**\n * Named command → \"command — what it does\" map embedded in dependencies.json.\n */\nexport type CommandMap = Record<string, string>;\n\n/**\n * Commands embedded in dependencies.json so AI (and humans) know how to\n * regenerate and DISPLAY the architecture + design graphs. These work in any\n * repo consuming @webpieces/nx-webpieces-rules.\n */\nexport const GRAPH_COMMANDS: CommandMap = {\n regenerateArchitecture:\n 'pnpm nx run architecture:generate — rewrites architecture/dependencies.json and ' +\n 'architecture/runtime-dependencies.json; run after adding/removing project dependencies',\n visualizeArchitecture:\n 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +\n 'as HTML in a browser',\n visualizeRuntimeArchitecture:\n 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +\n 'as HTML in a browser',\n regenerateDesigns:\n \"pnpm nx run-many --target=di-graph-generate — rewrites every project's \" +\n 'design.json/design.md (also runs automatically on every build)',\n visualizeDesign:\n \"pnpm wp-design-visualize <project> — opens a project's DI designs (its designFile) as \" +\n 'HTML, one graph per controller with the controller at the top; no args = interactive picker',\n};\n\n/**\n * The full contents of architecture/dependencies.json.\n */\nexport class DependenciesFile {\n constructor(\n public readonly aiInstructions: string,\n public readonly commands: CommandMap,\n public readonly projects: EnhancedGraph\n ) {}\n}\n\n/**\n * Load the blessed graph from disk. Understands both the current wrapper\n * format and the legacy flat map (which loads with empty aiInstructions).\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n * @returns The blessed graph file, or null if it doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): DependenciesFile | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n const parsed = JSON.parse(content);\n if (parsed !== null && typeof parsed === 'object' && 'projects' in parsed) {\n return new DependenciesFile(\n typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '',\n parsed.commands !== null && typeof parsed.commands === 'object' ? (parsed.commands as CommandMap) : {},\n parsed.projects as EnhancedGraph\n );\n }\n // Legacy flat format: the whole object is the project map\n return new DependenciesFile('', {}, parsed as EnhancedGraph);\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load graph from ${fullPath}`, { cause: error });\n }\n}\n\n/**\n * Format the dependencies file as JSON with multi-line arrays for readability\n */\nfunction formatGraphJson(file: DependenciesFile): string {\n const lines: string[] = ['{'];\n lines.push(` \"aiInstructions\": ${JSON.stringify(file.aiInstructions)},`);\n lines.push(` \"commands\": {`);\n const commandNames = Object.keys(file.commands);\n commandNames.forEach((name: string, index: number) => {\n const comma = index === commandNames.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`);\n });\n lines.push(` },`);\n lines.push(` \"projects\": {`);\n\n const keys = Object.keys(file.projects).sort();\n keys.forEach((key: string, index: number) => {\n const entry = file.projects[key];\n const isLast = index === keys.length - 1;\n const comma = isLast ? '' : ',';\n\n lines.push(` ${JSON.stringify(key)}: {`);\n lines.push(...formatEntryLines(entry));\n lines.push(` }${comma}`);\n });\n\n lines.push(' }');\n lines.push('}');\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * Format one project entry's fields (12-space indent). Optional metadata\n * fields are only emitted when present.\n */\nfunction formatEntryLines(entry: GraphEntry): string[] {\n const lines: string[] = [];\n lines.push(` \"level\": ${entry.level},`);\n\n pushOptionalArrayField(lines, 'framework', entry.framework);\n pushOptionalField(lines, 'role', entry.role);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\n pushApiRelationsField(lines, entry.apiRelations);\n\n if (entry.dependsOn.length === 0) {\n lines.push(` \"dependsOn\": []`);\n } else {\n lines.push(` \"dependsOn\": [`);\n entry.dependsOn.forEach((dep: string, depIndex: number) => {\n const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(dep)}${depComma}`);\n });\n lines.push(` ]`);\n }\n return lines;\n}\n\n/**\n * Emit one optional string field (12-space indent), skipped when undefined.\n */\nfunction pushOptionalField(lines: string[], field: string, value: string | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional string-array field (12-space indent) as a compact inline\n * JSON array (e.g. `\"framework\": [\"browser\",\"node\"],`), skipped when undefined.\n */\nfunction pushOptionalArrayField(lines: string[], field: string, value: string[] | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `apiRelations` object (pretty, multi-line, reindented under\n * the 12-space entry block) with a trailing comma, since `dependsOn` always\n * follows it. Skipped when absent/empty so plain-lib-only projects stay compact.\n * The scanner already sorts owners + refs, so the JSON is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushApiRelationsField(lines: string[], value: ProjectApiRelations | undefined): void {\n if (value === undefined || Object.keys(value).length === 0) return;\n const pretty = JSON.stringify(value, null, 4).split('\\n');\n pretty.forEach((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiRelations\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n lines.push(` ${prefix}${line}${suffix}`);\n });\n}\n\n/**\n * Save the graph to disk in the wrapper format with the standard aiInstructions.\n *\n * @param graph - The enriched project graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = formatGraphJson(new DependenciesFile(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph));\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
@@ -6,6 +6,7 @@
6
6
  * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)
7
7
  * 3. Group projects into layers for deterministic ordering
8
8
  */
9
+ import type { ProjectApiRelations } from './api-usage/api-relations';
9
10
  /**
10
11
  * Graph entry with level metadata plus AI-oriented metadata filled in by
11
12
  * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:
@@ -17,6 +18,9 @@
17
18
  * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc
18
19
  * - designFile: repo-relative path to the generated DI design.json (only for
19
20
  * project.json projects)
21
+ * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the
22
+ * APIs this project implements (serves) and/or uses (calls), each with its
23
+ * transport (rpc | pubsub). Derived by scanning source (see api-usage/).
20
24
  */
21
25
  export interface GraphEntry {
22
26
  level: number;
@@ -26,6 +30,7 @@ export interface GraphEntry {
26
30
  shortDescription?: string;
27
31
  responsibilitiesFile?: string;
28
32
  designFile?: string;
33
+ apiRelations?: ProjectApiRelations;
29
34
  }
30
35
  /**
31
36
  * Enhanced graph format with level information
@@ -1 +1 @@
1
- {"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
1
+ {"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}