@webpieces/nx-webpieces-rules 0.4.807 → 0.4.808

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.807",
3
+ "version": "0.4.808",
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",
@@ -18,11 +18,11 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@webpieces/ai-hook-rules": "0.4.807",
22
- "@webpieces/code-rules": "0.4.807",
23
- "@webpieces/eslint-rules": "0.4.807",
24
- "@webpieces/pr-gate": "0.4.807",
25
- "@webpieces/rules-config": "0.4.807",
21
+ "@webpieces/ai-hook-rules": "0.4.808",
22
+ "@webpieces/code-rules": "0.4.808",
23
+ "@webpieces/eslint-rules": "0.4.808",
24
+ "@webpieces/pr-gate": "0.4.808",
25
+ "@webpieces/rules-config": "0.4.808",
26
26
  "madge": "8.0.0"
27
27
  },
28
28
  "peerDependencies": {
@@ -18,6 +18,9 @@ const graph_loader_1 = require("../../lib/graph-loader");
18
18
  const api_contract_files_1 = require("../../lib/api-contract-files");
19
19
  const graph_metadata_1 = require("../../lib/graph-metadata");
20
20
  const api_scanner_1 = require("../../lib/api-usage/api-scanner");
21
+ // The unresolved-contract report moved beside the other contract reports when api-scanner.ts reached
22
+ // its file-size limit.
23
+ const api_contract_errors_1 = require("../../lib/api-usage/api-contract-errors");
21
24
  const external_systems_1 = require("../../lib/api-usage/external-systems");
22
25
  const runtime_config_1 = require("../../lib/runtime-config");
23
26
  const graph_visualizer_1 = require("../../lib/graph-visualizer");
@@ -72,7 +75,7 @@ function scanApiRelations(workspaceRoot, graph, projectInfos) {
72
75
  const externalApiPaths = (0, runtime_config_1.loadRuntimeConfig)(workspaceRoot).externalApiPaths;
73
76
  const scan = (0, api_scanner_1.scanAndAttachApiRelations)(workspaceRoot, graph, projectInfos, externalApiPaths);
74
77
  if (scan.unresolvedApiCalls.length > 0)
75
- console.warn((0, api_scanner_1.describeUnresolvedApiCalls)(scan.unresolvedApiCalls));
78
+ console.warn((0, api_contract_errors_1.describeUnresolvedApiCalls)(scan.unresolvedApiCalls));
76
79
  // A decorator argument we could not read costs the graph a basePath, a method, or a whole
77
80
  // contract — none of which leaves a trace in the output. Name them before anything is written.
78
81
  if (scan.nonLiteralDecoratorArgs.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAoNH,8BA2BC;AA5OD,0DAA+F;AAC/F,+DAAiE;AACjE,yDAAgE;AAChE,yDAA8D;AAC9D,yDAAuE;AACvE,qEAAgE;AAChE,6DAAoG;AAEpG,iEAMyC;AACzC,2EAA4E;AAE5E,6DAA6D;AAE7D,iEAA6D;AAC7D,2DAAqF;AACrF,yFAAgF;AAChF,2CAAwC;AAUxC;;;;;GAKG;AACH,2GAA2G;AAC3G,SAAS,oBAAoB,CACzB,aAAqB,EACrB,KAAoB,EACpB,cAA2B,EAC3B,YAA0B,EAC1B,eAAoC;IAEpC,OAAO,CAAC,GAAG,CAAC,iFAAiF,CAAC,CAAC;IAC/F,MAAM,MAAM,GAAG,IAAA,wCAAwB,EAAC,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,CAAC,CAAC;IAC9F,IAAA,gCAAgB,EAAC,MAAM,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;IAC/D,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IAC3D,OAAO,CAAC,GAAG,CACP,0BAA0B,YAAY,cAAc,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,kBAAkB;QAClG,GAAG,UAAU,YAAY,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,0BAA0B,CACtF,CAAC;IACF,iGAAiG;IACjG,gGAAgG;IAChG,oBAAoB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAA,qDAAsB,EAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC1C,8FAA8F;IAC9F,2FAA2F;IAC3F,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,sDAAsD,CAAC,CAAC;QACjG,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;QAC1E,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;AACL,CAAC;AAED,sGAAsG;AACtG,2GAA2G;AAC3G,SAAS,oBAAoB,CAAC,QAAkB;IAC5C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAClC,OAAO,CAAC,IAAI,CAAC,OAAO,QAAQ,CAAC,MAAM,wDAAwD,CAAC,CAAC;IAC7F,KAAK,MAAM,OAAO,IAAI,QAAQ;QAAE,OAAO,CAAC,IAAI,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,2GAA2G;AAC3G,SAAS,gBAAgB,CACrB,aAAqB,EACrB,KAAoB,EACpB,YAAsC;IAEtC,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;IACvE,MAAM,gBAAgB,GAAG,IAAA,kCAAiB,EAAC,aAAa,CAAC,CAAC,gBAAgB,CAAC;IAC3E,MAAM,IAAI,GAAG,IAAA,uCAAyB,EAAC,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;IAC7F,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,IAAA,wCAA0B,EAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC1G,0FAA0F;IAC1F,+FAA+F;IAC/F,IAAI,IAAI,CAAC,uBAAuB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,IAAA,6CAA+B,EAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,SAAS,GAAG,IAAA,+BAAiB,EAAC,IAAI,CAAC,CAAC;IAC1C,kGAAkG;IAClG,0FAA0F;IAC1F,MAAM,UAAU,GAAG,IAAA,6CAA+B,EAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,MAAM,kDAAkD,CAAC,CAAC;QACxF,KAAK,MAAM,QAAQ,IAAI,UAAU;YAAE,OAAO,CAAC,KAAK,CAAC,UAAU,QAAQ,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,IAAI,aAAa,CAAC,SAAS,EAAE,IAAA,uCAAoB,EAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED,oGAAoG;AACpG,MAAM,aAAa;IAEK;IACA;IAFpB,YACoB,YAA0B,EAC1B,eAAoC;QADpC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,oBAAe,GAAf,eAAe,CAAqB;IACrD,CAAC;CACP;AAED,+FAA+F;AAC/F,2GAA2G;AAC3G,SAAS,gBAAgB,CAAC,KAAoB;IAC1C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,WAAW,KAAK,KAAK;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,2GAA2G;AAC3G,SAAS,iBAAiB,CAAC,KAAoB;IAC3C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,KAAiB,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACrF,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACnC,OAAO,CAAC,GAAG,CAAC,gBAAgB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,OAAO,CAAC,GAAG,CAAC,cAAc,MAAM,CAAC,IAAI,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;AACxE,CAAC;AAED;;;;GAIG;AACH,2GAA2G;AAC3G,KAAK,UAAU,kBAAkB,CAAC,aAAqB,EAAE,SAA6B;IAClF,gFAAgF;IAChF,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;IACzE,MAAM,YAAY,GAAG,MAAM,IAAA,sCAAoB,GAAE,CAAC;IAElD,2FAA2F;IAC3F,oFAAoF;IACpF,8FAA8F;IAC9F,gDAAgD;IAChD,OAAO,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,IAAI,mCAAoB,EAAE,CAAC;IAC1C,MAAM,CAAC,aAAa,CAAC,YAAY,EAAE,sBAAsB,CAAC,CAAC;IAE3D,gEAAgE;IAChE,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;IAClD,MAAM,aAAa,GAAG,IAAA,qCAAsB,EAAC,YAAY,CAAC,CAAC;IAC3D,8FAA8F;IAC9F,6FAA6F;IAC7F,4CAA4C;IAC5C,MAAM,CAAC,mBAAmB,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,0BAA0B,CAAC,CAAC;IAErG,qEAAqE;IACrE,uEAAuE;IACvE,qEAAqE;IACrE,OAAO,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;IAClF,MAAM,YAAY,GAAG,MAAM,IAAA,mCAAkB,GAAE,CAAC;IAChD,IAAA,4BAAW,EAAC,aAAa,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;IAExD,wEAAwE;IACxE,0EAA0E;IAC1E,+BAA+B;IAC/B,MAAM,OAAO,GAAG,gBAAgB,CAAC,aAAa,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC;IAC7E,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAE1C,iGAAiG;IACjG,kGAAkG;IAClG,4FAA4F;IAC5F,MAAM,kBAAkB,GAAG,SAAS,IAAI,iCAAkB,CAAC;IAC3D,MAAM,aAAa,GAAG,IAAI,qCAAgB,EAAE,CAAC;IAC7C,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,aAAa,EAAE,kBAAkB,EAAE,YAAY,CAAC,CAAC;IACrF,OAAO,CAAC,GAAG,CAAC,WAAW,OAAO,CAAC,OAAO,CAAC,MAAM,uBAAuB,CAAC,CAAC;IACtE,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,OAAO;QAAE,OAAO,CAAC,GAAG,CAAC,wCAAwC,OAAO,EAAE,CAAC,CAAC;IAEtG,qFAAqF;IACrF,8FAA8F;IAC9F,qFAAqF;IACrF,OAAO,CAAC,GAAG,CAAC,sDAAsD,CAAC,CAAC;IACpE,MAAM,YAAY,GAAG,aAAa,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACzD,IAAA,wBAAS,EAAC,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,YAAY,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACnG,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IAE1C,wEAAwE;IACxE,qEAAqE;IACrE,MAAM,QAAQ,GAAG,IAAI,kCAAe,EAAE,CAAC,kBAAkB,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IACxF,OAAO,CAAC,GAAG,CAAC,WAAW,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE5C,sEAAsE;IACtE,wEAAwE;IACxE,yEAAyE;IACzE,oBAAoB,CAChB,aAAa,EACb,aAAa,EACb,gBAAgB,CAAC,aAAa,CAAC,EAC/B,YAAY,EACZ,OAAO,CAAC,eAAe,CAC1B,CAAC;IAEF,iBAAiB,CAAC,aAAa,CAAC,CAAC;AACrC,CAAC;AAEc,KAAK,UAAU,WAAW,CACrC,OAAgC,EAChC,OAAwB;IAExB,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACpC,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;IAEnD,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,kBAAkB,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;QACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,2FAA2F;QAC3F,2FAA2F;QAC3F,2EAA2E;QAC3E,MAAM,QAAQ,GAAG,KAAK,YAAY,4BAAa,CAAC,CAAC,CAAC,IAAA,qCAAsB,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;QAChG,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,QAAQ,CAAC,CAAC;QACtD,IAAI,KAAK,YAAY,wCAAuB,EAAE,CAAC;YAC3C,MAAM,MAAM,GAAG,IAAA,4BAAa,EAAC,aAAa,EAAE,+BAA+B,CAAC,CAAC;YAC7E,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,mBAAmB,GAAG,MAAM,GAAG,qDAAqD,CAAC,CAAC;QACxG,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json, plus\n * one contract file per API under architecture/apis/ (stale ones are deleted).\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate, RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { ProjectCycleDetector } from '../../lib/graph-cycles';\nimport { saveGraph, DEFAULT_GRAPH_PATH } from '../../lib/graph-loader';\nimport { ApiContractFiles } from '../../lib/api-contract-files';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { ProjectInfo } from '../../lib/project-info';\nimport {\n scanAndAttachApiRelations,\n describeUnresolvedApiCalls,\n describeNonLiteralDecoratorArgs,\n buildApiContracts,\n describeMismatchedEndpointKinds,\n} from '../../lib/api-usage/api-scanner';\nimport { buildExternalSystems } from '../../lib/api-usage/external-systems';\nimport type { ApiContracts, ExternalSystemDecls } from '../../lib/api-usage/api-relations';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport type { EnhancedGraph, GraphEntry } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraphReport, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { printAutoHiddenServers } from '../../lib/runtime-participant-resolver';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(\n workspaceRoot: string,\n graph: EnhancedGraph,\n hiddenProjects: Set<string>,\n apiContracts: ApiContracts,\n externalSystems: ExternalSystemDecls,\n): void {\n console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');\n const report = deriveRuntimeGraphReport(graph, hiddenProjects, apiContracts, externalSystems);\n saveRuntimeGraph(report.graph, workspaceRoot);\n const serviceCount = Object.keys(report.graph.services).length;\n const queueCount = Object.keys(report.graph.queues).length;\n console.log(\n `✅ Runtime graph saved (${serviceCount} services, ${report.graph.runtimeEdges.length} runtime edges, ` +\n `${queueCount} queues, ${report.graph.triggers.length} cron/external triggers)`,\n );\n // Every edge the derivation had to GUESS at. Loud on purpose: a fanned-out edge is committed and\n // then reasoned about as if it were derived, which is how a fictional call survives for months.\n printRuntimeWarnings(report.warnings);\n printAutoHiddenServers(report.autoHidden);\n // Printed here, FAILED by validate-runtime-architecture: generate must still write the graph,\n // or the validator would have nothing to compare against and the error would be unfixable.\n if (report.problems.length > 0) {\n console.error(`❌ ${report.problems.length} client call(s) name a service no module answers to:`);\n for (const problem of report.problems) console.error(` • ${problem}`);\n console.error(' This FAILS architecture:validate-runtime-architecture.');\n }\n}\n\n/** Print the derivation's guessed-edge warnings (nothing at all when the graph is fully targeted). */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printRuntimeWarnings(warnings: string[]): void {\n if (warnings.length === 0) return;\n console.warn(`⚠️ ${warnings.length} runtime edge(s) could not be targeted to ONE service:`);\n for (const warning of warnings) console.warn(` • ${warning}`);\n}\n\n/**\n * Scan for api relations and attach them, surfacing any contract we could NOT map back to source.\n * Unresolved contracts mean the graph we are about to WRITE is incomplete, so they must be loud —\n * a green run that quietly omits a service gets committed, rendered, and trusted.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction scanApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ScannedTables {\n console.log('🔎 Scanning source for implements/uses API relations...');\n const externalApiPaths = loadRuntimeConfig(workspaceRoot).externalApiPaths;\n const scan = scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths);\n if (scan.unresolvedApiCalls.length > 0) console.warn(describeUnresolvedApiCalls(scan.unresolvedApiCalls));\n // A decorator argument we could not read costs the graph a basePath, a method, or a whole\n // contract — none of which leaves a trace in the output. Name them before anything is written.\n if (scan.nonLiteralDecoratorArgs.length > 0) {\n console.warn(describeNonLiteralDecoratorArgs(scan.nonLiteralDecoratorArgs));\n }\n const contracts = buildApiContracts(scan);\n // An endpoint whose declared trigger its api kind cannot deliver would silently draw a queue or a\n // clock that nothing could ever fire — name it here, where the fix is one decorator away.\n const mismatches = describeMismatchedEndpointKinds(contracts);\n if (mismatches.length > 0) {\n console.error(`❌ ${mismatches.length} @Endpoint kind(s) conflict with their api kind:`);\n for (const mismatch of mismatches) console.error(` • ${mismatch}`);\n }\n return new ScannedTables(contracts, buildExternalSystems(scan.apiIndex, projectInfos));\n}\n\n/** The two committed tables one scan produces, kept together so callers cannot persist just one. */\nclass ScannedTables {\n constructor(\n public readonly apiContracts: ApiContracts,\n public readonly externalSystems: ExternalSystemDecls,\n ) {}\n}\n\n/** Projects tagged drawOnGraph:false — kept in the JSON, omitted from every rendered graph. */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction hiddenProjectsIn(graph: EnhancedGraph): Set<string> {\n const hidden = new Set<string>();\n for (const name of Object.keys(graph)) {\n if (graph[name].drawOnGraph === false) hidden.add(name);\n }\n return hidden;\n}\n\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printGraphSummary(graph: EnhancedGraph): void {\n const levels = new Set(Object.values(graph).map((entry: GraphEntry) => entry.level));\n console.log(`\\n📈 Graph Summary:`);\n console.log(` Projects: ${Object.keys(graph).length}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n}\n\n/**\n * The whole generation pipeline. Split out of runExecutor so that function stays a thin\n * try/catch-and-report shell — the error reporting is what a caller reads on failure, and it should\n * not be separated from the throw by fifty lines of steps.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nasync function generateEverything(workspaceRoot: string, graphPath: string | undefined): Promise<void> {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"📊 Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 1b: The graph is a BUILD graph — refuse a cyclic one, naming EVERY cycle. This runs\n // before the sort deliberately: the sort also refuses, but reports one cycle and an\n // undifferentiated list of everything tangled with it, so a repo with several cycles pays one\n // full regeneration per cycle to discover them.\n console.log('🔄 Checking the project graph is acyclic...');\n const cycles = new ProjectCycleDetector();\n cycles.assertAcyclic(reducedGraph, 'the nx project graph');\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('🔄 Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n // ...and assert the stratification it just produced actually holds: every dependency strictly\n // below its dependent. Safe to assert only because the graph was sorted a line ago — a stale\n // committed file is never checked this way.\n cycles.assertLevelsDescend(reducedGraph, cycles.levelsOf(enhancedGraph), 'the freshly sorted graph');\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n const scanned = scanApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n const apiContracts = scanned.apiContracts;\n\n // Step 4: Write one contract file per API (architecture/apis/<Api>.json) and delete the files of\n // APIs that no longer exist. dependencies.json only LINKS to them, so an endpoint change rewrites\n // an api file and never the dependency graph. The runtime validator reads these files back.\n const effectiveGraphPath = graphPath ?? DEFAULT_GRAPH_PATH;\n const contractFiles = new ApiContractFiles();\n const written = contractFiles.write(workspaceRoot, effectiveGraphPath, apiContracts);\n console.log(`✅ Wrote ${written.written.length} api contract file(s)`);\n for (const deleted of written.deleted) console.log(`🗑️ Deleted stale api contract file ${deleted}`);\n\n // Step 4a: Save the graph, INCLUDING the contract-file links and the external-system\n // declarations the runtime derivation reads back — generate derives from the in-memory graph,\n // validate from the files, so anything not written here would make the two disagree.\n console.log('💾 Saving graph to architecture/dependencies.json...');\n const contractRefs = contractFiles.refsFor(apiContracts);\n saveGraph(enhancedGraph, workspaceRoot, effectiveGraphPath, contractRefs, scanned.externalSystems);\n console.log('✅ Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`✅ Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n generateRuntimeGraph(\n workspaceRoot,\n enhancedGraph,\n hiddenProjectsIn(enhancedGraph),\n apiContracts,\n scanned.externalSystems,\n );\n\n printGraphSummary(enhancedGraph);\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n📊 Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n await generateEverything(workspaceRoot, graphPath);\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n // A RuleFailError carries its cures as Option[]; `Error.message` is only the aiMessage, so\n // printing that alone silently drops them. renderRuleFailForHuman is the ONE renderer that\n // numbers them, and this catch is the top-level handler for the nx target.\n const rendered = error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message;\n console.error('❌ Graph generation failed:', rendered);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️');\n }\n return { success: false };\n }\n}\n"]}
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json, plus\n * one contract file per API under architecture/apis/ (stale ones are deleted).\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate, RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { ProjectCycleDetector } from '../../lib/graph-cycles';\nimport { saveGraph, DEFAULT_GRAPH_PATH } from '../../lib/graph-loader';\nimport { ApiContractFiles } from '../../lib/api-contract-files';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { ProjectInfo } from '../../lib/project-info';\nimport {\n scanAndAttachApiRelations,\n describeNonLiteralDecoratorArgs,\n buildApiContracts,\n describeMismatchedEndpointKinds,\n} from '../../lib/api-usage/api-scanner';\n// The unresolved-contract report moved beside the other contract reports when api-scanner.ts reached\n// its file-size limit.\nimport { describeUnresolvedApiCalls } from '../../lib/api-usage/api-contract-errors';\nimport { buildExternalSystems } from '../../lib/api-usage/external-systems';\nimport type { ApiContracts, ExternalSystemDecls } from '../../lib/api-usage/api-relations';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport type { EnhancedGraph, GraphEntry } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraphReport, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { printAutoHiddenServers } from '../../lib/runtime-participant-resolver';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(\n workspaceRoot: string,\n graph: EnhancedGraph,\n hiddenProjects: Set<string>,\n apiContracts: ApiContracts,\n externalSystems: ExternalSystemDecls,\n): void {\n console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');\n const report = deriveRuntimeGraphReport(graph, hiddenProjects, apiContracts, externalSystems);\n saveRuntimeGraph(report.graph, workspaceRoot);\n const serviceCount = Object.keys(report.graph.services).length;\n const queueCount = Object.keys(report.graph.queues).length;\n console.log(\n `✅ Runtime graph saved (${serviceCount} services, ${report.graph.runtimeEdges.length} runtime edges, ` +\n `${queueCount} queues, ${report.graph.triggers.length} cron/external triggers)`,\n );\n // Every edge the derivation had to GUESS at. Loud on purpose: a fanned-out edge is committed and\n // then reasoned about as if it were derived, which is how a fictional call survives for months.\n printRuntimeWarnings(report.warnings);\n printAutoHiddenServers(report.autoHidden);\n // Printed here, FAILED by validate-runtime-architecture: generate must still write the graph,\n // or the validator would have nothing to compare against and the error would be unfixable.\n if (report.problems.length > 0) {\n console.error(`❌ ${report.problems.length} client call(s) name a service no module answers to:`);\n for (const problem of report.problems) console.error(` • ${problem}`);\n console.error(' This FAILS architecture:validate-runtime-architecture.');\n }\n}\n\n/** Print the derivation's guessed-edge warnings (nothing at all when the graph is fully targeted). */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printRuntimeWarnings(warnings: string[]): void {\n if (warnings.length === 0) return;\n console.warn(`⚠️ ${warnings.length} runtime edge(s) could not be targeted to ONE service:`);\n for (const warning of warnings) console.warn(` • ${warning}`);\n}\n\n/**\n * Scan for api relations and attach them, surfacing any contract we could NOT map back to source.\n * Unresolved contracts mean the graph we are about to WRITE is incomplete, so they must be loud —\n * a green run that quietly omits a service gets committed, rendered, and trusted.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction scanApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ScannedTables {\n console.log('🔎 Scanning source for implements/uses API relations...');\n const externalApiPaths = loadRuntimeConfig(workspaceRoot).externalApiPaths;\n const scan = scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths);\n if (scan.unresolvedApiCalls.length > 0) console.warn(describeUnresolvedApiCalls(scan.unresolvedApiCalls));\n // A decorator argument we could not read costs the graph a basePath, a method, or a whole\n // contract — none of which leaves a trace in the output. Name them before anything is written.\n if (scan.nonLiteralDecoratorArgs.length > 0) {\n console.warn(describeNonLiteralDecoratorArgs(scan.nonLiteralDecoratorArgs));\n }\n const contracts = buildApiContracts(scan);\n // An endpoint whose declared trigger its api kind cannot deliver would silently draw a queue or a\n // clock that nothing could ever fire — name it here, where the fix is one decorator away.\n const mismatches = describeMismatchedEndpointKinds(contracts);\n if (mismatches.length > 0) {\n console.error(`❌ ${mismatches.length} @Endpoint kind(s) conflict with their api kind:`);\n for (const mismatch of mismatches) console.error(` • ${mismatch}`);\n }\n return new ScannedTables(contracts, buildExternalSystems(scan.apiIndex, projectInfos));\n}\n\n/** The two committed tables one scan produces, kept together so callers cannot persist just one. */\nclass ScannedTables {\n constructor(\n public readonly apiContracts: ApiContracts,\n public readonly externalSystems: ExternalSystemDecls,\n ) {}\n}\n\n/** Projects tagged drawOnGraph:false — kept in the JSON, omitted from every rendered graph. */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction hiddenProjectsIn(graph: EnhancedGraph): Set<string> {\n const hidden = new Set<string>();\n for (const name of Object.keys(graph)) {\n if (graph[name].drawOnGraph === false) hidden.add(name);\n }\n return hidden;\n}\n\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printGraphSummary(graph: EnhancedGraph): void {\n const levels = new Set(Object.values(graph).map((entry: GraphEntry) => entry.level));\n console.log(`\\n📈 Graph Summary:`);\n console.log(` Projects: ${Object.keys(graph).length}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n}\n\n/**\n * The whole generation pipeline. Split out of runExecutor so that function stays a thin\n * try/catch-and-report shell — the error reporting is what a caller reads on failure, and it should\n * not be separated from the throw by fifty lines of steps.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nasync function generateEverything(workspaceRoot: string, graphPath: string | undefined): Promise<void> {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"📊 Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 1b: The graph is a BUILD graph — refuse a cyclic one, naming EVERY cycle. This runs\n // before the sort deliberately: the sort also refuses, but reports one cycle and an\n // undifferentiated list of everything tangled with it, so a repo with several cycles pays one\n // full regeneration per cycle to discover them.\n console.log('🔄 Checking the project graph is acyclic...');\n const cycles = new ProjectCycleDetector();\n cycles.assertAcyclic(reducedGraph, 'the nx project graph');\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('🔄 Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n // ...and assert the stratification it just produced actually holds: every dependency strictly\n // below its dependent. Safe to assert only because the graph was sorted a line ago — a stale\n // committed file is never checked this way.\n cycles.assertLevelsDescend(reducedGraph, cycles.levelsOf(enhancedGraph), 'the freshly sorted graph');\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n const scanned = scanApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n const apiContracts = scanned.apiContracts;\n\n // Step 4: Write one contract file per API (architecture/apis/<Api>.json) and delete the files of\n // APIs that no longer exist. dependencies.json only LINKS to them, so an endpoint change rewrites\n // an api file and never the dependency graph. The runtime validator reads these files back.\n const effectiveGraphPath = graphPath ?? DEFAULT_GRAPH_PATH;\n const contractFiles = new ApiContractFiles();\n const written = contractFiles.write(workspaceRoot, effectiveGraphPath, apiContracts);\n console.log(`✅ Wrote ${written.written.length} api contract file(s)`);\n for (const deleted of written.deleted) console.log(`🗑️ Deleted stale api contract file ${deleted}`);\n\n // Step 4a: Save the graph, INCLUDING the contract-file links and the external-system\n // declarations the runtime derivation reads back — generate derives from the in-memory graph,\n // validate from the files, so anything not written here would make the two disagree.\n console.log('💾 Saving graph to architecture/dependencies.json...');\n const contractRefs = contractFiles.refsFor(apiContracts);\n saveGraph(enhancedGraph, workspaceRoot, effectiveGraphPath, contractRefs, scanned.externalSystems);\n console.log('✅ Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`✅ Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n generateRuntimeGraph(\n workspaceRoot,\n enhancedGraph,\n hiddenProjectsIn(enhancedGraph),\n apiContracts,\n scanned.externalSystems,\n );\n\n printGraphSummary(enhancedGraph);\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n📊 Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n await generateEverything(workspaceRoot, graphPath);\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n // A RuleFailError carries its cures as Option[]; `Error.message` is only the aiMessage, so\n // printing that alone silently drops them. renderRuleFailForHuman is the ONE renderer that\n // numbers them, and this catch is the top-level handler for the nx target.\n const rendered = error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message;\n console.error('❌ Graph generation failed:', rendered);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️');\n }\n return { success: false };\n }\n}\n"]}
@@ -10,7 +10,31 @@
10
10
  *
11
11
  * Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.
12
12
  */
13
- import { EmptiedApiContract, UndeclaredEndpointOperation, UndeclaredExternalCaller, UnresolvedEndpointPath } from './api-relations';
13
+ import { EmptiedApiContract, UndeclaredEndpointOperation, UndeclaredExternalCaller, UnresolvedApiCall, UnresolvedEndpointPath } from './api-relations';
14
+ import { RootUnionFindings } from './root-union-scan';
15
+ /**
16
+ * Loud, actionable report for contracts the scan could not map to source. Callers print this
17
+ * instead of emitting a green graph that is quietly missing relations. Not fatal: a contract
18
+ * from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here.
19
+ */
20
+ export declare function describeUnresolvedApiCalls(calls: UnresolvedApiCall[]): string;
21
+ /**
22
+ * `no-root-union-api-type`: a request or response type that IS a union, on ANY `@ApiPath` contract.
23
+ *
24
+ * Fatal, and fatal EARLY, because the failure it prevents is discovered by somebody else at runtime
25
+ * and does not look like this contract's fault: a top-level `oneOf` is rejected by both the OpenAI
26
+ * and the Anthropic function-calling APIs, a server sends its whole tool list on every request, and
27
+ * so ONE such tool makes EVERY request 400. The user's report is "all the other tools broke".
28
+ *
29
+ * The second list is reasonless disables. A disable for this rule must carry an argument somebody
30
+ * wrote down: the escape hatch is deliberately per-site and never a blanket switch, because the
31
+ * blast radius is a whole session rather than one call.
32
+ */
33
+ export declare class RootUnionApiTypeError extends Error {
34
+ readonly findings: RootUnionFindings;
35
+ constructor(findings: RootUnionFindings);
36
+ private static render;
37
+ }
14
38
  /** Endpoints missing the operation declaration that drives retry safety and MCP annotations. */
15
39
  export declare class UndeclaredEndpointOperationError extends Error {
16
40
  readonly endpoints: readonly UndeclaredEndpointOperation[];
@@ -12,7 +12,81 @@
12
12
  * Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.EmptiedApiContractError = exports.UndeclaredExternalCallerError = exports.UnresolvedEndpointPathError = exports.MissingBasePathError = exports.UndeclaredEndpointOperationError = void 0;
15
+ exports.EmptiedApiContractError = exports.UndeclaredExternalCallerError = exports.UnresolvedEndpointPathError = exports.MissingBasePathError = exports.UndeclaredEndpointOperationError = exports.RootUnionApiTypeError = void 0;
16
+ exports.describeUnresolvedApiCalls = describeUnresolvedApiCalls;
17
+ const root_union_scan_1 = require("./root-union-scan");
18
+ /**
19
+ * Loud, actionable report for contracts the scan could not map to source. Callers print this
20
+ * instead of emitting a green graph that is quietly missing relations. Not fatal: a contract
21
+ * from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here.
22
+ */
23
+ // webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnclassifiedApiDep
24
+ function describeUnresolvedApiCalls(calls) {
25
+ const lines = [
26
+ `⚠️ ${calls.length} API contract(s) resolved to a declaration file with no matching workspace source.`,
27
+ ` Decorators (@ApiPath) are ERASED in .d.ts output, so these relations are MISSING from the graph:`,
28
+ ];
29
+ for (const call of calls) {
30
+ lines.push(` • ${call.api} at ${call.at} (${call.project}) → resolved to ${call.declaredIn}`);
31
+ }
32
+ lines.push(` If the api-lib IS in this workspace, add a tsconfig.base.json 'paths' entry mapping it to its`, ` src/index.ts, or confirm its project root is registered. If it is a published external package,`, ` this relation cannot be derived and the graph edge will not appear.`);
33
+ return lines.join('\n');
34
+ }
35
+ /** One line per offender, in the shape every error in this file uses. */
36
+ // webpieces-disable no-function-outside-class -- pure formatter shared by the one error below
37
+ function rootUnionLines(found) {
38
+ return found
39
+ .map((one) => ` • ${one.api}.${one.method} — ${one.side} type '${one.typeName}' at ${one.at}`)
40
+ .join('\n');
41
+ }
42
+ /**
43
+ * `no-root-union-api-type`: a request or response type that IS a union, on ANY `@ApiPath` contract.
44
+ *
45
+ * Fatal, and fatal EARLY, because the failure it prevents is discovered by somebody else at runtime
46
+ * and does not look like this contract's fault: a top-level `oneOf` is rejected by both the OpenAI
47
+ * and the Anthropic function-calling APIs, a server sends its whole tool list on every request, and
48
+ * so ONE such tool makes EVERY request 400. The user's report is "all the other tools broke".
49
+ *
50
+ * The second list is reasonless disables. A disable for this rule must carry an argument somebody
51
+ * wrote down: the escape hatch is deliberately per-site and never a blanket switch, because the
52
+ * blast radius is a whole session rather than one call.
53
+ */
54
+ class RootUnionApiTypeError extends Error {
55
+ findings;
56
+ constructor(findings) {
57
+ super(RootUnionApiTypeError.render(findings));
58
+ this.findings = findings;
59
+ this.name = 'RootUnionApiTypeError';
60
+ }
61
+ // webpieces-disable no-function-outside-class, max-lines-new-methods -- private static renderer of this class, and splitting one message hides what a reader actually sees
62
+ static render(findings) {
63
+ const parts = [];
64
+ if (findings.violations.length > 0) {
65
+ parts.push(`${findings.violations.length} @ApiPath method(s) declare a request or response type that IS a union:\n` +
66
+ rootUnionLines(findings.violations) +
67
+ `\n A union at the TOP LEVEL of a tool's parameter schema is rejected by BOTH the OpenAI and\n` +
68
+ ` the Anthropic function-calling APIs. A server sends its WHOLE tool list on every request, so\n` +
69
+ ` ONE of these makes EVERY request 400 and the entire client session unusable — not just that\n` +
70
+ ` tool. Nested composition, INSIDE a property, is fine and publishes as oneOf.\n` +
71
+ ` Fix it by wrapping the union in a property of an object:\n` +
72
+ ` export interface MoveWindowRequest { window: ScheduledWindow | AsapWindow }\n` +
73
+ ` This runs on EVERY @ApiPath contract, with or without @ApiType: @ApiType is a publishing\n` +
74
+ ` decision added later, so a shape that is not expressible must fail on the first line rather\n` +
75
+ ` than on the day somebody annotates it.\n` +
76
+ ` Last resort, per site: // webpieces-disable ${root_union_scan_1.ROOT_UNION_RULE} -- <reason>`);
77
+ }
78
+ if (findings.reasonlessDisables.length > 0) {
79
+ parts.push(`${findings.reasonlessDisables.length} disable(s) of ${root_union_scan_1.ROOT_UNION_RULE} give NO reason:\n` +
80
+ rootUnionLines(findings.reasonlessDisables) +
81
+ `\n The reason is MANDATORY — a reasonless disable is itself a violation. Write it as\n` +
82
+ ` // webpieces-disable ${root_union_scan_1.ROOT_UNION_RULE} -- <why this root union is deliberate>\n` +
83
+ ` The hatch is per-site and argued on purpose: this defect costs a whole client session, so\n` +
84
+ ` the next reader needs the argument, not just the suppression.`);
85
+ }
86
+ return parts.join('\n\n');
87
+ }
88
+ }
89
+ exports.RootUnionApiTypeError = RootUnionApiTypeError;
16
90
  /** Endpoints missing the operation declaration that drives retry safety and MCP annotations. */
17
91
  class UndeclaredEndpointOperationError extends Error {
18
92
  endpoints;
@@ -1 +1 @@
1
- {"version":3,"file":"api-contract-errors.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AASH,gGAAgG;AAChG,MAAa,gCAAiC,SAAQ,KAAK;IAC3B;IAA5B,YAA4B,SAAiD;QACzE,KAAK,CACD,GAAG,SAAS,CAAC,MAAM,sDAAsD;YACrE,SAAS;iBACJ,GAAG,CACA,CAAC,CAA8B,EAAE,EAAE,CAC/B,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,EAAE,EAAE,CAC/D;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,wFAAwF;YACxF,kFAAkF;YAClF,yDAAyD,CAChE,CAAC;QAZsB,cAAS,GAAT,SAAS,CAAwC;QAazE,IAAI,CAAC,IAAI,GAAG,kCAAkC,CAAC;IACnD,CAAC;CACJ;AAhBD,4EAgBC;AAED;;;GAGG;AACH,MAAa,oBAAqB,SAAQ,KAAK;IACf;IAA5B,YAA4B,SAA4B;QACpD,KAAK,CACD,GAAG,SAAS,CAAC,MAAM,8EAA8E;YAC7F,SAAS,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YACtD,8FAA8F;YAC9F,0FAA0F;YAC1F,qEAAqE,CAC5E,CAAC;QAPsB,cAAS,GAAT,SAAS,CAAmB;QAQpD,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACvC,CAAC;CACJ;AAXD,oDAWC;AAED;;;;;;GAMG;AACH,MAAa,2BAA4B,SAAQ,KAAK;IACtB;IAA5B,YAA4B,KAAwC;QAChE,KAAK,CACD,GAAG,KAAK,CAAC,MAAM,qDAAqD;YAChE,KAAK;iBACA,GAAG,CACA,CAAC,CAAyB,EAAE,EAAE,CAC1B,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,gBAAgB,CAAC,CAAC,QAAQ,aAAa,CAAC,CAAC,EAAE,EAAE,CAC/E;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,0FAA0F;YAC1F,wFAAwF;YACxF,kFAAkF;YAClF,wFAAwF;YACxF,sFAAsF;YACtF,yFAAyF;YACzF,6BAA6B,CACpC,CAAC;QAhBsB,UAAK,GAAL,KAAK,CAAmC;QAiBhE,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;IAC9C,CAAC;CACJ;AApBD,kEAoBC;AAED;;;;;;;;;GASG;AACH,MAAa,6BAA8B,SAAQ,KAAK;IACxB;IAA5B,YAA4B,OAA4C;QACpE,KAAK,CACD,GAAG,OAAO,CAAC,MAAM,2DAA2D;YACxE,OAAO;iBACF,GAAG,CACA,CAAC,CAA2B,EAAE,EAAE,CAC5B,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,EAAE,EAAE,CAC/D;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,wFAAwF;YACxF,uEAAuE;YACvE,0EAA0E;YAC1E,yFAAyF;YACzF,sEAAsE;YACtE,qGAAqG;YACrG,qFAAqF,CAC5F,CAAC;QAhBsB,YAAO,GAAP,OAAO,CAAqC;QAiBpE,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;IAChD,CAAC;CACJ;AApBD,sEAoBC;AAED;;;;;;GAMG;AACH,MAAa,uBAAwB,SAAQ,KAAK;IAClB;IAA5B,YAA4B,SAAwC;QAChE,KAAK,CACD,GAAG,SAAS,CAAC,MAAM,4EAA4E;YAC3F,SAAS;iBACJ,GAAG,CACA,CAAC,CAAqB,EAAE,EAAE,CACtB,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,+CAA+C,CAAC,CAAC,EAAE,EAAE,CAC3F;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,6FAA6F;YAC7F,uFAAuF;YACvF,mFAAmF;YACnF,qFAAqF;YACrF,mGAAmG;YACnG,mCAAmC,CAC1C,CAAC;QAfsB,cAAS,GAAT,SAAS,CAA+B;QAgBhE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IAC1C,CAAC;CACJ;AAnBD,0DAmBC","sourcesContent":["/**\n * The ways `buildApiContracts` refuses to emit a green, wrong api contract table.\n *\n * They share one rule: an entry that is PRESENT but incomplete is worse than an absent one.\n * Every other entry in the table is complete, so a consumer has no reason to suspect the one that\n * lost a field — it just computes a confidently wrong URL, or draws a service with no queues.\n *\n * Each aggregates EVERY offender into one message rather than throwing on the first: an author who\n * moved a constants module broke five decorators at once and wants all five named in one run.\n *\n * Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.\n */\n\nimport {\n EmptiedApiContract,\n UndeclaredEndpointOperation,\n UndeclaredExternalCaller,\n UnresolvedEndpointPath,\n} from './api-relations';\n\n/** Endpoints missing the operation declaration that drives retry safety and MCP annotations. */\nexport class UndeclaredEndpointOperationError extends Error {\n constructor(public readonly endpoints: readonly UndeclaredEndpointOperation[]) {\n super(\n `${endpoints.length} @Endpoint(s) do not declare a readable operation:\\n` +\n endpoints\n .map(\n (e: UndeclaredEndpointOperation) =>\n ` • ${e.api}.${e.method} — ${e.argument} at ${e.at}`,\n )\n .join('\\n') +\n `\\n operation is REQUIRED because it controls retry safety and generated MCP hints.\\n` +\n ` Pass exactly one of READ, WRITE_IDEMPOTENT, or WRITE as the third argument.\\n` +\n ` Do not infer operation semantics from the HTTP verb.`,\n );\n this.name = 'UndeclaredEndpointOperationError';\n }\n}\n\n/**\n * A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the\n * entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.\n */\nexport class MissingBasePathError extends Error {\n constructor(public readonly contracts: readonly string[]) {\n super(\n `${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\\n` +\n contracts.map((c: string) => ` • ${c}`).join('\\n') +\n `\\n basePath is REQUIRED in every api contract — an entry without it makes every consumer\\n` +\n ` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\\n` +\n ` or move the constant into the same module as the contract class.`,\n );\n this.name = 'MissingBasePathError';\n }\n}\n\n/**\n * `@Endpoint` paths the scan could not read. Fatal for the same reason MissingBasePathError is: the\n * two arguments are the two halves of ONE url. An http client builds its request as\n * `basePath + path`, so a contract shipped without a method's path is missing routing information,\n * and the consumer computes a confidently wrong URL. Skipping the method instead was worse still —\n * a class whose every path was an unreadable constant lost every method and vanished from the graph.\n */\nexport class UnresolvedEndpointPathError extends Error {\n constructor(public readonly paths: readonly UnresolvedEndpointPath[]) {\n super(\n `${paths.length} @Endpoint path(s) could not be read as a string:\\n` +\n paths\n .map(\n (p: UnresolvedEndpointPath) =>\n ` • ${p.api}.${p.method} — @Endpoint(${p.argument}, ...) at ${p.at}`,\n )\n .join('\\n') +\n `\\n path is REQUIRED in every api contract — every consumer builds its request URL as\\n` +\n ` basePath + path, so an unreadable path is MISSING ROUTING, not cosmetic metadata,\\n` +\n ` and a class whose every path is unreadable drops out of the graph entirely.\\n` +\n ` Inline the @Endpoint string literal, or move the constant into the SAME module as\\n` +\n ` the contract class — a same-module const IS resolved, one imported from another\\n` +\n ` module is NOT (this scan is parser-only by design: module resolution can land on a\\n` +\n ` decorator-erased .d.ts).`,\n );\n this.name = 'UnresolvedEndpointPathError';\n }\n}\n\n/**\n * `external` endpoints whose CALLER the scan could not read. Fatal, like the two above, because the\n * alternative is a diagram that lies by omission: the inbound box exists solely to name the system\n * calling us from outside, and with nothing to name it falls back to restating our own contract\n * name — which the reader already sees on the service box the arrow points at.\n *\n * `@Endpoint`'s TS overloads make `calledBy` a compile error to omit, so a scan reaching here saw a\n * JS caller, an `as any`, a cross-module constant this parser-only pass cannot fold, or a\n * `callerKind` that is not one of the declared kinds.\n */\nexport class UndeclaredExternalCallerError extends Error {\n constructor(public readonly callers: readonly UndeclaredExternalCaller[]) {\n super(\n `${callers.length} 'external' @Endpoint(s) do not declare WHO calls them:\\n` +\n callers\n .map(\n (c: UndeclaredExternalCaller) =>\n ` • ${c.api}.${c.method} — ${c.argument} at ${c.at}`,\n )\n .join('\\n') +\n `\\n An 'external' endpoint is driven by a system OUTSIDE this repo, and the runtime\\n` +\n ` architecture graph draws that system as an inbound box. Name it:\\n` +\n ` @Endpoint(POST, '/hook', WRITE, EXTERNAL, { calledBy: 'twilio' })\\n` +\n ` Add callerKind for anything that is not a vendor SaaS — database | cache | queue |\\n` +\n ` storage | saas | system — e.g. a GCP Pub/Sub push subscription:\\n` +\n ` @Endpoint(POST, '/push', WRITE, EXTERNAL, { calledBy: 'pubsub-push', callerKind: 'system' })\\n` +\n ` Use a string LITERAL or a SAME-module const: this scan is parser-only by design.`,\n );\n this.name = 'UndeclaredExternalCallerError';\n }\n}\n\n/**\n * Contract classes that declared `@Endpoint` methods and kept none of them. Fatal because the\n * alternative is the silent drop the api scan exists to close: buildApiContracts legitimately skips\n * a zero-method class (a vendor seam has no routes), and a class gutted by unreadable decorator\n * arguments used the very same exit — which is how a service lost two real Cloud Tasks queues and an\n * inbound webhook without a single line of output.\n */\nexport class EmptiedApiContractError extends Error {\n constructor(public readonly contracts: readonly EmptiedApiContract[]) {\n super(\n `${contracts.length} API contract class(es) declare @Endpoint methods but kept NONE of them:\\n` +\n contracts\n .map(\n (c: EmptiedApiContract) =>\n ` • ${c.api} — ${c.declared} @Endpoint method(s) declared, 0 usable, at ${c.at}`,\n )\n .join('\\n') +\n `\\n A contract with zero usable methods is DROPPED from the api contracts, so the class,\\n` +\n ` its queues and its triggers disappear from the architecture graph with no error.\\n` +\n ` Every @Endpoint argument must be readable: the path as a string literal or a\\n` +\n ` SAME-module const, the kind as 'rpc' | 'cloudtasks' | 'cron' | 'external', and\\n` +\n ` the options must declare operation. Fix the arguments above, or remove the decorators if the\\n` +\n ` class is genuinely not routed.`,\n );\n this.name = 'EmptiedApiContractError';\n }\n}\n"]}
1
+ {"version":3,"file":"api-contract-errors.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The ways `buildApiContracts` refuses to emit a green, wrong api contract table.\n *\n * They share one rule: an entry that is PRESENT but incomplete is worse than an absent one.\n * Every other entry in the table is complete, so a consumer has no reason to suspect the one that\n * lost a field — it just computes a confidently wrong URL, or draws a service with no queues.\n *\n * Each aggregates EVERY offender into one message rather than throwing on the first: an author who\n * moved a constants module broke five decorators at once and wants all five named in one run.\n *\n * Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.\n */\n\nimport {\n EmptiedApiContract,\n UndeclaredEndpointOperation,\n UndeclaredExternalCaller,\n UnresolvedApiCall,\n UnresolvedEndpointPath,\n} from './api-relations';\nimport { RootUnionApiType, RootUnionFindings, ROOT_UNION_RULE } from './root-union-scan';\n\n/**\n * Loud, actionable report for contracts the scan could not map to source. Callers print this\n * instead of emitting a green graph that is quietly missing relations. Not fatal: a contract\n * from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnclassifiedApiDep\nexport function describeUnresolvedApiCalls(calls: UnresolvedApiCall[]): string {\n const lines = [\n `⚠️ ${calls.length} API contract(s) resolved to a declaration file with no matching workspace source.`,\n ` Decorators (@ApiPath) are ERASED in .d.ts output, so these relations are MISSING from the graph:`,\n ];\n for (const call of calls) {\n lines.push(\n ` • ${call.api} at ${call.at} (${call.project}) → resolved to ${call.declaredIn}`,\n );\n }\n lines.push(\n ` If the api-lib IS in this workspace, add a tsconfig.base.json 'paths' entry mapping it to its`,\n ` src/index.ts, or confirm its project root is registered. If it is a published external package,`,\n ` this relation cannot be derived and the graph edge will not appear.`,\n );\n return lines.join('\\n');\n}\n\n/** One line per offender, in the shape every error in this file uses. */\n// webpieces-disable no-function-outside-class -- pure formatter shared by the one error below\nfunction rootUnionLines(found: readonly RootUnionApiType[]): string {\n return found\n .map(\n (one: RootUnionApiType) =>\n ` • ${one.api}.${one.method} — ${one.side} type '${one.typeName}' at ${one.at}`,\n )\n .join('\\n');\n}\n\n/**\n * `no-root-union-api-type`: a request or response type that IS a union, on ANY `@ApiPath` contract.\n *\n * Fatal, and fatal EARLY, because the failure it prevents is discovered by somebody else at runtime\n * and does not look like this contract's fault: a top-level `oneOf` is rejected by both the OpenAI\n * and the Anthropic function-calling APIs, a server sends its whole tool list on every request, and\n * so ONE such tool makes EVERY request 400. The user's report is \"all the other tools broke\".\n *\n * The second list is reasonless disables. A disable for this rule must carry an argument somebody\n * wrote down: the escape hatch is deliberately per-site and never a blanket switch, because the\n * blast radius is a whole session rather than one call.\n */\nexport class RootUnionApiTypeError extends Error {\n constructor(public readonly findings: RootUnionFindings) {\n super(RootUnionApiTypeError.render(findings));\n this.name = 'RootUnionApiTypeError';\n }\n\n // webpieces-disable no-function-outside-class, max-lines-new-methods -- private static renderer of this class, and splitting one message hides what a reader actually sees\n private static render(findings: RootUnionFindings): string {\n const parts: string[] = [];\n if (findings.violations.length > 0) {\n parts.push(\n `${findings.violations.length} @ApiPath method(s) declare a request or response type that IS a union:\\n` +\n rootUnionLines(findings.violations) +\n `\\n A union at the TOP LEVEL of a tool's parameter schema is rejected by BOTH the OpenAI and\\n` +\n ` the Anthropic function-calling APIs. A server sends its WHOLE tool list on every request, so\\n` +\n ` ONE of these makes EVERY request 400 and the entire client session unusable — not just that\\n` +\n ` tool. Nested composition, INSIDE a property, is fine and publishes as oneOf.\\n` +\n ` Fix it by wrapping the union in a property of an object:\\n` +\n ` export interface MoveWindowRequest { window: ScheduledWindow | AsapWindow }\\n` +\n ` This runs on EVERY @ApiPath contract, with or without @ApiType: @ApiType is a publishing\\n` +\n ` decision added later, so a shape that is not expressible must fail on the first line rather\\n` +\n ` than on the day somebody annotates it.\\n` +\n ` Last resort, per site: // webpieces-disable ${ROOT_UNION_RULE} -- <reason>`,\n );\n }\n if (findings.reasonlessDisables.length > 0) {\n parts.push(\n `${findings.reasonlessDisables.length} disable(s) of ${ROOT_UNION_RULE} give NO reason:\\n` +\n rootUnionLines(findings.reasonlessDisables) +\n `\\n The reason is MANDATORY — a reasonless disable is itself a violation. Write it as\\n` +\n ` // webpieces-disable ${ROOT_UNION_RULE} -- <why this root union is deliberate>\\n` +\n ` The hatch is per-site and argued on purpose: this defect costs a whole client session, so\\n` +\n ` the next reader needs the argument, not just the suppression.`,\n );\n }\n return parts.join('\\n\\n');\n }\n}\n\n/** Endpoints missing the operation declaration that drives retry safety and MCP annotations. */\nexport class UndeclaredEndpointOperationError extends Error {\n constructor(public readonly endpoints: readonly UndeclaredEndpointOperation[]) {\n super(\n `${endpoints.length} @Endpoint(s) do not declare a readable operation:\\n` +\n endpoints\n .map(\n (e: UndeclaredEndpointOperation) =>\n ` • ${e.api}.${e.method} — ${e.argument} at ${e.at}`,\n )\n .join('\\n') +\n `\\n operation is REQUIRED because it controls retry safety and generated MCP hints.\\n` +\n ` Pass exactly one of READ, WRITE_IDEMPOTENT, or WRITE as the third argument.\\n` +\n ` Do not infer operation semantics from the HTTP verb.`,\n );\n this.name = 'UndeclaredEndpointOperationError';\n }\n}\n\n/**\n * A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the\n * entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.\n */\nexport class MissingBasePathError extends Error {\n constructor(public readonly contracts: readonly string[]) {\n super(\n `${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\\n` +\n contracts.map((c: string) => ` • ${c}`).join('\\n') +\n `\\n basePath is REQUIRED in every api contract — an entry without it makes every consumer\\n` +\n ` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\\n` +\n ` or move the constant into the same module as the contract class.`,\n );\n this.name = 'MissingBasePathError';\n }\n}\n\n/**\n * `@Endpoint` paths the scan could not read. Fatal for the same reason MissingBasePathError is: the\n * two arguments are the two halves of ONE url. An http client builds its request as\n * `basePath + path`, so a contract shipped without a method's path is missing routing information,\n * and the consumer computes a confidently wrong URL. Skipping the method instead was worse still —\n * a class whose every path was an unreadable constant lost every method and vanished from the graph.\n */\nexport class UnresolvedEndpointPathError extends Error {\n constructor(public readonly paths: readonly UnresolvedEndpointPath[]) {\n super(\n `${paths.length} @Endpoint path(s) could not be read as a string:\\n` +\n paths\n .map(\n (p: UnresolvedEndpointPath) =>\n ` • ${p.api}.${p.method} — @Endpoint(${p.argument}, ...) at ${p.at}`,\n )\n .join('\\n') +\n `\\n path is REQUIRED in every api contract — every consumer builds its request URL as\\n` +\n ` basePath + path, so an unreadable path is MISSING ROUTING, not cosmetic metadata,\\n` +\n ` and a class whose every path is unreadable drops out of the graph entirely.\\n` +\n ` Inline the @Endpoint string literal, or move the constant into the SAME module as\\n` +\n ` the contract class — a same-module const IS resolved, one imported from another\\n` +\n ` module is NOT (this scan is parser-only by design: module resolution can land on a\\n` +\n ` decorator-erased .d.ts).`,\n );\n this.name = 'UnresolvedEndpointPathError';\n }\n}\n\n/**\n * `external` endpoints whose CALLER the scan could not read. Fatal, like the two above, because the\n * alternative is a diagram that lies by omission: the inbound box exists solely to name the system\n * calling us from outside, and with nothing to name it falls back to restating our own contract\n * name — which the reader already sees on the service box the arrow points at.\n *\n * `@Endpoint`'s TS overloads make `calledBy` a compile error to omit, so a scan reaching here saw a\n * JS caller, an `as any`, a cross-module constant this parser-only pass cannot fold, or a\n * `callerKind` that is not one of the declared kinds.\n */\nexport class UndeclaredExternalCallerError extends Error {\n constructor(public readonly callers: readonly UndeclaredExternalCaller[]) {\n super(\n `${callers.length} 'external' @Endpoint(s) do not declare WHO calls them:\\n` +\n callers\n .map(\n (c: UndeclaredExternalCaller) =>\n ` • ${c.api}.${c.method} — ${c.argument} at ${c.at}`,\n )\n .join('\\n') +\n `\\n An 'external' endpoint is driven by a system OUTSIDE this repo, and the runtime\\n` +\n ` architecture graph draws that system as an inbound box. Name it:\\n` +\n ` @Endpoint(POST, '/hook', WRITE, EXTERNAL, { calledBy: 'twilio' })\\n` +\n ` Add callerKind for anything that is not a vendor SaaS — database | cache | queue |\\n` +\n ` storage | saas | system — e.g. a GCP Pub/Sub push subscription:\\n` +\n ` @Endpoint(POST, '/push', WRITE, EXTERNAL, { calledBy: 'pubsub-push', callerKind: 'system' })\\n` +\n ` Use a string LITERAL or a SAME-module const: this scan is parser-only by design.`,\n );\n this.name = 'UndeclaredExternalCallerError';\n }\n}\n\n/**\n * Contract classes that declared `@Endpoint` methods and kept none of them. Fatal because the\n * alternative is the silent drop the api scan exists to close: buildApiContracts legitimately skips\n * a zero-method class (a vendor seam has no routes), and a class gutted by unreadable decorator\n * arguments used the very same exit — which is how a service lost two real Cloud Tasks queues and an\n * inbound webhook without a single line of output.\n */\nexport class EmptiedApiContractError extends Error {\n constructor(public readonly contracts: readonly EmptiedApiContract[]) {\n super(\n `${contracts.length} API contract class(es) declare @Endpoint methods but kept NONE of them:\\n` +\n contracts\n .map(\n (c: EmptiedApiContract) =>\n ` • ${c.api} — ${c.declared} @Endpoint method(s) declared, 0 usable, at ${c.at}`,\n )\n .join('\\n') +\n `\\n A contract with zero usable methods is DROPPED from the api contracts, so the class,\\n` +\n ` its queues and its triggers disappear from the architecture graph with no error.\\n` +\n ` Every @Endpoint argument must be readable: the path as a string literal or a\\n` +\n ` SAME-module const, the kind as 'rpc' | 'cloudtasks' | 'cron' | 'external', and\\n` +\n ` the options must declare operation. Fix the arguments above, or remove the decorators if the\\n` +\n ` class is genuinely not routed.`,\n );\n this.name = 'EmptiedApiContractError';\n }\n}\n"]}
@@ -12,7 +12,8 @@
12
12
  */
13
13
  import type { EnhancedGraph } from '../graph-sorter';
14
14
  import { ProjectInfo } from '../project-info';
15
- import { ApiScanResult, UnresolvedApiCall } from './api-scanner';
15
+ import { ApiScanResult } from './api-scanner';
16
+ import { UnresolvedApiCall } from './api-relations';
16
17
  /** One `role:server`/`role:client` project that depends on an api-lib it neither implements nor uses. */
17
18
  export interface UnclassifiedApiDep {
18
19
  project: string;
@@ -1 +1 @@
1
- {"version":3,"file":"api-relations-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations-validator.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAwCH,0DA8BC;AA6BD,gEAiBC;AAhHD,oDAA+C;AAG/C,8EAA8E;AAC9E,MAAM,aAAa,GAA0B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAiBlE,6EAA6E;AAC7E,qGAAqG;AACrG,SAAS,WAAW,CAAC,IAAmB,EAAE,MAAc;IACpD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QACxC,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,+GAA+G;AAC/G,SAAgB,uBAAuB,CACnC,KAAoB,EACpB,YAAsC,EACtC,IAAmB;IAEnB,MAAM,UAAU,GAAyB,EAAE,CAAC;IAC5C,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,IAAI,GAAG,IAAA,2BAAW,EAAC,IAAI,CAAC,CAAC,IAAI,CAAC;QACpC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7D,uFAAuF;QACvF,sFAAsF;QACtF,6CAA6C;QAC7C,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,SAAS;QAErD,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;QACjC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5C,IAAI,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5D,UAAU,CAAC,IAAI,CAAC;gBACZ,OAAO,EAAE,WAAW;gBACpB,IAAI;gBACJ,MAAM,EAAE,GAAG;gBACX,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC;gBAC5B,UAAU,EAAE,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAoB,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,WAAW,CAAC;aAClG,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IACD,OAAO,UAAU,CAAC;AACtB,CAAC;AAED;;;;;GAKG;AACH,iGAAiG;AACjG,SAAS,iBAAiB,CAAC,SAA6B;IACpD,MAAM,KAAK,GAAG;QACV,QAAQ,SAAS,CAAC,OAAO,WAAW,SAAS,CAAC,IAAI,yBAAyB,SAAS,CAAC,MAAM,YAAY;YACnG,kEAAkE;KACzE,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,UAAU,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CACN,YAAY,IAAI,CAAC,GAAG,OAAO,IAAI,CAAC,EAAE,gBAAgB,IAAI,CAAC,UAAU,gCAAgC,CACpG,CAAC;IACN,CAAC;IACD,KAAK,CAAC,IAAI,CACN,wGAAwG,EACxG,yDAAyD,SAAS,CAAC,MAAM,uBAAuB,EAChG,yDAAyD,CAC5D,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED,2EAA2E;AAC3E,iGAAiG;AACjG,SAAgB,0BAA0B,CAAC,SAA6B;IACpE,IAAI,SAAS,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,iBAAiB,CAAC,SAAS,CAAC,CAAC;IACzE,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClF,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC;IAC7C,MAAM,KAAK,GAAG;QACV,QAAQ,SAAS,CAAC,OAAO,WAAW,SAAS,CAAC,IAAI,yBAAyB,SAAS,CAAC,MAAM,IAAI;YAC3F,oDAAoD,OAAO,IAAI;QACnE,iBAAiB;QACjB,0FAA0F;YACtF,qEAAqE;YACrE,2BAA2B,MAAM,mCAAmC;YACpE,8CAA8C,MAAM,YAAY;QACpE,6DAA6D;YACzD,wBAAwB,MAAM,mBAAmB;QACrD,kDAAkD,SAAS,CAAC,MAAM,WAAW,SAAS,CAAC,OAAO,IAAI;KACrG,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * API Relations Validator\n *\n * Enforces that the dependency graph is TRUTHFUL: a runnable project (role server\n * or client) that compiles against an api-lib must actually IMPLEMENT (serve) or\n * USE (call) at least one of its APIs. A dependency that is neither is almost\n * always a mistake — a forgotten client wiring, a dead import, or a controller\n * that was never registered — and it would draw an unexplained edge in the graph.\n *\n * The check reuses the same source scan that produces `apiRelations`, so \"does P\n * relate to api-lib D\" is answered by real code, never a declaration.\n */\n\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult, UnresolvedApiCall } from './api-scanner';\n\n/** Roles that must justify every api-lib dependency (top-level runnables). */\nconst CHECKED_ROLES: ReadonlyArray<string> = ['server', 'client'];\n\n/** One `role:server`/`role:client` project that depends on an api-lib it neither implements nor uses. */\nexport interface UnclassifiedApiDep {\n project: string;\n role: string;\n apiLib: string;\n /** The API contract class names that api-lib exports (for the fix hint). */\n apis: string[];\n /**\n * Contracts this project DOES name at a call site but which never resolved to decorated\n * source. When non-empty the wiring almost certainly exists and the scan is blind — advice\n * to \"add a controller\" or \"remove the dependency\" would be actively wrong.\n */\n unresolved: UnresolvedApiCall[];\n}\n\n/** The API class names owned by `apiLib`, sorted (for a stable fix hint). */\n// webpieces-disable no-function-outside-class -- pure lookup helper, matches the validator-lib style\nfunction apisOwnedBy(scan: ApiScanResult, apiLib: string): string[] {\n const names: string[] = [];\n for (const info of scan.apiIndex.values()) {\n if (info.owner === apiLib) names.push(info.api);\n }\n return names.sort();\n}\n\n/**\n * Every server/client → api-lib edge for which the scan found NO implements and\n * NO uses. `graph` must already carry `apiRelations` (call scanAndAttachApiRelations first).\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors findUnclassified-style validators\nexport function findUnclassifiedApiDeps(\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n): UnclassifiedApiDep[] {\n const violations: UnclassifiedApiDep[] = [];\n for (const projectName of Object.keys(graph)) {\n const info = projectInfos.get(projectName);\n if (!info) continue;\n const role = resolveRole(info).role;\n if (role === null || !CHECKED_ROLES.includes(role)) continue;\n // Only flag projects whose production source was actually scanned. An all-test project\n // (e.g. an e2e harness whose only files are *.spec.ts) is never observed, so we can't\n // conclude its api-lib dependency is unused.\n if (!scan.scannedProjects.has(projectName)) continue;\n\n const entry = graph[projectName];\n for (const dep of entry.dependsOn) {\n if (!scan.apiLibProjects.has(dep)) continue;\n if (entry.apiRelations && entry.apiRelations[dep]) continue;\n violations.push({\n project: projectName,\n role,\n apiLib: dep,\n apis: apisOwnedBy(scan, dep),\n unresolved: scan.unresolvedApiCalls.filter((c: UnresolvedApiCall) => c.project === projectName),\n });\n }\n }\n return violations;\n}\n\n/**\n * The project DOES wire this contract up — we just couldn't see it, because the import resolved\n * to a decorator-erased declaration file. Report THAT, not a list of dead ends: telling a dev to\n * register a controller they already registered, or to delete a load-bearing dependency, sends\n * them chasing ghosts and gets the whole validator turned off.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style\nfunction describeBlindScan(violation: UnclassifiedApiDep): string {\n const lines = [\n ` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' and its ` +\n `wiring IS present in source, but the contract could not be read:`,\n ];\n for (const call of violation.unresolved) {\n lines.push(\n ` • ${call.api} at ${call.at} resolved to ${call.declaredIn} (decorators erased in .d.ts).`,\n );\n }\n lines.push(\n ` This is a CONFIG gap, not a wiring gap. Do NOT add a controller and do NOT remove the dependency.`,\n ` Fix: add a tsconfig.base.json 'paths' entry for '${violation.apiLib}' → its src/index.ts,`,\n ` so the import resolves to source instead of dist/.`,\n );\n return lines.join('\\n');\n}\n\n/** Human-readable, fix-oriented report for one unclassified dependency. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style\nexport function describeUnclassifiedApiDep(violation: UnclassifiedApiDep): string {\n if (violation.unresolved.length > 0) return describeBlindScan(violation);\n const apiHint = violation.apis.length > 0 ? violation.apis.join(', ') : 'the API';\n const theApi = violation.apis[0] ?? 'TheApi';\n const lines = [\n ` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' ` +\n `but neither IMPLEMENTS nor USES any of its APIs (${apiHint}).`,\n ` Do ONE of:`,\n ` 1. USE it as a client: inject ClientHttpFactory (@webpieces/http-client-node) or ` +\n `ClientHttpBrowserFactory (@webpieces/http-client-browser) and call ` +\n `factory.createRpcClient(${theApi}, config); for a @PubSub api use ` +\n `ClientCloudTasksFactory.createPubSubClient(${theApi}, config).`,\n ` 2. IMPLEMENT it: add a controller and register it — ` +\n `apiFactory.addRoutes(${theApi}, TheController).`,\n ` 3. If the dependency is unused, remove '${violation.apiLib}' from '${violation.project}'.`,\n ];\n return lines.join('\\n');\n}\n"]}
1
+ {"version":3,"file":"api-relations-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations-validator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations Validator\n *\n * Enforces that the dependency graph is TRUTHFUL: a runnable project (role server\n * or client) that compiles against an api-lib must actually IMPLEMENT (serve) or\n * USE (call) at least one of its APIs. A dependency that is neither is almost\n * always a mistake — a forgotten client wiring, a dead import, or a controller\n * that was never registered — and it would draw an unexplained edge in the graph.\n *\n * The check reuses the same source scan that produces `apiRelations`, so \"does P\n * relate to api-lib D\" is answered by real code, never a declaration.\n */\n\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult } from './api-scanner';\n// UnresolvedApiCall moved beside its sibling diagnostic DTOs when api-scanner.ts reached its limit.\nimport { UnresolvedApiCall } from './api-relations';\n\n/** Roles that must justify every api-lib dependency (top-level runnables). */\nconst CHECKED_ROLES: ReadonlyArray<string> = ['server', 'client'];\n\n/** One `role:server`/`role:client` project that depends on an api-lib it neither implements nor uses. */\nexport interface UnclassifiedApiDep {\n project: string;\n role: string;\n apiLib: string;\n /** The API contract class names that api-lib exports (for the fix hint). */\n apis: string[];\n /**\n * Contracts this project DOES name at a call site but which never resolved to decorated\n * source. When non-empty the wiring almost certainly exists and the scan is blind — advice\n * to \"add a controller\" or \"remove the dependency\" would be actively wrong.\n */\n unresolved: UnresolvedApiCall[];\n}\n\n/** The API class names owned by `apiLib`, sorted (for a stable fix hint). */\n// webpieces-disable no-function-outside-class -- pure lookup helper, matches the validator-lib style\nfunction apisOwnedBy(scan: ApiScanResult, apiLib: string): string[] {\n const names: string[] = [];\n for (const info of scan.apiIndex.values()) {\n if (info.owner === apiLib) names.push(info.api);\n }\n return names.sort();\n}\n\n/**\n * Every server/client → api-lib edge for which the scan found NO implements and\n * NO uses. `graph` must already carry `apiRelations` (call scanAndAttachApiRelations first).\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors findUnclassified-style validators\nexport function findUnclassifiedApiDeps(\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n): UnclassifiedApiDep[] {\n const violations: UnclassifiedApiDep[] = [];\n for (const projectName of Object.keys(graph)) {\n const info = projectInfos.get(projectName);\n if (!info) continue;\n const role = resolveRole(info).role;\n if (role === null || !CHECKED_ROLES.includes(role)) continue;\n // Only flag projects whose production source was actually scanned. An all-test project\n // (e.g. an e2e harness whose only files are *.spec.ts) is never observed, so we can't\n // conclude its api-lib dependency is unused.\n if (!scan.scannedProjects.has(projectName)) continue;\n\n const entry = graph[projectName];\n for (const dep of entry.dependsOn) {\n if (!scan.apiLibProjects.has(dep)) continue;\n if (entry.apiRelations && entry.apiRelations[dep]) continue;\n violations.push({\n project: projectName,\n role,\n apiLib: dep,\n apis: apisOwnedBy(scan, dep),\n unresolved: scan.unresolvedApiCalls.filter((c: UnresolvedApiCall) => c.project === projectName),\n });\n }\n }\n return violations;\n}\n\n/**\n * The project DOES wire this contract up — we just couldn't see it, because the import resolved\n * to a decorator-erased declaration file. Report THAT, not a list of dead ends: telling a dev to\n * register a controller they already registered, or to delete a load-bearing dependency, sends\n * them chasing ghosts and gets the whole validator turned off.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style\nfunction describeBlindScan(violation: UnclassifiedApiDep): string {\n const lines = [\n ` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' and its ` +\n `wiring IS present in source, but the contract could not be read:`,\n ];\n for (const call of violation.unresolved) {\n lines.push(\n ` • ${call.api} at ${call.at} resolved to ${call.declaredIn} (decorators erased in .d.ts).`,\n );\n }\n lines.push(\n ` This is a CONFIG gap, not a wiring gap. Do NOT add a controller and do NOT remove the dependency.`,\n ` Fix: add a tsconfig.base.json 'paths' entry for '${violation.apiLib}' → its src/index.ts,`,\n ` so the import resolves to source instead of dist/.`,\n );\n return lines.join('\\n');\n}\n\n/** Human-readable, fix-oriented report for one unclassified dependency. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style\nexport function describeUnclassifiedApiDep(violation: UnclassifiedApiDep): string {\n if (violation.unresolved.length > 0) return describeBlindScan(violation);\n const apiHint = violation.apis.length > 0 ? violation.apis.join(', ') : 'the API';\n const theApi = violation.apis[0] ?? 'TheApi';\n const lines = [\n ` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' ` +\n `but neither IMPLEMENTS nor USES any of its APIs (${apiHint}).`,\n ` Do ONE of:`,\n ` 1. USE it as a client: inject ClientHttpFactory (@webpieces/http-client-node) or ` +\n `ClientHttpBrowserFactory (@webpieces/http-client-browser) and call ` +\n `factory.createRpcClient(${theApi}, config); for a @PubSub api use ` +\n `ClientCloudTasksFactory.createPubSubClient(${theApi}, config).`,\n ` 2. IMPLEMENT it: add a controller and register it — ` +\n `apiFactory.addRoutes(${theApi}, TheController).`,\n ` 3. If the dependency is unused, remove '${violation.apiLib}' from '${violation.project}'.`,\n ];\n return lines.join('\\n');\n}\n"]}
@@ -218,6 +218,31 @@ export interface ApiContract {
218
218
  }
219
219
  /** apiClassName -> its committed contract. Serialized as one `architecture/apis/<ApiName>.json` each. */
220
220
  export type ApiContracts = Record<string, ApiContract>;
221
+ /**
222
+ * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an
223
+ * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken
224
+ * scan (a real api-lib whose source we never indexed), never a "this isn't an API" argument —
225
+ * so it is reported loudly instead of collapsing into a silent `return null`.
226
+ */
227
+ export declare class UnresolvedApiCall {
228
+ /** The project whose source makes the call. */
229
+ readonly project: string;
230
+ /** The contract class name as written at the call site. */
231
+ readonly api: string;
232
+ /** `path/to/file.ts:LINE` of the call site, workspace-relative. */
233
+ readonly at: string;
234
+ /** The declaration file the checker resolved to (where decorators are erased). */
235
+ readonly declaredIn: string;
236
+ constructor(
237
+ /** The project whose source makes the call. */
238
+ project: string,
239
+ /** The contract class name as written at the call site. */
240
+ api: string,
241
+ /** `path/to/file.ts:LINE` of the call site, workspace-relative. */
242
+ at: string,
243
+ /** The declaration file the checker resolved to (where decorators are erased). */
244
+ declaredIn: string);
245
+ }
221
246
  /**
222
247
  * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`
223
248
  * where SOME_CONST is imported from another module, a computed expression, an enum member, ...
@@ -15,7 +15,7 @@
15
15
  * only as binding identifiers, not as member names).
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.EmptiedApiContract = exports.UndeclaredEndpointOperation = exports.UndeclaredExternalCaller = exports.UnresolvedEndpointPath = exports.NonLiteralDecoratorArg = exports.EXTERNAL_SYSTEM_KINDS = void 0;
18
+ exports.EmptiedApiContract = exports.UndeclaredEndpointOperation = exports.UndeclaredExternalCaller = exports.UnresolvedEndpointPath = exports.NonLiteralDecoratorArg = exports.UnresolvedApiCall = exports.EXTERNAL_SYSTEM_KINDS = void 0;
19
19
  exports.isExternalSystemKind = isExternalSystemKind;
20
20
  exports.apiRefKey = apiRefKey;
21
21
  exports.deriveApiRelationKind = deriveApiRelationKind;
@@ -63,6 +63,33 @@ function isExternalSystemKind(value) {
63
63
  function apiRefKey(ref) {
64
64
  return `${ref.api} ${ref.targetService ?? ''}`;
65
65
  }
66
+ /**
67
+ * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an
68
+ * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken
69
+ * scan (a real api-lib whose source we never indexed), never a "this isn't an API" argument —
70
+ * so it is reported loudly instead of collapsing into a silent `return null`.
71
+ */
72
+ class UnresolvedApiCall {
73
+ project;
74
+ api;
75
+ at;
76
+ declaredIn;
77
+ constructor(
78
+ /** The project whose source makes the call. */
79
+ project,
80
+ /** The contract class name as written at the call site. */
81
+ api,
82
+ /** `path/to/file.ts:LINE` of the call site, workspace-relative. */
83
+ at,
84
+ /** The declaration file the checker resolved to (where decorators are erased). */
85
+ declaredIn) {
86
+ this.project = project;
87
+ this.api = api;
88
+ this.at = at;
89
+ this.declaredIn = declaredIn;
90
+ }
91
+ }
92
+ exports.UnresolvedApiCall = UnresolvedApiCall;
66
93
  /**
67
94
  * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`
68
95
  * where SOME_CONST is imported from another module, a computed expression, an enum member, ...
@@ -1 +1 @@
1
- {"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model —\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` — synchronous request/response over HTTP\n * - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a service→service edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * The kinds an external system can be DECLARED as. Each draws its own shape in the runtime viz, so\n * a datastore stops looking like an HTTP service.\n *\n * Lives here rather than beside the runtime graph model because that model already imports from this\n * file; putting it there and importing back would close a module cycle.\n */\nexport const EXTERNAL_SYSTEM_KINDS = [\n 'database',\n 'cache',\n 'queue',\n 'storage',\n 'saas',\n 'system',\n /**\n * A destination whose ADDRESS is supplied at runtime — a URL a partner registered, an OAuth\n * callback, a per-tenant host. Unlike every other kind it does not name one vendor: it names the\n * PLACE in our own system where somebody else's address is dialled, which is the fact a security\n * review is looking for.\n *\n * Declared like every other kind, on the CONTRACT: `@externalSystem runtime partner-webhooks`.\n * On the contract rather than at a `createRpcClient` call site deliberately — \"the far end of\n * this contract is outside our estate\" is a property of the CONTRACT, true for every caller of\n * it, so putting it there means one declaration however many services deliver over it, and\n * nothing to keep in step when a second one appears. It also means this kind rides the exact\n * same declare → resolve → draw pipeline `saas` and `database` already ride, rather than a\n * second mechanism that reads construction sites and can disagree with the first.\n */\n 'runtime',\n] as const;\n\nexport type ExternalSystemKind = (typeof EXTERNAL_SYSTEM_KINDS)[number];\n\n/** True for a string that names one of {@link EXTERNAL_SYSTEM_KINDS}. */\n// webpieces-disable no-function-outside-class -- type guard beside the type it guards, matching this file's DTO style\nexport function isExternalSystemKind(value: string): value is ExternalSystemKind {\n return (EXTERNAL_SYSTEM_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * ONE declared external system, keyed in {@link ExternalSystemDecls} by its IDENTITY.\n *\n * Identity, not display text: two projects each tagged `external:database:postgres` name the same\n * `postgres` node and converge on it with one arrow apiece, instead of drawing a database each.\n *\n * The two arrays are the two declaration sites, and a system may legitimately have both — a repo can\n * wrap a datastore behind a contract in one service and open it directly in another.\n */\nexport interface ExternalSystemDecl {\n kind: ExternalSystemKind;\n label: string;\n /** Contracts declaring it with an `@externalSystem` JSDoc tag; every user of one gets an arrow. */\n apis: string[];\n /** Projects declaring it with an `external:<kind>:<identity>` nx tag; each gets its OWN arrow. */\n projects: string[];\n}\n\n/** identity -> its declaration. Serialized as the `externalSystems` key of dependencies.json. */\nexport type ExternalSystemDecls = Record<string, ExternalSystemDecl>;\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` — it serves the api (a controller extends it)\n * - `uses` — it calls the api (generates a client)\n * - `uses-implements` — it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` → `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api — which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" — the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /**\n * ONLY on a `pubsub` uses ref. True means \"this producer was attributed to EVERY cloudtasks\n * method of the contract, not to the methods it actually enqueues\".\n *\n * A producer builds one client for the whole contract (`createPubSubClient(EmailTaskApi, cfg)`)\n * and enqueues through a proxy (`emailTasks.send(req)`) somewhere else entirely — often after\n * the client has been stored in a DI binding — so WHICH methods it enqueues is not statically\n * recoverable. The consumer side IS exact (addRoutes + the contract's method table). Recording\n * the difference keeps a producer-side queue from being read as proof that queue is used.\n */\n methodsInferred?: boolean;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/** Side-effect semantics copied from the required third `@Endpoint` argument. */\nexport type EndpointOperation = 'read' | 'write-idempotent' | 'write';\n\n/** HTTP verbs understood by the generated contract runtime. */\nexport type ContractHttpMethod = 'GET' | 'POST';\n\n/** One path/query/body mapping emitted into architecture/dependencies.json. */\nexport interface ApiParameterMeta {\n index: number;\n source: 'path' | 'query' | 'body';\n /** Path/query wire name; a JSON/form body occupies the whole entity and has no key. */\n wireName?: string;\n}\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /** Retry/idempotency contract; independent of the HTTP verb. */\n operation: EndpointOperation;\n /** Required in newly scanned source; optional only when reading older committed graph data. */\n httpMethod?: ContractHttpMethod;\n /** Explicit parameter mappings; absent only when the method has none. */\n parameters?: ApiParameterMeta[];\n /** Present when callers receive the transport-neutral full response. */\n responseType?: 'full';\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method — those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * inbound `external`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n /**\n * WHO outside this repo drives this endpoint, from `@Endpoint(POST, p, WRITE, EXTERNAL, { calledBy })`.\n *\n * ONLY on an `external` method, the same way `queueName` is only on the kinds that HAVE a queue.\n *\n * Deliberately the SAME {@link ExternalSystemDeclaration} the OUTBOUND `@externalSystem` tag\n * resolves to, not a parallel inbound-only type: an inbound `saas twilio` and an outbound\n * `saas twilio` are the same vendor, so sharing the type makes them share an IDENTITY and\n * converge on ONE node instead of drawing twilio twice facing opposite directions.\n *\n * Optional in the TYPE only for graphs generated before the caller was required — generation\n * FAILS on an `external` method whose caller cannot be read (UndeclaredExternalCallerError).\n */\n caller?: ExternalSystemDeclaration;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints — it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — a vendor seam\n * declaring WHAT it is a seam to. JSDoc rather than a decorator because these seams are plain TS\n * `interface`s, which cannot carry one.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\n/** The `(kind, label)` pair a single declaration resolves to. */\nexport interface ExternalSystemDeclaration {\n kind: ExternalSystemKind;\n label: string;\n}\n\n/**\n * The committed, per-contract view written to `architecture/apis/<ApiName>.json` (one file per API;\n * dependencies.json only links to it under `apiContractFiles` — see ApiContractFiles).\n *\n * The runtime graph is derived from the committed files so generate and validate can never\n * diverge — which means anything the runtime graph needs must be COMMITTED, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it — every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as one `architecture/apis/<ApiName>.json` each. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class — with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.\n *\n * Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because\n * this one is FATAL. Upstream components need the URL: an http client builds its request as\n * `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata — the same\n * reasoning that already makes basePath required. Skipping the method instead used to delete it, and\n * a class whose every path was a constant lost every method and vanished from the contract table with\n * nothing printed anywhere.\n */\nexport class UnresolvedEndpointPath {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** The path argument exactly as written, e.g. `PROCESS_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `external` `@Endpoint` whose CALLER could not be read from the source.\n *\n * Fatal for the same reason {@link UnresolvedEndpointPath} is. The inbound box exists to say who is\n * calling us from outside; with no caller it can only restate our own contract name, which is the\n * exact bug this diagnostic exists to make impossible to reintroduce. `@Endpoint`'s TS overloads\n * already require `calledBy`, so anything reaching here is a JS caller, an `as any`, a cross-module\n * constant the parser-only scan cannot fold, or an unknown `callerKind`.\n */\nexport class UndeclaredExternalCaller {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, `SOME_CONST`, `callerKind: 'vendor'`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */\nexport class UndeclaredEndpointOperation {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, a constant name, or an invalid literal. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * A contract class that DECLARED `@Endpoint` methods and kept none of them.\n *\n * The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method\n * class (correctly — a vendor seam has no routes), so a class gutted by unreadable decorator\n * arguments left through the same door as a legitimately routeless one. A class that declared\n * endpoints and produced none is never legitimate, so it is named instead.\n */\nexport class EmptiedApiContract {\n constructor(\n /** The contract class name. */\n public readonly api: string,\n /** How many `@Endpoint` decorators were written on it. */\n public readonly declared: number,\n /** `path/to/file.ts:LINE` of the class, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(\n implementsRefs: ApiRef[],\n usesRefs: ApiRef[],\n): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) ||\n (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
1
+ {"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model —\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` — synchronous request/response over HTTP\n * - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a service→service edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * The kinds an external system can be DECLARED as. Each draws its own shape in the runtime viz, so\n * a datastore stops looking like an HTTP service.\n *\n * Lives here rather than beside the runtime graph model because that model already imports from this\n * file; putting it there and importing back would close a module cycle.\n */\nexport const EXTERNAL_SYSTEM_KINDS = [\n 'database',\n 'cache',\n 'queue',\n 'storage',\n 'saas',\n 'system',\n /**\n * A destination whose ADDRESS is supplied at runtime — a URL a partner registered, an OAuth\n * callback, a per-tenant host. Unlike every other kind it does not name one vendor: it names the\n * PLACE in our own system where somebody else's address is dialled, which is the fact a security\n * review is looking for.\n *\n * Declared like every other kind, on the CONTRACT: `@externalSystem runtime partner-webhooks`.\n * On the contract rather than at a `createRpcClient` call site deliberately — \"the far end of\n * this contract is outside our estate\" is a property of the CONTRACT, true for every caller of\n * it, so putting it there means one declaration however many services deliver over it, and\n * nothing to keep in step when a second one appears. It also means this kind rides the exact\n * same declare → resolve → draw pipeline `saas` and `database` already ride, rather than a\n * second mechanism that reads construction sites and can disagree with the first.\n */\n 'runtime',\n] as const;\n\nexport type ExternalSystemKind = (typeof EXTERNAL_SYSTEM_KINDS)[number];\n\n/** True for a string that names one of {@link EXTERNAL_SYSTEM_KINDS}. */\n// webpieces-disable no-function-outside-class -- type guard beside the type it guards, matching this file's DTO style\nexport function isExternalSystemKind(value: string): value is ExternalSystemKind {\n return (EXTERNAL_SYSTEM_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * ONE declared external system, keyed in {@link ExternalSystemDecls} by its IDENTITY.\n *\n * Identity, not display text: two projects each tagged `external:database:postgres` name the same\n * `postgres` node and converge on it with one arrow apiece, instead of drawing a database each.\n *\n * The two arrays are the two declaration sites, and a system may legitimately have both — a repo can\n * wrap a datastore behind a contract in one service and open it directly in another.\n */\nexport interface ExternalSystemDecl {\n kind: ExternalSystemKind;\n label: string;\n /** Contracts declaring it with an `@externalSystem` JSDoc tag; every user of one gets an arrow. */\n apis: string[];\n /** Projects declaring it with an `external:<kind>:<identity>` nx tag; each gets its OWN arrow. */\n projects: string[];\n}\n\n/** identity -> its declaration. Serialized as the `externalSystems` key of dependencies.json. */\nexport type ExternalSystemDecls = Record<string, ExternalSystemDecl>;\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` — it serves the api (a controller extends it)\n * - `uses` — it calls the api (generates a client)\n * - `uses-implements` — it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` → `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api — which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" — the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /**\n * ONLY on a `pubsub` uses ref. True means \"this producer was attributed to EVERY cloudtasks\n * method of the contract, not to the methods it actually enqueues\".\n *\n * A producer builds one client for the whole contract (`createPubSubClient(EmailTaskApi, cfg)`)\n * and enqueues through a proxy (`emailTasks.send(req)`) somewhere else entirely — often after\n * the client has been stored in a DI binding — so WHICH methods it enqueues is not statically\n * recoverable. The consumer side IS exact (addRoutes + the contract's method table). Recording\n * the difference keeps a producer-side queue from being read as proof that queue is used.\n */\n methodsInferred?: boolean;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/** Side-effect semantics copied from the required third `@Endpoint` argument. */\nexport type EndpointOperation = 'read' | 'write-idempotent' | 'write';\n\n/** HTTP verbs understood by the generated contract runtime. */\nexport type ContractHttpMethod = 'GET' | 'POST';\n\n/** One path/query/body mapping emitted into architecture/dependencies.json. */\nexport interface ApiParameterMeta {\n index: number;\n source: 'path' | 'query' | 'body';\n /** Path/query wire name; a JSON/form body occupies the whole entity and has no key. */\n wireName?: string;\n}\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /** Retry/idempotency contract; independent of the HTTP verb. */\n operation: EndpointOperation;\n /** Required in newly scanned source; optional only when reading older committed graph data. */\n httpMethod?: ContractHttpMethod;\n /** Explicit parameter mappings; absent only when the method has none. */\n parameters?: ApiParameterMeta[];\n /** Present when callers receive the transport-neutral full response. */\n responseType?: 'full';\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method — those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * inbound `external`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n /**\n * WHO outside this repo drives this endpoint, from `@Endpoint(POST, p, WRITE, EXTERNAL, { calledBy })`.\n *\n * ONLY on an `external` method, the same way `queueName` is only on the kinds that HAVE a queue.\n *\n * Deliberately the SAME {@link ExternalSystemDeclaration} the OUTBOUND `@externalSystem` tag\n * resolves to, not a parallel inbound-only type: an inbound `saas twilio` and an outbound\n * `saas twilio` are the same vendor, so sharing the type makes them share an IDENTITY and\n * converge on ONE node instead of drawing twilio twice facing opposite directions.\n *\n * Optional in the TYPE only for graphs generated before the caller was required — generation\n * FAILS on an `external` method whose caller cannot be read (UndeclaredExternalCallerError).\n */\n caller?: ExternalSystemDeclaration;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints — it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — a vendor seam\n * declaring WHAT it is a seam to. JSDoc rather than a decorator because these seams are plain TS\n * `interface`s, which cannot carry one.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\n/** The `(kind, label)` pair a single declaration resolves to. */\nexport interface ExternalSystemDeclaration {\n kind: ExternalSystemKind;\n label: string;\n}\n\n/**\n * The committed, per-contract view written to `architecture/apis/<ApiName>.json` (one file per API;\n * dependencies.json only links to it under `apiContractFiles` — see ApiContractFiles).\n *\n * The runtime graph is derived from the committed files so generate and validate can never\n * diverge — which means anything the runtime graph needs must be COMMITTED, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it — every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as one `architecture/apis/<ApiName>.json` each. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an\n * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken\n * scan (a real api-lib whose source we never indexed), never a \"this isn't an API\" argument —\n * so it is reported loudly instead of collapsing into a silent `return null`.\n */\nexport class UnresolvedApiCall {\n constructor(\n /** The project whose source makes the call. */\n public readonly project: string,\n /** The contract class name as written at the call site. */\n public readonly api: string,\n /** `path/to/file.ts:LINE` of the call site, workspace-relative. */\n public readonly at: string,\n /** The declaration file the checker resolved to (where decorators are erased). */\n public readonly declaredIn: string,\n ) {}\n}\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class — with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.\n *\n * Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because\n * this one is FATAL. Upstream components need the URL: an http client builds its request as\n * `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata — the same\n * reasoning that already makes basePath required. Skipping the method instead used to delete it, and\n * a class whose every path was a constant lost every method and vanished from the contract table with\n * nothing printed anywhere.\n */\nexport class UnresolvedEndpointPath {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** The path argument exactly as written, e.g. `PROCESS_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `external` `@Endpoint` whose CALLER could not be read from the source.\n *\n * Fatal for the same reason {@link UnresolvedEndpointPath} is. The inbound box exists to say who is\n * calling us from outside; with no caller it can only restate our own contract name, which is the\n * exact bug this diagnostic exists to make impossible to reintroduce. `@Endpoint`'s TS overloads\n * already require `calledBy`, so anything reaching here is a JS caller, an `as any`, a cross-module\n * constant the parser-only scan cannot fold, or an unknown `callerKind`.\n */\nexport class UndeclaredExternalCaller {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, `SOME_CONST`, `callerKind: 'vendor'`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */\nexport class UndeclaredEndpointOperation {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, a constant name, or an invalid literal. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * A contract class that DECLARED `@Endpoint` methods and kept none of them.\n *\n * The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method\n * class (correctly — a vendor seam has no routes), so a class gutted by unreadable decorator\n * arguments left through the same door as a legitimately routeless one. A class that declared\n * endpoints and produced none is never legitimate, so it is named instead.\n */\nexport class EmptiedApiContract {\n constructor(\n /** The contract class name. */\n public readonly api: string,\n /** How many `@Endpoint` decorators were written on it. */\n public readonly declared: number,\n /** `path/to/file.ts:LINE` of the class, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(\n implementsRefs: ApiRef[],\n usesRefs: ApiRef[],\n): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) ||\n (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}