@webpieces/nx-webpieces-rules 0.4.808 → 0.4.809
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/src/executors/generate/executor.js +5 -0
- package/src/executors/generate/executor.js.map +1 -1
- package/src/lib/api-usage/api-contract-errors.d.ts +59 -0
- package/src/lib/api-usage/api-contract-errors.js +129 -1
- package/src/lib/api-usage/api-contract-errors.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules-scan.d.ts +107 -0
- package/src/lib/api-usage/api-doc-rules-scan.js +361 -0
- package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.d.ts +96 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.js +308 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.js.map +1 -0
- package/src/lib/api-usage/api-doc-rules.d.ts +149 -0
- package/src/lib/api-usage/api-doc-rules.js +242 -0
- package/src/lib/api-usage/api-doc-rules.js.map +1 -0
- package/src/lib/api-usage/api-scanner.d.ts +15 -1
- package/src/lib/api-usage/api-scanner.js +22 -2
- package/src/lib/api-usage/api-scanner.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.809",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
22
|
-
"@webpieces/
|
|
23
|
-
"@webpieces/
|
|
24
|
-
"@webpieces/
|
|
25
|
-
"@webpieces/
|
|
21
|
+
"@webpieces/ai-hook-rules": "0.4.809",
|
|
22
|
+
"@webpieces/api-doc-model": "0.4.809",
|
|
23
|
+
"@webpieces/code-rules": "0.4.809",
|
|
24
|
+
"@webpieces/eslint-rules": "0.4.809",
|
|
25
|
+
"@webpieces/pr-gate": "0.4.809",
|
|
26
|
+
"@webpieces/rules-config": "0.4.809",
|
|
26
27
|
"madge": "8.0.0"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
@@ -76,6 +76,11 @@ function scanApiRelations(workspaceRoot, graph, projectInfos) {
|
|
|
76
76
|
const scan = (0, api_scanner_1.scanAndAttachApiRelations)(workspaceRoot, graph, projectInfos, externalApiPaths);
|
|
77
77
|
if (scan.unresolvedApiCalls.length > 0)
|
|
78
78
|
console.warn((0, api_contract_errors_1.describeUnresolvedApiCalls)(scan.unresolvedApiCalls));
|
|
79
|
+
// #1014: restated on EVERY run, green or red. An exclusion announced once, at the moment
|
|
80
|
+
// somebody adds it, is read by the one person who already knows the decision.
|
|
81
|
+
if (scan.apiDocRules.mcpExclusions.length > 0) {
|
|
82
|
+
console.log((0, api_contract_errors_1.describeMcpExclusions)(scan.apiDocRules.mcpExclusions));
|
|
83
|
+
}
|
|
79
84
|
// A decorator argument we could not read costs the graph a basePath, a method, or a whole
|
|
80
85
|
// contract — none of which leaves a trace in the output. Name them before anything is written.
|
|
81
86
|
if (scan.nonLiteralDecoratorArgs.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json, plus\n * one contract file per API under architecture/apis/ (stale ones are deleted).\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate, RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { ProjectCycleDetector } from '../../lib/graph-cycles';\nimport { saveGraph, DEFAULT_GRAPH_PATH } from '../../lib/graph-loader';\nimport { ApiContractFiles } from '../../lib/api-contract-files';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { ProjectInfo } from '../../lib/project-info';\nimport {\n scanAndAttachApiRelations,\n describeNonLiteralDecoratorArgs,\n buildApiContracts,\n describeMismatchedEndpointKinds,\n} from '../../lib/api-usage/api-scanner';\n// The unresolved-contract report moved beside the other contract reports when api-scanner.ts reached\n// its file-size limit.\nimport { describeUnresolvedApiCalls } from '../../lib/api-usage/api-contract-errors';\nimport { buildExternalSystems } from '../../lib/api-usage/external-systems';\nimport type { ApiContracts, ExternalSystemDecls } from '../../lib/api-usage/api-relations';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport type { EnhancedGraph, GraphEntry } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraphReport, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { printAutoHiddenServers } from '../../lib/runtime-participant-resolver';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(\n workspaceRoot: string,\n graph: EnhancedGraph,\n hiddenProjects: Set<string>,\n apiContracts: ApiContracts,\n externalSystems: ExternalSystemDecls,\n): void {\n console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');\n const report = deriveRuntimeGraphReport(graph, hiddenProjects, apiContracts, externalSystems);\n saveRuntimeGraph(report.graph, workspaceRoot);\n const serviceCount = Object.keys(report.graph.services).length;\n const queueCount = Object.keys(report.graph.queues).length;\n console.log(\n `✅ Runtime graph saved (${serviceCount} services, ${report.graph.runtimeEdges.length} runtime edges, ` +\n `${queueCount} queues, ${report.graph.triggers.length} cron/external triggers)`,\n );\n // Every edge the derivation had to GUESS at. Loud on purpose: a fanned-out edge is committed and\n // then reasoned about as if it were derived, which is how a fictional call survives for months.\n printRuntimeWarnings(report.warnings);\n printAutoHiddenServers(report.autoHidden);\n // Printed here, FAILED by validate-runtime-architecture: generate must still write the graph,\n // or the validator would have nothing to compare against and the error would be unfixable.\n if (report.problems.length > 0) {\n console.error(`❌ ${report.problems.length} client call(s) name a service no module answers to:`);\n for (const problem of report.problems) console.error(` • ${problem}`);\n console.error(' This FAILS architecture:validate-runtime-architecture.');\n }\n}\n\n/** Print the derivation's guessed-edge warnings (nothing at all when the graph is fully targeted). */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printRuntimeWarnings(warnings: string[]): void {\n if (warnings.length === 0) return;\n console.warn(`⚠️ ${warnings.length} runtime edge(s) could not be targeted to ONE service:`);\n for (const warning of warnings) console.warn(` • ${warning}`);\n}\n\n/**\n * Scan for api relations and attach them, surfacing any contract we could NOT map back to source.\n * Unresolved contracts mean the graph we are about to WRITE is incomplete, so they must be loud —\n * a green run that quietly omits a service gets committed, rendered, and trusted.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction scanApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ScannedTables {\n console.log('🔎 Scanning source for implements/uses API relations...');\n const externalApiPaths = loadRuntimeConfig(workspaceRoot).externalApiPaths;\n const scan = scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths);\n if (scan.unresolvedApiCalls.length > 0) console.warn(describeUnresolvedApiCalls(scan.unresolvedApiCalls));\n // A decorator argument we could not read costs the graph a basePath, a method, or a whole\n // contract — none of which leaves a trace in the output. Name them before anything is written.\n if (scan.nonLiteralDecoratorArgs.length > 0) {\n console.warn(describeNonLiteralDecoratorArgs(scan.nonLiteralDecoratorArgs));\n }\n const contracts = buildApiContracts(scan);\n // An endpoint whose declared trigger its api kind cannot deliver would silently draw a queue or a\n // clock that nothing could ever fire — name it here, where the fix is one decorator away.\n const mismatches = describeMismatchedEndpointKinds(contracts);\n if (mismatches.length > 0) {\n console.error(`❌ ${mismatches.length} @Endpoint kind(s) conflict with their api kind:`);\n for (const mismatch of mismatches) console.error(` • ${mismatch}`);\n }\n return new ScannedTables(contracts, buildExternalSystems(scan.apiIndex, projectInfos));\n}\n\n/** The two committed tables one scan produces, kept together so callers cannot persist just one. */\nclass ScannedTables {\n constructor(\n public readonly apiContracts: ApiContracts,\n public readonly externalSystems: ExternalSystemDecls,\n ) {}\n}\n\n/** Projects tagged drawOnGraph:false — kept in the JSON, omitted from every rendered graph. */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction hiddenProjectsIn(graph: EnhancedGraph): Set<string> {\n const hidden = new Set<string>();\n for (const name of Object.keys(graph)) {\n if (graph[name].drawOnGraph === false) hidden.add(name);\n }\n return hidden;\n}\n\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printGraphSummary(graph: EnhancedGraph): void {\n const levels = new Set(Object.values(graph).map((entry: GraphEntry) => entry.level));\n console.log(`\\n📈 Graph Summary:`);\n console.log(` Projects: ${Object.keys(graph).length}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n}\n\n/**\n * The whole generation pipeline. Split out of runExecutor so that function stays a thin\n * try/catch-and-report shell — the error reporting is what a caller reads on failure, and it should\n * not be separated from the throw by fifty lines of steps.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nasync function generateEverything(workspaceRoot: string, graphPath: string | undefined): Promise<void> {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"📊 Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 1b: The graph is a BUILD graph — refuse a cyclic one, naming EVERY cycle. This runs\n // before the sort deliberately: the sort also refuses, but reports one cycle and an\n // undifferentiated list of everything tangled with it, so a repo with several cycles pays one\n // full regeneration per cycle to discover them.\n console.log('🔄 Checking the project graph is acyclic...');\n const cycles = new ProjectCycleDetector();\n cycles.assertAcyclic(reducedGraph, 'the nx project graph');\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('🔄 Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n // ...and assert the stratification it just produced actually holds: every dependency strictly\n // below its dependent. Safe to assert only because the graph was sorted a line ago — a stale\n // committed file is never checked this way.\n cycles.assertLevelsDescend(reducedGraph, cycles.levelsOf(enhancedGraph), 'the freshly sorted graph');\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n const scanned = scanApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n const apiContracts = scanned.apiContracts;\n\n // Step 4: Write one contract file per API (architecture/apis/<Api>.json) and delete the files of\n // APIs that no longer exist. dependencies.json only LINKS to them, so an endpoint change rewrites\n // an api file and never the dependency graph. The runtime validator reads these files back.\n const effectiveGraphPath = graphPath ?? DEFAULT_GRAPH_PATH;\n const contractFiles = new ApiContractFiles();\n const written = contractFiles.write(workspaceRoot, effectiveGraphPath, apiContracts);\n console.log(`✅ Wrote ${written.written.length} api contract file(s)`);\n for (const deleted of written.deleted) console.log(`🗑️ Deleted stale api contract file ${deleted}`);\n\n // Step 4a: Save the graph, INCLUDING the contract-file links and the external-system\n // declarations the runtime derivation reads back — generate derives from the in-memory graph,\n // validate from the files, so anything not written here would make the two disagree.\n console.log('💾 Saving graph to architecture/dependencies.json...');\n const contractRefs = contractFiles.refsFor(apiContracts);\n saveGraph(enhancedGraph, workspaceRoot, effectiveGraphPath, contractRefs, scanned.externalSystems);\n console.log('✅ Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`✅ Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n generateRuntimeGraph(\n workspaceRoot,\n enhancedGraph,\n hiddenProjectsIn(enhancedGraph),\n apiContracts,\n scanned.externalSystems,\n );\n\n printGraphSummary(enhancedGraph);\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n📊 Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n await generateEverything(workspaceRoot, graphPath);\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n // A RuleFailError carries its cures as Option[]; `Error.message` is only the aiMessage, so\n // printing that alone silently drops them. renderRuleFailForHuman is the ONE renderer that\n // numbers them, and this catch is the top-level handler for the nx target.\n const rendered = error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message;\n console.error('❌ Graph generation failed:', rendered);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️');\n }\n return { success: false };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json, plus\n * one contract file per API under architecture/apis/ (stale ones are deleted).\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate, RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { ProjectCycleDetector } from '../../lib/graph-cycles';\nimport { saveGraph, DEFAULT_GRAPH_PATH } from '../../lib/graph-loader';\nimport { ApiContractFiles } from '../../lib/api-contract-files';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { ProjectInfo } from '../../lib/project-info';\nimport {\n scanAndAttachApiRelations,\n describeNonLiteralDecoratorArgs,\n buildApiContracts,\n describeMismatchedEndpointKinds,\n} from '../../lib/api-usage/api-scanner';\n// The unresolved-contract report moved beside the other contract reports when api-scanner.ts reached\n// its file-size limit.\nimport {\n describeMcpExclusions,\n describeUnresolvedApiCalls,\n} from '../../lib/api-usage/api-contract-errors';\nimport { buildExternalSystems } from '../../lib/api-usage/external-systems';\nimport type { ApiContracts, ExternalSystemDecls } from '../../lib/api-usage/api-relations';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport type { EnhancedGraph, GraphEntry } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraphReport, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { printAutoHiddenServers } from '../../lib/runtime-participant-resolver';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(\n workspaceRoot: string,\n graph: EnhancedGraph,\n hiddenProjects: Set<string>,\n apiContracts: ApiContracts,\n externalSystems: ExternalSystemDecls,\n): void {\n console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');\n const report = deriveRuntimeGraphReport(graph, hiddenProjects, apiContracts, externalSystems);\n saveRuntimeGraph(report.graph, workspaceRoot);\n const serviceCount = Object.keys(report.graph.services).length;\n const queueCount = Object.keys(report.graph.queues).length;\n console.log(\n `✅ Runtime graph saved (${serviceCount} services, ${report.graph.runtimeEdges.length} runtime edges, ` +\n `${queueCount} queues, ${report.graph.triggers.length} cron/external triggers)`,\n );\n // Every edge the derivation had to GUESS at. Loud on purpose: a fanned-out edge is committed and\n // then reasoned about as if it were derived, which is how a fictional call survives for months.\n printRuntimeWarnings(report.warnings);\n printAutoHiddenServers(report.autoHidden);\n // Printed here, FAILED by validate-runtime-architecture: generate must still write the graph,\n // or the validator would have nothing to compare against and the error would be unfixable.\n if (report.problems.length > 0) {\n console.error(`❌ ${report.problems.length} client call(s) name a service no module answers to:`);\n for (const problem of report.problems) console.error(` • ${problem}`);\n console.error(' This FAILS architecture:validate-runtime-architecture.');\n }\n}\n\n/** Print the derivation's guessed-edge warnings (nothing at all when the graph is fully targeted). */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printRuntimeWarnings(warnings: string[]): void {\n if (warnings.length === 0) return;\n console.warn(`⚠️ ${warnings.length} runtime edge(s) could not be targeted to ONE service:`);\n for (const warning of warnings) console.warn(` • ${warning}`);\n}\n\n/**\n * Scan for api relations and attach them, surfacing any contract we could NOT map back to source.\n * Unresolved contracts mean the graph we are about to WRITE is incomplete, so they must be loud —\n * a green run that quietly omits a service gets committed, rendered, and trusted.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction scanApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ScannedTables {\n console.log('🔎 Scanning source for implements/uses API relations...');\n const externalApiPaths = loadRuntimeConfig(workspaceRoot).externalApiPaths;\n const scan = scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths);\n if (scan.unresolvedApiCalls.length > 0) console.warn(describeUnresolvedApiCalls(scan.unresolvedApiCalls));\n // #1014: restated on EVERY run, green or red. An exclusion announced once, at the moment\n // somebody adds it, is read by the one person who already knows the decision.\n if (scan.apiDocRules.mcpExclusions.length > 0) {\n console.log(describeMcpExclusions(scan.apiDocRules.mcpExclusions));\n }\n // A decorator argument we could not read costs the graph a basePath, a method, or a whole\n // contract — none of which leaves a trace in the output. Name them before anything is written.\n if (scan.nonLiteralDecoratorArgs.length > 0) {\n console.warn(describeNonLiteralDecoratorArgs(scan.nonLiteralDecoratorArgs));\n }\n const contracts = buildApiContracts(scan);\n // An endpoint whose declared trigger its api kind cannot deliver would silently draw a queue or a\n // clock that nothing could ever fire — name it here, where the fix is one decorator away.\n const mismatches = describeMismatchedEndpointKinds(contracts);\n if (mismatches.length > 0) {\n console.error(`❌ ${mismatches.length} @Endpoint kind(s) conflict with their api kind:`);\n for (const mismatch of mismatches) console.error(` • ${mismatch}`);\n }\n return new ScannedTables(contracts, buildExternalSystems(scan.apiIndex, projectInfos));\n}\n\n/** The two committed tables one scan produces, kept together so callers cannot persist just one. */\nclass ScannedTables {\n constructor(\n public readonly apiContracts: ApiContracts,\n public readonly externalSystems: ExternalSystemDecls,\n ) {}\n}\n\n/** Projects tagged drawOnGraph:false — kept in the JSON, omitted from every rendered graph. */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction hiddenProjectsIn(graph: EnhancedGraph): Set<string> {\n const hidden = new Set<string>();\n for (const name of Object.keys(graph)) {\n if (graph[name].drawOnGraph === false) hidden.add(name);\n }\n return hidden;\n}\n\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printGraphSummary(graph: EnhancedGraph): void {\n const levels = new Set(Object.values(graph).map((entry: GraphEntry) => entry.level));\n console.log(`\\n📈 Graph Summary:`);\n console.log(` Projects: ${Object.keys(graph).length}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n}\n\n/**\n * The whole generation pipeline. Split out of runExecutor so that function stays a thin\n * try/catch-and-report shell — the error reporting is what a caller reads on failure, and it should\n * not be separated from the throw by fifty lines of steps.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nasync function generateEverything(workspaceRoot: string, graphPath: string | undefined): Promise<void> {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"📊 Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 1b: The graph is a BUILD graph — refuse a cyclic one, naming EVERY cycle. This runs\n // before the sort deliberately: the sort also refuses, but reports one cycle and an\n // undifferentiated list of everything tangled with it, so a repo with several cycles pays one\n // full regeneration per cycle to discover them.\n console.log('🔄 Checking the project graph is acyclic...');\n const cycles = new ProjectCycleDetector();\n cycles.assertAcyclic(reducedGraph, 'the nx project graph');\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('🔄 Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n // ...and assert the stratification it just produced actually holds: every dependency strictly\n // below its dependent. Safe to assert only because the graph was sorted a line ago — a stale\n // committed file is never checked this way.\n cycles.assertLevelsDescend(reducedGraph, cycles.levelsOf(enhancedGraph), 'the freshly sorted graph');\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n const scanned = scanApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n const apiContracts = scanned.apiContracts;\n\n // Step 4: Write one contract file per API (architecture/apis/<Api>.json) and delete the files of\n // APIs that no longer exist. dependencies.json only LINKS to them, so an endpoint change rewrites\n // an api file and never the dependency graph. The runtime validator reads these files back.\n const effectiveGraphPath = graphPath ?? DEFAULT_GRAPH_PATH;\n const contractFiles = new ApiContractFiles();\n const written = contractFiles.write(workspaceRoot, effectiveGraphPath, apiContracts);\n console.log(`✅ Wrote ${written.written.length} api contract file(s)`);\n for (const deleted of written.deleted) console.log(`🗑️ Deleted stale api contract file ${deleted}`);\n\n // Step 4a: Save the graph, INCLUDING the contract-file links and the external-system\n // declarations the runtime derivation reads back — generate derives from the in-memory graph,\n // validate from the files, so anything not written here would make the two disagree.\n console.log('💾 Saving graph to architecture/dependencies.json...');\n const contractRefs = contractFiles.refsFor(apiContracts);\n saveGraph(enhancedGraph, workspaceRoot, effectiveGraphPath, contractRefs, scanned.externalSystems);\n console.log('✅ Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`✅ Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n generateRuntimeGraph(\n workspaceRoot,\n enhancedGraph,\n hiddenProjectsIn(enhancedGraph),\n apiContracts,\n scanned.externalSystems,\n );\n\n printGraphSummary(enhancedGraph);\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n📊 Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n await generateEverything(workspaceRoot, graphPath);\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n // A RuleFailError carries its cures as Option[]; `Error.message` is only the aiMessage, so\n // printing that alone silently drops them. renderRuleFailForHuman is the ONE renderer that\n // numbers them, and this catch is the top-level handler for the nx target.\n const rendered = error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message;\n console.error('❌ Graph generation failed:', rendered);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️');\n }\n return { success: false };\n }\n}\n"]}
|
|
@@ -12,6 +12,65 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { EmptiedApiContract, UndeclaredEndpointOperation, UndeclaredExternalCaller, UnresolvedApiCall, UnresolvedEndpointPath } from './api-relations';
|
|
14
14
|
import { RootUnionFindings } from './root-union-scan';
|
|
15
|
+
import { ApiRuleFindings, McpExclusion } from './api-doc-rules';
|
|
16
|
+
/**
|
|
17
|
+
* The endpoints declared PERMANENTLY outside MCP, with their reasons — restated on EVERY run of
|
|
18
|
+
* `api-rules-for-mcp`, green or red (#1014).
|
|
19
|
+
*
|
|
20
|
+
* Not a warning and not a finding. #1014 asked whether adding an `@InvalidEndpointForMcp` should
|
|
21
|
+
* warn, and the answer is that a one-off warning is read by the one person who already knows the
|
|
22
|
+
* decision. A list restated on every run is read by whoever comes next, which is who the reason was
|
|
23
|
+
* written for. Printed by the caller, exactly like `describeUnresolvedApiCalls`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function describeMcpExclusions(exclusions: readonly McpExclusion[]): string;
|
|
26
|
+
/**
|
|
27
|
+
* `api-rules-for-openapi`: a contract that cannot produce an OpenAPI document.
|
|
28
|
+
*
|
|
29
|
+
* Fatal because the failure it prevents is DOCUMENT-WIDE and arrives late. One field with no shape
|
|
30
|
+
* fails the whole document, so no partner can generate a client for ANY operation of that API — six
|
|
31
|
+
* of 98 endpoints in a measured upstream repo, from exactly two root causes, and five of those six
|
|
32
|
+
* were one field. The author who wrote that field had no way to know, because nothing looked at the
|
|
33
|
+
* contract until somebody added `@ApiType` months later.
|
|
34
|
+
*
|
|
35
|
+
* Its verdict is the GENERATOR'S: the scan drives `ApiDocExtractor` rather than restating what is
|
|
36
|
+
* expressible, so a contract that passes this is one where adding `@ApiType` then generates.
|
|
37
|
+
*/
|
|
38
|
+
export declare class ApiRulesForOpenApiError extends Error {
|
|
39
|
+
readonly findings: ApiRuleFindings;
|
|
40
|
+
constructor(findings: ApiRuleFindings);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `api-rules-for-mcp`: a `@WpMcpTool` that could not be served.
|
|
44
|
+
*
|
|
45
|
+
* Every one of these is something `McpToolRegistry` refuses to BOOT on — a tool it cannot find a
|
|
46
|
+
* schema for is a tool whose schema nobody checked — so the choice is only WHERE it is discovered.
|
|
47
|
+
* Here it names the method; at boot it names a process that will not start, in an environment where
|
|
48
|
+
* nobody is editing the contract.
|
|
49
|
+
*
|
|
50
|
+
* Separate from the OpenAPI error because the blast radii are different: this blocks ONE tool, where
|
|
51
|
+
* an OpenAPI defect blocks a whole document. A team publishing a partner API and no tools runs the
|
|
52
|
+
* first rule and not this one, which is only possible if they are two rules.
|
|
53
|
+
*/
|
|
54
|
+
export declare class ApiRulesForMcpError extends Error {
|
|
55
|
+
readonly findings: ApiRuleFindings;
|
|
56
|
+
/**
|
|
57
|
+
* The `@InvalidEndpointForMcp` endpoints, appended to the message.
|
|
58
|
+
*
|
|
59
|
+
* A red run is exactly when somebody is looking at this output, so it is also when the
|
|
60
|
+
* PERMANENT exclusions are most worth restating: the next question after "why is this tool
|
|
61
|
+
* blocked" is "which endpoints did we already decide can never be tools, and why".
|
|
62
|
+
*/
|
|
63
|
+
readonly exclusions: readonly McpExclusion[];
|
|
64
|
+
constructor(findings: ApiRuleFindings,
|
|
65
|
+
/**
|
|
66
|
+
* The `@InvalidEndpointForMcp` endpoints, appended to the message.
|
|
67
|
+
*
|
|
68
|
+
* A red run is exactly when somebody is looking at this output, so it is also when the
|
|
69
|
+
* PERMANENT exclusions are most worth restating: the next question after "why is this tool
|
|
70
|
+
* blocked" is "which endpoints did we already decide can never be tools, and why".
|
|
71
|
+
*/
|
|
72
|
+
exclusions?: readonly McpExclusion[]);
|
|
73
|
+
}
|
|
15
74
|
/**
|
|
16
75
|
* Loud, actionable report for contracts the scan could not map to source. Callers print this
|
|
17
76
|
* instead of emitting a green graph that is quietly missing relations. Not fatal: a contract
|
|
@@ -12,9 +12,137 @@
|
|
|
12
12
|
* Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.EmptiedApiContractError = exports.UndeclaredExternalCallerError = exports.UnresolvedEndpointPathError = exports.MissingBasePathError = exports.UndeclaredEndpointOperationError = exports.RootUnionApiTypeError = void 0;
|
|
15
|
+
exports.EmptiedApiContractError = exports.UndeclaredExternalCallerError = exports.UnresolvedEndpointPathError = exports.MissingBasePathError = exports.UndeclaredEndpointOperationError = exports.RootUnionApiTypeError = exports.ApiRulesForMcpError = exports.ApiRulesForOpenApiError = void 0;
|
|
16
|
+
exports.describeMcpExclusions = describeMcpExclusions;
|
|
16
17
|
exports.describeUnresolvedApiCalls = describeUnresolvedApiCalls;
|
|
17
18
|
const root_union_scan_1 = require("./root-union-scan");
|
|
19
|
+
const api_doc_rules_1 = require("./api-doc-rules");
|
|
20
|
+
/**
|
|
21
|
+
* The endpoints declared PERMANENTLY outside MCP, with their reasons — restated on EVERY run of
|
|
22
|
+
* `api-rules-for-mcp`, green or red (#1014).
|
|
23
|
+
*
|
|
24
|
+
* Not a warning and not a finding. #1014 asked whether adding an `@InvalidEndpointForMcp` should
|
|
25
|
+
* warn, and the answer is that a one-off warning is read by the one person who already knows the
|
|
26
|
+
* decision. A list restated on every run is read by whoever comes next, which is who the reason was
|
|
27
|
+
* written for. Printed by the caller, exactly like `describeUnresolvedApiCalls`.
|
|
28
|
+
*/
|
|
29
|
+
// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls
|
|
30
|
+
function describeMcpExclusions(exclusions) {
|
|
31
|
+
const lines = [
|
|
32
|
+
`ℹ️ ${exclusions.length} endpoint(s) are PERMANENTLY outside MCP (@InvalidEndpointForMcp):`,
|
|
33
|
+
];
|
|
34
|
+
for (const one of exclusions) {
|
|
35
|
+
lines.push(` • ${one.api}.${one.method} at ${one.at}`);
|
|
36
|
+
lines.push(` ${one.reason === '' ? '<reason could not be read>' : one.reason}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push(` These are DECLARATIONS, not suppressions: each says the endpoint can never be a tool and`, ` why. They are restated every run rather than announced once, so the reasons stay readable`, ` by whoever comes next. \`grep -rn InvalidEndpointForMcp\` is the same list from a shell.`);
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
/** The rule's message with its PERMANENT-exclusion list appended, when there is one. */
|
|
42
|
+
// webpieces-disable no-function-outside-class -- pure formatter beside the error that uses it
|
|
43
|
+
function appendExclusions(message, exclusions) {
|
|
44
|
+
if (exclusions.length === 0)
|
|
45
|
+
return message;
|
|
46
|
+
return `${message}\n\n${describeMcpExclusions(exclusions)}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One defect per line, with its cure INDENTED under it rather than collected into a footer.
|
|
50
|
+
*
|
|
51
|
+
* Every other error in this file prints one shared cure because its offenders share one root cause —
|
|
52
|
+
* an unreadable constant, a missing `calledBy`. These two do not: an open enum, an `unknown` value
|
|
53
|
+
* and a `void` RPC are three different edits, and a footer would make the reader work out which of
|
|
54
|
+
* the three applies to which line.
|
|
55
|
+
*/
|
|
56
|
+
// webpieces-disable no-function-outside-class -- pure formatter, mirrors rootUnionLines
|
|
57
|
+
function apiDefectLines(found) {
|
|
58
|
+
return found
|
|
59
|
+
.map((one) => {
|
|
60
|
+
const exposure = one.isExternal() ? ' [PARTNER-FACING]' : '';
|
|
61
|
+
return (` • ${one.where()}${exposure} — ${one.what}\n` +
|
|
62
|
+
` at ${one.at}\n` +
|
|
63
|
+
` cure: ${one.cure}`);
|
|
64
|
+
})
|
|
65
|
+
.join('\n');
|
|
66
|
+
}
|
|
67
|
+
/** The two halves every api-doc rule prints, so both refusals say the reasonless part identically. */
|
|
68
|
+
// webpieces-disable no-function-outside-class -- pure formatter shared by the two errors below
|
|
69
|
+
function apiRuleMessage(rule, headline, findings, why) {
|
|
70
|
+
const parts = [];
|
|
71
|
+
if (findings.violations.length > 0) {
|
|
72
|
+
parts.push(`${findings.violations.length} ${headline}:\n` +
|
|
73
|
+
apiDefectLines(findings.violations) +
|
|
74
|
+
`\n${why}\n` +
|
|
75
|
+
` This runs on EVERY @ApiPath contract, with or without @ApiType: @ApiType is a\n` +
|
|
76
|
+
` PUBLISHING decision added later, so a shape that is not expressible must fail on the\n` +
|
|
77
|
+
` first line rather than on the day somebody annotates it — by which time the type is\n` +
|
|
78
|
+
` in partners' generated clients and cannot be changed.\n` +
|
|
79
|
+
` Last resort, per site: // webpieces-disable ${rule} -- <reason>`);
|
|
80
|
+
}
|
|
81
|
+
if (findings.reasonlessDisables.length > 0) {
|
|
82
|
+
parts.push(`${findings.reasonlessDisables.length} disable(s) of ${rule} give NO reason:\n` +
|
|
83
|
+
apiDefectLines(findings.reasonlessDisables) +
|
|
84
|
+
`\n The reason is MANDATORY — a reasonless disable is itself a violation. Write it as\n` +
|
|
85
|
+
` // webpieces-disable ${rule} -- <why this contract is published like this>\n` +
|
|
86
|
+
` The hatch is per-site and argued on purpose: the next reader needs the argument,\n` +
|
|
87
|
+
` not just the suppression.`);
|
|
88
|
+
}
|
|
89
|
+
return parts.join('\n\n');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `api-rules-for-openapi`: a contract that cannot produce an OpenAPI document.
|
|
93
|
+
*
|
|
94
|
+
* Fatal because the failure it prevents is DOCUMENT-WIDE and arrives late. One field with no shape
|
|
95
|
+
* fails the whole document, so no partner can generate a client for ANY operation of that API — six
|
|
96
|
+
* of 98 endpoints in a measured upstream repo, from exactly two root causes, and five of those six
|
|
97
|
+
* were one field. The author who wrote that field had no way to know, because nothing looked at the
|
|
98
|
+
* contract until somebody added `@ApiType` months later.
|
|
99
|
+
*
|
|
100
|
+
* Its verdict is the GENERATOR'S: the scan drives `ApiDocExtractor` rather than restating what is
|
|
101
|
+
* expressible, so a contract that passes this is one where adding `@ApiType` then generates.
|
|
102
|
+
*/
|
|
103
|
+
class ApiRulesForOpenApiError extends Error {
|
|
104
|
+
findings;
|
|
105
|
+
constructor(findings) {
|
|
106
|
+
super(apiRuleMessage(api_doc_rules_1.OPENAPI_RULE, '@ApiPath contract defect(s) would block the OpenAPI document', findings, ` An OpenAPI failure is DOCUMENT-WIDE: one field with no shape blocks client generation\n` +
|
|
107
|
+
` for every operation of that API, not just the one that declares it.`));
|
|
108
|
+
this.findings = findings;
|
|
109
|
+
this.name = 'ApiRulesForOpenApiError';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.ApiRulesForOpenApiError = ApiRulesForOpenApiError;
|
|
113
|
+
/**
|
|
114
|
+
* `api-rules-for-mcp`: a `@WpMcpTool` that could not be served.
|
|
115
|
+
*
|
|
116
|
+
* Every one of these is something `McpToolRegistry` refuses to BOOT on — a tool it cannot find a
|
|
117
|
+
* schema for is a tool whose schema nobody checked — so the choice is only WHERE it is discovered.
|
|
118
|
+
* Here it names the method; at boot it names a process that will not start, in an environment where
|
|
119
|
+
* nobody is editing the contract.
|
|
120
|
+
*
|
|
121
|
+
* Separate from the OpenAPI error because the blast radii are different: this blocks ONE tool, where
|
|
122
|
+
* an OpenAPI defect blocks a whole document. A team publishing a partner API and no tools runs the
|
|
123
|
+
* first rule and not this one, which is only possible if they are two rules.
|
|
124
|
+
*/
|
|
125
|
+
class ApiRulesForMcpError extends Error {
|
|
126
|
+
findings;
|
|
127
|
+
exclusions;
|
|
128
|
+
constructor(findings,
|
|
129
|
+
/**
|
|
130
|
+
* The `@InvalidEndpointForMcp` endpoints, appended to the message.
|
|
131
|
+
*
|
|
132
|
+
* A red run is exactly when somebody is looking at this output, so it is also when the
|
|
133
|
+
* PERMANENT exclusions are most worth restating: the next question after "why is this tool
|
|
134
|
+
* blocked" is "which endpoints did we already decide can never be tools, and why".
|
|
135
|
+
*/
|
|
136
|
+
exclusions = []) {
|
|
137
|
+
super(appendExclusions(apiRuleMessage(api_doc_rules_1.MCP_RULE, '@WpMcpTool declaration(s) could not be served as MCP tools', findings, ` Each of these makes McpToolRegistry REFUSE TO BOOT: a registered tool the build never\n` +
|
|
138
|
+
` published is a tool whose schema nobody checked. Caught here, it names the method;\n` +
|
|
139
|
+
` caught at boot, it names a process that will not start.`), exclusions));
|
|
140
|
+
this.findings = findings;
|
|
141
|
+
this.exclusions = exclusions;
|
|
142
|
+
this.name = 'ApiRulesForMcpError';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.ApiRulesForMcpError = ApiRulesForMcpError;
|
|
18
146
|
/**
|
|
19
147
|
* Loud, actionable report for contracts the scan could not map to source. Callers print this
|
|
20
148
|
* instead of emitting a green graph that is quietly missing relations. Not fatal: a contract
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-contract-errors.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The ways `buildApiContracts` refuses to emit a green, wrong api contract table.\n *\n * They share one rule: an entry that is PRESENT but incomplete is worse than an absent one.\n * Every other entry in the table is complete, so a consumer has no reason to suspect the one that\n * lost a field — it just computes a confidently wrong URL, or draws a service with no queues.\n *\n * Each aggregates EVERY offender into one message rather than throwing on the first: an author who\n * moved a constants module broke five decorators at once and wants all five named in one run.\n *\n * Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.\n */\n\nimport {\n EmptiedApiContract,\n UndeclaredEndpointOperation,\n UndeclaredExternalCaller,\n UnresolvedApiCall,\n UnresolvedEndpointPath,\n} from './api-relations';\nimport { RootUnionApiType, RootUnionFindings, ROOT_UNION_RULE } from './root-union-scan';\n\n/**\n * Loud, actionable report for contracts the scan could not map to source. Callers print this\n * instead of emitting a green graph that is quietly missing relations. Not fatal: a contract\n * from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnclassifiedApiDep\nexport function describeUnresolvedApiCalls(calls: UnresolvedApiCall[]): string {\n const lines = [\n `⚠️ ${calls.length} API contract(s) resolved to a declaration file with no matching workspace source.`,\n ` Decorators (@ApiPath) are ERASED in .d.ts output, so these relations are MISSING from the graph:`,\n ];\n for (const call of calls) {\n lines.push(\n ` • ${call.api} at ${call.at} (${call.project}) → resolved to ${call.declaredIn}`,\n );\n }\n lines.push(\n ` If the api-lib IS in this workspace, add a tsconfig.base.json 'paths' entry mapping it to its`,\n ` src/index.ts, or confirm its project root is registered. If it is a published external package,`,\n ` this relation cannot be derived and the graph edge will not appear.`,\n );\n return lines.join('\\n');\n}\n\n/** One line per offender, in the shape every error in this file uses. */\n// webpieces-disable no-function-outside-class -- pure formatter shared by the one error below\nfunction rootUnionLines(found: readonly RootUnionApiType[]): string {\n return found\n .map(\n (one: RootUnionApiType) =>\n ` • ${one.api}.${one.method} — ${one.side} type '${one.typeName}' at ${one.at}`,\n )\n .join('\\n');\n}\n\n/**\n * `no-root-union-api-type`: a request or response type that IS a union, on ANY `@ApiPath` contract.\n *\n * Fatal, and fatal EARLY, because the failure it prevents is discovered by somebody else at runtime\n * and does not look like this contract's fault: a top-level `oneOf` is rejected by both the OpenAI\n * and the Anthropic function-calling APIs, a server sends its whole tool list on every request, and\n * so ONE such tool makes EVERY request 400. The user's report is \"all the other tools broke\".\n *\n * The second list is reasonless disables. A disable for this rule must carry an argument somebody\n * wrote down: the escape hatch is deliberately per-site and never a blanket switch, because the\n * blast radius is a whole session rather than one call.\n */\nexport class RootUnionApiTypeError extends Error {\n constructor(public readonly findings: RootUnionFindings) {\n super(RootUnionApiTypeError.render(findings));\n this.name = 'RootUnionApiTypeError';\n }\n\n // webpieces-disable no-function-outside-class, max-lines-new-methods -- private static renderer of this class, and splitting one message hides what a reader actually sees\n private static render(findings: RootUnionFindings): string {\n const parts: string[] = [];\n if (findings.violations.length > 0) {\n parts.push(\n `${findings.violations.length} @ApiPath method(s) declare a request or response type that IS a union:\\n` +\n rootUnionLines(findings.violations) +\n `\\n A union at the TOP LEVEL of a tool's parameter schema is rejected by BOTH the OpenAI and\\n` +\n ` the Anthropic function-calling APIs. A server sends its WHOLE tool list on every request, so\\n` +\n ` ONE of these makes EVERY request 400 and the entire client session unusable — not just that\\n` +\n ` tool. Nested composition, INSIDE a property, is fine and publishes as oneOf.\\n` +\n ` Fix it by wrapping the union in a property of an object:\\n` +\n ` export interface MoveWindowRequest { window: ScheduledWindow | AsapWindow }\\n` +\n ` This runs on EVERY @ApiPath contract, with or without @ApiType: @ApiType is a publishing\\n` +\n ` decision added later, so a shape that is not expressible must fail on the first line rather\\n` +\n ` than on the day somebody annotates it.\\n` +\n ` Last resort, per site: // webpieces-disable ${ROOT_UNION_RULE} -- <reason>`,\n );\n }\n if (findings.reasonlessDisables.length > 0) {\n parts.push(\n `${findings.reasonlessDisables.length} disable(s) of ${ROOT_UNION_RULE} give NO reason:\\n` +\n rootUnionLines(findings.reasonlessDisables) +\n `\\n The reason is MANDATORY — a reasonless disable is itself a violation. Write it as\\n` +\n ` // webpieces-disable ${ROOT_UNION_RULE} -- <why this root union is deliberate>\\n` +\n ` The hatch is per-site and argued on purpose: this defect costs a whole client session, so\\n` +\n ` the next reader needs the argument, not just the suppression.`,\n );\n }\n return parts.join('\\n\\n');\n }\n}\n\n/** Endpoints missing the operation declaration that drives retry safety and MCP annotations. */\nexport class UndeclaredEndpointOperationError extends Error {\n constructor(public readonly endpoints: readonly UndeclaredEndpointOperation[]) {\n super(\n `${endpoints.length} @Endpoint(s) do not declare a readable operation:\\n` +\n endpoints\n .map(\n (e: UndeclaredEndpointOperation) =>\n ` • ${e.api}.${e.method} — ${e.argument} at ${e.at}`,\n )\n .join('\\n') +\n `\\n operation is REQUIRED because it controls retry safety and generated MCP hints.\\n` +\n ` Pass exactly one of READ, WRITE_IDEMPOTENT, or WRITE as the third argument.\\n` +\n ` Do not infer operation semantics from the HTTP verb.`,\n );\n this.name = 'UndeclaredEndpointOperationError';\n }\n}\n\n/**\n * A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the\n * entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.\n */\nexport class MissingBasePathError extends Error {\n constructor(public readonly contracts: readonly string[]) {\n super(\n `${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\\n` +\n contracts.map((c: string) => ` • ${c}`).join('\\n') +\n `\\n basePath is REQUIRED in every api contract — an entry without it makes every consumer\\n` +\n ` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\\n` +\n ` or move the constant into the same module as the contract class.`,\n );\n this.name = 'MissingBasePathError';\n }\n}\n\n/**\n * `@Endpoint` paths the scan could not read. Fatal for the same reason MissingBasePathError is: the\n * two arguments are the two halves of ONE url. An http client builds its request as\n * `basePath + path`, so a contract shipped without a method's path is missing routing information,\n * and the consumer computes a confidently wrong URL. Skipping the method instead was worse still —\n * a class whose every path was an unreadable constant lost every method and vanished from the graph.\n */\nexport class UnresolvedEndpointPathError extends Error {\n constructor(public readonly paths: readonly UnresolvedEndpointPath[]) {\n super(\n `${paths.length} @Endpoint path(s) could not be read as a string:\\n` +\n paths\n .map(\n (p: UnresolvedEndpointPath) =>\n ` • ${p.api}.${p.method} — @Endpoint(${p.argument}, ...) at ${p.at}`,\n )\n .join('\\n') +\n `\\n path is REQUIRED in every api contract — every consumer builds its request URL as\\n` +\n ` basePath + path, so an unreadable path is MISSING ROUTING, not cosmetic metadata,\\n` +\n ` and a class whose every path is unreadable drops out of the graph entirely.\\n` +\n ` Inline the @Endpoint string literal, or move the constant into the SAME module as\\n` +\n ` the contract class — a same-module const IS resolved, one imported from another\\n` +\n ` module is NOT (this scan is parser-only by design: module resolution can land on a\\n` +\n ` decorator-erased .d.ts).`,\n );\n this.name = 'UnresolvedEndpointPathError';\n }\n}\n\n/**\n * `external` endpoints whose CALLER the scan could not read. Fatal, like the two above, because the\n * alternative is a diagram that lies by omission: the inbound box exists solely to name the system\n * calling us from outside, and with nothing to name it falls back to restating our own contract\n * name — which the reader already sees on the service box the arrow points at.\n *\n * `@Endpoint`'s TS overloads make `calledBy` a compile error to omit, so a scan reaching here saw a\n * JS caller, an `as any`, a cross-module constant this parser-only pass cannot fold, or a\n * `callerKind` that is not one of the declared kinds.\n */\nexport class UndeclaredExternalCallerError extends Error {\n constructor(public readonly callers: readonly UndeclaredExternalCaller[]) {\n super(\n `${callers.length} 'external' @Endpoint(s) do not declare WHO calls them:\\n` +\n callers\n .map(\n (c: UndeclaredExternalCaller) =>\n ` • ${c.api}.${c.method} — ${c.argument} at ${c.at}`,\n )\n .join('\\n') +\n `\\n An 'external' endpoint is driven by a system OUTSIDE this repo, and the runtime\\n` +\n ` architecture graph draws that system as an inbound box. Name it:\\n` +\n ` @Endpoint(POST, '/hook', WRITE, EXTERNAL, { calledBy: 'twilio' })\\n` +\n ` Add callerKind for anything that is not a vendor SaaS — database | cache | queue |\\n` +\n ` storage | saas | system — e.g. a GCP Pub/Sub push subscription:\\n` +\n ` @Endpoint(POST, '/push', WRITE, EXTERNAL, { calledBy: 'pubsub-push', callerKind: 'system' })\\n` +\n ` Use a string LITERAL or a SAME-module const: this scan is parser-only by design.`,\n );\n this.name = 'UndeclaredExternalCallerError';\n }\n}\n\n/**\n * Contract classes that declared `@Endpoint` methods and kept none of them. Fatal because the\n * alternative is the silent drop the api scan exists to close: buildApiContracts legitimately skips\n * a zero-method class (a vendor seam has no routes), and a class gutted by unreadable decorator\n * arguments used the very same exit — which is how a service lost two real Cloud Tasks queues and an\n * inbound webhook without a single line of output.\n */\nexport class EmptiedApiContractError extends Error {\n constructor(public readonly contracts: readonly EmptiedApiContract[]) {\n super(\n `${contracts.length} API contract class(es) declare @Endpoint methods but kept NONE of them:\\n` +\n contracts\n .map(\n (c: EmptiedApiContract) =>\n ` • ${c.api} — ${c.declared} @Endpoint method(s) declared, 0 usable, at ${c.at}`,\n )\n .join('\\n') +\n `\\n A contract with zero usable methods is DROPPED from the api contracts, so the class,\\n` +\n ` its queues and its triggers disappear from the architecture graph with no error.\\n` +\n ` Every @Endpoint argument must be readable: the path as a string literal or a\\n` +\n ` SAME-module const, the kind as 'rpc' | 'cloudtasks' | 'cron' | 'external', and\\n` +\n ` the options must declare operation. Fix the arguments above, or remove the decorators if the\\n` +\n ` class is genuinely not routed.`,\n );\n this.name = 'EmptiedApiContractError';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-contract-errors.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The ways `buildApiContracts` refuses to emit a green, wrong api contract table.\n *\n * They share one rule: an entry that is PRESENT but incomplete is worse than an absent one.\n * Every other entry in the table is complete, so a consumer has no reason to suspect the one that\n * lost a field — it just computes a confidently wrong URL, or draws a service with no queues.\n *\n * Each aggregates EVERY offender into one message rather than throwing on the first: an author who\n * moved a constants module broke five decorators at once and wants all five named in one run.\n *\n * Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.\n */\n\nimport {\n EmptiedApiContract,\n UndeclaredEndpointOperation,\n UndeclaredExternalCaller,\n UnresolvedApiCall,\n UnresolvedEndpointPath,\n} from './api-relations';\nimport { RootUnionApiType, RootUnionFindings, ROOT_UNION_RULE } from './root-union-scan';\nimport {\n ApiContractDefect,\n ApiRuleFindings,\n McpExclusion,\n MCP_RULE,\n OPENAPI_RULE,\n} from './api-doc-rules';\n\n/**\n * The endpoints declared PERMANENTLY outside MCP, with their reasons — restated on EVERY run of\n * `api-rules-for-mcp`, green or red (#1014).\n *\n * Not a warning and not a finding. #1014 asked whether adding an `@InvalidEndpointForMcp` should\n * warn, and the answer is that a one-off warning is read by the one person who already knows the\n * decision. A list restated on every run is read by whoever comes next, which is who the reason was\n * written for. Printed by the caller, exactly like `describeUnresolvedApiCalls`.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeMcpExclusions(exclusions: readonly McpExclusion[]): string {\n const lines = [\n `ℹ️ ${exclusions.length} endpoint(s) are PERMANENTLY outside MCP (@InvalidEndpointForMcp):`,\n ];\n for (const one of exclusions) {\n lines.push(` • ${one.api}.${one.method} at ${one.at}`);\n lines.push(` ${one.reason === '' ? '<reason could not be read>' : one.reason}`);\n }\n lines.push(\n ` These are DECLARATIONS, not suppressions: each says the endpoint can never be a tool and`,\n ` why. They are restated every run rather than announced once, so the reasons stay readable`,\n ` by whoever comes next. \\`grep -rn InvalidEndpointForMcp\\` is the same list from a shell.`,\n );\n return lines.join('\\n');\n}\n\n/** The rule's message with its PERMANENT-exclusion list appended, when there is one. */\n// webpieces-disable no-function-outside-class -- pure formatter beside the error that uses it\nfunction appendExclusions(message: string, exclusions: readonly McpExclusion[]): string {\n if (exclusions.length === 0) return message;\n return `${message}\\n\\n${describeMcpExclusions(exclusions)}`;\n}\n\n/**\n * One defect per line, with its cure INDENTED under it rather than collected into a footer.\n *\n * Every other error in this file prints one shared cure because its offenders share one root cause —\n * an unreadable constant, a missing `calledBy`. These two do not: an open enum, an `unknown` value\n * and a `void` RPC are three different edits, and a footer would make the reader work out which of\n * the three applies to which line.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors rootUnionLines\nfunction apiDefectLines(found: readonly ApiContractDefect[]): string {\n return found\n .map((one: ApiContractDefect) => {\n const exposure = one.isExternal() ? ' [PARTNER-FACING]' : '';\n return (\n ` • ${one.where()}${exposure} — ${one.what}\\n` +\n ` at ${one.at}\\n` +\n ` cure: ${one.cure}`\n );\n })\n .join('\\n');\n}\n\n/** The two halves every api-doc rule prints, so both refusals say the reasonless part identically. */\n// webpieces-disable no-function-outside-class -- pure formatter shared by the two errors below\nfunction apiRuleMessage(\n rule: string,\n headline: string,\n findings: ApiRuleFindings,\n why: string,\n): string {\n const parts: string[] = [];\n if (findings.violations.length > 0) {\n parts.push(\n `${findings.violations.length} ${headline}:\\n` +\n apiDefectLines(findings.violations) +\n `\\n${why}\\n` +\n ` This runs on EVERY @ApiPath contract, with or without @ApiType: @ApiType is a\\n` +\n ` PUBLISHING decision added later, so a shape that is not expressible must fail on the\\n` +\n ` first line rather than on the day somebody annotates it — by which time the type is\\n` +\n ` in partners' generated clients and cannot be changed.\\n` +\n ` Last resort, per site: // webpieces-disable ${rule} -- <reason>`,\n );\n }\n if (findings.reasonlessDisables.length > 0) {\n parts.push(\n `${findings.reasonlessDisables.length} disable(s) of ${rule} give NO reason:\\n` +\n apiDefectLines(findings.reasonlessDisables) +\n `\\n The reason is MANDATORY — a reasonless disable is itself a violation. Write it as\\n` +\n ` // webpieces-disable ${rule} -- <why this contract is published like this>\\n` +\n ` The hatch is per-site and argued on purpose: the next reader needs the argument,\\n` +\n ` not just the suppression.`,\n );\n }\n return parts.join('\\n\\n');\n}\n\n/**\n * `api-rules-for-openapi`: a contract that cannot produce an OpenAPI document.\n *\n * Fatal because the failure it prevents is DOCUMENT-WIDE and arrives late. One field with no shape\n * fails the whole document, so no partner can generate a client for ANY operation of that API — six\n * of 98 endpoints in a measured upstream repo, from exactly two root causes, and five of those six\n * were one field. The author who wrote that field had no way to know, because nothing looked at the\n * contract until somebody added `@ApiType` months later.\n *\n * Its verdict is the GENERATOR'S: the scan drives `ApiDocExtractor` rather than restating what is\n * expressible, so a contract that passes this is one where adding `@ApiType` then generates.\n */\nexport class ApiRulesForOpenApiError extends Error {\n constructor(public readonly findings: ApiRuleFindings) {\n super(\n apiRuleMessage(\n OPENAPI_RULE,\n '@ApiPath contract defect(s) would block the OpenAPI document',\n findings,\n ` An OpenAPI failure is DOCUMENT-WIDE: one field with no shape blocks client generation\\n` +\n ` for every operation of that API, not just the one that declares it.`,\n ),\n );\n this.name = 'ApiRulesForOpenApiError';\n }\n}\n\n/**\n * `api-rules-for-mcp`: a `@WpMcpTool` that could not be served.\n *\n * Every one of these is something `McpToolRegistry` refuses to BOOT on — a tool it cannot find a\n * schema for is a tool whose schema nobody checked — so the choice is only WHERE it is discovered.\n * Here it names the method; at boot it names a process that will not start, in an environment where\n * nobody is editing the contract.\n *\n * Separate from the OpenAPI error because the blast radii are different: this blocks ONE tool, where\n * an OpenAPI defect blocks a whole document. A team publishing a partner API and no tools runs the\n * first rule and not this one, which is only possible if they are two rules.\n */\nexport class ApiRulesForMcpError extends Error {\n constructor(\n public readonly findings: ApiRuleFindings,\n /**\n * The `@InvalidEndpointForMcp` endpoints, appended to the message.\n *\n * A red run is exactly when somebody is looking at this output, so it is also when the\n * PERMANENT exclusions are most worth restating: the next question after \"why is this tool\n * blocked\" is \"which endpoints did we already decide can never be tools, and why\".\n */\n public readonly exclusions: readonly McpExclusion[] = [],\n ) {\n super(\n appendExclusions(\n apiRuleMessage(\n MCP_RULE,\n '@WpMcpTool declaration(s) could not be served as MCP tools',\n findings,\n ` Each of these makes McpToolRegistry REFUSE TO BOOT: a registered tool the build never\\n` +\n ` published is a tool whose schema nobody checked. Caught here, it names the method;\\n` +\n ` caught at boot, it names a process that will not start.`,\n ),\n exclusions,\n ),\n );\n this.name = 'ApiRulesForMcpError';\n }\n}\n\n/**\n * Loud, actionable report for contracts the scan could not map to source. Callers print this\n * instead of emitting a green graph that is quietly missing relations. Not fatal: a contract\n * from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnclassifiedApiDep\nexport function describeUnresolvedApiCalls(calls: UnresolvedApiCall[]): string {\n const lines = [\n `⚠️ ${calls.length} API contract(s) resolved to a declaration file with no matching workspace source.`,\n ` Decorators (@ApiPath) are ERASED in .d.ts output, so these relations are MISSING from the graph:`,\n ];\n for (const call of calls) {\n lines.push(\n ` • ${call.api} at ${call.at} (${call.project}) → resolved to ${call.declaredIn}`,\n );\n }\n lines.push(\n ` If the api-lib IS in this workspace, add a tsconfig.base.json 'paths' entry mapping it to its`,\n ` src/index.ts, or confirm its project root is registered. If it is a published external package,`,\n ` this relation cannot be derived and the graph edge will not appear.`,\n );\n return lines.join('\\n');\n}\n\n/** One line per offender, in the shape every error in this file uses. */\n// webpieces-disable no-function-outside-class -- pure formatter shared by the one error below\nfunction rootUnionLines(found: readonly RootUnionApiType[]): string {\n return found\n .map(\n (one: RootUnionApiType) =>\n ` • ${one.api}.${one.method} — ${one.side} type '${one.typeName}' at ${one.at}`,\n )\n .join('\\n');\n}\n\n/**\n * `no-root-union-api-type`: a request or response type that IS a union, on ANY `@ApiPath` contract.\n *\n * Fatal, and fatal EARLY, because the failure it prevents is discovered by somebody else at runtime\n * and does not look like this contract's fault: a top-level `oneOf` is rejected by both the OpenAI\n * and the Anthropic function-calling APIs, a server sends its whole tool list on every request, and\n * so ONE such tool makes EVERY request 400. The user's report is \"all the other tools broke\".\n *\n * The second list is reasonless disables. A disable for this rule must carry an argument somebody\n * wrote down: the escape hatch is deliberately per-site and never a blanket switch, because the\n * blast radius is a whole session rather than one call.\n */\nexport class RootUnionApiTypeError extends Error {\n constructor(public readonly findings: RootUnionFindings) {\n super(RootUnionApiTypeError.render(findings));\n this.name = 'RootUnionApiTypeError';\n }\n\n // webpieces-disable no-function-outside-class, max-lines-new-methods -- private static renderer of this class, and splitting one message hides what a reader actually sees\n private static render(findings: RootUnionFindings): string {\n const parts: string[] = [];\n if (findings.violations.length > 0) {\n parts.push(\n `${findings.violations.length} @ApiPath method(s) declare a request or response type that IS a union:\\n` +\n rootUnionLines(findings.violations) +\n `\\n A union at the TOP LEVEL of a tool's parameter schema is rejected by BOTH the OpenAI and\\n` +\n ` the Anthropic function-calling APIs. A server sends its WHOLE tool list on every request, so\\n` +\n ` ONE of these makes EVERY request 400 and the entire client session unusable — not just that\\n` +\n ` tool. Nested composition, INSIDE a property, is fine and publishes as oneOf.\\n` +\n ` Fix it by wrapping the union in a property of an object:\\n` +\n ` export interface MoveWindowRequest { window: ScheduledWindow | AsapWindow }\\n` +\n ` This runs on EVERY @ApiPath contract, with or without @ApiType: @ApiType is a publishing\\n` +\n ` decision added later, so a shape that is not expressible must fail on the first line rather\\n` +\n ` than on the day somebody annotates it.\\n` +\n ` Last resort, per site: // webpieces-disable ${ROOT_UNION_RULE} -- <reason>`,\n );\n }\n if (findings.reasonlessDisables.length > 0) {\n parts.push(\n `${findings.reasonlessDisables.length} disable(s) of ${ROOT_UNION_RULE} give NO reason:\\n` +\n rootUnionLines(findings.reasonlessDisables) +\n `\\n The reason is MANDATORY — a reasonless disable is itself a violation. Write it as\\n` +\n ` // webpieces-disable ${ROOT_UNION_RULE} -- <why this root union is deliberate>\\n` +\n ` The hatch is per-site and argued on purpose: this defect costs a whole client session, so\\n` +\n ` the next reader needs the argument, not just the suppression.`,\n );\n }\n return parts.join('\\n\\n');\n }\n}\n\n/** Endpoints missing the operation declaration that drives retry safety and MCP annotations. */\nexport class UndeclaredEndpointOperationError extends Error {\n constructor(public readonly endpoints: readonly UndeclaredEndpointOperation[]) {\n super(\n `${endpoints.length} @Endpoint(s) do not declare a readable operation:\\n` +\n endpoints\n .map(\n (e: UndeclaredEndpointOperation) =>\n ` • ${e.api}.${e.method} — ${e.argument} at ${e.at}`,\n )\n .join('\\n') +\n `\\n operation is REQUIRED because it controls retry safety and generated MCP hints.\\n` +\n ` Pass exactly one of READ, WRITE_IDEMPOTENT, or WRITE as the third argument.\\n` +\n ` Do not infer operation semantics from the HTTP verb.`,\n );\n this.name = 'UndeclaredEndpointOperationError';\n }\n}\n\n/**\n * A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the\n * entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.\n */\nexport class MissingBasePathError extends Error {\n constructor(public readonly contracts: readonly string[]) {\n super(\n `${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\\n` +\n contracts.map((c: string) => ` • ${c}`).join('\\n') +\n `\\n basePath is REQUIRED in every api contract — an entry without it makes every consumer\\n` +\n ` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\\n` +\n ` or move the constant into the same module as the contract class.`,\n );\n this.name = 'MissingBasePathError';\n }\n}\n\n/**\n * `@Endpoint` paths the scan could not read. Fatal for the same reason MissingBasePathError is: the\n * two arguments are the two halves of ONE url. An http client builds its request as\n * `basePath + path`, so a contract shipped without a method's path is missing routing information,\n * and the consumer computes a confidently wrong URL. Skipping the method instead was worse still —\n * a class whose every path was an unreadable constant lost every method and vanished from the graph.\n */\nexport class UnresolvedEndpointPathError extends Error {\n constructor(public readonly paths: readonly UnresolvedEndpointPath[]) {\n super(\n `${paths.length} @Endpoint path(s) could not be read as a string:\\n` +\n paths\n .map(\n (p: UnresolvedEndpointPath) =>\n ` • ${p.api}.${p.method} — @Endpoint(${p.argument}, ...) at ${p.at}`,\n )\n .join('\\n') +\n `\\n path is REQUIRED in every api contract — every consumer builds its request URL as\\n` +\n ` basePath + path, so an unreadable path is MISSING ROUTING, not cosmetic metadata,\\n` +\n ` and a class whose every path is unreadable drops out of the graph entirely.\\n` +\n ` Inline the @Endpoint string literal, or move the constant into the SAME module as\\n` +\n ` the contract class — a same-module const IS resolved, one imported from another\\n` +\n ` module is NOT (this scan is parser-only by design: module resolution can land on a\\n` +\n ` decorator-erased .d.ts).`,\n );\n this.name = 'UnresolvedEndpointPathError';\n }\n}\n\n/**\n * `external` endpoints whose CALLER the scan could not read. Fatal, like the two above, because the\n * alternative is a diagram that lies by omission: the inbound box exists solely to name the system\n * calling us from outside, and with nothing to name it falls back to restating our own contract\n * name — which the reader already sees on the service box the arrow points at.\n *\n * `@Endpoint`'s TS overloads make `calledBy` a compile error to omit, so a scan reaching here saw a\n * JS caller, an `as any`, a cross-module constant this parser-only pass cannot fold, or a\n * `callerKind` that is not one of the declared kinds.\n */\nexport class UndeclaredExternalCallerError extends Error {\n constructor(public readonly callers: readonly UndeclaredExternalCaller[]) {\n super(\n `${callers.length} 'external' @Endpoint(s) do not declare WHO calls them:\\n` +\n callers\n .map(\n (c: UndeclaredExternalCaller) =>\n ` • ${c.api}.${c.method} — ${c.argument} at ${c.at}`,\n )\n .join('\\n') +\n `\\n An 'external' endpoint is driven by a system OUTSIDE this repo, and the runtime\\n` +\n ` architecture graph draws that system as an inbound box. Name it:\\n` +\n ` @Endpoint(POST, '/hook', WRITE, EXTERNAL, { calledBy: 'twilio' })\\n` +\n ` Add callerKind for anything that is not a vendor SaaS — database | cache | queue |\\n` +\n ` storage | saas | system — e.g. a GCP Pub/Sub push subscription:\\n` +\n ` @Endpoint(POST, '/push', WRITE, EXTERNAL, { calledBy: 'pubsub-push', callerKind: 'system' })\\n` +\n ` Use a string LITERAL or a SAME-module const: this scan is parser-only by design.`,\n );\n this.name = 'UndeclaredExternalCallerError';\n }\n}\n\n/**\n * Contract classes that declared `@Endpoint` methods and kept none of them. Fatal because the\n * alternative is the silent drop the api scan exists to close: buildApiContracts legitimately skips\n * a zero-method class (a vendor seam has no routes), and a class gutted by unreadable decorator\n * arguments used the very same exit — which is how a service lost two real Cloud Tasks queues and an\n * inbound webhook without a single line of output.\n */\nexport class EmptiedApiContractError extends Error {\n constructor(public readonly contracts: readonly EmptiedApiContract[]) {\n super(\n `${contracts.length} API contract class(es) declare @Endpoint methods but kept NONE of them:\\n` +\n contracts\n .map(\n (c: EmptiedApiContract) =>\n ` • ${c.api} — ${c.declared} @Endpoint method(s) declared, 0 usable, at ${c.at}`,\n )\n .join('\\n') +\n `\\n A contract with zero usable methods is DROPPED from the api contracts, so the class,\\n` +\n ` its queues and its triggers disappear from the architecture graph with no error.\\n` +\n ` Every @Endpoint argument must be readable: the path as a string literal or a\\n` +\n ` SAME-module const, the kind as 'rpc' | 'cloudtasks' | 'cron' | 'external', and\\n` +\n ` the options must declare operation. Fix the arguments above, or remove the decorators if the\\n` +\n ` class is genuinely not routed.`,\n );\n this.name = 'EmptiedApiContractError';\n }\n}\n"]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of "is this contract
|
|
3
|
+
* publishable", on EVERY `@ApiPath` class in the workspace, `@ApiType` or not.
|
|
4
|
+
*
|
|
5
|
+
* ## The acceptance contract, and why this file drives the generator instead of copying it
|
|
6
|
+
*
|
|
7
|
+
* The rules exist so that ADDING `@ApiType(...)` (and `@WpMcpTool`) to a contract that passes them
|
|
8
|
+
* always works. That is only true if "expressible" has exactly ONE definition, so this scan runs the
|
|
9
|
+
* generator's own code — `ApiDocExtractor` / `TypeResolver` from `@webpieces/api-doc-model` for the
|
|
10
|
+
* OpenAPI half, and `McpSchemaRenderer` tool-by-tool for the MCP half. A second implementation of
|
|
11
|
+
* "what can be published" would drift from the first on the release that improved either one, and
|
|
12
|
+
* the drift would be silent: the rules would stay green while generation started failing. The two
|
|
13
|
+
* packages ship on the same release train, so the dependency is in lockstep by construction.
|
|
14
|
+
*
|
|
15
|
+
* Two things here are STRICTER than the generator, deliberately, and both are publishing rules
|
|
16
|
+
* rather than expressibility ones (being stricter cannot break the acceptance contract — it can only
|
|
17
|
+
* refuse something that would have generated):
|
|
18
|
+
*
|
|
19
|
+
* - an `unknown` VALUE TYPE anywhere (`Record<string, unknown>`, `unknown[]`, a bare `unknown`
|
|
20
|
+
* field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON
|
|
21
|
+
* Schema means "anything" — a partner-facing field with no shape, which is the defect the
|
|
22
|
+
* unmapped guard exists for, arriving through a door the guard does not watch.
|
|
23
|
+
* - an RPC whose response is `void`. Fire-and-forget is the CONTRACT of a `cloudtasks` or `cron`
|
|
24
|
+
* endpoint and is allowed there; an RPC that answers nothing can never gain a field without a
|
|
25
|
+
* breaking change, where a named empty response object grows additively forever.
|
|
26
|
+
*
|
|
27
|
+
* ## Why it lives in the rules engine and not in the doc parser
|
|
28
|
+
*
|
|
29
|
+
* `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`
|
|
30
|
+
* is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which
|
|
31
|
+
* had already opted in would let a team discover, six months afterwards, that the type was never
|
|
32
|
+
* expressible, by which time it is in partners' generated clients. Every check below runs on every
|
|
33
|
+
* contract in the workspace.
|
|
34
|
+
*
|
|
35
|
+
* ## Root-level unions are NOT re-checked here
|
|
36
|
+
*
|
|
37
|
+
* `no-root-union-api-type` (#1009) already refuses them, workspace-wide, with its own config key and
|
|
38
|
+
* its own per-site hatch. One implementation. The MCP half still reports one when it meets it,
|
|
39
|
+
* because `McpSchemaRenderer` refuses it as its own backstop and this scan reports whatever the
|
|
40
|
+
* renderer says — which is the correct division: the OpenAPI document publishes a root union
|
|
41
|
+
* perfectly well, and only a tool schema cannot carry one.
|
|
42
|
+
*/
|
|
43
|
+
import { ProjectInfo } from '../project-info';
|
|
44
|
+
import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
|
|
45
|
+
/**
|
|
46
|
+
* Walks every project's `src`, extracts every `@ApiPath` contract with the generator's own
|
|
47
|
+
* extractor, and judges the result against the two rules.
|
|
48
|
+
*
|
|
49
|
+
* ONE `ts.Program` over every contract file in the workspace, because a DTO a contract reaches
|
|
50
|
+
* routinely lives in another project and the checker has to be able to follow the import — the same
|
|
51
|
+
* reason the repo sweep in `@webpieces/api-doc-model`'s own spec builds one program rather than one
|
|
52
|
+
* per file.
|
|
53
|
+
*/
|
|
54
|
+
export declare class ApiDocRulesScan {
|
|
55
|
+
private readonly workspaceRoot;
|
|
56
|
+
private readonly projectInfos;
|
|
57
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json — see `defaultRules`. */
|
|
58
|
+
private readonly openApiRule;
|
|
59
|
+
private readonly mcpRule;
|
|
60
|
+
/**
|
|
61
|
+
* Every `@InvalidEndpointForMcp` endpoint met on a file the MCP rule applies to.
|
|
62
|
+
*
|
|
63
|
+
* Collected even on a run with no findings at all, because restating them IS the feature: the
|
|
64
|
+
* alternative considered in #1014 was a one-off warning when somebody adds one, and a warning
|
|
65
|
+
* printed once at the moment of the decision is read by the one person who already knows.
|
|
66
|
+
*/
|
|
67
|
+
private readonly exclusions;
|
|
68
|
+
constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>,
|
|
69
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json — see `defaultRules`. */
|
|
70
|
+
openApiRule?: ApiDocRule, mcpRule?: ApiDocRule);
|
|
71
|
+
run(): ApiDocRulesFindings;
|
|
72
|
+
/** Every contract in one file, or the ONE refusal that stopped the file being read at all. */
|
|
73
|
+
private judgeFile;
|
|
74
|
+
/**
|
|
75
|
+
* An extraction that REFUSED. Reported under the shared list because it stops BOTH documents:
|
|
76
|
+
* `@Endpoint` arguments that cannot be constant-folded, a bound on a non-numeric field, and the
|
|
77
|
+
* `@ApiType(..., MCP)` ⇔ `@WpMcpTool` biconditional all fail here, before a model exists.
|
|
78
|
+
*/
|
|
79
|
+
private reportExtractionFailure;
|
|
80
|
+
/** ONE contract: the shared OpenAPI checks, then the MCP-only ones. */
|
|
81
|
+
private judgeModel;
|
|
82
|
+
/** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */
|
|
83
|
+
private judgeUnmapped;
|
|
84
|
+
/** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */
|
|
85
|
+
private judgeUnknownValues;
|
|
86
|
+
/**
|
|
87
|
+
* An RPC must NAME a response DTO, even an empty one.
|
|
88
|
+
*
|
|
89
|
+
* `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing
|
|
90
|
+
* to shape — and is allowed. On an RPC it is a one-way door: a `void` response can never gain a
|
|
91
|
+
* field without breaking every generated client, where `{}` grows additively forever. This is a
|
|
92
|
+
* contract-EVOLUTION rule, which is why it is here and not in the MCP half: it is worth having on
|
|
93
|
+
* an RPC that never becomes a tool.
|
|
94
|
+
*/
|
|
95
|
+
private judgeRpcResponses;
|
|
96
|
+
/** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */
|
|
97
|
+
private judgeTools;
|
|
98
|
+
/** Every non-test `.ts` under a project's `src` whose text DECLARES an `@ApiPath` contract. */
|
|
99
|
+
private contractFiles;
|
|
100
|
+
/**
|
|
101
|
+
* `tsconfig.base.json`'s options when the workspace has one, so an `@webpieces/*` import in a
|
|
102
|
+
* contract RESOLVES and the checker can follow a DTO into another project. Without that the
|
|
103
|
+
* resolver reports every cross-project type as unmapped, which would be a rule failing on its
|
|
104
|
+
* own inability to read rather than on anything the author wrote.
|
|
105
|
+
*/
|
|
106
|
+
private compilerOptions;
|
|
107
|
+
}
|