@webpieces/nx-webpieces-rules 0.4.854 → 0.4.855

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/executors.json +5 -0
  2. package/package.json +7 -7
  3. package/src/executors/generate/executor.js +31 -55
  4. package/src/executors/generate/executor.js.map +1 -1
  5. package/src/executors/runtime-wiring-check/executor.d.ts +10 -0
  6. package/src/executors/runtime-wiring-check/executor.js +42 -0
  7. package/src/executors/runtime-wiring-check/executor.js.map +1 -0
  8. package/src/executors/runtime-wiring-check/schema.json +14 -0
  9. package/src/executors/validate-api-lib-tag/executor.js +5 -3
  10. package/src/executors/validate-api-lib-tag/executor.js.map +1 -1
  11. package/src/executors/validate-api-relations/executor.js +11 -8
  12. package/src/executors/validate-api-relations/executor.js.map +1 -1
  13. package/src/executors/validate-architecture-unchanged/executor.d.ts +1 -1
  14. package/src/executors/validate-architecture-unchanged/executor.js +22 -24
  15. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  16. package/src/lib/api-usage/api-contract-approval.d.ts +7 -0
  17. package/src/lib/api-usage/api-contract-approval.js +50 -0
  18. package/src/lib/api-usage/api-contract-approval.js.map +1 -0
  19. package/src/lib/api-usage/api-contract-evidence.d.ts +6 -0
  20. package/src/lib/api-usage/api-contract-evidence.js +41 -0
  21. package/src/lib/api-usage/api-contract-evidence.js.map +1 -0
  22. package/src/lib/api-usage/api-relations-validator.d.ts +13 -2
  23. package/src/lib/api-usage/api-relations-validator.js +23 -0
  24. package/src/lib/api-usage/api-relations-validator.js.map +1 -1
  25. package/src/lib/api-usage/api-relations.d.ts +3 -0
  26. package/src/lib/api-usage/api-relations.js.map +1 -1
  27. package/src/lib/api-usage/api-scanner.d.ts +40 -0
  28. package/src/lib/api-usage/api-scanner.js +7 -1
  29. package/src/lib/api-usage/api-scanner.js.map +1 -1
  30. package/src/lib/graph-comparator.js +11 -1
  31. package/src/lib/graph-comparator.js.map +1 -1
  32. package/src/lib/graph-loader.js +12 -6
  33. package/src/lib/graph-loader.js.map +1 -1
  34. package/src/lib/graph-navigation.d.ts +5 -0
  35. package/src/lib/graph-navigation.js +102 -0
  36. package/src/lib/graph-navigation.js.map +1 -0
  37. package/src/lib/graph-sorter.d.ts +2 -0
  38. package/src/lib/graph-sorter.js.map +1 -1
  39. package/src/lib/graph-visualizer.d.ts +0 -11
  40. package/src/lib/graph-visualizer.js +13 -9
  41. package/src/lib/graph-visualizer.js.map +1 -1
  42. package/src/lib/runtime-details.d.ts +13 -0
  43. package/src/lib/runtime-details.js +191 -0
  44. package/src/lib/runtime-details.js.map +1 -0
  45. package/src/lib/runtime-graph-model.d.ts +4 -1
  46. package/src/lib/runtime-graph-model.js.map +1 -1
  47. package/src/lib/runtime-graph.js +10 -4
  48. package/src/lib/runtime-graph.js.map +1 -1
  49. package/src/lib/runtime-html-page.d.ts +2 -1
  50. package/src/lib/runtime-html-page.js +6 -1
  51. package/src/lib/runtime-html-page.js.map +1 -1
  52. package/src/lib/runtime-visualizer.d.ts +0 -46
  53. package/src/lib/runtime-visualizer.js +29 -43
  54. package/src/lib/runtime-visualizer.js.map +1 -1
  55. package/src/lib/runtime-viz-theme.js +4 -3
  56. package/src/lib/runtime-viz-theme.js.map +1 -1
  57. package/src/lib/runtime-wiring/DeclarationCompileAssertions.d.ts +4 -0
  58. package/src/lib/runtime-wiring/DeclarationCompileAssertions.js +27 -0
  59. package/src/lib/runtime-wiring/DeclarationCompileAssertions.js.map +1 -0
  60. package/src/lib/runtime-wiring/approved-graph.d.ts +8 -0
  61. package/src/lib/runtime-wiring/approved-graph.js +115 -0
  62. package/src/lib/runtime-wiring/approved-graph.js.map +1 -0
  63. package/src/lib/runtime-wiring/assembler.d.ts +24 -0
  64. package/src/lib/runtime-wiring/assembler.js +116 -0
  65. package/src/lib/runtime-wiring/assembler.js.map +1 -0
  66. package/src/lib/runtime-wiring/codec.d.ts +14 -0
  67. package/src/lib/runtime-wiring/codec.js +162 -0
  68. package/src/lib/runtime-wiring/codec.js.map +1 -0
  69. package/src/lib/runtime-wiring/declaration.d.ts +68 -0
  70. package/src/lib/runtime-wiring/declaration.js +88 -0
  71. package/src/lib/runtime-wiring/declaration.js.map +1 -0
  72. package/src/lib/runtime-wiring/source-extractor.d.ts +28 -0
  73. package/src/lib/runtime-wiring/source-extractor.js +253 -0
  74. package/src/lib/runtime-wiring/source-extractor.js.map +1 -0
  75. package/src/lib/runtime-wiring/source-values.d.ts +21 -0
  76. package/src/lib/runtime-wiring/source-values.js +204 -0
  77. package/src/lib/runtime-wiring/source-values.js.map +1 -0
  78. package/src/lib/runtime-wiring/verification.d.ts +9 -0
  79. package/src/lib/runtime-wiring/verification.js +56 -0
  80. package/src/lib/runtime-wiring/verification.js.map +1 -0
  81. package/src/plugin.d.ts +0 -21
  82. package/src/plugin.js +12 -7
  83. package/src/plugin.js.map +1 -1
  84. package/src/runtime-wiring-targets.d.ts +7 -0
  85. package/src/runtime-wiring-targets.js +58 -0
  86. package/src/runtime-wiring-targets.js.map +1 -0
  87. package/src/validation-targets.js +10 -2
  88. package/src/validation-targets.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-architecture-unchanged/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA0FH,gDAIC;AA4ED,8BAqEC;AA5OD,0DAA+F;AAC/F,+DAAiE;AACjE,yDAAgE;AAChE,iEAA2D;AAC3D,yDAA2E;AAE3E,6DAAoG;AACpG,mDAAoD;AACpD,iEAA+F;AAC/F,2EAA4E;AAE5E,qEAAgE;AAEhE,6DAA6D;AAC7D,mDAA+C;AAE/C,2CAAwC;AAUxC,MAAM,WAAW,GAAG,2BAA2B,CAAC;AAEhD;;;GAGG;AACH,SAAS,wBAAwB,CAAC,aAAqB;IACnD,MAAM,MAAM,GAAG,IAAA,4BAAa,EAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAEzD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,SAAS,cAAc,CAAC,OAAe,EAAE,aAAqB;IAC1D,MAAM,MAAM,GAAG,wBAAwB,CAAC,aAAa,CAAC,CAAC;IAEvD,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;IAC/D,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvB,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,CAAC,mBAAmB,GAAG,MAAM,GAAG,wCAAwC,CAAC,CAAC;IACvF,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACzB,OAAO,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAC/C,OAAO,CAAC,KAAK,CAAC,oGAAoG,CAAC,CAAC;IACpH,OAAO,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,2GAA2G;AAC3G,SAAS,yBAAyB,CAC9B,OAA4B,EAC5B,KAA0B;IAE1B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpF,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,KAAK,SAAS;YACf,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,uDAAuD,CAAC,CAAC;aAChF,IAAI,CAAC,KAAK,SAAS;YACpB,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,mDAAmD,CAAC,CAAC;aAC5E,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,iCAAiC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7F,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,2BAA2B,OAAO,CAAC,MAAM,cAAc,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACvF,CAAC;AAED;;;;GAIG;AACH,+GAA+G;AAC/G,SAAgB,kBAAkB,CAAC,OAA4B,EAAE,KAAuB;IACpF,MAAM,SAAS,GAAG,yBAAyB,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAC9F,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACzC,OAAO,2BAA2B,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;AACvF,CAAC;AAED;;;GAGG;AACH,+GAA+G;AAC/G,SAAS,2BAA2B,CAChC,OAA4B,EAC5B,KAA0B;IAE1B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpF,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,KAAK,SAAS;YACf,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,yDAAyD,CAAC,CAAC;aAClF,IAAI,CAAC,KAAK,SAAS;YACpB,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,yDAAyD,CAAC,CAAC;aAClF,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YAC5C,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,gCAAgC,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,0BAA0B,OAAO,CAAC,MAAM,iBAAiB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACzF,CAAC;AAED;;;;GAIG;AACH,MAAa,mBAAmB;IAC5B,KAAK,CAAC,KAAK,CAAC,aAAqB;QAC7B,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;QACzD,MAAM,YAAY,GAAG,MAAM,IAAA,sCAAoB,GAAE,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QAClD,MAAM,YAAY,GAAG,IAAA,qCAAsB,EAAC,YAAY,CAAC,CAAC;QAC1D,OAAO,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;QAClF,MAAM,YAAY,GAAG,MAAM,IAAA,mCAAkB,GAAE,CAAC;QAChD,IAAA,4BAAW,EAAC,YAAY,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;QACvD,IAAI,yBAAa,EAAE,CAAC,UAAU,CAAC,YAAY,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;QAC1E,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;QACvE,8FAA8F;QAC9F,uFAAuF;QACvF,MAAM,gBAAgB,GAAG,IAAA,kCAAiB,EAAC,aAAa,CAAC,CAAC,gBAAgB,CAAC;QAC3E,MAAM,IAAI,GAAG,IAAA,uCAAyB,EAAC,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QACpG,OAAO,IAAI,mBAAmB,CAC1B,YAAY,EACZ,IAAI,qCAAgB,EAAE,CAAC,OAAO,CAAC,IAAA,+BAAiB,EAAC,IAAI,CAAC,CAAC,EACvD,IAAA,uCAAoB,EAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CACpD,CAAC;IACN,CAAC;CACJ;AArBD,kDAqBC;AAED,gGAAgG;AAChG,MAAa,mBAAmB;IAER;IACA;IACA;IAHpB,YACoB,KAAoB,EACpB,gBAAqC,EACrC,eAAoC;QAFpC,UAAK,GAAL,KAAK,CAAe;QACpB,qBAAgB,GAAhB,gBAAgB,CAAqB;QACrC,oBAAe,GAAf,eAAe,CAAqB;IACrD,CAAC;CACP;AAND,kDAMC;AAED,yFAAyF;AACzF,2GAA2G;AAC3G,SAAS,kBAAkB;IACvB,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC1E,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACxD,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;IACzD,OAAO,CAAC,KAAK,CAAC,qFAAqF,CAAC,CAAC;IACrG,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;AAChE,CAAC;AAEc,KAAK,UAAU,WAAW,CACrC,OAA6C,EAC7C,OAAwB;IAExB,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACpC,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,yFAAyF;IACzF,yFAAyF;IACzF,IAAI,IAAI,oBAAQ,EAAE,CAAC,UAAU,CAAC,aAAa,EAAE,iCAAiC,EAAE,IAAI,CAAC,EAAE,CAAC;QACpF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;IAExD,8DAA8D;IAC9D,IAAI,CAAC;QACD,8BAA8B;QAC9B,IAAI,CAAC,IAAA,8BAAe,EAAC,aAAa,EAAE,SAAS,CAAC,EAAE,CAAC;YAC7C,kBAAkB,EAAE,CAAC;YACrB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QAED,wEAAwE;QACxE,2CAA2C;QAC3C,MAAM,YAAY,GAAG,MAAM,IAAI,mBAAmB,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QAE1E,2BAA2B;QAC3B,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QACzC,MAAM,UAAU,GAAG,IAAA,+BAAgB,EAAC,aAAa,EAAE,SAAS,CAAC,CAAC;QAE9D,IAAI,CAAC,UAAU,EAAE,CAAC;YACd,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAC9C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QAED,yBAAyB;QACzB,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;QAC5D,MAAM,UAAU,GAAG,IAAA,gCAAa,EAAC,YAAY,CAAC,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;QAE1E,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC;YACxB,cAAc,CAAC,UAAU,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;YAClD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QAED,6FAA6F;QAC7F,0FAA0F;QAC1F,sEAAsE;QACtE,MAAM,UAAU,GAAG,kBAAkB,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QAChE,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACtB,cAAc,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;YAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,8DAA8D,CAAC,CAAC;QAC5E,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,yFAAyF;QACzF,wFAAwF;QACxF,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,mCAAmC,EAAE,QAAQ,CAAC,CAAC;QAC7D,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 * Validate Architecture Unchanged Executor\n *\n * Validates that the current architecture graph matches the saved blessed graph.\n * This ensures no unapproved architecture changes have been made.\n *\n * Usage:\n * nx run architecture:validate-architecture-unchanged\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 { compareGraphs } from '../../lib/graph-comparator';\nimport { loadBlessedGraph, graphFileExists } from '../../lib/graph-loader';\nimport type { DependenciesFile } from '../../lib/graph-loader';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { TagTruthCheck } from '../../lib/tag-truth';\nimport { scanAndAttachApiRelations, buildApiContracts } from '../../lib/api-usage/api-scanner';\nimport { buildExternalSystems } from '../../lib/api-usage/external-systems';\nimport type { ExternalSystemDecls } from '../../lib/api-usage/api-relations';\nimport { ApiContractFiles } from '../../lib/api-contract-files';\nimport type { ApiContractFileRefs } from '../../lib/api-contract-files';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport { RuleGate } from '../../lib/rule-gate';\nimport type { EnhancedGraph } from '../../lib/graph-sorter';\nimport { toError } from '../../toError';\n\nexport interface ValidateArchitectureUnchangedOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst TMP_MD_FILE = 'webpieces.dependencies.md';\n\n/**\n * Write the instructions documentation to .webpieces/instruct-ai/.\n * Sourced from @webpieces/rules-config.\n */\nfunction writeTmpInstructionsFile(workspaceRoot: string): string {\n const mdPath = writeTemplate(workspaceRoot, TMP_MD_FILE);\n\n return mdPath;\n}\n\n/**\n * Report a current-vs-saved graph mismatch and write the AI instructions file.\n */\nfunction reportMismatch(summary: string, workspaceRoot: string): void {\n const mdPath = writeTmpInstructionsFile(workspaceRoot);\n\n console.error('❌ Architecture has changed since last update!');\n console.error('\\nDifferences:');\n console.error(summary);\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for instructions on how to fix *** ⚠️');\n console.error('');\n console.error('To fix:');\n console.error(' 1. Review the changes above');\n console.error(' 2. If intentional, ASK USER to run: nx run architecture:generate since this is a critical change');\n console.error(' 3. Commit the updated architecture/dependencies.json');\n}\n\n/**\n * Which APIs dependencies.json links to a contract file, compared by LINK only — an added or removed\n * API. The endpoints inside `architecture/apis/<Api>.json` are deliberately NOT compared (#949): an\n * endpoint change must not fail this rule. Null when the links match.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, matches reportMismatch in this file\nfunction describeContractLinkDrift(\n current: ApiContractFileRefs,\n saved: ApiContractFileRefs,\n): string | null {\n const names = [...new Set([...Object.keys(current), ...Object.keys(saved)])].sort();\n const changes: string[] = [];\n for (const name of names) {\n const a = current[name];\n const b = saved[name];\n if (a === undefined)\n changes.push(` - ${name}: linked in dependencies.json but no longer in source`);\n else if (b === undefined)\n changes.push(` + ${name}: in source but not linked from dependencies.json`);\n else if (a !== b) changes.push(` ~ ${name}: contract file link changed (${b} -> ${a})`);\n }\n if (changes.length === 0) return null;\n return `apiContractFiles drift (${changes.length} api(s)):\\n${changes.join('\\n')}`;\n}\n\n/**\n * Drift in EITHER side table of dependencies.json — the api contract-file links or the\n * external-system declarations — or null when both match. The first difference found is reported;\n * fixing it is the same single command either way, so listing both adds noise rather than information.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, matches describeContractLinkDrift above\nexport function describeTableDrift(current: CurrentArchitecture, saved: DependenciesFile): string | null {\n const linkDrift = describeContractLinkDrift(current.apiContractFiles, saved.apiContractFiles);\n if (linkDrift !== null) return linkDrift;\n return describeExternalSystemDrift(current.externalSystems, saved.externalSystems);\n}\n\n/**\n * The same drift report for the declared external systems, or null when they match. Named per\n * system so the message points at the database that changed rather than dumping two JSON blobs.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, matches describeContractLinkDrift above\nfunction describeExternalSystemDrift(\n current: ExternalSystemDecls,\n saved: ExternalSystemDecls,\n): string | null {\n const names = [...new Set([...Object.keys(current), ...Object.keys(saved)])].sort();\n const changes: string[] = [];\n for (const name of names) {\n const a = current[name];\n const b = saved[name];\n if (a === undefined)\n changes.push(` - ${name}: in dependencies.json but no longer declared in source`);\n else if (b === undefined)\n changes.push(` + ${name}: declared in source but missing from dependencies.json`);\n else if (JSON.stringify(a) !== JSON.stringify(b))\n changes.push(` ~ ${name}: kind/label/declarers changed`);\n }\n if (changes.length === 0) return null;\n return `externalSystems drift (${changes.length} system(s)):\\n${changes.join('\\n')}`;\n}\n\n/**\n * Build the current dependency graph exactly as the generator does: reduce the nx\n * graph, sort into levels, enrich with metadata, and attach the derived\n * apiRelations — so this validator compares like-for-like against the committed file.\n */\nexport class CurrentGraphBuilder {\n async build(workspaceRoot: string): Promise<CurrentArchitecture> {\n console.log('📊 Generating current dependency graph...');\n const reducedGraph = await generateReducedGraph();\n console.log('🔄 Computing topological layers...');\n const currentGraph = sortGraphTopologically(reducedGraph);\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(currentGraph, projectInfos, workspaceRoot);\n new TagTruthCheck().assertTrue(currentGraph, projectInfos, workspaceRoot);\n console.log('🔎 Scanning source for implements/uses API relations...');\n // The SAME externalApiPaths the generator uses: scanning without them would drop every vendor\n // relation from the regenerated graph and report drift against a perfectly fresh file.\n const externalApiPaths = loadRuntimeConfig(workspaceRoot).externalApiPaths;\n const scan = scanAndAttachApiRelations(workspaceRoot, currentGraph, projectInfos, externalApiPaths);\n return new CurrentArchitecture(\n currentGraph,\n new ApiContractFiles().refsFor(buildApiContracts(scan)),\n buildExternalSystems(scan.apiIndex, projectInfos),\n );\n }\n}\n\n/** The regenerated graph plus the two tables beside it — everything dependencies.json holds. */\nexport class CurrentArchitecture {\n constructor(\n public readonly graph: EnhancedGraph,\n public readonly apiContractFiles: ApiContractFileRefs,\n public readonly externalSystems: ExternalSystemDecls,\n ) {}\n}\n\n/** The bootstrap path: there is nothing to diff against yet, so say how to create it. */\n// webpieces-disable no-function-outside-class -- executor step helper, matches reportMismatch in this file\nfunction reportMissingGraph(): void {\n console.error('❌ No saved graph found at architecture/dependencies.json');\n console.error('');\n console.error('To initialize:');\n console.error(' 1. Run: nx run architecture:generate');\n console.error(' 2. Run: nx run architecture:visualize');\n console.error(' 3. Manually inspect the generated graph to confirm it is the desired architecture');\n console.error(' 4. Commit architecture/dependencies.json');\n}\n\nexport default async function runExecutor(\n options: ValidateArchitectureUnchangedOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n // Epoch-gateable: this rule diffs against the blessed architecture/dependencies.json, so\n // \"grandfather the current drift until <epoch>\" is meaningful — hence honorEpoch = true.\n if (new RuleGate().isDisabled(workspaceRoot, 'validate-architecture-unchanged', true)) {\n return { success: true };\n }\n\n console.log('\\n🔍 Validating Architecture Unchanged\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Check if saved graph exists\n if (!graphFileExists(workspaceRoot, graphPath)) {\n reportMissingGraph();\n return { success: false };\n }\n\n // Steps 1-3: build + enrich + scan the current graph (same pipeline the\n // generator runs, so any drift is caught).\n const currentGraph = await new CurrentGraphBuilder().build(workspaceRoot);\n\n // Step 4: Load saved graph\n console.log('📂 Loading saved graph...');\n const savedGraph = loadBlessedGraph(workspaceRoot, graphPath);\n\n if (!savedGraph) {\n console.error('❌ Could not load saved graph');\n return { success: false };\n }\n\n // Step 5: Compare graphs\n console.log('🔍 Comparing current graph to saved graph...');\n const comparison = compareGraphs(currentGraph.graph, savedGraph.projects);\n\n if (!comparison.identical) {\n reportMismatch(comparison.summary, workspaceRoot);\n return { success: false };\n }\n\n // Step 6: Compare the two side tables as well: which APIs are linked to a contract file, and\n // the external-system declarations. The endpoints inside architecture/apis/<Api>.json are\n // NOT compared — an endpoint change must never fail this rule (#949).\n const tableDrift = describeTableDrift(currentGraph, savedGraph);\n if (tableDrift !== null) {\n reportMismatch(tableDrift, workspaceRoot);\n return { success: false };\n }\n\n console.log('✅ Architecture unchanged - current graph matches saved graph');\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n // A RuleFailError (e.g. a dependencies.json still carrying the moved `apiContracts` key)\n // carries its cures in Option[]; render them rather than dropping them with `.message`.\n const rendered = error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message;\n console.error('❌ Architecture validation 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/validate-architecture-unchanged/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Architecture Unchanged Executor\n *\n * Validates that the current architecture graph matches the saved blessed graph.\n * This ensures no unapproved architecture changes have been made.\n *\n * Usage:\n * nx run architecture:validate-architecture-unchanged\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport {\n writeTemplate,\n Option,\n RuleFailError,\n renderRuleFailForHuman,\n} from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { compareGraphs } from '../../lib/graph-comparator';\nimport { loadBlessedGraph, graphFileExists } from '../../lib/graph-loader';\nimport type { DependenciesFile } from '../../lib/graph-loader';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { TagTruthCheck } from '../../lib/tag-truth';\nimport { ApprovedWiringGraph } from '../../lib/runtime-wiring/approved-graph';\nimport type { ExternalSystemDecls } from '../../lib/api-usage/api-relations';\nimport { ApiContractFiles } from '../../lib/api-contract-files';\nimport type { ApiContractFileRefs } from '../../lib/api-contract-files';\nimport { RuleGate } from '../../lib/rule-gate';\nimport type { EnhancedGraph } from '../../lib/graph-sorter';\nimport { toError } from '../../toError';\n\nexport interface ValidateArchitectureUnchangedOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst TMP_MD_FILE = 'webpieces.dependencies.md';\n\n/**\n * Write the instructions documentation to .webpieces/instruct-ai/.\n * Sourced from @webpieces/rules-config.\n */\nfunction writeTmpInstructionsFile(workspaceRoot: string): string {\n const mdPath = writeTemplate(workspaceRoot, TMP_MD_FILE);\n\n return mdPath;\n}\n\n/**\n * Report a current-vs-saved graph mismatch and write the AI instructions file.\n */\nfunction reportMismatch(summary: string, workspaceRoot: string): void {\n const mdPath = writeTmpInstructionsFile(workspaceRoot);\n\n console.error('❌ Architecture has changed since last update!');\n console.error('\\nDifferences:');\n console.error(summary);\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for instructions on how to fix *** ⚠️');\n console.error('');\n console.error('To fix:');\n console.error(' 1. Review the changes above');\n console.error(\n ' 2. If intentional, ASK USER to run: nx run architecture:generate since this is a critical change',\n );\n console.error(' 3. Commit the updated architecture/dependencies.json');\n}\n\n/**\n * Which APIs dependencies.json links to a contract file, compared by LINK only — an added or removed\n * API. The endpoints inside `architecture/apis/<Api>.json` are deliberately NOT compared (#949): an\n * endpoint change must not fail this rule. Null when the links match.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, matches reportMismatch in this file\nfunction describeContractLinkDrift(\n current: ApiContractFileRefs,\n saved: ApiContractFileRefs,\n): string | null {\n const names = [...new Set([...Object.keys(current), ...Object.keys(saved)])].sort();\n const changes: string[] = [];\n for (const name of names) {\n const a = current[name];\n const b = saved[name];\n if (a === undefined)\n changes.push(` - ${name}: linked in dependencies.json but no longer in source`);\n else if (b === undefined)\n changes.push(` + ${name}: in source but not linked from dependencies.json`);\n else if (a !== b) changes.push(` ~ ${name}: contract file link changed (${b} -> ${a})`);\n }\n if (changes.length === 0) return null;\n return `apiContractFiles drift (${changes.length} api(s)):\\n${changes.join('\\n')}`;\n}\n\n/**\n * Drift in EITHER side table of dependencies.json — the api contract-file links or the\n * external-system declarations — or null when both match. The first difference found is reported;\n * fixing it is the same single command either way, so listing both adds noise rather than information.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, matches describeContractLinkDrift above\nexport function describeTableDrift(\n current: CurrentArchitecture,\n saved: DependenciesFile,\n): string | null {\n const linkDrift = describeContractLinkDrift(current.apiContractFiles, saved.apiContractFiles);\n if (linkDrift !== null) return linkDrift;\n return describeExternalSystemDrift(current.externalSystems, saved.externalSystems);\n}\n\n/**\n * The same drift report for the declared external systems, or null when they match. Named per\n * system so the message points at the database that changed rather than dumping two JSON blobs.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, matches describeContractLinkDrift above\nfunction describeExternalSystemDrift(\n current: ExternalSystemDecls,\n saved: ExternalSystemDecls,\n): string | null {\n const names = [...new Set([...Object.keys(current), ...Object.keys(saved)])].sort();\n const changes: string[] = [];\n for (const name of names) {\n const a = current[name];\n const b = saved[name];\n if (a === undefined)\n changes.push(` - ${name}: in dependencies.json but no longer declared in source`);\n else if (b === undefined)\n changes.push(` + ${name}: declared in source but missing from dependencies.json`);\n else if (JSON.stringify(a) !== JSON.stringify(b))\n changes.push(` ~ ${name}: kind/label/declarers changed`);\n }\n if (changes.length === 0) return null;\n return `externalSystems drift (${changes.length} system(s)):\\n${changes.join('\\n')}`;\n}\n\n/**\n * Build the current dependency graph exactly as the generator does: reduce the nx\n * graph, sort into levels, enrich with metadata, and attach the derived\n * apiRelations — so this validator compares like-for-like against the committed file.\n */\nexport class CurrentGraphBuilder {\n async build(workspaceRoot: string, graphPath?: string): Promise<CurrentArchitecture> {\n console.log('📊 Generating current dependency graph...');\n const reducedGraph = await generateReducedGraph();\n console.log('🔄 Computing topological layers...');\n const currentGraph = sortGraphTopologically(reducedGraph);\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(currentGraph, projectInfos, workspaceRoot);\n new TagTruthCheck().assertTrue(currentGraph, projectInfos, workspaceRoot);\n new ApprovedWiringGraph().attach(workspaceRoot, currentGraph, projectInfos, graphPath);\n const saved = loadBlessedGraph(workspaceRoot, graphPath);\n if (saved === null)\n throw new RuleFailError(\n 'validate-runtime-architecture',\n 'Missing approved graph.',\n undefined,\n undefined,\n [\n new Option(\n 'Migrate src/wiring.ts and review runtime-deps.json and API contract approvals.',\n true,\n ),\n ],\n );\n return new CurrentArchitecture(currentGraph, saved.apiContractFiles, saved.externalSystems);\n }\n}\n\n/** The regenerated graph plus the two tables beside it — everything dependencies.json holds. */\nexport class CurrentArchitecture {\n constructor(\n public readonly graph: EnhancedGraph,\n public readonly apiContractFiles: ApiContractFileRefs,\n public readonly externalSystems: ExternalSystemDecls,\n ) {}\n}\n\n/** The bootstrap path: there is nothing to diff against yet, so say how to create it. */\n// webpieces-disable no-function-outside-class -- executor step helper, matches reportMismatch in this file\nfunction reportMissingGraph(): never {\n throw new RuleFailError(\n 'validate-runtime-architecture',\n 'Missing reviewed architecture/dependencies.json.',\n undefined,\n undefined,\n [\n new Option(\n 'Run owning API source build checks to emit contract candidates, review and copy them into architecture/apis/<ApiName>.json, and create dependencies.json with projects:{}, apiContractFiles:{\"<ApiName>\":\"apis/<ApiName>.json\"}, and reviewed externalSystems. Approve tagged owners runtime-deps.json candidates, then run architecture:generate and review the resulting graph.',\n true,\n ),\n ],\n );\n}\n\nexport default async function runExecutor(\n options: ValidateArchitectureUnchangedOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n // Epoch-gateable: this rule diffs against the blessed architecture/dependencies.json, so\n // \"grandfather the current drift until <epoch>\" is meaningful — hence honorEpoch = true.\n if (new RuleGate().isDisabled(workspaceRoot, 'validate-architecture-unchanged', true)) {\n return { success: true };\n }\n\n console.log('\\n🔍 Validating Architecture Unchanged\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Check if saved graph exists\n if (!graphFileExists(workspaceRoot, graphPath)) {\n reportMissingGraph();\n return { success: false };\n }\n\n // Steps 1-3: build + enrich + scan the current graph (same pipeline the\n // generator runs, so any drift is caught).\n const currentGraph = await new CurrentGraphBuilder().build(workspaceRoot, graphPath);\n\n // Step 4: Load saved graph\n console.log('📂 Loading saved graph...');\n const savedGraph = loadBlessedGraph(workspaceRoot, graphPath);\n\n if (!savedGraph) {\n console.error('❌ Could not load saved graph');\n return { success: false };\n }\n\n // Step 5: Compare graphs\n console.log('🔍 Comparing current graph to saved graph...');\n const comparison = compareGraphs(currentGraph.graph, savedGraph.projects);\n\n if (!comparison.identical) {\n reportMismatch(comparison.summary, workspaceRoot);\n return { success: false };\n }\n\n // Step 6: Compare the two side tables as well: which APIs are linked to a contract file, and\n // the external-system declarations. The endpoints inside architecture/apis/<Api>.json are\n // NOT compared — an endpoint change must never fail this rule (#949).\n const tableDrift = describeTableDrift(currentGraph, savedGraph);\n if (tableDrift !== null) {\n reportMismatch(tableDrift, workspaceRoot);\n return { success: false };\n }\n\n console.log('✅ Architecture unchanged - current graph matches saved graph');\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n // A RuleFailError (e.g. a dependencies.json still carrying the moved `apiContracts` key)\n // carries its cures in Option[]; render them rather than dropping them with `.message`.\n const rendered =\n error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message;\n console.error('❌ Architecture validation failed:', rendered);\n reportMetadataFailure(error, workspaceRoot);\n return { success: false };\n }\n}\n\n// webpieces-disable no-function-outside-class -- executor diagnostic formatting\nfunction reportMetadataFailure(error: Error, workspaceRoot: string): void {\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error(\n '⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️',\n );\n }\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import type { ApiContracts, ExternalSystemDecls } from './api-relations';
2
+ import type { ProjectInfo } from '../project-info';
3
+ /** Source-sensitive build proof; graph generation reads these approvals without extracting source. */
4
+ export declare class ApiContractApproval {
5
+ verify(workspaceRoot: string, infos: ReadonlyMap<string, ProjectInfo>, contracts: ApiContracts, systems: ExternalSystemDecls): void;
6
+ private fail;
7
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiContractApproval = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const util_1 = require("util");
8
+ const rules_config_1 = require("@webpieces/rules-config");
9
+ const api_contract_files_1 = require("../api-contract-files");
10
+ const graph_loader_1 = require("../graph-loader");
11
+ /** Source-sensitive build proof; graph generation reads these approvals without extracting source. */
12
+ class ApiContractApproval {
13
+ verify(workspaceRoot, infos, contracts, systems) {
14
+ const owners = new Set(Object.values(contracts).map((contract) => contract.owner));
15
+ const candidates = [];
16
+ for (const owner of owners) {
17
+ const info = infos.get(owner);
18
+ if (info === undefined)
19
+ this.fail(`Missing owning project ${owner} for API contract proof.`);
20
+ const project = JSON.parse(fs.readFileSync(path.join(workspaceRoot, info.root, 'project.json'), 'utf8'));
21
+ const outputPath = project.targets?.build?.options?.outputPath;
22
+ if (typeof outputPath !== 'string' || outputPath.length === 0)
23
+ this.fail(`${owner} must declare its build.options.outputPath for contract candidates.`);
24
+ const own = Object.fromEntries(Object.entries(contracts).filter((entry) => entry[1].owner === owner));
25
+ const result = new api_contract_files_1.ApiContractFiles().write(workspaceRoot, `${outputPath}/runtime-contracts.candidate.json`, own);
26
+ candidates.push(...result.written);
27
+ fs.writeFileSync(path.join(workspaceRoot, outputPath, 'external-systems.candidate.json'), JSON.stringify(systems, null, 4) + '\n');
28
+ }
29
+ const saved = (0, graph_loader_1.loadBlessedGraph)(workspaceRoot);
30
+ if (saved === null)
31
+ this.fail('Missing saved API contract references; create reviewed contract declarations before graph generation.');
32
+ const expected = new api_contract_files_1.ApiContractFiles().load(workspaceRoot, 'architecture/dependencies.json', saved.apiContractFiles);
33
+ if (!(0, util_1.isDeepStrictEqual)(JSON.parse(JSON.stringify(expected)), JSON.parse(JSON.stringify(contracts))) ||
34
+ !(0, util_1.isDeepStrictEqual)(JSON.parse(JSON.stringify(saved.externalSystems)), JSON.parse(JSON.stringify(systems))))
35
+ this.fail(`Current API/DTO contracts or external declarations differ from reviewed graph metadata. Candidates:\n${candidates.join('\n')}\nReview each owning contract's candidate and explicitly update architecture/apis and contract references; generation never extracts source or approves changes.`);
36
+ }
37
+ fail(message) {
38
+ throw new rules_config_1.RuleFailError('validate-runtime-architecture', message, undefined, undefined, [
39
+ new rules_config_1.Option('Review the owning project output candidates, correct source defects, and explicitly update the approved API metadata before generation.', true),
40
+ ]);
41
+ }
42
+ }
43
+ exports.ApiContractApproval = ApiContractApproval;
44
+ class ProjectBuildOutput {
45
+ targets;
46
+ constructor(targets) {
47
+ this.targets = targets;
48
+ }
49
+ }
50
+ //# sourceMappingURL=api-contract-approval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-contract-approval.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-approval.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { isDeepStrictEqual } from 'util';\nimport { Option, RuleFailError } from '@webpieces/rules-config';\nimport type { ApiContracts, ExternalSystemDecls } from './api-relations';\nimport type { ProjectInfo } from '../project-info';\nimport { ApiContractFiles } from '../api-contract-files';\nimport { loadBlessedGraph } from '../graph-loader';\n\n/** Source-sensitive build proof; graph generation reads these approvals without extracting source. */\nexport class ApiContractApproval {\n verify(\n workspaceRoot: string,\n infos: ReadonlyMap<string, ProjectInfo>,\n contracts: ApiContracts,\n systems: ExternalSystemDecls,\n ): void {\n const owners = new Set(Object.values(contracts).map((contract) => contract.owner));\n const candidates: string[] = [];\n for (const owner of owners) {\n const info = infos.get(owner);\n if (info === undefined)\n this.fail(`Missing owning project ${owner} for API contract proof.`);\n const project = JSON.parse(\n fs.readFileSync(path.join(workspaceRoot, info!.root, 'project.json'), 'utf8'),\n ) as ProjectBuildOutput;\n const outputPath = project.targets?.build?.options?.outputPath;\n if (typeof outputPath !== 'string' || outputPath.length === 0)\n this.fail(\n `${owner} must declare its build.options.outputPath for contract candidates.`,\n );\n const own = Object.fromEntries(\n Object.entries(contracts).filter((entry) => entry[1].owner === owner),\n );\n const result = new ApiContractFiles().write(\n workspaceRoot,\n `${outputPath}/runtime-contracts.candidate.json`,\n own,\n );\n candidates.push(...result.written);\n fs.writeFileSync(\n path.join(workspaceRoot, outputPath, 'external-systems.candidate.json'),\n JSON.stringify(systems, null, 4) + '\\n',\n );\n }\n const saved = loadBlessedGraph(workspaceRoot);\n if (saved === null)\n this.fail(\n 'Missing saved API contract references; create reviewed contract declarations before graph generation.',\n );\n const expected = new ApiContractFiles().load(\n workspaceRoot,\n 'architecture/dependencies.json',\n saved!.apiContractFiles,\n );\n if (\n !isDeepStrictEqual(\n JSON.parse(JSON.stringify(expected)),\n JSON.parse(JSON.stringify(contracts)),\n ) ||\n !isDeepStrictEqual(\n JSON.parse(JSON.stringify(saved!.externalSystems)),\n JSON.parse(JSON.stringify(systems)),\n )\n )\n this.fail(\n `Current API/DTO contracts or external declarations differ from reviewed graph metadata. Candidates:\\n${candidates.join('\\n')}\\nReview each owning contract's candidate and explicitly update architecture/apis and contract references; generation never extracts source or approves changes.`,\n );\n }\n\n private fail(message: string): never {\n throw new RuleFailError('validate-runtime-architecture', message, undefined, undefined, [\n new Option(\n 'Review the owning project output candidates, correct source defects, and explicitly update the approved API metadata before generation.',\n true,\n ),\n ]);\n }\n}\n\nclass ProjectBuildOutput {\n constructor(public readonly targets?: { build?: { options?: { outputPath?: string } } }) {}\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { ApiScanResult } from './api-scanner';
2
+ import type { ProjectInfo } from '../project-info';
3
+ /** Build evidence for API/DTO correctness. No runtime relationship scan or project compiler loop. */
4
+ export declare class ApiContractEvidence {
5
+ collect(workspaceRoot: string, infos: Map<string, ProjectInfo>, externalApiPaths: readonly string[]): ApiScanResult;
6
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiContractEvidence = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const api_contract_approval_1 = require("./api-contract-approval");
6
+ const external_systems_1 = require("./external-systems");
7
+ const fs = tslib_1.__importStar(require("fs"));
8
+ const path = tslib_1.__importStar(require("path"));
9
+ const api_scanner_1 = require("./api-scanner");
10
+ const api_ast_1 = require("./api-ast");
11
+ const root_union_scan_1 = require("./root-union-scan");
12
+ const api_doc_rules_1 = require("./api-doc-rules");
13
+ const api_doc_rules_scan_1 = require("./api-doc-rules-scan");
14
+ const wire_closure_1 = require("./wire-closure");
15
+ /** Build evidence for API/DTO correctness. No runtime relationship scan or project compiler loop. */
16
+ class ApiContractEvidence {
17
+ collect(workspaceRoot, infos, externalApiPaths) {
18
+ const diagnostics = new api_ast_1.DecoratorArgDiagnostics(workspaceRoot);
19
+ const index = new api_scanner_1.ApiSourceIndexBuilder(workspaceRoot, infos, externalApiPaths, diagnostics).build();
20
+ const result = {
21
+ relationsByProject: new Map(),
22
+ apiLibProjects: index.owners,
23
+ apiIndex: index.byName,
24
+ scannedProjects: new Set([...infos.values()]
25
+ .filter((info) => fs.existsSync(path.join(workspaceRoot, info.root, 'src')))
26
+ .map((info) => info.name)),
27
+ unresolvedApiCalls: [],
28
+ nonLiteralDecoratorArgs: diagnostics.all(),
29
+ unresolvedEndpointPaths: diagnostics.unresolvedEndpointPaths(),
30
+ emptiedApiContracts: diagnostics.emptiedContracts(),
31
+ undeclaredExternalCallers: diagnostics.undeclaredExternalCallers(),
32
+ undeclaredEndpointOperations: diagnostics.undeclaredEndpointOperations(),
33
+ rootUnions: new root_union_scan_1.RootUnionScan(workspaceRoot, infos, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot)).run(),
34
+ apiDocRules: new api_doc_rules_scan_1.ApiDocRulesScan(workspaceRoot, infos, api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.OPENAPI_RULE), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.MCP_RULE), wire_closure_1.WireClosureRule.fromConfig(workspaceRoot)).run(),
35
+ };
36
+ new api_contract_approval_1.ApiContractApproval().verify(workspaceRoot, infos, (0, api_scanner_1.buildApiContracts)(result), (0, external_systems_1.buildExternalSystems)(index.byName, infos)); // Preserves the previous generator's API/DTO correctness failures.
37
+ return result;
38
+ }
39
+ }
40
+ exports.ApiContractEvidence = ApiContractEvidence;
41
+ //# sourceMappingURL=api-contract-evidence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-contract-evidence.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-evidence.ts"],"names":[],"mappings":";;;;AAAA,mEAA8D;AAC9D,yDAA0D;AAC1D,+CAAyB;AACzB,mDAA6B;AAC7B,+CAAyE;AACzE,uCAAoD;AACpD,uDAAiE;AACjE,mDAAqE;AACrE,6DAAuD;AACvD,iDAAiD;AAIjD,qGAAqG;AACrG,MAAa,mBAAmB;IAC5B,OAAO,CACH,aAAqB,EACrB,KAA+B,EAC/B,gBAAmC;QAEnC,MAAM,WAAW,GAAG,IAAI,iCAAuB,CAAC,aAAa,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,IAAI,mCAAqB,CACnC,aAAa,EACb,KAAK,EACL,gBAAgB,EAChB,WAAW,CACd,CAAC,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAkB;YAC1B,kBAAkB,EAAE,IAAI,GAAG,EAAE;YAC7B,cAAc,EAAE,KAAK,CAAC,MAAM;YAC5B,QAAQ,EAAE,KAAK,CAAC,MAAM;YACtB,eAAe,EAAE,IAAI,GAAG,CACpB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;iBACd,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;iBAC3E,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAChC;YACD,kBAAkB,EAAE,EAAE;YACtB,uBAAuB,EAAE,WAAW,CAAC,GAAG,EAAE;YAC1C,uBAAuB,EAAE,WAAW,CAAC,uBAAuB,EAAE;YAC9D,mBAAmB,EAAE,WAAW,CAAC,gBAAgB,EAAE;YACnD,yBAAyB,EAAE,WAAW,CAAC,yBAAyB,EAAE;YAClE,4BAA4B,EAAE,WAAW,CAAC,4BAA4B,EAAE;YACxE,UAAU,EAAE,IAAI,+BAAa,CACzB,aAAa,EACb,KAAK,EACL,+BAAa,CAAC,UAAU,CAAC,aAAa,CAAC,CAC1C,CAAC,GAAG,EAAE;YACP,WAAW,EAAE,IAAI,oCAAe,CAC5B,aAAa,EACb,KAAK,EACL,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,4BAAY,CAAC,EAClD,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,wBAAQ,CAAC,EAC9C,8BAAe,CAAC,UAAU,CAAC,aAAa,CAAC,CAC5C,CAAC,GAAG,EAAE;SACV,CAAC;QACF,IAAI,2CAAmB,EAAE,CAAC,MAAM,CAC5B,aAAa,EACb,KAAK,EACL,IAAA,+BAAiB,EAAC,MAAM,CAAC,EACzB,IAAA,uCAAoB,EAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAC5C,CAAC,CAAC,mEAAmE;QACtE,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAjDD,kDAiDC","sourcesContent":["import { ApiContractApproval } from './api-contract-approval';\nimport { buildExternalSystems } from './external-systems';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ApiSourceIndexBuilder, buildApiContracts } from './api-scanner';\nimport { DecoratorArgDiagnostics } from './api-ast';\nimport { RootUnionRule, RootUnionScan } from './root-union-scan';\nimport { ApiDocRule, MCP_RULE, OPENAPI_RULE } from './api-doc-rules';\nimport { ApiDocRulesScan } from './api-doc-rules-scan';\nimport { WireClosureRule } from './wire-closure';\nimport type { ApiScanResult } from './api-scanner';\nimport type { ProjectInfo } from '../project-info';\n\n/** Build evidence for API/DTO correctness. No runtime relationship scan or project compiler loop. */\nexport class ApiContractEvidence {\n collect(\n workspaceRoot: string,\n infos: Map<string, ProjectInfo>,\n externalApiPaths: readonly string[],\n ): ApiScanResult {\n const diagnostics = new DecoratorArgDiagnostics(workspaceRoot);\n const index = new ApiSourceIndexBuilder(\n workspaceRoot,\n infos,\n externalApiPaths,\n diagnostics,\n ).build();\n const result: ApiScanResult = {\n relationsByProject: new Map(),\n apiLibProjects: index.owners,\n apiIndex: index.byName,\n scannedProjects: new Set(\n [...infos.values()]\n .filter((info) => fs.existsSync(path.join(workspaceRoot, info.root, 'src')))\n .map((info) => info.name),\n ),\n unresolvedApiCalls: [],\n nonLiteralDecoratorArgs: diagnostics.all(),\n unresolvedEndpointPaths: diagnostics.unresolvedEndpointPaths(),\n emptiedApiContracts: diagnostics.emptiedContracts(),\n undeclaredExternalCallers: diagnostics.undeclaredExternalCallers(),\n undeclaredEndpointOperations: diagnostics.undeclaredEndpointOperations(),\n rootUnions: new RootUnionScan(\n workspaceRoot,\n infos,\n RootUnionRule.fromConfig(workspaceRoot),\n ).run(),\n apiDocRules: new ApiDocRulesScan(\n workspaceRoot,\n infos,\n ApiDocRule.fromConfig(workspaceRoot, OPENAPI_RULE),\n ApiDocRule.fromConfig(workspaceRoot, MCP_RULE),\n WireClosureRule.fromConfig(workspaceRoot),\n ).run(),\n };\n new ApiContractApproval().verify(\n workspaceRoot,\n infos,\n buildApiContracts(result),\n buildExternalSystems(index.byName, infos),\n ); // Preserves the previous generator's API/DTO correctness failures.\n return result;\n }\n}\n"]}
@@ -12,7 +12,6 @@
12
12
  */
13
13
  import type { EnhancedGraph } from '../graph-sorter';
14
14
  import { ProjectInfo } from '../project-info';
15
- import { ApiScanResult } from './api-scanner';
16
15
  import { UnresolvedApiCall } from './api-relations';
17
16
  /** One `role:server`/`role:client` project that depends on an api-lib it neither implements nor uses. */
18
17
  export interface UnclassifiedApiDep {
@@ -28,10 +27,22 @@ export interface UnclassifiedApiDep {
28
27
  */
29
28
  unresolved: UnresolvedApiCall[];
30
29
  }
30
+ export declare class ApiOwnerIdentity {
31
+ readonly api: string;
32
+ readonly owner: string;
33
+ constructor(api: string, owner: string);
34
+ }
35
+ export declare class ApiRelationEvidence {
36
+ readonly scannedProjects: Set<string>;
37
+ readonly apiLibProjects: Set<string>;
38
+ readonly unresolvedApiCalls: UnresolvedApiCall[];
39
+ readonly apiIndex: ReadonlyMap<string, ApiOwnerIdentity>;
40
+ constructor(scannedProjects: Set<string>, apiLibProjects: Set<string>, unresolvedApiCalls: UnresolvedApiCall[], apiIndex: ReadonlyMap<string, ApiOwnerIdentity>);
41
+ }
31
42
  /**
32
43
  * Every server/client → api-lib edge for which the scan found NO implements and
33
44
  * NO uses. `graph` must already carry `apiRelations` (call scanAndAttachApiRelations first).
34
45
  */
35
- export declare function findUnclassifiedApiDeps(graph: EnhancedGraph, projectInfos: Map<string, ProjectInfo>, scan: ApiScanResult): UnclassifiedApiDep[];
46
+ export declare function findUnclassifiedApiDeps(graph: EnhancedGraph, projectInfos: Map<string, ProjectInfo>, scan: ApiRelationEvidence): UnclassifiedApiDep[];
36
47
  /** Human-readable, fix-oriented report for one unclassified dependency. */
37
48
  export declare function describeUnclassifiedApiDep(violation: UnclassifiedApiDep): string;
@@ -12,11 +12,34 @@
12
12
  * relate to api-lib D" is answered by real code, never a declaration.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ApiRelationEvidence = exports.ApiOwnerIdentity = void 0;
15
16
  exports.findUnclassifiedApiDeps = findUnclassifiedApiDeps;
16
17
  exports.describeUnclassifiedApiDep = describeUnclassifiedApiDep;
17
18
  const role_resolver_1 = require("../role-resolver");
18
19
  /** Roles that must justify every api-lib dependency (top-level runnables). */
19
20
  const CHECKED_ROLES = ['server', 'client'];
21
+ class ApiOwnerIdentity {
22
+ api;
23
+ owner;
24
+ constructor(api, owner) {
25
+ this.api = api;
26
+ this.owner = owner;
27
+ }
28
+ }
29
+ exports.ApiOwnerIdentity = ApiOwnerIdentity;
30
+ class ApiRelationEvidence {
31
+ scannedProjects;
32
+ apiLibProjects;
33
+ unresolvedApiCalls;
34
+ apiIndex;
35
+ constructor(scannedProjects, apiLibProjects, unresolvedApiCalls, apiIndex) {
36
+ this.scannedProjects = scannedProjects;
37
+ this.apiLibProjects = apiLibProjects;
38
+ this.unresolvedApiCalls = unresolvedApiCalls;
39
+ this.apiIndex = apiIndex;
40
+ }
41
+ }
42
+ exports.ApiRelationEvidence = ApiRelationEvidence;
20
43
  /** The API class names owned by `apiLib`, sorted (for a stable fix hint). */
21
44
  // webpieces-disable no-function-outside-class -- pure lookup helper, matches the validator-lib style
22
45
  function apisOwnedBy(scan, apiLib) {
@@ -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":"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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"]}
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\nexport class ApiOwnerIdentity {\n constructor(\n public readonly api: string,\n public readonly owner: string,\n ) {}\n}\n\nexport class ApiRelationEvidence {\n constructor(\n public readonly scannedProjects: Set<string>,\n public readonly apiLibProjects: Set<string>,\n public readonly unresolvedApiCalls: UnresolvedApiCall[],\n public readonly apiIndex: ReadonlyMap<string, ApiOwnerIdentity>,\n ) {}\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: ApiRelationEvidence, 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: ApiRelationEvidence,\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(\n (c: UnresolvedApiCall) => c.project === projectName,\n ),\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"]}
@@ -78,6 +78,9 @@ export interface ApiRef {
78
78
  * falls back to the old fan-out and says so out loud.
79
79
  */
80
80
  targetService?: string;
81
+ /** Qualified selected library export that contributed this relationship. */
82
+ declaredVia?: string;
83
+ conditional?: string;
81
84
  /**
82
85
  * ONLY on a `pubsub` uses ref. True means "this producer was attributed to EVERY cloudtasks
83
86
  * method of the contract, not to the methods it actually enqueues".
@@ -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 * 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"]}
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 /** Qualified selected library export that contributed this relationship. */\n declaredVia?: string;\n conditional?: 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"]}
@@ -33,6 +33,7 @@ import { ApiClassInfo, ApiContracts, EmptiedApiContract, NonLiteralDecoratorArg,
33
33
  import { RootUnionFindings, RootUnionRule } from './root-union-scan';
34
34
  import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
35
35
  import { WireClosureRule } from './wire-closure';
36
+ import { DecoratorArgDiagnostics } from './api-ast';
36
37
  /** The whole-workspace result of a scan. */
37
38
  export interface ApiScanResult {
38
39
  /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */
@@ -84,6 +85,44 @@ export interface ApiScanResult {
84
85
  */
85
86
  apiDocRules: ApiDocRulesFindings;
86
87
  }
88
+ /**
89
+ * Every API contract in the workspace, keyed by class name, read from SOURCE.
90
+ *
91
+ * Name-keyed because a call site only ever gives us a name once its import has resolved into a
92
+ * decorator-erased declaration. Two api-libs exporting the same class name collide (last wins) —
93
+ * the same collision the published `apiIndex` has always had.
94
+ */
95
+ declare class ApiSourceIndex {
96
+ readonly byName: Map<string, ApiClassInfo>;
97
+ readonly owners: Set<string>;
98
+ constructor(byName: Map<string, ApiClassInfo>, owners: Set<string>);
99
+ lookup(api: string): ApiClassInfo | null;
100
+ }
101
+ /**
102
+ * Builds the ApiSourceIndex by parsing each project's own `src/**` directly.
103
+ *
104
+ * Deliberately parser-only (no ts.Program, no checker): we need the decorators exactly as
105
+ * written, and a plain parse cannot be diverted to a `.d.ts` by module resolution — which is
106
+ * the entire bug this guards against. It is also cheap enough to run over every project.
107
+ */
108
+ export declare class ApiSourceIndexBuilder {
109
+ private readonly workspaceRoot;
110
+ private readonly projectInfos;
111
+ /** Globs of project roots holding vendor contracts — see ExternalApiIndex. */
112
+ private readonly externalApiPaths;
113
+ /** Sink for decorator arguments this parser-only pass cannot reduce to a string. */
114
+ private readonly diagnostics;
115
+ private readonly byName;
116
+ private readonly owners;
117
+ constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>,
118
+ /** Globs of project roots holding vendor contracts — see ExternalApiIndex. */
119
+ externalApiPaths: readonly string[],
120
+ /** Sink for decorator arguments this parser-only pass cannot reduce to a string. */
121
+ diagnostics: DecoratorArgDiagnostics);
122
+ build(): ApiSourceIndex;
123
+ private indexProject;
124
+ private indexNode;
125
+ }
87
126
  /** Statically scans every project for its api-lib implements/uses relationships. */
88
127
  export declare class ApiUsageScanner {
89
128
  private readonly workspaceRoot;
@@ -196,3 +235,4 @@ export declare function describeNonLiteralDecoratorArgs(args: readonly NonLitera
196
235
  * ENDPOINT_KINDS_BY_API_KIND at BUILD time, where it can name the file instead of throwing at wiring.
197
236
  */
198
237
  export declare function describeMismatchedEndpointKinds(contracts: ApiContracts): string[];
238
+ export {};
@@ -29,7 +29,7 @@
29
29
  * `recoverFromDeclaration`.
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.ApiUsageScanner = void 0;
32
+ exports.ApiUsageScanner = exports.ApiSourceIndexBuilder = void 0;
33
33
  exports.scanAndAttachApiRelations = scanAndAttachApiRelations;
34
34
  exports.buildApiContracts = buildApiContracts;
35
35
  exports.describeNonLiteralDecoratorArgs = describeNonLiteralDecoratorArgs;
@@ -150,11 +150,17 @@ class ApiSourceIndexBuilder {
150
150
  : (0, api_ast_1.apiClassInfoFromNode)(node, project, this.diagnostics);
151
151
  if (info) {
152
152
  this.owners.add(project);
153
+ const previous = this.byName.get(info.api);
154
+ if (previous !== undefined && previous.owner !== project)
155
+ throw new rules_config_1.RuleFailError('validate-runtime-architecture', `Ambiguous saved contract name ${info.api}: ${previous.owner}#${info.api} and ${project}#${info.api}.`, undefined, undefined, [
156
+ new rules_config_1.Option('Use distinct exported contract names in the saved contract table; source ownership must never be merged by short name.', true),
157
+ ]);
153
158
  this.byName.set(info.api, info);
154
159
  }
155
160
  ts.forEachChild(node, (child) => this.indexNode(child, project, external));
156
161
  }
157
162
  }
163
+ exports.ApiSourceIndexBuilder = ApiSourceIndexBuilder;
158
164
  /** Per-owner accumulator that dedupes API refs while a single project is scanned. */
159
165
  class RelationAccumulator {
160
166
  implementsByOwner = new Map();