@webpieces/nx-webpieces-rules 0.4.464 → 0.4.465

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.464",
3
+ "version": "0.4.465",
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.4.464",
25
- "@webpieces/code-rules": "0.4.464",
26
- "@webpieces/eslint-rules": "0.4.464",
27
- "@webpieces/pr-gate": "0.4.464",
28
- "@webpieces/rules-config": "0.4.464",
24
+ "@webpieces/ai-hook-rules": "0.4.465",
25
+ "@webpieces/code-rules": "0.4.465",
26
+ "@webpieces/eslint-rules": "0.4.465",
27
+ "@webpieces/pr-gate": "0.4.465",
28
+ "@webpieces/rules-config": "0.4.465",
29
29
  "madge": "8.0.0"
30
30
  },
31
31
  "peerDependencies": {
@@ -88,16 +88,7 @@ function enrichGraph(graph, infos, workspaceRoot) {
88
88
  else if (roleResolution.role !== null) {
89
89
  entry.role = roleResolution.role;
90
90
  }
91
- // The runtime name clients address this app by — declared, never derived
92
- // (see service-name-resolver.ts). Most projects declare none.
93
- const serviceResolution = (0, service_name_resolver_1.resolveServiceName)(info, workspaceRoot);
94
- if (serviceResolution.problem !== null) {
95
- problems.push(serviceResolution.problem);
96
- }
97
- else if (serviceResolution.serviceName !== null) {
98
- entry.serviceName = serviceResolution.serviceName;
99
- serviceNames.set(projectName, serviceResolution.serviceName);
100
- }
91
+ enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);
101
92
  // Only persist the field when hidden (false); drawn projects (the
102
93
  // default) stay clean in dependencies.json with no drawOnGraph line.
103
94
  const drawResolution = (0, draw_on_graph_resolver_1.resolveDrawOnGraph)(info);
@@ -218,6 +209,31 @@ function validateRoleDependencies(graph, problems) {
218
209
  }
219
210
  }
220
211
  }
212
+ /**
213
+ * Fill the two symmetric client-addressing fields — `serviceName` (the name clients address THIS app
214
+ * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the
215
+ * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).
216
+ */
217
+ // webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities
218
+ function enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems) {
219
+ const serviceResolution = (0, service_name_resolver_1.resolveServiceName)(info, workspaceRoot);
220
+ if (serviceResolution.problem !== null) {
221
+ problems.push(serviceResolution.problem);
222
+ }
223
+ else if (serviceResolution.serviceName !== null) {
224
+ entry.serviceName = serviceResolution.serviceName;
225
+ serviceNames.set(projectName, serviceResolution.serviceName);
226
+ }
227
+ // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site
228
+ // literal and the fan-out fallback.
229
+ const callsResolution = (0, service_name_resolver_1.resolveCallsService)(info, workspaceRoot);
230
+ if (callsResolution.problem !== null) {
231
+ problems.push(callsResolution.problem);
232
+ }
233
+ else if (callsResolution.callsService !== null) {
234
+ entry.callsService = callsResolution.callsService;
235
+ }
236
+ }
221
237
  function enrichResponsibilities(entry, info, workspaceRoot, problems) {
222
238
  const responsibilitiesFile = toRepoRelative(info.root, exports.RESPONSIBILITIES_FILE_NAME);
223
239
  entry.responsibilitiesFile = responsibilitiesFile;
@@ -1 +1 @@
1
- {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * serviceName — from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n // The runtime name clients address this app by — declared, never derived\n // (see service-name-resolver.ts). Most projects declare none.\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. Every\n // project.json project gets a design.json written, but plain libs get an\n // empty `{ designs: [] }` — those must NOT become clickable in the arch\n // viz (designHtmlHref keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice — the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child → parent: react → browser, angular → browser, express → node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express). For a dependency edge Consumer C → Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angular→browser, express→node). Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | designed-lib | lib | client).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle → app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps — it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design — a committed design.json\n * whose `designs[]` is non-empty (i.e. it has ≥1 @DocumentDesign root). Plain\n * libs get a `{ designs: [] }` file written, which must read as \"no design\" so\n * the arch viz does not render them as clickable. A missing/unparseable file is\n * treated as \"no design\".\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * serviceName — from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveCallsService, resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. Every\n // project.json project gets a design.json written, but plain libs get an\n // empty `{ designs: [] }` — those must NOT become clickable in the arch\n // viz (designHtmlHref keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice — the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child → parent: react → browser, angular → browser, express → node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express). For a dependency edge Consumer C → Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angular→browser, express→node). Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | designed-lib | lib | client).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle → app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps — it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\n/**\n * Fill the two symmetric client-addressing fields — `serviceName` (the name clients address THIS app\n * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the\n * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).\n */\n// webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities\nfunction enrichClientNames(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n projectName: string,\n serviceNames: Map<string, string>,\n problems: string[]\n): void {\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site\n // literal and the fan-out fallback.\n const callsResolution = resolveCallsService(info, workspaceRoot);\n if (callsResolution.problem !== null) {\n problems.push(callsResolution.problem);\n } else if (callsResolution.callsService !== null) {\n entry.callsService = callsResolution.callsService;\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design — a committed design.json\n * whose `designs[]` is non-empty (i.e. it has ≥1 @DocumentDesign root). Plain\n * libs get a `{ designs: [] }` file written, which must read as \"no design\" so\n * the arch viz does not render them as clickable. A missing/unparseable file is\n * treated as \"no design\".\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
@@ -33,6 +33,15 @@ export interface GraphEntry {
33
33
  * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.
34
34
  */
35
35
  serviceName?: string;
36
+ /**
37
+ * The service(s) this project's clients call when the call site cannot carry a literal
38
+ * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as
39
+ * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims
40
+ * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a
41
+ * project that never calls anything, or one whose call sites all carry literals.
42
+ * See service-name-resolver.ts.
43
+ */
44
+ callsService?: string | Record<string, string>;
36
45
  /**
37
46
  * When false, the project is hidden from the rendered architecture graphs
38
47
  * (its box AND every edge touching it are omitted from dependencies.html and
@@ -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\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 /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\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"]}
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 /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\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"]}
@@ -12,15 +12,20 @@
12
12
  * Y and X `implements` api Y. This edge does not exist in the compile-time
13
13
  * dependencies.json (both Z and X only compile-depend on the api library Y).
14
14
  *
15
- * WHICH X is decided by the call site, not by "everyone who implements Y":
16
- * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` names its target, kept as
17
- * `ApiRef.targetService`, and matched against each node's DECLARED `serviceName`
18
- * (project.json metadata.webpieces.serviceName). Fanning an edge out to every
19
- * implementer instead is catastrophic for a company-wide contract registered in a
20
- * shared library it manufactures calls that cannot happen, and cycles that do
21
- * not exist. When a target cannot be resolved the old fan-out still happens, but
22
- * a warning names the call site (see RuntimeGraphReport.warnings): a wrong-but-
23
- * green graph is worse than a failing one, so it must never degrade silently.
15
+ * WHICH X is decided by the call site, not by "everyone who implements Y", in
16
+ * priority order: (1) a literal at the call site —
17
+ * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` kept as
18
+ * `ApiRef.targetService`; else (2) the calling project's declared `callsService`
19
+ * (project.json metadata.webpieces.callsService), the symmetric half of
20
+ * `serviceName` for the shared-library case where the client is built once from a
21
+ * config field so no literal can sit at the call site; else (3) fan-out. A named
22
+ * target from (1) or (2) is matched against each node's DECLARED `serviceName`.
23
+ * Fanning an edge out to every implementer is catastrophic for a company-wide
24
+ * contract registered in a shared library — it manufactures calls that cannot
25
+ * happen, and cycles that do not exist. When a target cannot be resolved the old
26
+ * fan-out still happens, but a warning names the call site (see
27
+ * RuntimeGraphReport.warnings): a wrong-but-green graph is worse than a failing
28
+ * one, so it must never degrade silently.
24
29
  */
25
30
  import type { EnhancedGraph } from './graph-sorter';
26
31
  import type { ApiTransport } from './api-usage/api-relations';
@@ -32,6 +37,13 @@ export interface RuntimeService {
32
37
  * project.json. Absent for a service nothing calls by name (e.g. a browser app).
33
38
  */
34
39
  serviceName?: string;
40
+ /**
41
+ * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,
42
+ * declared in its project.json (metadata.webpieces.callsService). A single name, or an
43
+ * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors
44
+ * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.
45
+ */
46
+ callsService?: string | Record<string, string>;
35
47
  implements: string[];
36
48
  /**
37
49
  * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis
@@ -13,15 +13,20 @@
13
13
  * Y and X `implements` api Y. This edge does not exist in the compile-time
14
14
  * dependencies.json (both Z and X only compile-depend on the api library Y).
15
15
  *
16
- * WHICH X is decided by the call site, not by "everyone who implements Y":
17
- * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` names its target, kept as
18
- * `ApiRef.targetService`, and matched against each node's DECLARED `serviceName`
19
- * (project.json metadata.webpieces.serviceName). Fanning an edge out to every
20
- * implementer instead is catastrophic for a company-wide contract registered in a
21
- * shared library it manufactures calls that cannot happen, and cycles that do
22
- * not exist. When a target cannot be resolved the old fan-out still happens, but
23
- * a warning names the call site (see RuntimeGraphReport.warnings): a wrong-but-
24
- * green graph is worse than a failing one, so it must never degrade silently.
16
+ * WHICH X is decided by the call site, not by "everyone who implements Y", in
17
+ * priority order: (1) a literal at the call site —
18
+ * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` kept as
19
+ * `ApiRef.targetService`; else (2) the calling project's declared `callsService`
20
+ * (project.json metadata.webpieces.callsService), the symmetric half of
21
+ * `serviceName` for the shared-library case where the client is built once from a
22
+ * config field so no literal can sit at the call site; else (3) fan-out. A named
23
+ * target from (1) or (2) is matched against each node's DECLARED `serviceName`.
24
+ * Fanning an edge out to every implementer is catastrophic for a company-wide
25
+ * contract registered in a shared library — it manufactures calls that cannot
26
+ * happen, and cycles that do not exist. When a target cannot be resolved the old
27
+ * fan-out still happens, but a warning names the call site (see
28
+ * RuntimeGraphReport.warnings): a wrong-but-green graph is worse than a failing
29
+ * one, so it must never degrade silently.
25
30
  */
26
31
  Object.defineProperty(exports, "__esModule", { value: true });
27
32
  exports.RuntimeGraphReport = exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
@@ -302,25 +307,61 @@ class RuntimeGraphDeriver {
302
307
  return { edges: this.edgesFromKeys(viaByKey), unresolved: sortUnresolved(unresolved) };
303
308
  }
304
309
  /**
305
- * WHICH implementers this one `uses` reaches. A call site naming its target resolves to exactly
306
- * ONE node; anything else falls back to the historical fan-out the only safe superset — and
307
- * records WHY, so a fanned-out (i.e. possibly fictional) edge can never pass for a derived one.
310
+ * WHICH implementers this one `uses` reaches. Resolution order (most specific wins):
311
+ * 1. a literal `ClientConfig` at the call site (`ref.targetService`)names ONE node;
312
+ * 2. else the calling project's declared `callsService` (project.json) for the shared-library
313
+ * case where the literal cannot sit at the call site, it lives one indirection away in the app;
314
+ * 3. else the historical fan-out — the only safe superset — recording WHY, so a fanned-out (i.e.
315
+ * possibly fictional) edge can never pass for a derived one.
316
+ * A named target (1 or 2) that resolves to no module, or to a module that does not serve the
317
+ * contract, is a PROBLEM (fails the build), never a silent drop.
308
318
  */
309
319
  targetsFor(user, ref, implementers) {
310
- const wanted = ref.targetService;
311
- if (wanted === undefined)
312
- return this.untargetedFanOut(user, ref, implementers);
320
+ const literal = ref.targetService;
321
+ if (literal !== undefined)
322
+ return this.resolveNamedTarget(user, ref, implementers, literal, 'literal');
323
+ const declared = this.callsServiceFor(user, ref.api);
324
+ if (declared !== undefined)
325
+ return this.resolveNamedTarget(user, ref, implementers, declared, 'callsService');
326
+ return this.untargetedFanOut(user, ref, implementers);
327
+ }
328
+ /**
329
+ * The declared `callsService` target for this node's use of `api`, or undefined when none applies.
330
+ * A string declaration aims every untargeted use at one service; a map aims per api-class.
331
+ */
332
+ callsServiceFor(user, api) {
333
+ const declared = this.projects[user]?.callsService;
334
+ if (declared === undefined)
335
+ return undefined;
336
+ if (typeof declared === 'string')
337
+ return declared;
338
+ return declared[api];
339
+ }
340
+ /**
341
+ * Resolve a NAMED target (from a call-site literal or a project-level `callsService`) to the one
342
+ * module that serves it. A name no module answers to, or a module that does not serve the
343
+ * contract, fails the build — the wording names which declaration to fix, `source` deciding it.
344
+ */
345
+ resolveNamedTarget(user, ref, implementers, wanted, source) {
313
346
  const node = this.nodeByServiceName.get(wanted);
347
+ const served = `${implementers.length} module(s) serve this contract (${implementers.join(', ')})`;
314
348
  if (node === undefined) {
315
- this.problems.push(`${user} calls "${ref.api}" at service '${wanted}', but NO module answers to that name. ` +
316
- `${implementers.length} module(s) serve this contract (${implementers.join(', ')}). Either use ` +
317
- `the module name, or declare metadata.webpieces.serviceName: '${wanted}' on the module that ` +
318
- `serves it (and translate any environment prefix in ClientRegistry.setDeriver, not here).`);
349
+ this.problems.push(source === 'callsService'
350
+ ? `${user} declares metadata.webpieces.callsService '${wanted}' in its project.json, but NO ` +
351
+ `module answers to that name. ${served}. Point callsService at a module name, or declare ` +
352
+ `metadata.webpieces.serviceName: '${wanted}' on the module that serves "${ref.api}".`
353
+ : `${user} calls "${ref.api}" at service '${wanted}', but NO module answers to that name. ` +
354
+ `${served}. Either use the module name, or declare metadata.webpieces.serviceName: ` +
355
+ `'${wanted}' on the module that serves it (and translate any environment prefix in ` +
356
+ `ClientRegistry.setDeriver, not here).`);
319
357
  return implementers;
320
358
  }
321
359
  if (!implementers.includes(node)) {
322
- this.problems.push(`${user} calls "${ref.api}" at service '${wanted}' (module ${node}), but ${node} does NOT serve ` +
323
- `that contract ${implementers.join(', ')} do. At runtime that call has nothing to answer it.`);
360
+ const via = source === 'callsService'
361
+ ? `${user} declares callsService '${wanted}' (module ${node})`
362
+ : `${user} calls "${ref.api}" at service '${wanted}' (module ${node})`;
363
+ this.problems.push(`${via}, but ${node} does NOT serve "${ref.api}" — ${implementers.join(', ')} do. At runtime that ` +
364
+ `call has nothing to answer it.`);
324
365
  return implementers;
325
366
  }
326
367
  return [node];
@@ -334,7 +375,9 @@ class RuntimeGraphDeriver {
334
375
  if (implementers.length > 1) {
335
376
  this.warnings.push(`${user} uses "${ref.api}" with no literal client config, and ${implementers.length} services ` +
336
377
  `implement it (${implementers.join(', ')}) — an edge is drawn to EVERY one, so all but one ` +
337
- `are fiction. Name the target: createRpcClient(${ref.api}, new ClientConfig('<serviceName>')).`);
378
+ `are fiction. Name the target: createRpcClient(${ref.api}, new ClientConfig('<serviceName>')); ` +
379
+ `or, when the client is built in a shared library (no literal can sit at the call site), ` +
380
+ `declare metadata.webpieces.callsService: '<serviceName>' on ${user}'s project.json.`);
338
381
  }
339
382
  return implementers;
340
383
  }
@@ -356,6 +399,7 @@ class RuntimeGraphDeriver {
356
399
  const service = {
357
400
  level: 0,
358
401
  serviceName: this.projects[decl.name]?.serviceName,
402
+ callsService: this.projects[decl.name]?.callsService,
359
403
  implements: decl.implementsApis.map((r) => r.api),
360
404
  implementsVia: decl.implementsVia.size > 0 ? sortedRecord(decl.implementsVia) : undefined,
361
405
  // One api used against two services is two refs but ONE api in this list.
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-graph.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Graph\n *\n * Derives the runtime microservice graph SOLELY from architecture/dependencies.json\n * (the single source of truth): each project's `apiRelations` carry which api\n * classes it implements/uses and their transport (rpc | pubsub). Both\n * `architecture:generate` and `architecture:validate-runtime-architecture` call\n * the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph\n * can never diverge.\n *\n * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api\n * Y and X `implements` api Y. This edge does not exist in the compile-time\n * dependencies.json (both Z and X only compile-depend on the api library Y).\n *\n * WHICH X is decided by the call site, not by \"everyone who implements Y\":\n * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` names its target, kept as\n * `ApiRef.targetService`, and matched against each node's DECLARED `serviceName`\n * (project.json metadata.webpieces.serviceName). Fanning an edge out to every\n * implementer instead is catastrophic for a company-wide contract registered in a\n * shared library — it manufactures calls that cannot happen, and cycles that do\n * not exist. When a target cannot be resolved the old fan-out still happens, but\n * a warning names the call site (see RuntimeGraphReport.warnings): a wrong-but-\n * green graph is worse than a failing one, so it must never degrade silently.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { sortGraphTopologically } from './graph-sorter';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { ApiRef, ApiTransport } from './api-usage/api-relations';\nimport { apiRefKey, sortApiRefs } from './api-usage/api-relations';\nimport { toError } from '../toError';\n\nexport const DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n}\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n}\n\n/**\n * The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not\n * part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from\n * passing for a derived one. Executors print it.\n */\nexport class RuntimeGraphReport {\n constructor(\n public readonly graph: RuntimeGraph,\n /** Human-readable lines naming every call site whose target the graph could not pin down. */\n public readonly warnings: string[],\n /**\n * Call sites that name a target the repo does not contain. Unlike a warning these FAIL the\n * build: the contract IS served in-repo, so the name is a typo or a stale rename, and the\n * only reason it stayed invisible is that the graph quietly fanned the edge out instead.\n * (A call to a service outside the repo never reaches here — nothing in-repo implements its\n * contract, so it is `unresolvedUses`.)\n */\n public readonly problems: string[] = [],\n ) {}\n}\n\n/** Adjacency (service -> [targets]) used for leveling + cycle checks. */\nfunction adjacencyFromEdges(serviceNames: string[], edges: RuntimeEdge[]): Record<string, string[]> {\n const adj: Record<string, string[]> = {};\n for (const name of serviceNames) adj[name] = [];\n for (const edge of edges) {\n if (!adj[edge.from]) adj[edge.from] = [];\n adj[edge.from].push(edge.to);\n }\n return adj;\n}\n\n/** Adjacency (service -> [targets]) from a loaded runtime graph. */\nexport function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]> {\n return adjacencyFromEdges(Object.keys(graph.services), graph.runtimeEdges);\n}\n\n/** Assign levels via topological sort; falls back to level 0 when a cycle exists. */\nfunction assignLevels(adjacency: Record<string, string[]>): Record<string, number> {\n const levels: Record<string, number> = {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const sorted = sortGraphTopologically(adjacency);\n for (const name of Object.keys(sorted)) levels[name] = sorted[name].level;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n for (const name of Object.keys(adjacency)) levels[name] = 0;\n }\n return levels;\n}\n\n/** One project's implements/uses at api-CLASS granularity, from dependencies.json apiRelations. */\ninterface ScanDecl {\n name: string;\n implementsApis: ApiRef[];\n usesApis: ApiRef[];\n /** apiClassName -> the embedded LIBRARY project that declared the implements (never the node itself). */\n implementsVia: Map<string, string>;\n}\n\n/**\n * Accumulates one node's effective relations while its dependsOn closure is walked, keeping the\n * PROVENANCE the walk would otherwise throw away: which library contributed an implements, and\n * which api-lib owns each contract.\n */\nclass RelationSink {\n readonly implementsApis: ApiRef[] = [];\n readonly usesApis: ApiRef[] = [];\n readonly implementsVia = new Map<string, string>();\n\n constructor(\n /** The runtime node these relations are attributed to. */\n private readonly node: string,\n ) {}\n\n /** `from` is the project whose apiRelations declared this — the node itself, or a lib it embeds. */\n addImplements(ref: ApiRef, from: string): void {\n this.implementsApis.push(ref);\n // First contributor wins, matching dedupApiRefs' keep-the-first rule on the ref list.\n if (from !== this.node && !this.implementsVia.has(ref.api)) this.implementsVia.set(ref.api, from);\n }\n\n addUses(ref: ApiRef): void {\n this.usesApis.push(ref);\n }\n}\n\n/**\n * Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:\n * implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows\n * through a queue (drawn producer → queue → consumer by the visualizer).\n *\n * Nodes are the runnable apps only — role:server / role:client. A library is NEVER a node; its\n * implements/uses (a shared client-factory or route-registration lib) are attributed to the\n * servers/clients that embed it, walked transitively over dependsOn (see collectDecls).\n */\nclass RuntimeGraphDeriver {\n /** apiClassName -> the api-lib project that owns the contract (from the apiRelations key). */\n private readonly apiOwners = new Map<string, string>();\n /** Addressable name -> the runtime node answering to it; how a targeted call resolves. */\n private readonly nodeByServiceName = new Map<string, string>();\n private readonly warnings: string[] = [];\n private readonly problems: string[] = [];\n\n constructor(\n private readonly projects: EnhancedGraph,\n /** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */\n private readonly hiddenProjects: Set<string>\n ) {\n // A node ALWAYS answers to its own module name, so a repo whose deployed names match its\n // project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away\n // from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.\n for (const name of Object.keys(projects).sort()) {\n if (this.isNode(name)) this.nodeByServiceName.set(name, name);\n }\n // `serviceName` then adds the alias a repo needs when its deployed name is NOT its module\n // name (helper-svr serving 'helper-portal'). Colliding with a module is a misconfiguration,\n // not a precedence question: the alias is silently unreachable, so say so.\n for (const name of Object.keys(projects).sort()) {\n const declared = projects[name].serviceName;\n if (!this.isNode(name) || declared === undefined) continue;\n const claimant = this.nodeByServiceName.get(declared);\n if (claimant === undefined) {\n this.nodeByServiceName.set(declared, name);\n } else if (claimant !== name) {\n this.problems.push(\n `${name} declares serviceName '${declared}', but that is already the module name of ` +\n `${claimant} — the alias can never be reached. Rename one of them.`,\n );\n }\n }\n }\n\n assemble(): RuntimeGraphReport {\n const decls = this.collectDecls();\n const apis = this.buildApis(decls);\n const edgeResult = this.buildEdges(decls, apis);\n const services = this.buildServices(decls, edgeResult.edges);\n const apisObj: Record<string, RuntimeApi> = {};\n for (const api of Array.from(apis.keys()).sort()) apisObj[api] = apis.get(api)!;\n const graph: RuntimeGraph = {\n services,\n apis: apisObj,\n runtimeEdges: edgeResult.edges,\n unresolvedUses: edgeResult.unresolved,\n };\n return new RuntimeGraphReport(graph, this.warnings, this.problems);\n }\n\n /**\n * One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective\n * relations are its OWN apiRelations PLUS those of every library in its transitive\n * dependsOn closure: a lib that calls createRpcClient/addRoutes runs inside the\n * server/client that embeds it, so the relation is attributed to that node — the\n * lib is never a runtime node. Recursion stops at other nodes (each server/client\n * owns its own relations). dependsOn is transitively reduced, but reduction\n * preserves reachability, so the recursive walk still reaches every lib a node\n * embeds through libraries.\n */\n private collectDecls(): ScanDecl[] {\n const decls: ScanDecl[] = [];\n for (const name of Object.keys(this.projects).sort()) {\n if (!this.isNode(name)) continue;\n const sink = new RelationSink(name);\n this.collectEffectiveRelations(name, sink, new Set<string>([name]));\n if (sink.implementsApis.length > 0 || sink.usesApis.length > 0) {\n decls.push({\n name,\n implementsApis: dedupApiRefs(sortApiRefs(sink.implementsApis)),\n usesApis: dedupApiRefs(sortApiRefs(sink.usesApis)),\n implementsVia: sink.implementsVia,\n });\n }\n }\n return decls;\n }\n\n /** True when a project is a runtime node — a server or client app (never a library). */\n private isNode(name: string): boolean {\n const role = this.projects[name]?.role;\n return role === 'server' || role === 'client';\n }\n\n /**\n * Accumulate `name`'s own api relations, then recurse through its LIBRARY deps\n * (skipping other nodes, which own their own relations). `visited` guards against\n * re-walking a lib reachable by more than one path (and any cycle).\n */\n private collectEffectiveRelations(name: string, sink: RelationSink, visited: Set<string>): void {\n const entry = this.projects[name];\n if (entry === undefined) return;\n const relations = entry.apiRelations;\n if (relations !== undefined) {\n for (const owner of Object.keys(relations).sort()) {\n for (const ref of relations[owner].implements) {\n this.apiOwners.set(ref.api, owner);\n sink.addImplements(ref, name);\n }\n for (const ref of relations[owner].uses) {\n this.apiOwners.set(ref.api, owner);\n sink.addUses(ref);\n }\n }\n }\n for (const dep of entry.dependsOn) {\n if (visited.has(dep)) continue;\n visited.add(dep);\n if (this.isNode(dep)) continue; // another server/client owns its own relations\n this.collectEffectiveRelations(dep, sink, visited);\n }\n }\n\n /** apiClassName -> { implementedBy, usedBy, type }. */\n private buildApis(decls: ScanDecl[]): Map<string, RuntimeApi> {\n const apis = new Map<string, RuntimeApi>();\n const ensure = (api: string, type: ApiTransport): RuntimeApi => {\n let entry = apis.get(api);\n if (!entry) {\n entry = { implementedBy: [], usedBy: [], type };\n apis.set(api, entry);\n }\n return entry;\n };\n for (const decl of decls) {\n for (const ref of decl.implementsApis) ensure(ref.api, ref.type).implementedBy.push(decl.name);\n for (const ref of decl.usesApis) ensure(ref.api, ref.type).usedBy.push(decl.name);\n }\n for (const api of apis.keys()) {\n const entry = apis.get(api)!;\n entry.implementedBy.sort();\n entry.usedBy = Array.from(new Set(entry.usedBy)).sort();\n const owner = this.apiOwners.get(api);\n if (owner !== undefined) entry.owner = owner;\n }\n return apis;\n }\n\n /** Inferred edges U -> I via api A, split by transport; plus uses with no implementer. */\n private buildEdges(decls: ScanDecl[], apis: Map<string, RuntimeApi>): EdgeResult {\n const viaByKey = new Map<string, Set<string>>();\n const unresolved: RuntimeUnresolved[] = [];\n for (const decl of decls) {\n for (const ref of decl.usesApis) {\n const implementers = apis.get(ref.api)?.implementedBy ?? [];\n if (implementers.length === 0) {\n unresolved.push({ service: decl.name, api: ref.api });\n continue;\n }\n for (const target of this.targetsFor(decl.name, ref, implementers)) {\n if (target === decl.name) continue;\n const key = `${decl.name} ${target} ${ref.type}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(ref.api);\n }\n }\n }\n return { edges: this.edgesFromKeys(viaByKey), unresolved: sortUnresolved(unresolved) };\n }\n\n /**\n * WHICH implementers this one `uses` reaches. A call site naming its target resolves to exactly\n * ONE node; anything else falls back to the historical fan-out — the only safe superset — and\n * records WHY, so a fanned-out (i.e. possibly fictional) edge can never pass for a derived one.\n */\n private targetsFor(user: string, ref: ApiRef, implementers: string[]): string[] {\n const wanted = ref.targetService;\n if (wanted === undefined) return this.untargetedFanOut(user, ref, implementers);\n\n const node = this.nodeByServiceName.get(wanted);\n if (node === undefined) {\n this.problems.push(\n `${user} calls \"${ref.api}\" at service '${wanted}', but NO module answers to that name. ` +\n `${implementers.length} module(s) serve this contract (${implementers.join(', ')}). Either use ` +\n `the module name, or declare metadata.webpieces.serviceName: '${wanted}' on the module that ` +\n `serves it (and translate any environment prefix in ClientRegistry.setDeriver, not here).`,\n );\n return implementers;\n }\n if (!implementers.includes(node)) {\n this.problems.push(\n `${user} calls \"${ref.api}\" at service '${wanted}' (module ${node}), but ${node} does NOT serve ` +\n `that contract — ${implementers.join(', ')} do. At runtime that call has nothing to answer it.`,\n );\n return implementers;\n }\n return [node];\n }\n\n /**\n * A use with no literal client config. One implementer is unambiguous, so it stays silent; more\n * than one means every edge but one is fiction — exactly the failure this mechanism exists to\n * stop — so it is reported even though the graph still (conservatively) draws them all.\n */\n private untargetedFanOut(user: string, ref: ApiRef, implementers: string[]): string[] {\n if (implementers.length > 1) {\n this.warnings.push(\n `${user} uses \"${ref.api}\" with no literal client config, and ${implementers.length} services ` +\n `implement it (${implementers.join(', ')}) — an edge is drawn to EVERY one, so all but one ` +\n `are fiction. Name the target: createRpcClient(${ref.api}, new ClientConfig('<serviceName>')).`,\n );\n }\n return implementers;\n }\n\n private edgesFromKeys(viaByKey: Map<string, Set<string>>): RuntimeEdge[] {\n const edges: RuntimeEdge[] = [];\n for (const key of viaByKey.keys()) {\n const parts = key.split(' ');\n edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByKey.get(key)!).sort(), type: parts[2] as ApiTransport });\n }\n edges.sort(\n (a: RuntimeEdge, b: RuntimeEdge) =>\n a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.type ?? '').localeCompare(b.type ?? ''),\n );\n return edges;\n }\n\n private buildServices(decls: ScanDecl[], edges: RuntimeEdge[]): Record<string, RuntimeService> {\n const services: Record<string, RuntimeService> = {};\n for (const decl of decls) {\n const dependsOn = Array.from(\n new Set(edges.filter((e: RuntimeEdge) => e.from === decl.name).map((e: RuntimeEdge) => e.to)),\n ).sort();\n // Keys are written in this order; an undefined value is omitted by JSON.stringify, so\n // the committed JSON stays clean AND deterministic without conditional assembly.\n const service: RuntimeService = {\n level: 0,\n serviceName: this.projects[decl.name]?.serviceName,\n implements: decl.implementsApis.map((r: ApiRef) => r.api),\n implementsVia: decl.implementsVia.size > 0 ? sortedRecord(decl.implementsVia) : undefined,\n // One api used against two services is two refs but ONE api in this list.\n uses: Array.from(new Set(decl.usesApis.map((r: ApiRef) => r.api))),\n dependsOn,\n };\n services[decl.name] = service;\n if (this.hiddenProjects.has(decl.name)) services[decl.name].drawOnGraph = false;\n }\n const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edges));\n for (const name of Object.keys(services)) services[name].level = levels[name] ?? 0;\n return services;\n }\n}\n\n/**\n * Derive the runtime graph from dependencies.json's project `apiRelations` — the\n * single source of truth shared by generate + validate. `hiddenProjects`\n * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the\n * runtime visualizer omits their nodes + edges.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraph(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>()\n): RuntimeGraph {\n return deriveRuntimeGraphReport(projects, hiddenProjects).graph;\n}\n\n/**\n * The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use\n * this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that\n * only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph\n * file that records its own doubts would just get committed and stop being read.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraphReport(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>()\n): RuntimeGraphReport {\n return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();\n}\n\n/** Drop duplicate api refs, keeping the first — needed after a node absorbs the same api from both\n * its own relations and an embedded lib's. Keyed by api AND target service: the same contract aimed\n * at two different services is two distinct relations (two distinct edges), not a duplicate. Input\n * is pre-sorted, so output stays deterministic. */\n// webpieces-disable no-function-outside-class -- pure list helper, matches the sibling helpers in this file\nfunction dedupApiRefs(refs: ApiRef[]): ApiRef[] {\n const seen = new Set<string>();\n const out: ApiRef[] = [];\n for (const ref of refs) {\n const key = apiRefKey(ref);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(ref);\n }\n return out;\n}\n\n/** Sort a Map into a plain object with sorted keys, so the committed JSON is deterministic. */\n// webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file\nfunction sortedRecord(map: Map<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const key of [...map.keys()].sort()) out[key] = map.get(key)!;\n return out;\n}\n\n/** Sort AND de-duplicate: one api used against two targets must not be reported unresolved twice. */\n// webpieces-disable no-function-outside-class -- pure sort helper, matches the sibling helpers in this file\nfunction sortUnresolved(unresolved: RuntimeUnresolved[]): RuntimeUnresolved[] {\n const byKey = new Map<string, RuntimeUnresolved>();\n for (const entry of unresolved) byKey.set(`${entry.service} ${entry.api}`, entry);\n return [...byKey.values()].sort(\n (a: RuntimeUnresolved, b: RuntimeUnresolved) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api),\n );\n}\n\n/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */\nfunction formatRuntimeJson(graph: RuntimeGraph): string {\n return JSON.stringify(graph, null, 4) + '\\n';\n}\n\nexport function saveRuntimeGraph(\n graph: RuntimeGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');\n}\n\nexport function runtimeGraphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): boolean {\n return fs.existsSync(path.join(workspaceRoot, graphPath));\n}\n\nexport function loadRuntimeGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): RuntimeGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n if (!fs.existsSync(fullPath)) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as RuntimeGraph;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });\n }\n}\n\n/** Serialize for an in-memory equality check (matches the on-disk format). */\nexport function serializeRuntimeGraph(graph: RuntimeGraph): string {\n return formatRuntimeJson(graph);\n}\n"]}
1
+ {"version":3,"file":"runtime-graph.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Graph\n *\n * Derives the runtime microservice graph SOLELY from architecture/dependencies.json\n * (the single source of truth): each project's `apiRelations` carry which api\n * classes it implements/uses and their transport (rpc | pubsub). Both\n * `architecture:generate` and `architecture:validate-runtime-architecture` call\n * the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph\n * can never diverge.\n *\n * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api\n * Y and X `implements` api Y. This edge does not exist in the compile-time\n * dependencies.json (both Z and X only compile-depend on the api library Y).\n *\n * WHICH X is decided by the call site, not by \"everyone who implements Y\", in\n * priority order: (1) a literal at the call site —\n * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` — kept as\n * `ApiRef.targetService`; else (2) the calling project's declared `callsService`\n * (project.json metadata.webpieces.callsService), the symmetric half of\n * `serviceName` for the shared-library case where the client is built once from a\n * config field so no literal can sit at the call site; else (3) fan-out. A named\n * target from (1) or (2) is matched against each node's DECLARED `serviceName`.\n * Fanning an edge out to every implementer is catastrophic for a company-wide\n * contract registered in a shared library — it manufactures calls that cannot\n * happen, and cycles that do not exist. When a target cannot be resolved the old\n * fan-out still happens, but a warning names the call site (see\n * RuntimeGraphReport.warnings): a wrong-but-green graph is worse than a failing\n * one, so it must never degrade silently.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { sortGraphTopologically } from './graph-sorter';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { ApiRef, ApiTransport } from './api-usage/api-relations';\nimport { apiRefKey, sortApiRefs } from './api-usage/api-relations';\nimport { toError } from '../toError';\n\nexport const DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, string>;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n}\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n}\n\n/**\n * The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not\n * part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from\n * passing for a derived one. Executors print it.\n */\nexport class RuntimeGraphReport {\n constructor(\n public readonly graph: RuntimeGraph,\n /** Human-readable lines naming every call site whose target the graph could not pin down. */\n public readonly warnings: string[],\n /**\n * Call sites that name a target the repo does not contain. Unlike a warning these FAIL the\n * build: the contract IS served in-repo, so the name is a typo or a stale rename, and the\n * only reason it stayed invisible is that the graph quietly fanned the edge out instead.\n * (A call to a service outside the repo never reaches here — nothing in-repo implements its\n * contract, so it is `unresolvedUses`.)\n */\n public readonly problems: string[] = [],\n ) {}\n}\n\n/** Adjacency (service -> [targets]) used for leveling + cycle checks. */\nfunction adjacencyFromEdges(serviceNames: string[], edges: RuntimeEdge[]): Record<string, string[]> {\n const adj: Record<string, string[]> = {};\n for (const name of serviceNames) adj[name] = [];\n for (const edge of edges) {\n if (!adj[edge.from]) adj[edge.from] = [];\n adj[edge.from].push(edge.to);\n }\n return adj;\n}\n\n/** Adjacency (service -> [targets]) from a loaded runtime graph. */\nexport function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]> {\n return adjacencyFromEdges(Object.keys(graph.services), graph.runtimeEdges);\n}\n\n/** Assign levels via topological sort; falls back to level 0 when a cycle exists. */\nfunction assignLevels(adjacency: Record<string, string[]>): Record<string, number> {\n const levels: Record<string, number> = {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const sorted = sortGraphTopologically(adjacency);\n for (const name of Object.keys(sorted)) levels[name] = sorted[name].level;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n for (const name of Object.keys(adjacency)) levels[name] = 0;\n }\n return levels;\n}\n\n/** One project's implements/uses at api-CLASS granularity, from dependencies.json apiRelations. */\ninterface ScanDecl {\n name: string;\n implementsApis: ApiRef[];\n usesApis: ApiRef[];\n /** apiClassName -> the embedded LIBRARY project that declared the implements (never the node itself). */\n implementsVia: Map<string, string>;\n}\n\n/**\n * Accumulates one node's effective relations while its dependsOn closure is walked, keeping the\n * PROVENANCE the walk would otherwise throw away: which library contributed an implements, and\n * which api-lib owns each contract.\n */\nclass RelationSink {\n readonly implementsApis: ApiRef[] = [];\n readonly usesApis: ApiRef[] = [];\n readonly implementsVia = new Map<string, string>();\n\n constructor(\n /** The runtime node these relations are attributed to. */\n private readonly node: string,\n ) {}\n\n /** `from` is the project whose apiRelations declared this — the node itself, or a lib it embeds. */\n addImplements(ref: ApiRef, from: string): void {\n this.implementsApis.push(ref);\n // First contributor wins, matching dedupApiRefs' keep-the-first rule on the ref list.\n if (from !== this.node && !this.implementsVia.has(ref.api)) this.implementsVia.set(ref.api, from);\n }\n\n addUses(ref: ApiRef): void {\n this.usesApis.push(ref);\n }\n}\n\n/**\n * Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:\n * implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows\n * through a queue (drawn producer → queue → consumer by the visualizer).\n *\n * Nodes are the runnable apps only — role:server / role:client. A library is NEVER a node; its\n * implements/uses (a shared client-factory or route-registration lib) are attributed to the\n * servers/clients that embed it, walked transitively over dependsOn (see collectDecls).\n */\nclass RuntimeGraphDeriver {\n /** apiClassName -> the api-lib project that owns the contract (from the apiRelations key). */\n private readonly apiOwners = new Map<string, string>();\n /** Addressable name -> the runtime node answering to it; how a targeted call resolves. */\n private readonly nodeByServiceName = new Map<string, string>();\n private readonly warnings: string[] = [];\n private readonly problems: string[] = [];\n\n constructor(\n private readonly projects: EnhancedGraph,\n /** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */\n private readonly hiddenProjects: Set<string>\n ) {\n // A node ALWAYS answers to its own module name, so a repo whose deployed names match its\n // project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away\n // from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.\n for (const name of Object.keys(projects).sort()) {\n if (this.isNode(name)) this.nodeByServiceName.set(name, name);\n }\n // `serviceName` then adds the alias a repo needs when its deployed name is NOT its module\n // name (helper-svr serving 'helper-portal'). Colliding with a module is a misconfiguration,\n // not a precedence question: the alias is silently unreachable, so say so.\n for (const name of Object.keys(projects).sort()) {\n const declared = projects[name].serviceName;\n if (!this.isNode(name) || declared === undefined) continue;\n const claimant = this.nodeByServiceName.get(declared);\n if (claimant === undefined) {\n this.nodeByServiceName.set(declared, name);\n } else if (claimant !== name) {\n this.problems.push(\n `${name} declares serviceName '${declared}', but that is already the module name of ` +\n `${claimant} — the alias can never be reached. Rename one of them.`,\n );\n }\n }\n }\n\n assemble(): RuntimeGraphReport {\n const decls = this.collectDecls();\n const apis = this.buildApis(decls);\n const edgeResult = this.buildEdges(decls, apis);\n const services = this.buildServices(decls, edgeResult.edges);\n const apisObj: Record<string, RuntimeApi> = {};\n for (const api of Array.from(apis.keys()).sort()) apisObj[api] = apis.get(api)!;\n const graph: RuntimeGraph = {\n services,\n apis: apisObj,\n runtimeEdges: edgeResult.edges,\n unresolvedUses: edgeResult.unresolved,\n };\n return new RuntimeGraphReport(graph, this.warnings, this.problems);\n }\n\n /**\n * One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective\n * relations are its OWN apiRelations PLUS those of every library in its transitive\n * dependsOn closure: a lib that calls createRpcClient/addRoutes runs inside the\n * server/client that embeds it, so the relation is attributed to that node — the\n * lib is never a runtime node. Recursion stops at other nodes (each server/client\n * owns its own relations). dependsOn is transitively reduced, but reduction\n * preserves reachability, so the recursive walk still reaches every lib a node\n * embeds through libraries.\n */\n private collectDecls(): ScanDecl[] {\n const decls: ScanDecl[] = [];\n for (const name of Object.keys(this.projects).sort()) {\n if (!this.isNode(name)) continue;\n const sink = new RelationSink(name);\n this.collectEffectiveRelations(name, sink, new Set<string>([name]));\n if (sink.implementsApis.length > 0 || sink.usesApis.length > 0) {\n decls.push({\n name,\n implementsApis: dedupApiRefs(sortApiRefs(sink.implementsApis)),\n usesApis: dedupApiRefs(sortApiRefs(sink.usesApis)),\n implementsVia: sink.implementsVia,\n });\n }\n }\n return decls;\n }\n\n /** True when a project is a runtime node — a server or client app (never a library). */\n private isNode(name: string): boolean {\n const role = this.projects[name]?.role;\n return role === 'server' || role === 'client';\n }\n\n /**\n * Accumulate `name`'s own api relations, then recurse through its LIBRARY deps\n * (skipping other nodes, which own their own relations). `visited` guards against\n * re-walking a lib reachable by more than one path (and any cycle).\n */\n private collectEffectiveRelations(name: string, sink: RelationSink, visited: Set<string>): void {\n const entry = this.projects[name];\n if (entry === undefined) return;\n const relations = entry.apiRelations;\n if (relations !== undefined) {\n for (const owner of Object.keys(relations).sort()) {\n for (const ref of relations[owner].implements) {\n this.apiOwners.set(ref.api, owner);\n sink.addImplements(ref, name);\n }\n for (const ref of relations[owner].uses) {\n this.apiOwners.set(ref.api, owner);\n sink.addUses(ref);\n }\n }\n }\n for (const dep of entry.dependsOn) {\n if (visited.has(dep)) continue;\n visited.add(dep);\n if (this.isNode(dep)) continue; // another server/client owns its own relations\n this.collectEffectiveRelations(dep, sink, visited);\n }\n }\n\n /** apiClassName -> { implementedBy, usedBy, type }. */\n private buildApis(decls: ScanDecl[]): Map<string, RuntimeApi> {\n const apis = new Map<string, RuntimeApi>();\n const ensure = (api: string, type: ApiTransport): RuntimeApi => {\n let entry = apis.get(api);\n if (!entry) {\n entry = { implementedBy: [], usedBy: [], type };\n apis.set(api, entry);\n }\n return entry;\n };\n for (const decl of decls) {\n for (const ref of decl.implementsApis) ensure(ref.api, ref.type).implementedBy.push(decl.name);\n for (const ref of decl.usesApis) ensure(ref.api, ref.type).usedBy.push(decl.name);\n }\n for (const api of apis.keys()) {\n const entry = apis.get(api)!;\n entry.implementedBy.sort();\n entry.usedBy = Array.from(new Set(entry.usedBy)).sort();\n const owner = this.apiOwners.get(api);\n if (owner !== undefined) entry.owner = owner;\n }\n return apis;\n }\n\n /** Inferred edges U -> I via api A, split by transport; plus uses with no implementer. */\n private buildEdges(decls: ScanDecl[], apis: Map<string, RuntimeApi>): EdgeResult {\n const viaByKey = new Map<string, Set<string>>();\n const unresolved: RuntimeUnresolved[] = [];\n for (const decl of decls) {\n for (const ref of decl.usesApis) {\n const implementers = apis.get(ref.api)?.implementedBy ?? [];\n if (implementers.length === 0) {\n unresolved.push({ service: decl.name, api: ref.api });\n continue;\n }\n for (const target of this.targetsFor(decl.name, ref, implementers)) {\n if (target === decl.name) continue;\n const key = `${decl.name} ${target} ${ref.type}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(ref.api);\n }\n }\n }\n return { edges: this.edgesFromKeys(viaByKey), unresolved: sortUnresolved(unresolved) };\n }\n\n /**\n * WHICH implementers this one `uses` reaches. Resolution order (most specific wins):\n * 1. a literal `ClientConfig` at the call site (`ref.targetService`) — names ONE node;\n * 2. else the calling project's declared `callsService` (project.json) — for the shared-library\n * case where the literal cannot sit at the call site, it lives one indirection away in the app;\n * 3. else the historical fan-out — the only safe superset — recording WHY, so a fanned-out (i.e.\n * possibly fictional) edge can never pass for a derived one.\n * A named target (1 or 2) that resolves to no module, or to a module that does not serve the\n * contract, is a PROBLEM (fails the build), never a silent drop.\n */\n private targetsFor(user: string, ref: ApiRef, implementers: string[]): string[] {\n const literal = ref.targetService;\n if (literal !== undefined) return this.resolveNamedTarget(user, ref, implementers, literal, 'literal');\n\n const declared = this.callsServiceFor(user, ref.api);\n if (declared !== undefined) return this.resolveNamedTarget(user, ref, implementers, declared, 'callsService');\n\n return this.untargetedFanOut(user, ref, implementers);\n }\n\n /**\n * The declared `callsService` target for this node's use of `api`, or undefined when none applies.\n * A string declaration aims every untargeted use at one service; a map aims per api-class.\n */\n private callsServiceFor(user: string, api: string): string | undefined {\n const declared = this.projects[user]?.callsService;\n if (declared === undefined) return undefined;\n if (typeof declared === 'string') return declared;\n return declared[api];\n }\n\n /**\n * Resolve a NAMED target (from a call-site literal or a project-level `callsService`) to the one\n * module that serves it. A name no module answers to, or a module that does not serve the\n * contract, fails the build — the wording names which declaration to fix, `source` deciding it.\n */\n private resolveNamedTarget(\n user: string,\n ref: ApiRef,\n implementers: string[],\n wanted: string,\n source: 'literal' | 'callsService',\n ): string[] {\n const node = this.nodeByServiceName.get(wanted);\n const served = `${implementers.length} module(s) serve this contract (${implementers.join(', ')})`;\n if (node === undefined) {\n this.problems.push(\n source === 'callsService'\n ? `${user} declares metadata.webpieces.callsService '${wanted}' in its project.json, but NO ` +\n `module answers to that name. ${served}. Point callsService at a module name, or declare ` +\n `metadata.webpieces.serviceName: '${wanted}' on the module that serves \"${ref.api}\".`\n : `${user} calls \"${ref.api}\" at service '${wanted}', but NO module answers to that name. ` +\n `${served}. Either use the module name, or declare metadata.webpieces.serviceName: ` +\n `'${wanted}' on the module that serves it (and translate any environment prefix in ` +\n `ClientRegistry.setDeriver, not here).`,\n );\n return implementers;\n }\n if (!implementers.includes(node)) {\n const via =\n source === 'callsService'\n ? `${user} declares callsService '${wanted}' (module ${node})`\n : `${user} calls \"${ref.api}\" at service '${wanted}' (module ${node})`;\n this.problems.push(\n `${via}, but ${node} does NOT serve \"${ref.api}\" — ${implementers.join(', ')} do. At runtime that ` +\n `call has nothing to answer it.`,\n );\n return implementers;\n }\n return [node];\n }\n\n /**\n * A use with no literal client config. One implementer is unambiguous, so it stays silent; more\n * than one means every edge but one is fiction — exactly the failure this mechanism exists to\n * stop — so it is reported even though the graph still (conservatively) draws them all.\n */\n private untargetedFanOut(user: string, ref: ApiRef, implementers: string[]): string[] {\n if (implementers.length > 1) {\n this.warnings.push(\n `${user} uses \"${ref.api}\" with no literal client config, and ${implementers.length} services ` +\n `implement it (${implementers.join(', ')}) — an edge is drawn to EVERY one, so all but one ` +\n `are fiction. Name the target: createRpcClient(${ref.api}, new ClientConfig('<serviceName>')); ` +\n `or, when the client is built in a shared library (no literal can sit at the call site), ` +\n `declare metadata.webpieces.callsService: '<serviceName>' on ${user}'s project.json.`,\n );\n }\n return implementers;\n }\n\n private edgesFromKeys(viaByKey: Map<string, Set<string>>): RuntimeEdge[] {\n const edges: RuntimeEdge[] = [];\n for (const key of viaByKey.keys()) {\n const parts = key.split(' ');\n edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByKey.get(key)!).sort(), type: parts[2] as ApiTransport });\n }\n edges.sort(\n (a: RuntimeEdge, b: RuntimeEdge) =>\n a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.type ?? '').localeCompare(b.type ?? ''),\n );\n return edges;\n }\n\n private buildServices(decls: ScanDecl[], edges: RuntimeEdge[]): Record<string, RuntimeService> {\n const services: Record<string, RuntimeService> = {};\n for (const decl of decls) {\n const dependsOn = Array.from(\n new Set(edges.filter((e: RuntimeEdge) => e.from === decl.name).map((e: RuntimeEdge) => e.to)),\n ).sort();\n // Keys are written in this order; an undefined value is omitted by JSON.stringify, so\n // the committed JSON stays clean AND deterministic without conditional assembly.\n const service: RuntimeService = {\n level: 0,\n serviceName: this.projects[decl.name]?.serviceName,\n callsService: this.projects[decl.name]?.callsService,\n implements: decl.implementsApis.map((r: ApiRef) => r.api),\n implementsVia: decl.implementsVia.size > 0 ? sortedRecord(decl.implementsVia) : undefined,\n // One api used against two services is two refs but ONE api in this list.\n uses: Array.from(new Set(decl.usesApis.map((r: ApiRef) => r.api))),\n dependsOn,\n };\n services[decl.name] = service;\n if (this.hiddenProjects.has(decl.name)) services[decl.name].drawOnGraph = false;\n }\n const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edges));\n for (const name of Object.keys(services)) services[name].level = levels[name] ?? 0;\n return services;\n }\n}\n\n/**\n * Derive the runtime graph from dependencies.json's project `apiRelations` — the\n * single source of truth shared by generate + validate. `hiddenProjects`\n * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the\n * runtime visualizer omits their nodes + edges.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraph(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>()\n): RuntimeGraph {\n return deriveRuntimeGraphReport(projects, hiddenProjects).graph;\n}\n\n/**\n * The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use\n * this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that\n * only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph\n * file that records its own doubts would just get committed and stop being read.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraphReport(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>()\n): RuntimeGraphReport {\n return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();\n}\n\n/** Drop duplicate api refs, keeping the first — needed after a node absorbs the same api from both\n * its own relations and an embedded lib's. Keyed by api AND target service: the same contract aimed\n * at two different services is two distinct relations (two distinct edges), not a duplicate. Input\n * is pre-sorted, so output stays deterministic. */\n// webpieces-disable no-function-outside-class -- pure list helper, matches the sibling helpers in this file\nfunction dedupApiRefs(refs: ApiRef[]): ApiRef[] {\n const seen = new Set<string>();\n const out: ApiRef[] = [];\n for (const ref of refs) {\n const key = apiRefKey(ref);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(ref);\n }\n return out;\n}\n\n/** Sort a Map into a plain object with sorted keys, so the committed JSON is deterministic. */\n// webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file\nfunction sortedRecord(map: Map<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const key of [...map.keys()].sort()) out[key] = map.get(key)!;\n return out;\n}\n\n/** Sort AND de-duplicate: one api used against two targets must not be reported unresolved twice. */\n// webpieces-disable no-function-outside-class -- pure sort helper, matches the sibling helpers in this file\nfunction sortUnresolved(unresolved: RuntimeUnresolved[]): RuntimeUnresolved[] {\n const byKey = new Map<string, RuntimeUnresolved>();\n for (const entry of unresolved) byKey.set(`${entry.service} ${entry.api}`, entry);\n return [...byKey.values()].sort(\n (a: RuntimeUnresolved, b: RuntimeUnresolved) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api),\n );\n}\n\n/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */\nfunction formatRuntimeJson(graph: RuntimeGraph): string {\n return JSON.stringify(graph, null, 4) + '\\n';\n}\n\nexport function saveRuntimeGraph(\n graph: RuntimeGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');\n}\n\nexport function runtimeGraphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): boolean {\n return fs.existsSync(path.join(workspaceRoot, graphPath));\n}\n\nexport function loadRuntimeGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): RuntimeGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n if (!fs.existsSync(fullPath)) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as RuntimeGraph;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });\n }\n}\n\n/** Serialize for an in-memory equality check (matches the on-disk format). */\nexport function serializeRuntimeGraph(graph: RuntimeGraph): string {\n return formatRuntimeJson(graph);\n}\n"]}
@@ -25,6 +25,8 @@
25
25
  import { ProjectInfo } from './project-info';
26
26
  /** The project.json key path holding the declared name: metadata.webpieces.serviceName. */
27
27
  export declare const SERVICE_NAME_METADATA_PATH = "metadata.webpieces.serviceName";
28
+ /** The project.json key path holding a client's declared target: metadata.webpieces.callsService. */
29
+ export declare const CALLS_SERVICE_METADATA_PATH = "metadata.webpieces.callsService";
28
30
  export declare class ServiceNameResolution {
29
31
  /** Declared service name, or null when none is declared (or resolution failed). */
30
32
  readonly serviceName: string | null;
@@ -42,6 +44,35 @@ export declare class ServiceNameResolution {
42
44
  * or not a string) is a problem, because that is a typo the author wants told about.
43
45
  */
44
46
  export declare function resolveServiceName(info: ProjectInfo, workspaceRoot: string): ServiceNameResolution;
47
+ /**
48
+ * A client's declared target — the service it addresses when the call site cannot carry a literal
49
+ * `ClientConfig` (the client is built in a SHARED library from a config field, so the literal lives
50
+ * in the app, not the lib, one indirection away). Symmetric to `serviceName`: the IMPLEMENTING side
51
+ * declares the name it answers to, the CALLING side declares the name it calls.
52
+ *
53
+ * Two shapes:
54
+ * - a single string — every untargeted `uses` in this project aims at that one service (the common
55
+ * case: most clients talk to exactly one server);
56
+ * - an `{ apiClassName: serviceName }` map — the mixed case, a client that genuinely calls several.
57
+ */
58
+ export declare class CallsServiceResolution {
59
+ /** Declared target(s), or null when none is declared (or resolution failed). */
60
+ readonly callsService: string | Record<string, string> | null;
61
+ /** Problem description when the declaration is present but unusable, otherwise null. */
62
+ readonly problem: string | null;
63
+ constructor(
64
+ /** Declared target(s), or null when none is declared (or resolution failed). */
65
+ callsService: string | Record<string, string> | null,
66
+ /** Problem description when the declaration is present but unusable, otherwise null. */
67
+ problem: string | null);
68
+ }
69
+ /**
70
+ * Read `metadata.webpieces.callsService` from the project's project.json. A missing file, a missing
71
+ * key, or an unparseable file all resolve to "not declared". A PRESENT value must be either a
72
+ * non-empty string or a non-empty object of non-empty string values — anything else is a typo the
73
+ * author wants told about.
74
+ */
75
+ export declare function resolveCallsService(info: ProjectInfo, workspaceRoot: string): CallsServiceResolution;
45
76
  /**
46
77
  * Two projects claiming the same service name make every targeted client edge ambiguous, so the
47
78
  * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.
@@ -24,8 +24,9 @@
24
24
  * client call, and the runtime graph says so out loud rather than guessing.
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.ServiceNameResolution = exports.SERVICE_NAME_METADATA_PATH = void 0;
27
+ exports.CallsServiceResolution = exports.ServiceNameResolution = exports.CALLS_SERVICE_METADATA_PATH = exports.SERVICE_NAME_METADATA_PATH = void 0;
28
28
  exports.resolveServiceName = resolveServiceName;
29
+ exports.resolveCallsService = resolveCallsService;
29
30
  exports.validateUniqueServiceNames = validateUniqueServiceNames;
30
31
  const tslib_1 = require("tslib");
31
32
  const fs = tslib_1.__importStar(require("fs"));
@@ -33,6 +34,8 @@ const path = tslib_1.__importStar(require("path"));
33
34
  const toError_1 = require("../toError");
34
35
  /** The project.json key path holding the declared name: metadata.webpieces.serviceName. */
35
36
  exports.SERVICE_NAME_METADATA_PATH = 'metadata.webpieces.serviceName';
37
+ /** The project.json key path holding a client's declared target: metadata.webpieces.callsService. */
38
+ exports.CALLS_SERVICE_METADATA_PATH = 'metadata.webpieces.callsService';
36
39
  class ServiceNameResolution {
37
40
  serviceName;
38
41
  problem;
@@ -53,7 +56,7 @@ exports.ServiceNameResolution = ServiceNameResolution;
53
56
  */
54
57
  // webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveRole/resolveDrawOnGraph
55
58
  function resolveServiceName(info, workspaceRoot) {
56
- const raw = readServiceNameField(path.join(workspaceRoot, info.root, 'project.json'));
59
+ const raw = readWebpiecesField(path.join(workspaceRoot, info.root, 'project.json'), 'serviceName');
57
60
  if (raw === undefined)
58
61
  return new ServiceNameResolution(null, null);
59
62
  if (typeof raw !== 'string' || raw.trim().length === 0) {
@@ -61,6 +64,72 @@ function resolveServiceName(info, workspaceRoot) {
61
64
  }
62
65
  return new ServiceNameResolution(raw.trim(), null);
63
66
  }
67
+ /**
68
+ * A client's declared target — the service it addresses when the call site cannot carry a literal
69
+ * `ClientConfig` (the client is built in a SHARED library from a config field, so the literal lives
70
+ * in the app, not the lib, one indirection away). Symmetric to `serviceName`: the IMPLEMENTING side
71
+ * declares the name it answers to, the CALLING side declares the name it calls.
72
+ *
73
+ * Two shapes:
74
+ * - a single string — every untargeted `uses` in this project aims at that one service (the common
75
+ * case: most clients talk to exactly one server);
76
+ * - an `{ apiClassName: serviceName }` map — the mixed case, a client that genuinely calls several.
77
+ */
78
+ class CallsServiceResolution {
79
+ callsService;
80
+ problem;
81
+ constructor(
82
+ /** Declared target(s), or null when none is declared (or resolution failed). */
83
+ callsService,
84
+ /** Problem description when the declaration is present but unusable, otherwise null. */
85
+ problem) {
86
+ this.callsService = callsService;
87
+ this.problem = problem;
88
+ }
89
+ }
90
+ exports.CallsServiceResolution = CallsServiceResolution;
91
+ /**
92
+ * Read `metadata.webpieces.callsService` from the project's project.json. A missing file, a missing
93
+ * key, or an unparseable file all resolve to "not declared". A PRESENT value must be either a
94
+ * non-empty string or a non-empty object of non-empty string values — anything else is a typo the
95
+ * author wants told about.
96
+ */
97
+ // webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveServiceName
98
+ function resolveCallsService(info, workspaceRoot) {
99
+ const raw = readWebpiecesField(path.join(workspaceRoot, info.root, 'project.json'), 'callsService');
100
+ if (raw === undefined)
101
+ return new CallsServiceResolution(null, null);
102
+ if (typeof raw === 'string') {
103
+ if (raw.trim().length === 0) {
104
+ return new CallsServiceResolution(null, `${info.name}: ${exports.CALLS_SERVICE_METADATA_PATH} in project.json must be a non-empty string`);
105
+ }
106
+ return new CallsServiceResolution(raw.trim(), null);
107
+ }
108
+ if (isPlainRecord(raw)) {
109
+ const apis = Object.keys(raw);
110
+ if (apis.length === 0) {
111
+ return new CallsServiceResolution(null, `${info.name}: ${exports.CALLS_SERVICE_METADATA_PATH} map in project.json must have at least one entry`);
112
+ }
113
+ const map = {};
114
+ for (const api of apis) {
115
+ const target = raw[api];
116
+ if (typeof target !== 'string' || target.trim().length === 0) {
117
+ return new CallsServiceResolution(null, `${info.name}: ${exports.CALLS_SERVICE_METADATA_PATH}['${api}'] in project.json must be a ` +
118
+ `non-empty string (an api-class -> serviceName map)`);
119
+ }
120
+ map[api] = target.trim();
121
+ }
122
+ return new CallsServiceResolution(map, null);
123
+ }
124
+ return new CallsServiceResolution(null, `${info.name}: ${exports.CALLS_SERVICE_METADATA_PATH} in project.json must be a non-empty string, or an ` +
125
+ `{ apiClassName: serviceName } map for a client that calls several services`);
126
+ }
127
+ /** True when a value is a plain object (a map), not an array or null. */
128
+ // webpieces-disable no-any-unknown -- narrowing opaque project.json JSON
129
+ // webpieces-disable no-function-outside-class -- pure type guard for the resolver above
130
+ function isPlainRecord(value) {
131
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
132
+ }
64
133
  /**
65
134
  * Two projects claiming the same service name make every targeted client edge ambiguous, so the
66
135
  * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.
@@ -83,16 +152,16 @@ function validateUniqueServiceNames(names, problems) {
83
152
  `Give each project a distinct ${exports.SERVICE_NAME_METADATA_PATH}.`);
84
153
  }
85
154
  }
86
- /** The raw `metadata.webpieces.serviceName` value, or undefined when not present at all. */
155
+ /** The raw `metadata.webpieces.<field>` value, or undefined when not present at all. */
87
156
  // webpieces-disable no-any-unknown -- project.json is opaque consumer JSON until narrowed here
88
157
  // webpieces-disable no-function-outside-class -- pure file accessor, matches the sibling resolvers
89
- function readServiceNameField(projectJsonPath) {
158
+ function readWebpiecesField(projectJsonPath, field) {
90
159
  if (!fs.existsSync(projectJsonPath))
91
160
  return undefined;
92
161
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
93
162
  try {
94
163
  const parsed = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
95
- return parsed?.metadata?.webpieces?.serviceName;
164
+ return parsed?.metadata?.webpieces?.[field];
96
165
  }
97
166
  catch (err) {
98
167
  const error = (0, toError_1.toError)(err);
@@ -1 +1 @@
1
- {"version":3,"file":"service-name-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/service-name-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Service Name Resolver\n *\n * Resolves the `serviceName` field written per project into\n * architecture/dependencies.json — the name a CLIENT uses to address this app at\n * runtime (`new ClientConfig('helper-fsdb')`), i.e. its deployed Cloud Run\n * service name, NOT its nx project name.\n *\n * It is DECLARED, never derived. The three naming spaces have no mechanical\n * relationship, and any strip-the-suffix rule gets it wrong somewhere:\n *\n * nx project | serviceName | Cloud Run\n * helper-svr | helper-portal | helper-portal\n * helper-fsdb-svr | helper-fsdb | helper-fsdb\n * lang-server | lang | lang\n *\n * Declared in the project's own project.json, next to the code it names:\n *\n * { \"metadata\": { \"webpieces\": { \"serviceName\": \"helper-fsdb\" } } }\n *\n * Absent is legal and common — a library or a browser app is never addressed by\n * name. A node WITHOUT one simply cannot be the resolved target of a targeted\n * client call, and the runtime graph says so out loud rather than guessing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\n/** The project.json key path holding the declared name: metadata.webpieces.serviceName. */\nexport const SERVICE_NAME_METADATA_PATH = 'metadata.webpieces.serviceName';\n\nexport class ServiceNameResolution {\n constructor(\n /** Declared service name, or null when none is declared (or resolution failed). */\n public readonly serviceName: string | null,\n /** Problem description when the declaration is present but unusable, otherwise null. */\n public readonly problem: string | null,\n ) {}\n}\n\n/**\n * Read `metadata.webpieces.serviceName` from the project's project.json. A missing file, a missing\n * key, or an unparseable file all resolve to \"not declared\" — only a PRESENT-but-wrong value (empty\n * or not a string) is a problem, because that is a typo the author wants told about.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveRole/resolveDrawOnGraph\nexport function resolveServiceName(info: ProjectInfo, workspaceRoot: string): ServiceNameResolution {\n const raw = readServiceNameField(path.join(workspaceRoot, info.root, 'project.json'));\n if (raw === undefined) return new ServiceNameResolution(null, null);\n if (typeof raw !== 'string' || raw.trim().length === 0) {\n return new ServiceNameResolution(\n null,\n `${info.name}: ${SERVICE_NAME_METADATA_PATH} in project.json must be a non-empty string`,\n );\n }\n return new ServiceNameResolution(raw.trim(), null);\n}\n\n/**\n * Two projects claiming the same service name make every targeted client edge ambiguous, so the\n * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.\n */\n// webpieces-disable no-function-outside-class -- pure validator, mirrors validateRoleDependencies\nexport function validateUniqueServiceNames(names: Map<string, string>, problems: string[]): void {\n const projectsByName = new Map<string, string[]>();\n for (const project of [...names.keys()].sort()) {\n const serviceName = names.get(project)!;\n const claimants = projectsByName.get(serviceName) ?? [];\n claimants.push(project);\n projectsByName.set(serviceName, claimants);\n }\n for (const serviceName of [...projectsByName.keys()].sort()) {\n const claimants = projectsByName.get(serviceName)!;\n if (claimants.length < 2) continue;\n problems.push(\n `serviceName '${serviceName}' is declared by ${claimants.length} projects ` +\n `(${claimants.join(', ')}) — a client naming it could not be routed to one of them. ` +\n `Give each project a distinct ${SERVICE_NAME_METADATA_PATH}.`,\n );\n }\n}\n\n/** The raw `metadata.webpieces.serviceName` value, or undefined when not present at all. */\n// webpieces-disable no-any-unknown -- project.json is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- pure file accessor, matches the sibling resolvers\nfunction readServiceNameField(projectJsonPath: string): unknown {\n if (!fs.existsSync(projectJsonPath)) return undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));\n return parsed?.metadata?.webpieces?.serviceName;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${projectJsonPath}: ${error.message}`);\n return undefined;\n }\n}\n"]}
1
+ {"version":3,"file":"service-name-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/service-name-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Service Name Resolver\n *\n * Resolves the `serviceName` field written per project into\n * architecture/dependencies.json — the name a CLIENT uses to address this app at\n * runtime (`new ClientConfig('helper-fsdb')`), i.e. its deployed Cloud Run\n * service name, NOT its nx project name.\n *\n * It is DECLARED, never derived. The three naming spaces have no mechanical\n * relationship, and any strip-the-suffix rule gets it wrong somewhere:\n *\n * nx project | serviceName | Cloud Run\n * helper-svr | helper-portal | helper-portal\n * helper-fsdb-svr | helper-fsdb | helper-fsdb\n * lang-server | lang | lang\n *\n * Declared in the project's own project.json, next to the code it names:\n *\n * { \"metadata\": { \"webpieces\": { \"serviceName\": \"helper-fsdb\" } } }\n *\n * Absent is legal and common — a library or a browser app is never addressed by\n * name. A node WITHOUT one simply cannot be the resolved target of a targeted\n * client call, and the runtime graph says so out loud rather than guessing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\n/** The project.json key path holding the declared name: metadata.webpieces.serviceName. */\nexport const SERVICE_NAME_METADATA_PATH = 'metadata.webpieces.serviceName';\n\n/** The project.json key path holding a client's declared target: metadata.webpieces.callsService. */\nexport const CALLS_SERVICE_METADATA_PATH = 'metadata.webpieces.callsService';\n\nexport class ServiceNameResolution {\n constructor(\n /** Declared service name, or null when none is declared (or resolution failed). */\n public readonly serviceName: string | null,\n /** Problem description when the declaration is present but unusable, otherwise null. */\n public readonly problem: string | null,\n ) {}\n}\n\n/**\n * Read `metadata.webpieces.serviceName` from the project's project.json. A missing file, a missing\n * key, or an unparseable file all resolve to \"not declared\" — only a PRESENT-but-wrong value (empty\n * or not a string) is a problem, because that is a typo the author wants told about.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveRole/resolveDrawOnGraph\nexport function resolveServiceName(info: ProjectInfo, workspaceRoot: string): ServiceNameResolution {\n const raw = readWebpiecesField(path.join(workspaceRoot, info.root, 'project.json'), 'serviceName');\n if (raw === undefined) return new ServiceNameResolution(null, null);\n if (typeof raw !== 'string' || raw.trim().length === 0) {\n return new ServiceNameResolution(\n null,\n `${info.name}: ${SERVICE_NAME_METADATA_PATH} in project.json must be a non-empty string`,\n );\n }\n return new ServiceNameResolution(raw.trim(), null);\n}\n\n/**\n * A client's declared target — the service it addresses when the call site cannot carry a literal\n * `ClientConfig` (the client is built in a SHARED library from a config field, so the literal lives\n * in the app, not the lib, one indirection away). Symmetric to `serviceName`: the IMPLEMENTING side\n * declares the name it answers to, the CALLING side declares the name it calls.\n *\n * Two shapes:\n * - a single string — every untargeted `uses` in this project aims at that one service (the common\n * case: most clients talk to exactly one server);\n * - an `{ apiClassName: serviceName }` map — the mixed case, a client that genuinely calls several.\n */\nexport class CallsServiceResolution {\n constructor(\n /** Declared target(s), or null when none is declared (or resolution failed). */\n public readonly callsService: string | Record<string, string> | null,\n /** Problem description when the declaration is present but unusable, otherwise null. */\n public readonly problem: string | null,\n ) {}\n}\n\n/**\n * Read `metadata.webpieces.callsService` from the project's project.json. A missing file, a missing\n * key, or an unparseable file all resolve to \"not declared\". A PRESENT value must be either a\n * non-empty string or a non-empty object of non-empty string values — anything else is a typo the\n * author wants told about.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveServiceName\nexport function resolveCallsService(info: ProjectInfo, workspaceRoot: string): CallsServiceResolution {\n const raw = readWebpiecesField(path.join(workspaceRoot, info.root, 'project.json'), 'callsService');\n if (raw === undefined) return new CallsServiceResolution(null, null);\n\n if (typeof raw === 'string') {\n if (raw.trim().length === 0) {\n return new CallsServiceResolution(\n null,\n `${info.name}: ${CALLS_SERVICE_METADATA_PATH} in project.json must be a non-empty string`,\n );\n }\n return new CallsServiceResolution(raw.trim(), null);\n }\n\n if (isPlainRecord(raw)) {\n const apis = Object.keys(raw);\n if (apis.length === 0) {\n return new CallsServiceResolution(\n null,\n `${info.name}: ${CALLS_SERVICE_METADATA_PATH} map in project.json must have at least one entry`,\n );\n }\n const map: Record<string, string> = {};\n for (const api of apis) {\n const target = raw[api];\n if (typeof target !== 'string' || target.trim().length === 0) {\n return new CallsServiceResolution(\n null,\n `${info.name}: ${CALLS_SERVICE_METADATA_PATH}['${api}'] in project.json must be a ` +\n `non-empty string (an api-class -> serviceName map)`,\n );\n }\n map[api] = target.trim();\n }\n return new CallsServiceResolution(map, null);\n }\n\n return new CallsServiceResolution(\n null,\n `${info.name}: ${CALLS_SERVICE_METADATA_PATH} in project.json must be a non-empty string, or an ` +\n `{ apiClassName: serviceName } map for a client that calls several services`,\n );\n}\n\n/** True when a value is a plain object (a map), not an array or null. */\n// webpieces-disable no-any-unknown -- narrowing opaque project.json JSON\n// webpieces-disable no-function-outside-class -- pure type guard for the resolver above\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Two projects claiming the same service name make every targeted client edge ambiguous, so the\n * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.\n */\n// webpieces-disable no-function-outside-class -- pure validator, mirrors validateRoleDependencies\nexport function validateUniqueServiceNames(names: Map<string, string>, problems: string[]): void {\n const projectsByName = new Map<string, string[]>();\n for (const project of [...names.keys()].sort()) {\n const serviceName = names.get(project)!;\n const claimants = projectsByName.get(serviceName) ?? [];\n claimants.push(project);\n projectsByName.set(serviceName, claimants);\n }\n for (const serviceName of [...projectsByName.keys()].sort()) {\n const claimants = projectsByName.get(serviceName)!;\n if (claimants.length < 2) continue;\n problems.push(\n `serviceName '${serviceName}' is declared by ${claimants.length} projects ` +\n `(${claimants.join(', ')}) — a client naming it could not be routed to one of them. ` +\n `Give each project a distinct ${SERVICE_NAME_METADATA_PATH}.`,\n );\n }\n}\n\n/** The raw `metadata.webpieces.<field>` value, or undefined when not present at all. */\n// webpieces-disable no-any-unknown -- project.json is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- pure file accessor, matches the sibling resolvers\nfunction readWebpiecesField(projectJsonPath: string, field: string): unknown {\n if (!fs.existsSync(projectJsonPath)) return undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));\n return parsed?.metadata?.webpieces?.[field];\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${projectJsonPath}: ${error.message}`);\n return undefined;\n }\n}\n"]}