@webpieces/nx-webpieces-rules 0.4.831 → 0.4.833

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 (46) hide show
  1. package/package.json +8 -8
  2. package/src/executors/generate/executor.js +2 -0
  3. package/src/executors/generate/executor.js.map +1 -1
  4. package/src/executors/openapi-components-generate/executor.d.ts +2 -1
  5. package/src/executors/openapi-components-generate/executor.js +5 -2
  6. package/src/executors/openapi-components-generate/executor.js.map +1 -1
  7. package/src/executors/openapi-generate/executor.d.ts +15 -1
  8. package/src/executors/openapi-generate/executor.js +30 -2
  9. package/src/executors/openapi-generate/executor.js.map +1 -1
  10. package/src/executors/validate-api-lib-tag/executor.d.ts +5 -3
  11. package/src/executors/validate-api-lib-tag/executor.js +7 -5
  12. package/src/executors/validate-api-lib-tag/executor.js.map +1 -1
  13. package/src/executors/validate-architecture-unchanged/executor.js +2 -0
  14. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  15. package/src/lib/api-docs/components-wiring.d.ts +6 -0
  16. package/src/lib/api-docs/components-wiring.js +18 -1
  17. package/src/lib/api-docs/components-wiring.js.map +1 -1
  18. package/src/lib/api-usage/api-doc-rules-scan.d.ts +17 -1
  19. package/src/lib/api-usage/api-doc-rules-scan.js +28 -3
  20. package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -1
  21. package/src/lib/api-usage/api-lib-tag-validator.d.ts +50 -14
  22. package/src/lib/api-usage/api-lib-tag-validator.js +189 -22
  23. package/src/lib/api-usage/api-lib-tag-validator.js.map +1 -1
  24. package/src/lib/api-usage/api-scanner.d.ts +3 -1
  25. package/src/lib/api-usage/api-scanner.js +6 -3
  26. package/src/lib/api-usage/api-scanner.js.map +1 -1
  27. package/src/lib/api-usage/wire-closure.d.ts +49 -0
  28. package/src/lib/api-usage/wire-closure.js +126 -0
  29. package/src/lib/api-usage/wire-closure.js.map +1 -0
  30. package/src/lib/di-graph/analyzer-strategy.d.ts +1 -0
  31. package/src/lib/di-graph/analyzer-strategy.js +3 -0
  32. package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
  33. package/src/lib/framework-resolver.d.ts +1 -1
  34. package/src/lib/framework-resolver.js +5 -2
  35. package/src/lib/framework-resolver.js.map +1 -1
  36. package/src/lib/graph-metadata.d.ts +9 -2
  37. package/src/lib/graph-metadata.js +12 -3
  38. package/src/lib/graph-metadata.js.map +1 -1
  39. package/src/lib/graph-visualizer.js +6 -0
  40. package/src/lib/graph-visualizer.js.map +1 -1
  41. package/src/lib/role-resolver.d.ts +9 -3
  42. package/src/lib/role-resolver.js +12 -4
  43. package/src/lib/role-resolver.js.map +1 -1
  44. package/src/lib/tag-truth.d.ts +106 -0
  45. package/src/lib/tag-truth.js +323 -0
  46. package/src/lib/tag-truth.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"components-wiring.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-docs/components-wiring.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAGH,oDAA8D;AAC9D,6DAAyE;AACzE,uDAAgG;AAIhG,2EAA2E;AAC3E,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,MAAM,iBAAiB,GAAG,kCAAsB,CAAC,iBAAiB,CAAC;AACnE,MAAM,mBAAmB,GAAG,IAAI,iBAAiB,EAAE,CAAC;AAEpD,uFAAuF;AACvF,MAAM,eAAe,GAAsB,CAAC,iBAAiB,EAAE,kCAAsB,CAAC,cAAc,CAAC,CAAC;AAEtG,MAAa,gBAAgB;IAEJ;IACA;IACA;IAHrB,YACqB,QAAwD,EACxD,YAAoE,EACpE,QAA2B;QAF3B,aAAQ,GAAR,QAAQ,CAAgD;QACxD,iBAAY,GAAZ,YAAY,CAAwD;QACpE,aAAQ,GAAR,QAAQ,CAAmB;IAC7C,CAAC;IAEJ,iEAAiE;IACjE,SAAS;QACL,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;aAC5B,MAAM,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,kDAA+B,CAAC,CAAC;aACrG,IAAI,EAAE,CAAC;IAChB,CAAC;IAED,QAAQ;QACJ,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC5C,MAAM,QAAQ,GAA4B,EAAE,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1E,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YAC3D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACpC,KAAK,MAAM,UAAU,IAAI,eAAe,EAAE,CAAC;gBACvC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAE,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC;gBAC1D,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;oBAAE,SAAS;gBACjE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YACxE,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IACpB,CAAC;IAED,iGAAiG;IACzF,OAAO,CAAC,IAAY;QACxB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAE,CAAC;QACrC,MAAM,MAAM,GAAG,IAAI,kCAAsB,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;aAC/E,YAAY,CAAC,iBAAiB,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,OAAO,CAAC,IAAI,uCAAqB,CAAC,IAAI,EAAE,MAAM,CAAC,OAAQ,CAAC,OAAO,EAAE,GAAG,IAAI,KAAK,MAAM,CAAC,OAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC1G,CAAC;QACD,IAAI,MAAM,CAAC,KAAK,CAAC,UAAU,KAAK,YAAY;YAAE,OAAO,EAAE,CAAC;QACxD,OAAO,CAAC,IAAI,uCAAqB,CAC7B,IAAI,EACJ,GAAG,IAAI,IAAI,iBAAiB,eAAe,MAAM,CAAC,KAAK,CAAC,UAAU,0BAA0B,YAAY,MAAM;gBAC1G,gFAAgF,EACpF,GAAG,IAAI,QAAQ,OAAO,CAAC,IAAI,8CAA8C,YAAY,YAAY;gBAC7F,WAAW,iBAAiB,mBAAmB,YAAY,OAAO,mBAAmB,KAAK,CACjG,CAAC,CAAC;IACP,CAAC;IAEO,WAAW,CACf,IAAY,EACZ,UAAkB,EAClB,MAA2B,EAC3B,QAA2B;QAE3B,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAE,CAAC,IAAI,eAAe,CAAC;QAC1D,MAAM,MAAM,GAAG,CAAC,GAAI,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAsB,EAAE,mBAAmB,CAAC,CAAC;QACxF,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,KAAqB,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QAC5F,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;YACrD,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,gGAAgG,CAAC;QACvG,OAAO,IAAI,uCAAqB,CAC5B,GAAG,IAAI,IAAI,UAAU,EAAE,EACvB,qCAAqC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY;YACvG,sCAAsC,mBAAmB,iCAAiC;YAC1F,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,uBAAuB,gCAAgC;YAC3G,+BAA+B,EACnC,MAAM,KAAK,iBAAiB,UAAU,iBAAiB,IAAI,GAAG,QAAQ,GAAG,CAC5E,CAAC;IACN,CAAC;IAED,wGAAwG;IAChG,mBAAmB,CAAC,IAAY,EAAE,SAA8B;QACpE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;QAChC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QACrC,MAAM,KAAK,GAAa,CAAC,IAAI,CAAC,CAAC;QAC/B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;YAC/B,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;gBACxD,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;oBAAE,SAAS;gBAC1C,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;gBAC5B,IAAI,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;oBAAE,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;gBACnE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAClC,CAAC;QACL,CAAC;QACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,8FAA8F;IACtF,aAAa,CAAC,MAA2B;QAC7C,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAqB,EAAE,EAAE;YAC3D,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,OAAO,KAAK,KAAK,mBAAmB,CAAC;YACpE,OAAO,KAAK,CAAC,MAAM,KAAK,iBAAiB,IAAI,KAAK,CAAC,YAAY,KAAK,IAAI,CAAC;QAC7E,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AA9FD,4CA8FC","sourcesContent":["/**\n * The CHAINED half of `validate-nx-wiring` (#1058): DTO libraries tagged `generate:openapi-components`\n * publish `components.openapi.json`, and every document that `$ref`s one must be generated AFTER it.\n *\n * ```\n * <dto-lib>:openapi-components-generate dependsOn [\"build\", \"^openapi-components-generate\"]\n * <api-lib>:openapi-generate dependsOn [\"build\", \"^openapi-components-generate\"]\n * ```\n *\n * The `^` edge is what orders a chain of any depth — contract lib → DTO lib A → DTO lib B — because\n * nx skips a dependency without the target and keeps walking ITS dependencies. Without it nx may render\n * a contract document before the library document it references exists, and the generator's\n * fail-closed refusal (\"never generated\") fires intermittently, mostly in CI. So it is refused here,\n * on the resolved graph, with the exact `dependsOn` to write.\n */\n\nimport type { ProjectConfiguration, TargetConfiguration, TargetDependencyConfig } from '@nx/devkit';\nimport { GeneratedApiDocsLayout } from '@webpieces/core-util';\nimport { GENERATE_OPENAPI_COMPONENTS_TAG } from '../../generate-targets';\nimport { DeclaredDependsOn, GenerateWiringProblem, ProjectDependency } from './generate-wiring';\n\ntype DependsOnEntry = string | TargetDependencyConfig;\n\n/** The tsc target a library keeps, and its generating target dependsOn. */\nconst BUILD_TARGET = 'build';\nconst COMPONENTS_TARGET = GeneratedApiDocsLayout.COMPONENTS_TARGET;\nconst UPSTREAM_COMPONENTS = `^${COMPONENTS_TARGET}`;\n\n/** The targets that READ upstream components documents, and so must run after them. */\nconst READING_TARGETS: readonly string[] = [COMPONENTS_TARGET, GeneratedApiDocsLayout.OPENAPI_TARGET];\n\nexport class ComponentsWiring {\n constructor(\n private readonly projects: Readonly<Record<string, ProjectConfiguration>>,\n private readonly dependencies: Readonly<Record<string, readonly ProjectDependency[]>>,\n private readonly declared: DeclaredDependsOn,\n ) {}\n\n /** The DTO libraries tagged to publish a components document. */\n libraries(): string[] {\n return Object.keys(this.projects)\n .filter((name: string) => (this.projects[name]!.tags ?? []).includes(GENERATE_OPENAPI_COMPONENTS_TAG))\n .sort();\n }\n\n problems(): GenerateWiringProblem[] {\n const libraries = new Set(this.libraries());\n const problems: GenerateWiringProblem[] = [];\n for (const name of this.libraries()) problems.push(...this.shapeOf(name));\n for (const name of Object.keys(this.projects).sort()) {\n const upstream = this.librariesUpstreamOf(name, libraries);\n if (upstream.length === 0) continue;\n for (const targetName of READING_TARGETS) {\n const target = this.projects[name]!.targets?.[targetName];\n if (target === undefined || this.namesUpstream(target)) continue;\n problems.push(this.missingEdge(name, targetName, target, upstream));\n }\n }\n return problems;\n }\n\n /** `openapi-components-generate` dependsOn exactly the library's own `build`, the tsc target. */\n private shapeOf(name: string): GenerateWiringProblem[] {\n const project = this.projects[name]!;\n const lookup = new GeneratedApiDocsLayout(project.root, name, project.targets ?? {})\n .outputTarget(COMPONENTS_TARGET);\n if (lookup.found === undefined) {\n return [new GenerateWiringProblem(name, lookup.problem!.problem, `${name}: ${lookup.problem!.cure}`)];\n }\n if (lookup.found.targetName === BUILD_TARGET) return [];\n return [new GenerateWiringProblem(\n name,\n `${name}:${COMPONENTS_TARGET} dependsOn \"${lookup.found.targetName}\", and must dependsOn \"${BUILD_TARGET}\" — ` +\n 'the library\\'s @nx/js:tsc target keeps the name build (TS6059, nrwl/nx#18257).',\n `${name}: in ${project.root}/project.json, name the @nx/js:tsc target \"${BUILD_TARGET}\" and set ` +\n `targets.${COMPONENTS_TARGET}.dependsOn to [\"${BUILD_TARGET}\", \"${UPSTREAM_COMPONENTS}\"].`,\n )];\n }\n\n private missingEdge(\n name: string,\n targetName: string,\n target: TargetConfiguration,\n upstream: readonly string[],\n ): GenerateWiringProblem {\n const where = `${this.projects[name]!.root}/project.json`;\n const wanted = [...((target.dependsOn ?? []) as DependsOnEntry[]), UPSTREAM_COMPONENTS];\n const line = `[${wanted.map((entry: DependsOnEntry) => JSON.stringify(entry)).join(', ')}]`;\n const replaces = this.declared.declares(name, targetName)\n ? ''\n : ' (a project.json dependsOn replaces nx.json targetDefaults, which is why it lists every entry)';\n return new GenerateWiringProblem(\n `${name}:${targetName}`,\n `references the components document${upstream.length === 1 ? '' : 's'} of ${upstream.join(', ')}, and its ` +\n `effective dependsOn does not name \"${UPSTREAM_COMPONENTS}\" — so nx may render it before ` +\n `${upstream.length === 1 ? 'that document exists' : 'those documents exist'}, and generation fails closed ` +\n 'intermittently, mostly in CI.',\n `In ${where}, set targets.${targetName}.dependsOn to ${line}${replaces}.`,\n );\n }\n\n /** The components-publishing libraries `name` reaches through the dependency graph, itself excluded. */\n private librariesUpstreamOf(name: string, libraries: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n const seen = new Set<string>([name]);\n const queue: string[] = [name];\n while (queue.length > 0) {\n const current = queue.shift()!;\n for (const dependency of this.dependencies[current] ?? []) {\n if (seen.has(dependency.target)) continue;\n seen.add(dependency.target);\n if (libraries.has(dependency.target)) found.add(dependency.target);\n queue.push(dependency.target);\n }\n }\n return [...found].sort();\n }\n\n /** Whether `target` dependsOn `^openapi-components-generate`, in either of nx's spellings. */\n private namesUpstream(target: TargetConfiguration): boolean {\n return (target.dependsOn ?? []).some((entry: DependsOnEntry) => {\n if (typeof entry === 'string') return entry === UPSTREAM_COMPONENTS;\n return entry.target === COMPONENTS_TARGET && entry.dependencies === true;\n });\n }\n}\n"]}
1
+ {"version":3,"file":"components-wiring.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-docs/components-wiring.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The CHAINED half of `validate-nx-wiring` (#1058): DTO libraries tagged `generate:openapi-components`\n * publish `components.openapi.json`, and every document that `$ref`s one must be generated AFTER it.\n *\n * ```\n * <dto-lib>:openapi-components-generate dependsOn [\"build\", \"^openapi-components-generate\"]\n * <api-lib>:openapi-generate dependsOn [\"build\", \"^openapi-components-generate\"]\n * ```\n *\n * The `^` edge is what orders a chain of any depth — contract lib → DTO lib A → DTO lib B — because\n * nx skips a dependency without the target and keeps walking ITS dependencies. Without it nx may render\n * a contract document before the library document it references exists, and the generator's\n * fail-closed refusal (\"never generated\") fires intermittently, mostly in CI. So it is refused here,\n * on the resolved graph, with the exact `dependsOn` to write.\n *\n * The tag itself is allowed only on a `role:api-lib` project (#1064, D5): a components document puts a\n * library's types on the wire, and every wire type is declared in an api library. Tagging a general\n * library such as `company-core` must not be a way out of moving the type.\n */\n\nimport type { ProjectConfiguration, TargetConfiguration, TargetDependencyConfig } from '@nx/devkit';\nimport { GeneratedApiDocsLayout } from '@webpieces/core-util';\nimport { GENERATE_OPENAPI_COMPONENTS_TAG } from '../../generate-targets';\nimport { DeclaredDependsOn, GenerateWiringProblem, ProjectDependency } from './generate-wiring';\n\ntype DependsOnEntry = string | TargetDependencyConfig;\n\n/** The tsc target a library keeps, and its generating target dependsOn. */\nconst BUILD_TARGET = 'build';\nconst COMPONENTS_TARGET = GeneratedApiDocsLayout.COMPONENTS_TARGET;\nconst UPSTREAM_COMPONENTS = `^${COMPONENTS_TARGET}`;\nconst API_LIB_ROLE_TAG = 'role:api-lib';\n\n/** The targets that READ upstream components documents, and so must run after them. */\nconst READING_TARGETS: readonly string[] = [COMPONENTS_TARGET, GeneratedApiDocsLayout.OPENAPI_TARGET];\n\nexport class ComponentsWiring {\n constructor(\n private readonly projects: Readonly<Record<string, ProjectConfiguration>>,\n private readonly dependencies: Readonly<Record<string, readonly ProjectDependency[]>>,\n private readonly declared: DeclaredDependsOn,\n ) {}\n\n /** The DTO libraries tagged to publish a components document. */\n libraries(): string[] {\n return Object.keys(this.projects)\n .filter((name: string) => (this.projects[name]!.tags ?? []).includes(GENERATE_OPENAPI_COMPONENTS_TAG))\n .sort();\n }\n\n problems(): GenerateWiringProblem[] {\n const libraries = new Set(this.libraries());\n const problems: GenerateWiringProblem[] = [];\n for (const name of this.libraries()) problems.push(...this.roleOf(name), ...this.shapeOf(name));\n for (const name of Object.keys(this.projects).sort()) {\n const upstream = this.librariesUpstreamOf(name, libraries);\n if (upstream.length === 0) continue;\n for (const targetName of READING_TARGETS) {\n const target = this.projects[name]!.targets?.[targetName];\n if (target === undefined || this.namesUpstream(target)) continue;\n problems.push(this.missingEdge(name, targetName, target, upstream));\n }\n }\n return problems;\n }\n\n /** Only a `role:api-lib` publishes a components document (D5). */\n private roleOf(name: string): GenerateWiringProblem[] {\n const project = this.projects[name]!;\n const tags = project.tags ?? [];\n if (tags.includes(API_LIB_ROLE_TAG)) return [];\n const carried = tags.filter((tag: string) => tag.startsWith('role:')).join(', ') || 'no role tag';\n return [new GenerateWiringProblem(\n name,\n `${name} is tagged \"${GENERATE_OPENAPI_COMPONENTS_TAG}\" but carries ${carried} — only a role:api-lib ` +\n 'publishes a components document, because every type a contract reaches is declared in an api library.',\n `${name}: in ${project.root}/project.json, drop \"${GENERATE_OPENAPI_COMPONENTS_TAG}\" and move the wire ` +\n `types into a role:api-lib DTO library tagged with it — or, when ${name} holds only contracts and ` +\n 'wire types, retag it role:api-lib.',\n )];\n }\n\n /** `openapi-components-generate` dependsOn exactly the library's own `build`, the tsc target. */\n private shapeOf(name: string): GenerateWiringProblem[] {\n const project = this.projects[name]!;\n const lookup = new GeneratedApiDocsLayout(project.root, name, project.targets ?? {})\n .outputTarget(COMPONENTS_TARGET);\n if (lookup.found === undefined) {\n return [new GenerateWiringProblem(name, lookup.problem!.problem, `${name}: ${lookup.problem!.cure}`)];\n }\n if (lookup.found.targetName === BUILD_TARGET) return [];\n return [new GenerateWiringProblem(\n name,\n `${name}:${COMPONENTS_TARGET} dependsOn \"${lookup.found.targetName}\", and must dependsOn \"${BUILD_TARGET}\" — ` +\n 'the library\\'s @nx/js:tsc target keeps the name build (TS6059, nrwl/nx#18257).',\n `${name}: in ${project.root}/project.json, name the @nx/js:tsc target \"${BUILD_TARGET}\" and set ` +\n `targets.${COMPONENTS_TARGET}.dependsOn to [\"${BUILD_TARGET}\", \"${UPSTREAM_COMPONENTS}\"].`,\n )];\n }\n\n private missingEdge(\n name: string,\n targetName: string,\n target: TargetConfiguration,\n upstream: readonly string[],\n ): GenerateWiringProblem {\n const where = `${this.projects[name]!.root}/project.json`;\n const wanted = [...((target.dependsOn ?? []) as DependsOnEntry[]), UPSTREAM_COMPONENTS];\n const line = `[${wanted.map((entry: DependsOnEntry) => JSON.stringify(entry)).join(', ')}]`;\n const replaces = this.declared.declares(name, targetName)\n ? ''\n : ' (a project.json dependsOn replaces nx.json targetDefaults, which is why it lists every entry)';\n return new GenerateWiringProblem(\n `${name}:${targetName}`,\n `references the components document${upstream.length === 1 ? '' : 's'} of ${upstream.join(', ')}, and its ` +\n `effective dependsOn does not name \"${UPSTREAM_COMPONENTS}\" — so nx may render it before ` +\n `${upstream.length === 1 ? 'that document exists' : 'those documents exist'}, and generation fails closed ` +\n 'intermittently, mostly in CI.',\n `In ${where}, set targets.${targetName}.dependsOn to ${line}${replaces}.`,\n );\n }\n\n /** The components-publishing libraries `name` reaches through the dependency graph, itself excluded. */\n private librariesUpstreamOf(name: string, libraries: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n const seen = new Set<string>([name]);\n const queue: string[] = [name];\n while (queue.length > 0) {\n const current = queue.shift()!;\n for (const dependency of this.dependencies[current] ?? []) {\n if (seen.has(dependency.target)) continue;\n seen.add(dependency.target);\n if (libraries.has(dependency.target)) found.add(dependency.target);\n queue.push(dependency.target);\n }\n }\n return [...found].sort();\n }\n\n /** Whether `target` dependsOn `^openapi-components-generate`, in either of nx's spellings. */\n private namesUpstream(target: TargetConfiguration): boolean {\n return (target.dependsOn ?? []).some((entry: DependsOnEntry) => {\n if (typeof entry === 'string') return entry === UPSTREAM_COMPONENTS;\n return entry.target === COMPONENTS_TARGET && entry.dependencies === true;\n });\n }\n}\n"]}
@@ -39,6 +39,12 @@
39
39
  * expressible, by which time it is in partners' generated clients. So every check below runs on every
40
40
  * contract in scope, opted in or not — `@ApiType` narrows nothing here.
41
41
  *
42
+ * ## The wire closure (#1064, D4)
43
+ *
44
+ * Every named type a contract reaches must be DECLARED in a `role:api-lib` project and end in the
45
+ * suffix its `required-type-suffix` entry demands — see `wire-closure.ts`. It is a shared defect: a type
46
+ * declared in a general library is wrong on the wire whichever document it would have been in.
47
+ *
42
48
  * ## Root-level unions are NOT re-checked here
43
49
  *
44
50
  * `no-root-union-api-type` (#1009) already refuses them, workspace-wide, with its own config key and
@@ -49,6 +55,7 @@
49
55
  */
50
56
  import { ProjectInfo } from '../project-info';
51
57
  import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
58
+ import { WireClosureRule } from './wire-closure';
52
59
  /**
53
60
  * Walks every project's `src`, extracts every `@ApiPath` contract with the generator's own
54
61
  * extractor, and judges the result against the two rules.
@@ -64,6 +71,8 @@ export declare class ApiDocRulesScan {
64
71
  /** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
65
72
  private readonly openApiRule;
66
73
  private readonly mcpRule;
74
+ /** The suffix half of the wire closure; the role half always runs when either rule does. */
75
+ private readonly wireClosureRule;
67
76
  /**
68
77
  * Every `@InvalidEndpointForMcp` endpoint met on a file the MCP rule applies to.
69
78
  *
@@ -74,7 +83,9 @@ export declare class ApiDocRulesScan {
74
83
  private readonly exclusions;
75
84
  constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>,
76
85
  /** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
77
- openApiRule?: ApiDocRule, mcpRule?: ApiDocRule);
86
+ openApiRule?: ApiDocRule, mcpRule?: ApiDocRule,
87
+ /** The suffix half of the wire closure; the role half always runs when either rule does. */
88
+ wireClosureRule?: WireClosureRule);
78
89
  run(): ApiDocRulesFindings;
79
90
  /** Every contract in one file, or the ONE refusal that stopped the file being read at all. */
80
91
  private judgeFile;
@@ -86,6 +97,11 @@ export declare class ApiDocRulesScan {
86
97
  private reportExtractionFailure;
87
98
  /** ONE contract: the shared OpenAPI checks, then the MCP-only ones. */
88
99
  private judgeModel;
100
+ /**
101
+ * D4 — every named type the contract reaches is declared in a `role:api-lib` project and carries its
102
+ * suffix. Shared: a type that should never have left a general library blocks every document.
103
+ */
104
+ private judgeWireClosure;
89
105
  /** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */
90
106
  private judgeUnmapped;
91
107
  /** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */
@@ -40,6 +40,12 @@
40
40
  * expressible, by which time it is in partners' generated clients. So every check below runs on every
41
41
  * contract in scope, opted in or not — `@ApiType` narrows nothing here.
42
42
  *
43
+ * ## The wire closure (#1064, D4)
44
+ *
45
+ * Every named type a contract reaches must be DECLARED in a `role:api-lib` project and end in the
46
+ * suffix its `required-type-suffix` entry demands — see `wire-closure.ts`. It is a shared defect: a type
47
+ * declared in a general library is wrong on the wire whichever document it would have been in.
48
+ *
43
49
  * ## Root-level unions are NOT re-checked here
44
50
  *
45
51
  * `no-root-union-api-type` (#1009) already refuses them, workspace-wide, with its own config key and
@@ -58,6 +64,7 @@ const api_doc_model_1 = require("@webpieces/api-doc-model");
58
64
  const api_ast_1 = require("./api-ast");
59
65
  const api_doc_rules_1 = require("./api-doc-rules");
60
66
  const api_doc_rules_verdicts_1 = require("./api-doc-rules-verdicts");
67
+ const wire_closure_1 = require("./wire-closure");
61
68
  /** `@ApiPath(` at COLUMN ZERO — a docstring that TALKS about a contract declares none. */
62
69
  const DECLARES_CONTRACT = /^@ApiPath\(/m;
63
70
  /** ONE contract file, and which of the two rules apply to the project that owns it. */
@@ -185,6 +192,7 @@ class ApiDocRulesScan {
185
192
  projectInfos;
186
193
  openApiRule;
187
194
  mcpRule;
195
+ wireClosureRule;
188
196
  /**
189
197
  * Every `@InvalidEndpointForMcp` endpoint met on a file the MCP rule applies to.
190
198
  *
@@ -195,11 +203,14 @@ class ApiDocRulesScan {
195
203
  exclusions = [];
196
204
  constructor(workspaceRoot, projectInfos,
197
205
  /** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
198
- openApiRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.OPENAPI_RULE), mcpRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.MCP_RULE)) {
206
+ openApiRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.OPENAPI_RULE), mcpRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.MCP_RULE),
207
+ /** The suffix half of the wire closure; the role half always runs when either rule does. */
208
+ wireClosureRule = wire_closure_1.WireClosureRule.withoutSuffixes()) {
199
209
  this.workspaceRoot = workspaceRoot;
200
210
  this.projectInfos = projectInfos;
201
211
  this.openApiRule = openApiRule;
202
212
  this.mcpRule = mcpRule;
213
+ this.wireClosureRule = wireClosureRule;
203
214
  }
204
215
  run() {
205
216
  if (!this.openApiRule.enabled && !this.mcpRule.enabled)
@@ -211,14 +222,15 @@ class ApiDocRulesScan {
211
222
  const mcp = this.mcpRule.enabled ? new DefectCollector(api_doc_rules_1.MCP_RULE) : undefined;
212
223
  const program = ts.createProgram(files.map((file) => file.absPath), this.compilerOptions());
213
224
  const toolNames = new Map();
225
+ const closure = new wire_closure_1.WireClosure(this.workspaceRoot, this.projectInfos, this.wireClosureRule);
214
226
  for (const file of files) {
215
227
  const sink = new DefectSink(file.openApi ? openApi : undefined, file.mcp ? mcp : undefined);
216
- this.judgeFile(file, program, sink, toolNames);
228
+ this.judgeFile(file, program, sink, toolNames, closure);
217
229
  }
218
230
  return new api_doc_rules_1.ApiDocRulesFindings(openApi?.findings() ?? new api_doc_rules_1.ApiRuleFindings([], []), mcp?.findings() ?? new api_doc_rules_1.ApiRuleFindings([], []), this.exclusions);
219
231
  }
220
232
  /** Every contract in one file, or the ONE refusal that stopped the file being read at all. */
221
- judgeFile(file, program, sink, toolNames) {
233
+ judgeFile(file, program, sink, toolNames, closure) {
222
234
  if (!sink.anyRuleRuns())
223
235
  return;
224
236
  const source = program.getSourceFile(file.absPath);
@@ -228,6 +240,7 @@ class ApiDocRulesScan {
228
240
  try {
229
241
  for (const model of new api_doc_model_1.ApiDocExtractor().extractAllFrom(program, source)) {
230
242
  this.judgeModel(model, source, sink, toolNames);
243
+ this.judgeWireClosure(model, sink, source.fileName, closure);
231
244
  }
232
245
  }
233
246
  catch (err) {
@@ -254,6 +267,18 @@ class ApiDocRulesScan {
254
267
  this.judgeAnsweringResponses(model, source, lines, sink);
255
268
  this.judgeTools(model, source, lines, sink, toolNames);
256
269
  }
270
+ /**
271
+ * D4 — every named type the contract reaches is declared in a `role:api-lib` project and carries its
272
+ * suffix. Shared: a type that should never have left a general library blocks every document.
273
+ */
274
+ judgeWireClosure(model, sink, fallback, closure) {
275
+ for (const type of model.types.values()) {
276
+ const site = Site.parse(type.location, fallback);
277
+ for (const verdict of closure.judge(type, site.absPath)) {
278
+ sink.shared(() => new api_doc_rules_1.ApiContractDefect(model.contractName, '', verdict.what, site.relativeTo(this.workspaceRoot), verdict.cure, model.apiTypes), site);
279
+ }
280
+ }
281
+ }
257
282
  /** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */
258
283
  judgeUnmapped(model, sink, fallback) {
259
284
  for (const unmapped of model.unmapped) {
@@ -1 +1 @@
1
- {"version":3,"file":"api-doc-rules-scan.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules-scan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of \"is this contract\n * publishable\", on every `@ApiPath` class IN SCOPE, `@ApiType` or not.\n *\n * \"In scope\" is the rule's `mode` (#1017): `AFFECTED_PROJECT` scans the contracts of the projects the\n * diff touched — the granularity nx already builds at, and the mode a consumer normally picks —\n * while `RUN_EVERY_TIME` scans the whole workspace for a migration sweep. `ApiDocRule.coversProject`\n * is the one place that answers it, for both rules.\n *\n * ## The acceptance contract, and why this file drives the generator instead of copying it\n *\n * The rules exist so that ADDING `@ApiType(...)` (and `@WpMcpTool`) to a contract that passes them\n * always works. That is only true if \"expressible\" has exactly ONE definition, so this scan runs the\n * generator's own code — `ApiDocExtractor` / `TypeResolver` from `@webpieces/api-doc-model` for the\n * OpenAPI half, and `McpSchemaRenderer` tool-by-tool for the MCP half. A second implementation of\n * \"what can be published\" would drift from the first on the release that improved either one, and\n * the drift would be silent: the rules would stay green while generation started failing. The two\n * packages ship on the same release train, so the dependency is in lockstep by construction.\n *\n * Two things here are STRICTER than the generator, deliberately, and both are publishing rules\n * rather than expressibility ones (being stricter cannot break the acceptance contract — it can only\n * refuse something that would have generated):\n *\n * - an `unknown` VALUE TYPE anywhere (`Record<string, unknown>`, `unknown[]`, a bare `unknown`\n * field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON\n * Schema means \"anything\" — a partner-facing field with no shape, which is the defect the\n * unmapped guard exists for, arriving through a door the guard does not watch.\n * - an `rpc` or `external` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a\n * `cloudtasks` or `cron` endpoint and is allowed there; an endpoint somebody WAITS on that answers\n * nothing can never gain a field without a breaking change, where a named empty response object\n * grows additively forever (#1017 — #1016 read this narrowly as rpc-only because `external` was\n * unstated).\n *\n * ## Why it lives in the rules engine and not in the doc parser\n *\n * `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`\n * is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which\n * had already opted in would let a team discover, six months afterwards, that the type was never\n * expressible, by which time it is in partners' generated clients. So every check below runs on every\n * contract in scope, opted in or not — `@ApiType` narrows nothing here.\n *\n * ## Root-level unions are NOT re-checked here\n *\n * `no-root-union-api-type` (#1009) already refuses them, workspace-wide, with its own config key and\n * its own per-site hatch. One implementation. The MCP half still reports one when it meets it,\n * because `McpSchemaRenderer` refuses it as its own backstop and this scan reports whatever the\n * renderer says — which is the correct division: the OpenAPI document publishes a root union\n * perfectly well, and only a tool schema cannot carry one.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport {\n ApiDocExtractionError,\n ApiDocExtractor,\n ApiDocModel,\n DocumentedEndpoint,\n McpRenderError,\n McpSchemaRenderer,\n TypeRef,\n UnmappedType,\n} from '@webpieces/api-doc-model';\nimport { ProjectInfo } from '../project-info';\nimport { collectTsFiles, isTestFile } from './api-ast';\nimport {\n ApiContractDefect,\n ApiDocRule,\n ApiDocRulesFindings,\n ApiRuleFindings,\n DisableComment,\n McpExclusion,\n MCP_RULE,\n OPENAPI_RULE,\n} from './api-doc-rules';\nimport {\n ANSWERING_KINDS,\n ContractLines,\n unknownValueCure,\n VOID_RPC_CURE,\n carriesUnknown,\n classifyUnmapped,\n contractLinesOf,\n contractNamesIn,\n isVoidLike,\n toolFailures,\n} from './api-doc-rules-verdicts';\n\n/** `@ApiPath(` at COLUMN ZERO — a docstring that TALKS about a contract declares none. */\nconst DECLARES_CONTRACT = /^@ApiPath\\(/m;\n\n/** ONE contract file, and which of the two rules apply to the project that owns it. */\nclass ContractFile {\n constructor(\n public readonly absPath: string,\n public readonly openApi: boolean,\n public readonly mcp: boolean,\n ) {}\n}\n\n/** Where one declaration sits, already split out of the extractor's `File.ts:LINE:COL` spelling. */\nclass Site {\n constructor(\n public readonly absPath: string,\n public readonly line: number,\n ) {}\n\n /** `path/to/File.ts:LINE`, workspace-relative — what a refusal prints. */\n relativeTo(workspaceRoot: string): string {\n return `${path.relative(workspaceRoot, this.absPath)}:${this.line}`;\n }\n\n /** `abs/File.ts:12:5` -> a Site. An unparseable one falls back to line 1 of `fallback`. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static parse(location: string, fallback: string): Site {\n const match = location.match(/^(.*):(\\d+):\\d+$/);\n if (match === null) return new Site(fallback, 1);\n return new Site(match[1], Number(match[2]));\n }\n}\n\n/**\n * ONE rule's accumulator. It is what applies the per-site disable, so the disable semantics live in\n * exactly one place and cannot differ between the two rules.\n */\nclass DefectCollector {\n private readonly violations: ApiContractDefect[] = [];\n private readonly reasonless: ApiContractDefect[] = [];\n\n constructor(private readonly ruleName: string) {}\n\n /** The rule this collector reports under — what a shared defect's cure has to name. */\n rule(): string {\n return this.ruleName;\n }\n\n add(defect: ApiContractDefect, site: Site): void {\n const disable = DisableComment.readAt(site.absPath, site.line, this.ruleName);\n if (disable === undefined) {\n this.violations.push(defect);\n return;\n }\n if (!disable.hasReason) this.reasonless.push(defect);\n }\n\n findings(): ApiRuleFindings {\n return new ApiRuleFindings(\n DefectCollector.externalFirst(this.violations),\n DefectCollector.externalFirst(this.reasonless),\n );\n }\n\n /**\n * Partner-facing contracts first. The same defect is a different size depending on who reads the\n * document it would have been in, and a list that buries the `external-customer` ones among\n * thirty internal ones has hidden the only urgent line in it.\n */\n // webpieces-disable no-function-outside-class -- private static ordering of this class\n private static externalFirst(found: readonly ApiContractDefect[]): ApiContractDefect[] {\n return [...found].sort(\n (a: ApiContractDefect, b: ApiContractDefect) =>\n Number(b.isExternal()) - Number(a.isExternal()),\n );\n }\n}\n\n/**\n * Routes a defect to the rule that owns it.\n *\n * Every OpenAPI-level defect ALSO blocks MCP, so it is reported by whichever rule is running —\n * `api-rules-for-openapi` when that one is on, and `api-rules-for-mcp` alone when it is not. It is\n * never reported twice: a team running both would otherwise read every shared defect in two places\n * and have to work out that they are one.\n */\nclass DefectSink {\n constructor(\n private readonly openApi: DefectCollector | undefined,\n private readonly mcp: DefectCollector | undefined,\n ) {}\n\n /**\n * A defect that blocks the OpenAPI document, and therefore every tool on it too.\n *\n * The defect is BUILT from the rule that ends up reporting it, not handed in ready-made, because\n * a shared defect does not know in advance which rule will carry it: `api-rules-for-openapi` when\n * that one runs, and `api-rules-for-mcp` alone when it does not. A cure that named a fixed rule\n * would, on the mcp-only configuration, prescribe a `// webpieces-disable` line the collector\n * reading that site does not look for.\n */\n shared(build: (rule: string) => ApiContractDefect, site: Site): void {\n const target = this.openApi ?? this.mcp;\n if (target === undefined) return;\n target.add(build(target.rule()), site);\n }\n\n /** A defect that blocks ONE tool and nothing else. */\n mcpOnly(defect: ApiContractDefect, site: Site): void {\n this.mcp?.add(defect, site);\n }\n\n anyRuleRuns(): boolean {\n return this.openApi !== undefined || this.mcp !== undefined;\n }\n\n /** True when `api-rules-for-mcp` applies to this file — what the exclusion list is scoped to. */\n mcpRuns(): boolean {\n return this.mcp !== undefined;\n }\n}\n\n/**\n * Walks every project's `src`, extracts every `@ApiPath` contract with the generator's own\n * extractor, and judges the result against the two rules.\n *\n * ONE `ts.Program` over every contract file in the workspace, because a DTO a contract reaches\n * routinely lives in another project and the checker has to be able to follow the import — the same\n * reason the repo sweep in `@webpieces/api-doc-model`'s own spec builds one program rather than one\n * per file.\n */\nexport class ApiDocRulesScan {\n /**\n * Every `@InvalidEndpointForMcp` endpoint met on a file the MCP rule applies to.\n *\n * Collected even on a run with no findings at all, because restating them IS the feature: the\n * alternative considered in #1014 was a one-off warning when somebody adds one, and a warning\n * printed once at the moment of the decision is read by the one person who already knows.\n */\n private readonly exclusions: McpExclusion[] = [];\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n /** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */\n private readonly openApiRule: ApiDocRule = ApiDocRule.off(OPENAPI_RULE),\n private readonly mcpRule: ApiDocRule = ApiDocRule.off(MCP_RULE),\n ) {}\n\n run(): ApiDocRulesFindings {\n if (!this.openApiRule.enabled && !this.mcpRule.enabled) return ApiDocRulesFindings.empty();\n const files = this.contractFiles();\n if (files.length === 0) return ApiDocRulesFindings.empty();\n\n const openApi = this.openApiRule.enabled ? new DefectCollector(OPENAPI_RULE) : undefined;\n const mcp = this.mcpRule.enabled ? new DefectCollector(MCP_RULE) : undefined;\n const program = ts.createProgram(\n files.map((file: ContractFile) => file.absPath),\n this.compilerOptions(),\n );\n const toolNames = new Map<string, string>();\n for (const file of files) {\n const sink = new DefectSink(\n file.openApi ? openApi : undefined,\n file.mcp ? mcp : undefined,\n );\n this.judgeFile(file, program, sink, toolNames);\n }\n return new ApiDocRulesFindings(\n openApi?.findings() ?? new ApiRuleFindings([], []),\n mcp?.findings() ?? new ApiRuleFindings([], []),\n this.exclusions,\n );\n }\n\n /** Every contract in one file, or the ONE refusal that stopped the file being read at all. */\n private judgeFile(\n file: ContractFile,\n program: ts.Program,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n if (!sink.anyRuleRuns()) return;\n const source = program.getSourceFile(file.absPath);\n if (source === undefined) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an extraction refusal IS a finding; it is reported, not propagated\n try {\n for (const model of new ApiDocExtractor().extractAllFrom(program, source)) {\n this.judgeModel(model, source, sink, toolNames);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof ApiDocExtractionError)) throw err;\n this.reportExtractionFailure(err, file, source, sink);\n }\n }\n\n /**\n * An extraction that REFUSED. Reported under the shared list because it stops BOTH documents:\n * `@Endpoint` arguments that cannot be constant-folded, a bound on a non-numeric field, and the\n * `@ApiType(..., MCP)` ⇔ `@WpMcpTool` biconditional all fail here, before a model exists.\n */\n private reportExtractionFailure(\n error: ApiDocExtractionError,\n file: ContractFile,\n source: ts.SourceFile,\n sink: DefectSink,\n ): void {\n const site = Site.parse(error.location, file.absPath);\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n contractNamesIn(source)[0] ?? path.basename(file.absPath),\n '',\n `the contract cannot be read at all — ${error.message}`,\n site.relativeTo(this.workspaceRoot),\n error.cure,\n [],\n ),\n site,\n );\n }\n\n /** ONE contract: the shared OpenAPI checks, then the MCP-only ones. */\n private judgeModel(\n model: ApiDocModel,\n source: ts.SourceFile,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n const lines = contractLinesOf(source, model.contractName);\n this.judgeUnmapped(model, sink, source.fileName);\n this.judgeUnknownValues(model, sink, source.fileName);\n this.judgeAnsweringResponses(model, source, lines, sink);\n this.judgeTools(model, source, lines, sink, toolNames);\n }\n\n /** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */\n private judgeUnmapped(model: ApiDocModel, sink: DefectSink, fallback: string): void {\n for (const unmapped of model.unmapped) {\n const site = Site.parse(unmapped.location, fallback);\n const verdict = classifyUnmapped(unmapped);\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n '',\n verdict.what,\n site.relativeTo(this.workspaceRoot),\n verdict.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */\n private judgeUnknownValues(model: ApiDocModel, sink: DefectSink, fallback: string): void {\n for (const type of model.types.values()) {\n for (const field of type.fields) {\n if (!carriesUnknown(field.type)) continue;\n const site = Site.parse(field.location, fallback);\n sink.shared(\n (rule: string): ApiContractDefect =>\n new ApiContractDefect(\n model.contractName,\n '',\n `'${type.name}.${field.name}' publishes an 'unknown' value, so the ` +\n 'document states no shape for it at all',\n site.relativeTo(this.workspaceRoot),\n unknownValueCure(rule),\n model.apiTypes,\n ),\n site,\n );\n }\n }\n }\n\n /**\n * An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.\n *\n * `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing\n * to shape — and is allowed. On an `rpc` OR an `external` it is a one-way door: both are\n * synchronous request/response, an outside caller reads what comes back, and a `void` response\n * can never gain a field without breaking every generated client where `{}` grows additively\n * forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it\n * is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).\n */\n private judgeAnsweringResponses(\n model: ApiDocModel,\n source: ts.SourceFile,\n lines: ContractLines,\n sink: DefectSink,\n ): void {\n for (const endpoint of model.endpoints) {\n // `.some` and not `.includes`: the model's `kind` is a widened string, and the list is\n // typed EndpointKind so a kind that stops existing is a compile error here.\n if (!ANSWERING_KINDS.some((kind: string): boolean => kind === endpoint.kind)) continue;\n if (endpoint.response !== undefined && !isVoidLike(endpoint.response)) continue;\n const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n endpoint.methodName,\n `an ${endpoint.kind} endpoint returns nothing a document can name (void, ` +\n 'unknown, or no declared return type)',\n site.relativeTo(this.workspaceRoot),\n VOID_RPC_CURE,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */\n private judgeTools(\n model: ApiDocModel,\n source: ts.SourceFile,\n lines: ContractLines,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n const renderer = new McpSchemaRenderer(model);\n for (const endpoint of model.endpoints) {\n if (endpoint.invalidForMcp !== undefined) {\n if (!sink.mcpRuns()) continue;\n // @InvalidEndpointForMcp IS the answer to \"could this be a tool\". The decorator\n // carries the argument, so the rule asks nothing further and no webpieces-disable is\n // needed — a suppression would be a second, weaker spelling of the same declaration.\n this.exclusions.push(\n new McpExclusion(\n model.contractName,\n endpoint.methodName,\n endpoint.invalidForMcp,\n new Site(\n source.fileName,\n lines.lineOf(endpoint.methodName),\n ).relativeTo(this.workspaceRoot),\n ),\n );\n continue;\n }\n if (endpoint.mcpTool === undefined) continue;\n const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));\n for (const failure of toolFailures(endpoint, renderer, toolNames, model)) {\n sink.mcpOnly(\n new ApiContractDefect(\n model.contractName,\n endpoint.methodName,\n failure.what,\n site.relativeTo(this.workspaceRoot),\n failure.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n for (const methodName of lines.toolsWithoutEndpoint) {\n const site = new Site(source.fileName, lines.lineOf(methodName));\n sink.mcpOnly(\n new ApiContractDefect(\n model.contractName,\n methodName,\n 'carries @WpMcpTool but is not an @Endpoint, so it is not routed at all',\n site.relativeTo(this.workspaceRoot),\n \"Add @Endpoint(POST, '/path', READ, RPC) to it, or drop the @WpMcpTool.\",\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** Every non-test `.ts` under a project's `src` whose text DECLARES an `@ApiPath` contract. */\n private contractFiles(): ContractFile[] {\n const found: ContractFile[] = [];\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n // ONE question, asked of the rule itself: `mode` (OFF / AFFECTED_PROJECT /\n // RUN_EVERY_TIME) and `allowedPaths` are both folded into coversProject, so the\n // affected-project narrowing cannot be honoured in one branch and skipped in the other.\n const openApi = this.openApiRule.coversProject(info.root);\n const mcp = this.mcpRule.coversProject(info.root);\n if (!openApi && !mcp) continue;\n const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');\n if (!fs.existsSync(srcDir)) continue;\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue; // a fixture is not a published contract\n if (!DECLARES_CONTRACT.test(fs.readFileSync(file, 'utf8'))) continue;\n found.push(new ContractFile(file, openApi, mcp));\n }\n }\n return found.sort((a: ContractFile, b: ContractFile) => a.absPath.localeCompare(b.absPath));\n }\n\n /**\n * `tsconfig.base.json`'s options when the workspace has one, so an `@webpieces/*` import in a\n * contract RESOLVES and the checker can follow a DTO into another project. Without that the\n * resolver reports every cross-project type as unmapped, which would be a rule failing on its\n * own inability to read rather than on anything the author wrote.\n */\n private compilerOptions(): ts.CompilerOptions {\n const base = path.join(this.workspaceRoot, 'tsconfig.base.json');\n const declared = fs.existsSync(base)\n ? ts.parseJsonConfigFileContent(\n ts.readConfigFile(base, ts.sys.readFile).config,\n ts.sys,\n this.workspaceRoot,\n ).options\n : {};\n return {\n ...declared,\n noEmit: true,\n skipLibCheck: true,\n types: [],\n experimentalDecorators: true,\n };\n }\n}\n"]}
1
+ {"version":3,"file":"api-doc-rules-scan.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules-scan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of \"is this contract\n * publishable\", on every `@ApiPath` class IN SCOPE, `@ApiType` or not.\n *\n * \"In scope\" is the rule's `mode` (#1017): `AFFECTED_PROJECT` scans the contracts of the projects the\n * diff touched — the granularity nx already builds at, and the mode a consumer normally picks —\n * while `RUN_EVERY_TIME` scans the whole workspace for a migration sweep. `ApiDocRule.coversProject`\n * is the one place that answers it, for both rules.\n *\n * ## The acceptance contract, and why this file drives the generator instead of copying it\n *\n * The rules exist so that ADDING `@ApiType(...)` (and `@WpMcpTool`) to a contract that passes them\n * always works. That is only true if \"expressible\" has exactly ONE definition, so this scan runs the\n * generator's own code — `ApiDocExtractor` / `TypeResolver` from `@webpieces/api-doc-model` for the\n * OpenAPI half, and `McpSchemaRenderer` tool-by-tool for the MCP half. A second implementation of\n * \"what can be published\" would drift from the first on the release that improved either one, and\n * the drift would be silent: the rules would stay green while generation started failing. The two\n * packages ship on the same release train, so the dependency is in lockstep by construction.\n *\n * Two things here are STRICTER than the generator, deliberately, and both are publishing rules\n * rather than expressibility ones (being stricter cannot break the acceptance contract — it can only\n * refuse something that would have generated):\n *\n * - an `unknown` VALUE TYPE anywhere (`Record<string, unknown>`, `unknown[]`, a bare `unknown`\n * field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON\n * Schema means \"anything\" — a partner-facing field with no shape, which is the defect the\n * unmapped guard exists for, arriving through a door the guard does not watch.\n * - an `rpc` or `external` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a\n * `cloudtasks` or `cron` endpoint and is allowed there; an endpoint somebody WAITS on that answers\n * nothing can never gain a field without a breaking change, where a named empty response object\n * grows additively forever (#1017 — #1016 read this narrowly as rpc-only because `external` was\n * unstated).\n *\n * ## Why it lives in the rules engine and not in the doc parser\n *\n * `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`\n * is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which\n * had already opted in would let a team discover, six months afterwards, that the type was never\n * expressible, by which time it is in partners' generated clients. So every check below runs on every\n * contract in scope, opted in or not — `@ApiType` narrows nothing here.\n *\n * ## The wire closure (#1064, D4)\n *\n * Every named type a contract reaches must be DECLARED in a `role:api-lib` project and end in the\n * suffix its `required-type-suffix` entry demands — see `wire-closure.ts`. It is a shared defect: a type\n * declared in a general library is wrong on the wire whichever document it would have been in.\n *\n * ## Root-level unions are NOT re-checked here\n *\n * `no-root-union-api-type` (#1009) already refuses them, workspace-wide, with its own config key and\n * its own per-site hatch. One implementation. The MCP half still reports one when it meets it,\n * because `McpSchemaRenderer` refuses it as its own backstop and this scan reports whatever the\n * renderer says — which is the correct division: the OpenAPI document publishes a root union\n * perfectly well, and only a tool schema cannot carry one.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport {\n ApiDocExtractionError,\n ApiDocExtractor,\n ApiDocModel,\n DocumentedEndpoint,\n McpRenderError,\n McpSchemaRenderer,\n TypeRef,\n UnmappedType,\n} from '@webpieces/api-doc-model';\nimport { ProjectInfo } from '../project-info';\nimport { collectTsFiles, isTestFile } from './api-ast';\nimport {\n ApiContractDefect,\n ApiDocRule,\n ApiDocRulesFindings,\n ApiRuleFindings,\n DisableComment,\n McpExclusion,\n MCP_RULE,\n OPENAPI_RULE,\n} from './api-doc-rules';\nimport {\n ANSWERING_KINDS,\n ContractLines,\n unknownValueCure,\n VOID_RPC_CURE,\n carriesUnknown,\n classifyUnmapped,\n contractLinesOf,\n contractNamesIn,\n isVoidLike,\n toolFailures,\n} from './api-doc-rules-verdicts';\nimport { WireClosure, WireClosureRule } from './wire-closure';\n\n/** `@ApiPath(` at COLUMN ZERO — a docstring that TALKS about a contract declares none. */\nconst DECLARES_CONTRACT = /^@ApiPath\\(/m;\n\n/** ONE contract file, and which of the two rules apply to the project that owns it. */\nclass ContractFile {\n constructor(\n public readonly absPath: string,\n public readonly openApi: boolean,\n public readonly mcp: boolean,\n ) {}\n}\n\n/** Where one declaration sits, already split out of the extractor's `File.ts:LINE:COL` spelling. */\nclass Site {\n constructor(\n public readonly absPath: string,\n public readonly line: number,\n ) {}\n\n /** `path/to/File.ts:LINE`, workspace-relative — what a refusal prints. */\n relativeTo(workspaceRoot: string): string {\n return `${path.relative(workspaceRoot, this.absPath)}:${this.line}`;\n }\n\n /** `abs/File.ts:12:5` -> a Site. An unparseable one falls back to line 1 of `fallback`. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static parse(location: string, fallback: string): Site {\n const match = location.match(/^(.*):(\\d+):\\d+$/);\n if (match === null) return new Site(fallback, 1);\n return new Site(match[1], Number(match[2]));\n }\n}\n\n/**\n * ONE rule's accumulator. It is what applies the per-site disable, so the disable semantics live in\n * exactly one place and cannot differ between the two rules.\n */\nclass DefectCollector {\n private readonly violations: ApiContractDefect[] = [];\n private readonly reasonless: ApiContractDefect[] = [];\n\n constructor(private readonly ruleName: string) {}\n\n /** The rule this collector reports under — what a shared defect's cure has to name. */\n rule(): string {\n return this.ruleName;\n }\n\n add(defect: ApiContractDefect, site: Site): void {\n const disable = DisableComment.readAt(site.absPath, site.line, this.ruleName);\n if (disable === undefined) {\n this.violations.push(defect);\n return;\n }\n if (!disable.hasReason) this.reasonless.push(defect);\n }\n\n findings(): ApiRuleFindings {\n return new ApiRuleFindings(\n DefectCollector.externalFirst(this.violations),\n DefectCollector.externalFirst(this.reasonless),\n );\n }\n\n /**\n * Partner-facing contracts first. The same defect is a different size depending on who reads the\n * document it would have been in, and a list that buries the `external-customer` ones among\n * thirty internal ones has hidden the only urgent line in it.\n */\n // webpieces-disable no-function-outside-class -- private static ordering of this class\n private static externalFirst(found: readonly ApiContractDefect[]): ApiContractDefect[] {\n return [...found].sort(\n (a: ApiContractDefect, b: ApiContractDefect) =>\n Number(b.isExternal()) - Number(a.isExternal()),\n );\n }\n}\n\n/**\n * Routes a defect to the rule that owns it.\n *\n * Every OpenAPI-level defect ALSO blocks MCP, so it is reported by whichever rule is running —\n * `api-rules-for-openapi` when that one is on, and `api-rules-for-mcp` alone when it is not. It is\n * never reported twice: a team running both would otherwise read every shared defect in two places\n * and have to work out that they are one.\n */\nclass DefectSink {\n constructor(\n private readonly openApi: DefectCollector | undefined,\n private readonly mcp: DefectCollector | undefined,\n ) {}\n\n /**\n * A defect that blocks the OpenAPI document, and therefore every tool on it too.\n *\n * The defect is BUILT from the rule that ends up reporting it, not handed in ready-made, because\n * a shared defect does not know in advance which rule will carry it: `api-rules-for-openapi` when\n * that one runs, and `api-rules-for-mcp` alone when it does not. A cure that named a fixed rule\n * would, on the mcp-only configuration, prescribe a `// webpieces-disable` line the collector\n * reading that site does not look for.\n */\n shared(build: (rule: string) => ApiContractDefect, site: Site): void {\n const target = this.openApi ?? this.mcp;\n if (target === undefined) return;\n target.add(build(target.rule()), site);\n }\n\n /** A defect that blocks ONE tool and nothing else. */\n mcpOnly(defect: ApiContractDefect, site: Site): void {\n this.mcp?.add(defect, site);\n }\n\n anyRuleRuns(): boolean {\n return this.openApi !== undefined || this.mcp !== undefined;\n }\n\n /** True when `api-rules-for-mcp` applies to this file — what the exclusion list is scoped to. */\n mcpRuns(): boolean {\n return this.mcp !== undefined;\n }\n}\n\n/**\n * Walks every project's `src`, extracts every `@ApiPath` contract with the generator's own\n * extractor, and judges the result against the two rules.\n *\n * ONE `ts.Program` over every contract file in the workspace, because a DTO a contract reaches\n * routinely lives in another project and the checker has to be able to follow the import — the same\n * reason the repo sweep in `@webpieces/api-doc-model`'s own spec builds one program rather than one\n * per file.\n */\nexport class ApiDocRulesScan {\n /**\n * Every `@InvalidEndpointForMcp` endpoint met on a file the MCP rule applies to.\n *\n * Collected even on a run with no findings at all, because restating them IS the feature: the\n * alternative considered in #1014 was a one-off warning when somebody adds one, and a warning\n * printed once at the moment of the decision is read by the one person who already knows.\n */\n private readonly exclusions: McpExclusion[] = [];\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n /** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */\n private readonly openApiRule: ApiDocRule = ApiDocRule.off(OPENAPI_RULE),\n private readonly mcpRule: ApiDocRule = ApiDocRule.off(MCP_RULE),\n /** The suffix half of the wire closure; the role half always runs when either rule does. */\n private readonly wireClosureRule: WireClosureRule = WireClosureRule.withoutSuffixes(),\n ) {}\n\n run(): ApiDocRulesFindings {\n if (!this.openApiRule.enabled && !this.mcpRule.enabled) return ApiDocRulesFindings.empty();\n const files = this.contractFiles();\n if (files.length === 0) return ApiDocRulesFindings.empty();\n\n const openApi = this.openApiRule.enabled ? new DefectCollector(OPENAPI_RULE) : undefined;\n const mcp = this.mcpRule.enabled ? new DefectCollector(MCP_RULE) : undefined;\n const program = ts.createProgram(\n files.map((file: ContractFile) => file.absPath),\n this.compilerOptions(),\n );\n const toolNames = new Map<string, string>();\n const closure = new WireClosure(this.workspaceRoot, this.projectInfos, this.wireClosureRule);\n for (const file of files) {\n const sink = new DefectSink(\n file.openApi ? openApi : undefined,\n file.mcp ? mcp : undefined,\n );\n this.judgeFile(file, program, sink, toolNames, closure);\n }\n return new ApiDocRulesFindings(\n openApi?.findings() ?? new ApiRuleFindings([], []),\n mcp?.findings() ?? new ApiRuleFindings([], []),\n this.exclusions,\n );\n }\n\n /** Every contract in one file, or the ONE refusal that stopped the file being read at all. */\n private judgeFile(\n file: ContractFile,\n program: ts.Program,\n sink: DefectSink,\n toolNames: Map<string, string>,\n closure: WireClosure,\n ): void {\n if (!sink.anyRuleRuns()) return;\n const source = program.getSourceFile(file.absPath);\n if (source === undefined) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an extraction refusal IS a finding; it is reported, not propagated\n try {\n for (const model of new ApiDocExtractor().extractAllFrom(program, source)) {\n this.judgeModel(model, source, sink, toolNames);\n this.judgeWireClosure(model, sink, source.fileName, closure);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof ApiDocExtractionError)) throw err;\n this.reportExtractionFailure(err, file, source, sink);\n }\n }\n\n /**\n * An extraction that REFUSED. Reported under the shared list because it stops BOTH documents:\n * `@Endpoint` arguments that cannot be constant-folded, a bound on a non-numeric field, and the\n * `@ApiType(..., MCP)` ⇔ `@WpMcpTool` biconditional all fail here, before a model exists.\n */\n private reportExtractionFailure(\n error: ApiDocExtractionError,\n file: ContractFile,\n source: ts.SourceFile,\n sink: DefectSink,\n ): void {\n const site = Site.parse(error.location, file.absPath);\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n contractNamesIn(source)[0] ?? path.basename(file.absPath),\n '',\n `the contract cannot be read at all — ${error.message}`,\n site.relativeTo(this.workspaceRoot),\n error.cure,\n [],\n ),\n site,\n );\n }\n\n /** ONE contract: the shared OpenAPI checks, then the MCP-only ones. */\n private judgeModel(\n model: ApiDocModel,\n source: ts.SourceFile,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n const lines = contractLinesOf(source, model.contractName);\n this.judgeUnmapped(model, sink, source.fileName);\n this.judgeUnknownValues(model, sink, source.fileName);\n this.judgeAnsweringResponses(model, source, lines, sink);\n this.judgeTools(model, source, lines, sink, toolNames);\n }\n\n /**\n * D4 — every named type the contract reaches is declared in a `role:api-lib` project and carries its\n * suffix. Shared: a type that should never have left a general library blocks every document.\n */\n private judgeWireClosure(model: ApiDocModel, sink: DefectSink, fallback: string, closure: WireClosure): void {\n for (const type of model.types.values()) {\n const site = Site.parse(type.location, fallback);\n for (const verdict of closure.judge(type, site.absPath)) {\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n '',\n verdict.what,\n site.relativeTo(this.workspaceRoot),\n verdict.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n }\n\n /** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */\n private judgeUnmapped(model: ApiDocModel, sink: DefectSink, fallback: string): void {\n for (const unmapped of model.unmapped) {\n const site = Site.parse(unmapped.location, fallback);\n const verdict = classifyUnmapped(unmapped);\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n '',\n verdict.what,\n site.relativeTo(this.workspaceRoot),\n verdict.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */\n private judgeUnknownValues(model: ApiDocModel, sink: DefectSink, fallback: string): void {\n for (const type of model.types.values()) {\n for (const field of type.fields) {\n if (!carriesUnknown(field.type)) continue;\n const site = Site.parse(field.location, fallback);\n sink.shared(\n (rule: string): ApiContractDefect =>\n new ApiContractDefect(\n model.contractName,\n '',\n `'${type.name}.${field.name}' publishes an 'unknown' value, so the ` +\n 'document states no shape for it at all',\n site.relativeTo(this.workspaceRoot),\n unknownValueCure(rule),\n model.apiTypes,\n ),\n site,\n );\n }\n }\n }\n\n /**\n * An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.\n *\n * `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing\n * to shape — and is allowed. On an `rpc` OR an `external` it is a one-way door: both are\n * synchronous request/response, an outside caller reads what comes back, and a `void` response\n * can never gain a field without breaking every generated client where `{}` grows additively\n * forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it\n * is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).\n */\n private judgeAnsweringResponses(\n model: ApiDocModel,\n source: ts.SourceFile,\n lines: ContractLines,\n sink: DefectSink,\n ): void {\n for (const endpoint of model.endpoints) {\n // `.some` and not `.includes`: the model's `kind` is a widened string, and the list is\n // typed EndpointKind so a kind that stops existing is a compile error here.\n if (!ANSWERING_KINDS.some((kind: string): boolean => kind === endpoint.kind)) continue;\n if (endpoint.response !== undefined && !isVoidLike(endpoint.response)) continue;\n const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n endpoint.methodName,\n `an ${endpoint.kind} endpoint returns nothing a document can name (void, ` +\n 'unknown, or no declared return type)',\n site.relativeTo(this.workspaceRoot),\n VOID_RPC_CURE,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */\n private judgeTools(\n model: ApiDocModel,\n source: ts.SourceFile,\n lines: ContractLines,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n const renderer = new McpSchemaRenderer(model);\n for (const endpoint of model.endpoints) {\n if (endpoint.invalidForMcp !== undefined) {\n if (!sink.mcpRuns()) continue;\n // @InvalidEndpointForMcp IS the answer to \"could this be a tool\". The decorator\n // carries the argument, so the rule asks nothing further and no webpieces-disable is\n // needed — a suppression would be a second, weaker spelling of the same declaration.\n this.exclusions.push(\n new McpExclusion(\n model.contractName,\n endpoint.methodName,\n endpoint.invalidForMcp,\n new Site(\n source.fileName,\n lines.lineOf(endpoint.methodName),\n ).relativeTo(this.workspaceRoot),\n ),\n );\n continue;\n }\n if (endpoint.mcpTool === undefined) continue;\n const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));\n for (const failure of toolFailures(endpoint, renderer, toolNames, model)) {\n sink.mcpOnly(\n new ApiContractDefect(\n model.contractName,\n endpoint.methodName,\n failure.what,\n site.relativeTo(this.workspaceRoot),\n failure.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n for (const methodName of lines.toolsWithoutEndpoint) {\n const site = new Site(source.fileName, lines.lineOf(methodName));\n sink.mcpOnly(\n new ApiContractDefect(\n model.contractName,\n methodName,\n 'carries @WpMcpTool but is not an @Endpoint, so it is not routed at all',\n site.relativeTo(this.workspaceRoot),\n \"Add @Endpoint(POST, '/path', READ, RPC) to it, or drop the @WpMcpTool.\",\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** Every non-test `.ts` under a project's `src` whose text DECLARES an `@ApiPath` contract. */\n private contractFiles(): ContractFile[] {\n const found: ContractFile[] = [];\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n // ONE question, asked of the rule itself: `mode` (OFF / AFFECTED_PROJECT /\n // RUN_EVERY_TIME) and `allowedPaths` are both folded into coversProject, so the\n // affected-project narrowing cannot be honoured in one branch and skipped in the other.\n const openApi = this.openApiRule.coversProject(info.root);\n const mcp = this.mcpRule.coversProject(info.root);\n if (!openApi && !mcp) continue;\n const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');\n if (!fs.existsSync(srcDir)) continue;\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue; // a fixture is not a published contract\n if (!DECLARES_CONTRACT.test(fs.readFileSync(file, 'utf8'))) continue;\n found.push(new ContractFile(file, openApi, mcp));\n }\n }\n return found.sort((a: ContractFile, b: ContractFile) => a.absPath.localeCompare(b.absPath));\n }\n\n /**\n * `tsconfig.base.json`'s options when the workspace has one, so an `@webpieces/*` import in a\n * contract RESOLVES and the checker can follow a DTO into another project. Without that the\n * resolver reports every cross-project type as unmapped, which would be a rule failing on its\n * own inability to read rather than on anything the author wrote.\n */\n private compilerOptions(): ts.CompilerOptions {\n const base = path.join(this.workspaceRoot, 'tsconfig.base.json');\n const declared = fs.existsSync(base)\n ? ts.parseJsonConfigFileContent(\n ts.readConfigFile(base, ts.sys.readFile).config,\n ts.sys,\n this.workspaceRoot,\n ).options\n : {};\n return {\n ...declared,\n noEmit: true,\n skipLibCheck: true,\n types: [],\n experimentalDecorators: true,\n };\n }\n}\n"]}
@@ -1,27 +1,63 @@
1
1
  /**
2
2
  * API-lib tag validator (two-way)
3
3
  *
4
- * Keeps the `role:api-lib` tag and the CODE in sync, both directions:
5
- * - a project tagged `role:api-lib` MUST export ≥1 API contract (an `abstract class` carrying
6
- * `@ApiPath`/`@Rpc`/`@PubSub`) — else the tag is a lie;
7
- * - a project that DOES export such a contract MUST be tagged `role:api-lib` — else the arch
8
- * graph, the edge line-styles, and validate-api-relations can't treat it as an api-lib.
4
+ * Keeps the two api roles and the CODE in sync, both directions (#1064, D1 + D3):
5
+ * - a project that exports an `@ApiPath`/`@Rpc`/`@PubSub` contract (or a vendor contract under
6
+ * `externalApiPaths`) MUST be tagged `role:api-lib` or `role:api-client` — else the arch graph, the
7
+ * edge line-styles, and validate-api-relations can't treat it as one;
8
+ * - a project tagged `role:api-lib` MUST export a contract or ONLY wire types — else the tag is a lie.
9
+ * A contract is any of: an `@ApiPath`/`@Rpc`/`@PubSub` class (from the scan), an IPC contract
10
+ * (`@WpInternal` / `@WpIpcEndpoint`), or an in-process abstract `…Api` behind a DI token. A DTO-only
11
+ * library exports interfaces, type aliases, enums and data classes (no methods) and nothing else;
12
+ * - a project tagged `role:api-client` MUST export a contract (its abstract `XxxApi`) — the bundled
13
+ * `XxxClient` beside it is the point of the role, so it is not judged as a wire type.
9
14
  *
10
- * "Exports an API contract" is answered by the same source scan that owns apiRelations
11
- * (scan.apiLibProjects), so the tag can never drift from the code.
15
+ * "Exports an @ApiPath contract" is answered by the same source scan that owns apiRelations
16
+ * (scan.apiLibProjects), so the tag can never drift from the code. The IPC / in-process / DTO-only
17
+ * reading is a parser-only pass over the project's own non-test `src/**` — {@link ApiLibExportShape}.
12
18
  */
13
19
  import { ProjectInfo } from '../project-info';
14
20
  import { ApiScanResult } from './api-scanner';
15
21
  /**
16
22
  * A tag/code mismatch:
17
- * - 'missing-tag' — exports an API contract but is not tagged role:api-lib.
18
- * - 'unnecessary-tag' — tagged role:api-lib but exports no API contract.
23
+ * - 'missing-tag' — exports an API contract but is tagged neither role:api-lib nor role:api-client.
24
+ * - 'unnecessary-tag' — tagged role:api-lib / role:api-client but exports no contract, and (for
25
+ * api-lib) is not a DTO-only library either; `offenders` names what it exports.
19
26
  */
20
- export interface ApiLibTagViolation {
21
- project: string;
22
- kind: 'missing-tag' | 'unnecessary-tag';
27
+ export declare class ApiLibTagViolation {
28
+ readonly project: string;
29
+ readonly kind: 'missing-tag' | 'unnecessary-tag';
30
+ /** The role it carries (for 'unnecessary-tag'), else the role it should carry. */
31
+ readonly role: string;
32
+ /** Exports that are neither a contract nor a wire type — empty for 'missing-tag'. */
33
+ readonly offenders: readonly string[];
34
+ constructor(project: string, kind: 'missing-tag' | 'unnecessary-tag',
35
+ /** The role it carries (for 'unnecessary-tag'), else the role it should carry. */
36
+ role: string,
37
+ /** Exports that are neither a contract nor a wire type — empty for 'missing-tag'. */
38
+ offenders?: readonly string[]);
23
39
  }
24
- /** Both-directions tag ⇔ code check. Uses the scan's detected api-lib set as ground truth. */
25
- export declare function findApiLibTagViolations(projectInfos: Map<string, ProjectInfo>, scan: ApiScanResult): ApiLibTagViolation[];
40
+ /** What a library's own source exports, sorted into contracts, wire types and everything else. */
41
+ export declare class ApiLibExportShape {
42
+ /** True when it exports an IPC contract or an in-process abstract `…Api`. */
43
+ readonly exportsContract: boolean;
44
+ /** True when it exports at least one wire type (interface, alias, enum, data class). */
45
+ readonly exportsWireType: boolean;
46
+ /** `kind Name (file)` of every export that is neither — the implementation an api-lib must not hold. */
47
+ readonly offenders: readonly string[];
48
+ constructor(
49
+ /** True when it exports an IPC contract or an in-process abstract `…Api`. */
50
+ exportsContract: boolean,
51
+ /** True when it exports at least one wire type (interface, alias, enum, data class). */
52
+ exportsWireType: boolean,
53
+ /** `kind Name (file)` of every export that is neither — the implementation an api-lib must not hold. */
54
+ offenders: readonly string[]);
55
+ /** A role:api-lib passes when it holds a contract or wire types, and nothing else. */
56
+ isApiLib(): boolean;
57
+ /** Parser-only: every top-level EXPORTED declaration in the project's non-test `src/**`. */
58
+ static read(projectDir: string): ApiLibExportShape;
59
+ }
60
+ /** Both-directions tag ⇔ code check. Uses the scan's detected api-lib set as ground truth for @ApiPath. */
61
+ export declare function findApiLibTagViolations(projectInfos: Map<string, ProjectInfo>, scan: ApiScanResult, workspaceRoot: string): ApiLibTagViolation[];
26
62
  /** Human-readable, fix-oriented report for one tag/code mismatch. */
27
63
  export declare function describeApiLibTagViolation(violation: ApiLibTagViolation): string;