@webpieces/nx-webpieces-rules 0.4.957 → 0.4.959
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 -7
- package/src/lib/api-usage/api-ast.d.ts +11 -3
- package/src/lib/api-usage/api-ast.js +109 -14
- package/src/lib/api-usage/api-ast.js.map +1 -1
- package/src/lib/api-usage/api-contract-errors.d.ts +6 -1
- package/src/lib/api-usage/api-contract-errors.js +24 -6
- package/src/lib/api-usage/api-contract-errors.js.map +1 -1
- package/src/lib/api-usage/api-contract-evidence.js +1 -0
- package/src/lib/api-usage/api-contract-evidence.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules-scan.d.ts +8 -8
- package/src/lib/api-usage/api-doc-rules-scan.js +8 -8
- package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules-verdicts.d.ts +9 -7
- package/src/lib/api-usage/api-doc-rules-verdicts.js +15 -14
- package/src/lib/api-usage/api-doc-rules-verdicts.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +33 -8
- package/src/lib/api-usage/api-relations.js +32 -1
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +3 -1
- package/src/lib/api-usage/api-scanner.js +7 -2
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/runtime-graph-model.d.ts +2 -2
- package/src/lib/runtime-graph-model.js.map +1 -1
- package/src/lib/runtime-graph.js +6 -4
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-wiring/service-targets.js +6 -0
- package/src/lib/runtime-wiring/service-targets.js.map +1 -1
- package/src/lib/runtime-wiring/source-extractor.d.ts +5 -0
- package/src/lib/runtime-wiring/source-extractor.js +20 -0
- package/src/lib/runtime-wiring/source-extractor.js.map +1 -1
- package/src/lib/runtime-wiring/source-values.d.ts +6 -1
- package/src/lib/runtime-wiring/source-values.js +21 -0
- package/src/lib/runtime-wiring/source-values.js.map +1 -1
|
@@ -12,7 +12,7 @@
|
|
|
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 = exports.ApiRulesForMcpError = exports.ApiRulesForOpenApiError = void 0;
|
|
15
|
+
exports.EmptiedApiContractError = exports.UndeclaredExternalCallerError = exports.UnresolvedEndpointPathError = exports.MissingBasePathError = exports.UndeclaredEndpointOperationError = exports.VoidResponseMismatchError = exports.RootUnionApiTypeError = exports.ApiRulesForMcpError = exports.ApiRulesForOpenApiError = void 0;
|
|
16
16
|
exports.describeMcpExclusions = describeMcpExclusions;
|
|
17
17
|
exports.describeUnresolvedApiCalls = describeUnresolvedApiCalls;
|
|
18
18
|
const root_union_scan_1 = require("./root-union-scan");
|
|
@@ -215,6 +215,24 @@ class RootUnionApiTypeError extends Error {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
exports.RootUnionApiTypeError = RootUnionApiTypeError;
|
|
218
|
+
/** Endpoints whose `responseBody: NONE` and `Promise<void>` signature disagree (#1339). */
|
|
219
|
+
class VoidResponseMismatchError extends Error {
|
|
220
|
+
endpoints;
|
|
221
|
+
constructor(endpoints) {
|
|
222
|
+
super(`${endpoints.length} @Endpoint(s) disagree with their return type about a response body:\n` +
|
|
223
|
+
endpoints
|
|
224
|
+
.map((e) => e.declaresNone
|
|
225
|
+
? ` • ${e.api}.${e.method} declares responseBody: NONE but does not return Promise<void> — at ${e.at}`
|
|
226
|
+
: ` • ${e.api}.${e.method} returns Promise<void> but does not declare responseBody: NONE — at ${e.at}`)
|
|
227
|
+
.join('\n') +
|
|
228
|
+
`\n A Promise<void> endpoint answers 204 with 0 bytes, and the runtime cannot see 'void'\n` +
|
|
229
|
+
` (design:returntype is just Promise). Add { responseBody: NONE } to @Endpoint's options on\n` +
|
|
230
|
+
` every Promise<void> method, and only on those.`);
|
|
231
|
+
this.endpoints = endpoints;
|
|
232
|
+
this.name = 'VoidResponseMismatchError';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
exports.VoidResponseMismatchError = VoidResponseMismatchError;
|
|
218
236
|
/** Endpoints missing the operation declaration that drives retry safety and MCP annotations. */
|
|
219
237
|
class UndeclaredEndpointOperationError extends Error {
|
|
220
238
|
endpoints;
|
|
@@ -287,16 +305,16 @@ exports.UnresolvedEndpointPathError = UnresolvedEndpointPathError;
|
|
|
287
305
|
class UndeclaredExternalCallerError extends Error {
|
|
288
306
|
callers;
|
|
289
307
|
constructor(callers) {
|
|
290
|
-
super(`${callers.length} 'external' @Endpoint(s) do not declare WHO calls them:\n` +
|
|
308
|
+
super(`${callers.length} 'external-inbound' @Endpoint(s) do not declare WHO calls them:\n` +
|
|
291
309
|
callers
|
|
292
310
|
.map((c) => ` • ${c.api}.${c.method} — ${c.argument} at ${c.at}`)
|
|
293
311
|
.join('\n') +
|
|
294
|
-
`\n An
|
|
312
|
+
`\n An EXTERNAL_INBOUND endpoint is driven by a system OUTSIDE this repo, and the runtime\n` +
|
|
295
313
|
` architecture graph draws that system as an inbound box. Name it:\n` +
|
|
296
|
-
` @Endpoint(POST, '/hook', WRITE,
|
|
314
|
+
` @Endpoint(POST, '/hook', WRITE, EXTERNAL_INBOUND, { calledBy: 'twilio' })\n` +
|
|
297
315
|
` Add callerKind for anything that is not a vendor SaaS — database | cache | queue |\n` +
|
|
298
316
|
` storage | saas | system — e.g. a GCP Pub/Sub push subscription:\n` +
|
|
299
|
-
` @Endpoint(POST, '/push', WRITE,
|
|
317
|
+
` @Endpoint(POST, '/push', WRITE, EXTERNAL_INBOUND, { calledBy: 'pubsub-push', callerKind: 'system' })\n` +
|
|
300
318
|
` Use a string LITERAL or a SAME-module const: this scan is parser-only by design.`);
|
|
301
319
|
this.callers = callers;
|
|
302
320
|
this.name = 'UndeclaredExternalCallerError';
|
|
@@ -320,7 +338,7 @@ class EmptiedApiContractError extends Error {
|
|
|
320
338
|
`\n A contract with zero usable methods is DROPPED from the api contracts, so the class,\n` +
|
|
321
339
|
` its queues and its triggers disappear from the architecture graph with no error.\n` +
|
|
322
340
|
` Every @Endpoint argument must be readable: the path as a string literal or a\n` +
|
|
323
|
-
` SAME-module const, the kind as
|
|
341
|
+
` SAME-module const, the kind as RPC | CLOUDTASKS | CRON | EXTERNAL_INBOUND | EXTERNAL_OUTBOUND, and\n` +
|
|
324
342
|
` the options must declare operation. Fix the arguments above, or remove the decorators if the\n` +
|
|
325
343
|
` class is genuinely not routed.`);
|
|
326
344
|
this.contracts = contracts;
|
|
@@ -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';\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"]}
|
|
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 VoidResponseMismatch,\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 whose `responseBody: NONE` and `Promise<void>` signature disagree (#1339). */\nexport class VoidResponseMismatchError extends Error {\n constructor(public readonly endpoints: readonly VoidResponseMismatch[]) {\n super(\n `${endpoints.length} @Endpoint(s) disagree with their return type about a response body:\\n` +\n endpoints\n .map((e: VoidResponseMismatch) =>\n e.declaresNone\n ? ` • ${e.api}.${e.method} declares responseBody: NONE but does not return Promise<void> — at ${e.at}`\n : ` • ${e.api}.${e.method} returns Promise<void> but does not declare responseBody: NONE — at ${e.at}`,\n )\n .join('\\n') +\n `\\n A Promise<void> endpoint answers 204 with 0 bytes, and the runtime cannot see 'void'\\n` +\n ` (design:returntype is just Promise). Add { responseBody: NONE } to @Endpoint's options on\\n` +\n ` every Promise<void> method, and only on those.`,\n );\n this.name = 'VoidResponseMismatchError';\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-inbound' @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_INBOUND 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_INBOUND, { 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_INBOUND, { 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_INBOUND | EXTERNAL_OUTBOUND, 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"]}
|
|
@@ -30,6 +30,7 @@ class ApiContractEvidence {
|
|
|
30
30
|
emptiedApiContracts: diagnostics.emptiedContracts(),
|
|
31
31
|
undeclaredExternalCallers: diagnostics.undeclaredExternalCallers(),
|
|
32
32
|
undeclaredEndpointOperations: diagnostics.undeclaredEndpointOperations(),
|
|
33
|
+
voidResponseMismatches: diagnostics.voidResponseMismatches(),
|
|
33
34
|
rootUnions: new root_union_scan_1.RootUnionScan(workspaceRoot, infos, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot)).run(),
|
|
34
35
|
apiDocRules: new api_doc_rules_scan_1.ApiDocRulesScan(workspaceRoot, infos, api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.OPENAPI_RULE), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.MCP_RULE), wire_closure_1.WireClosureRule.fromConfig(workspaceRoot)).run(),
|
|
35
36
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-contract-evidence.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-evidence.ts"],"names":[],"mappings":";;;;AAAA,mEAA8D;AAC9D,yDAA0D;AAC1D,+CAAyB;AACzB,mDAA6B;AAC7B,+CAAyE;AACzE,uCAAoD;AACpD,uDAAiE;AACjE,mDAAqE;AACrE,6DAAuD;AACvD,iDAAiD;AAIjD,qGAAqG;AACrG,MAAa,mBAAmB;IAC5B,OAAO,CACH,aAAqB,EACrB,KAA+B,EAC/B,gBAAmC;QAEnC,MAAM,WAAW,GAAG,IAAI,iCAAuB,CAAC,aAAa,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,IAAI,mCAAqB,CACnC,aAAa,EACb,KAAK,EACL,gBAAgB,EAChB,WAAW,CACd,CAAC,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAkB;YAC1B,kBAAkB,EAAE,IAAI,GAAG,EAAE;YAC7B,cAAc,EAAE,KAAK,CAAC,MAAM;YAC5B,QAAQ,EAAE,KAAK,CAAC,MAAM;YACtB,eAAe,EAAE,IAAI,GAAG,CACpB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;iBACd,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;iBAC3E,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAChC;YACD,kBAAkB,EAAE,EAAE;YACtB,uBAAuB,EAAE,WAAW,CAAC,GAAG,EAAE;YAC1C,uBAAuB,EAAE,WAAW,CAAC,uBAAuB,EAAE;YAC9D,mBAAmB,EAAE,WAAW,CAAC,gBAAgB,EAAE;YACnD,yBAAyB,EAAE,WAAW,CAAC,yBAAyB,EAAE;YAClE,4BAA4B,EAAE,WAAW,CAAC,4BAA4B,EAAE;YACxE,UAAU,EAAE,IAAI,+BAAa,CACzB,aAAa,EACb,KAAK,EACL,+BAAa,CAAC,UAAU,CAAC,aAAa,CAAC,CAC1C,CAAC,GAAG,EAAE;YACP,WAAW,EAAE,IAAI,oCAAe,CAC5B,aAAa,EACb,KAAK,EACL,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,4BAAY,CAAC,EAClD,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,wBAAQ,CAAC,EAC9C,8BAAe,CAAC,UAAU,CAAC,aAAa,CAAC,CAC5C,CAAC,GAAG,EAAE;SACV,CAAC;QACF,IAAI,2CAAmB,EAAE,CAAC,MAAM,CAC5B,aAAa,EACb,KAAK,EACL,IAAA,+BAAiB,EAAC,MAAM,CAAC,EACzB,IAAA,uCAAoB,EAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAC5C,CAAC,CAAC,mEAAmE;QACtE,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;
|
|
1
|
+
{"version":3,"file":"api-contract-evidence.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-evidence.ts"],"names":[],"mappings":";;;;AAAA,mEAA8D;AAC9D,yDAA0D;AAC1D,+CAAyB;AACzB,mDAA6B;AAC7B,+CAAyE;AACzE,uCAAoD;AACpD,uDAAiE;AACjE,mDAAqE;AACrE,6DAAuD;AACvD,iDAAiD;AAIjD,qGAAqG;AACrG,MAAa,mBAAmB;IAC5B,OAAO,CACH,aAAqB,EACrB,KAA+B,EAC/B,gBAAmC;QAEnC,MAAM,WAAW,GAAG,IAAI,iCAAuB,CAAC,aAAa,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,IAAI,mCAAqB,CACnC,aAAa,EACb,KAAK,EACL,gBAAgB,EAChB,WAAW,CACd,CAAC,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAkB;YAC1B,kBAAkB,EAAE,IAAI,GAAG,EAAE;YAC7B,cAAc,EAAE,KAAK,CAAC,MAAM;YAC5B,QAAQ,EAAE,KAAK,CAAC,MAAM;YACtB,eAAe,EAAE,IAAI,GAAG,CACpB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;iBACd,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;iBAC3E,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAChC;YACD,kBAAkB,EAAE,EAAE;YACtB,uBAAuB,EAAE,WAAW,CAAC,GAAG,EAAE;YAC1C,uBAAuB,EAAE,WAAW,CAAC,uBAAuB,EAAE;YAC9D,mBAAmB,EAAE,WAAW,CAAC,gBAAgB,EAAE;YACnD,yBAAyB,EAAE,WAAW,CAAC,yBAAyB,EAAE;YAClE,4BAA4B,EAAE,WAAW,CAAC,4BAA4B,EAAE;YACxE,sBAAsB,EAAE,WAAW,CAAC,sBAAsB,EAAE;YAC5D,UAAU,EAAE,IAAI,+BAAa,CACzB,aAAa,EACb,KAAK,EACL,+BAAa,CAAC,UAAU,CAAC,aAAa,CAAC,CAC1C,CAAC,GAAG,EAAE;YACP,WAAW,EAAE,IAAI,oCAAe,CAC5B,aAAa,EACb,KAAK,EACL,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,4BAAY,CAAC,EAClD,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,wBAAQ,CAAC,EAC9C,8BAAe,CAAC,UAAU,CAAC,aAAa,CAAC,CAC5C,CAAC,GAAG,EAAE;SACV,CAAC;QACF,IAAI,2CAAmB,EAAE,CAAC,MAAM,CAC5B,aAAa,EACb,KAAK,EACL,IAAA,+BAAiB,EAAC,MAAM,CAAC,EACzB,IAAA,uCAAoB,EAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAC5C,CAAC,CAAC,mEAAmE;QACtE,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAlDD,kDAkDC","sourcesContent":["import { ApiContractApproval } from './api-contract-approval';\nimport { buildExternalSystems } from './external-systems';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ApiSourceIndexBuilder, buildApiContracts } from './api-scanner';\nimport { DecoratorArgDiagnostics } from './api-ast';\nimport { RootUnionRule, RootUnionScan } from './root-union-scan';\nimport { ApiDocRule, MCP_RULE, OPENAPI_RULE } from './api-doc-rules';\nimport { ApiDocRulesScan } from './api-doc-rules-scan';\nimport { WireClosureRule } from './wire-closure';\nimport type { ApiScanResult } from './api-scanner';\nimport type { ProjectInfo } from '../project-info';\n\n/** Build evidence for API/DTO correctness. No runtime relationship scan or project compiler loop. */\nexport class ApiContractEvidence {\n collect(\n workspaceRoot: string,\n infos: Map<string, ProjectInfo>,\n externalApiPaths: readonly string[],\n ): ApiScanResult {\n const diagnostics = new DecoratorArgDiagnostics(workspaceRoot);\n const index = new ApiSourceIndexBuilder(\n workspaceRoot,\n infos,\n externalApiPaths,\n diagnostics,\n ).build();\n const result: ApiScanResult = {\n relationsByProject: new Map(),\n apiLibProjects: index.owners,\n apiIndex: index.byName,\n scannedProjects: new Set(\n [...infos.values()]\n .filter((info) => fs.existsSync(path.join(workspaceRoot, info.root, 'src')))\n .map((info) => info.name),\n ),\n unresolvedApiCalls: [],\n nonLiteralDecoratorArgs: diagnostics.all(),\n unresolvedEndpointPaths: diagnostics.unresolvedEndpointPaths(),\n emptiedApiContracts: diagnostics.emptiedContracts(),\n undeclaredExternalCallers: diagnostics.undeclaredExternalCallers(),\n undeclaredEndpointOperations: diagnostics.undeclaredEndpointOperations(),\n voidResponseMismatches: diagnostics.voidResponseMismatches(),\n rootUnions: new RootUnionScan(\n workspaceRoot,\n infos,\n RootUnionRule.fromConfig(workspaceRoot),\n ).run(),\n apiDocRules: new ApiDocRulesScan(\n workspaceRoot,\n infos,\n ApiDocRule.fromConfig(workspaceRoot, OPENAPI_RULE),\n ApiDocRule.fromConfig(workspaceRoot, MCP_RULE),\n WireClosureRule.fromConfig(workspaceRoot),\n ).run(),\n };\n new ApiContractApproval().verify(\n workspaceRoot,\n infos,\n buildApiContracts(result),\n buildExternalSystems(index.byName, infos),\n ); // Preserves the previous generator's API/DTO correctness failures.\n return result;\n }\n}\n"]}
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
* field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON
|
|
26
26
|
* Schema means "anything" — a partner-facing field with no shape, which is the defect the
|
|
27
27
|
* unmapped guard exists for, arriving through a door the guard does not watch.
|
|
28
|
-
* - an `rpc`
|
|
29
|
-
* `cloudtasks` or `cron` endpoint and is allowed there
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* - an `rpc` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a
|
|
29
|
+
* `cloudtasks` or `cron` endpoint and is allowed there, and an `external-inbound` /
|
|
30
|
+
* `external-outbound` endpoint's wire is the outside system's (#1339); an rpc somebody WAITS on
|
|
31
|
+
* that answers nothing can never gain a field without a breaking change, where a named empty
|
|
32
|
+
* response object grows additively forever (#1017).
|
|
33
33
|
*
|
|
34
34
|
* ## Why it lives in the rules engine and not in the doc parser
|
|
35
35
|
*
|
|
@@ -110,9 +110,9 @@ export declare class ApiDocRulesScan {
|
|
|
110
110
|
* An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.
|
|
111
111
|
*
|
|
112
112
|
* `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing
|
|
113
|
-
* to shape — and is allowed
|
|
114
|
-
*
|
|
115
|
-
* can never gain a field without breaking every generated client where `{}` grows additively
|
|
113
|
+
* to shape — and is allowed, as it is on `external-inbound` / `external-outbound`, whose wire an
|
|
114
|
+
* outside system owns (#1339). On an `rpc` it is a one-way door: a synchronous request/response
|
|
115
|
+
* whose caller reads what comes back, and a `void` response can never gain a field without breaking every generated client where `{}` grows additively
|
|
116
116
|
* forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it
|
|
117
117
|
* is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).
|
|
118
118
|
*/
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
* field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON
|
|
27
27
|
* Schema means "anything" — a partner-facing field with no shape, which is the defect the
|
|
28
28
|
* unmapped guard exists for, arriving through a door the guard does not watch.
|
|
29
|
-
* - an `rpc`
|
|
30
|
-
* `cloudtasks` or `cron` endpoint and is allowed there
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* - an `rpc` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a
|
|
30
|
+
* `cloudtasks` or `cron` endpoint and is allowed there, and an `external-inbound` /
|
|
31
|
+
* `external-outbound` endpoint's wire is the outside system's (#1339); an rpc somebody WAITS on
|
|
32
|
+
* that answers nothing can never gain a field without a breaking change, where a named empty
|
|
33
|
+
* response object grows additively forever (#1017).
|
|
34
34
|
*
|
|
35
35
|
* ## Why it lives in the rules engine and not in the doc parser
|
|
36
36
|
*
|
|
@@ -303,9 +303,9 @@ class ApiDocRulesScan {
|
|
|
303
303
|
* An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.
|
|
304
304
|
*
|
|
305
305
|
* `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing
|
|
306
|
-
* to shape — and is allowed
|
|
307
|
-
*
|
|
308
|
-
* can never gain a field without breaking every generated client where `{}` grows additively
|
|
306
|
+
* to shape — and is allowed, as it is on `external-inbound` / `external-outbound`, whose wire an
|
|
307
|
+
* outside system owns (#1339). On an `rpc` it is a one-way door: a synchronous request/response
|
|
308
|
+
* whose caller reads what comes back, and a `void` response can never gain a field without breaking every generated client where `{}` grows additively
|
|
309
309
|
* forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it
|
|
310
310
|
* is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).
|
|
311
311
|
*/
|
|
@@ -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 * ## 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 OperationIds,\n OperationIdStyle,\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 = /@/;\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 // The scan asks only whether each tool RENDERS; it publishes no prose, so the operationId\n // spelling a `{@link}` would take (#1284) changes no verdict. The generator's own default.\n const renderer = new McpSchemaRenderer(\n model,\n new OperationIds(OperationIdStyle.CONTRACT_METHOD),\n );\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` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a\n * `cloudtasks` or `cron` endpoint and is allowed there, and an `external-inbound` /\n * `external-outbound` endpoint's wire is the outside system's (#1339); an rpc somebody WAITS on\n * that answers nothing can never gain a field without a breaking change, where a named empty\n * response object grows additively forever (#1017).\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 OperationIds,\n OperationIdStyle,\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 = /@/;\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, as it is on `external-inbound` / `external-outbound`, whose wire an\n * outside system owns (#1339). On an `rpc` it is a one-way door: a synchronous request/response\n * whose caller reads what comes back, and a `void` response 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 // The scan asks only whether each tool RENDERS; it publishes no prose, so the operationId\n // spelling a `{@link}` would take (#1284) changes no verdict. The generator's own default.\n const renderer = new McpSchemaRenderer(\n model,\n new OperationIds(OperationIdStyle.CONTRACT_METHOD),\n );\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"]}
|
|
@@ -21,14 +21,16 @@ export declare const RPC_KIND: EndpointKind;
|
|
|
21
21
|
/**
|
|
22
22
|
* The endpoint kinds whose response a caller WAITS FOR, and which therefore must name a DTO (#1017).
|
|
23
23
|
*
|
|
24
|
-
* `rpc`
|
|
25
|
-
* back, so there IS a body and it must be a named type that can gain a field later. `void` on
|
|
26
|
-
* is a one-way door — a `void` response can never grow without breaking every generated client,
|
|
27
|
-
* an empty DTO grows additively forever.
|
|
28
|
-
* unstated at the time; it is stated now.
|
|
24
|
+
* `rpc` is synchronous request/response between our own services: somebody posts and reads what
|
|
25
|
+
* comes back, so there IS a body and it must be a named type that can gain a field later. `void` on
|
|
26
|
+
* it is a one-way door — a `void` response can never grow without breaking every generated client,
|
|
27
|
+
* where an empty DTO grows additively forever.
|
|
29
28
|
*
|
|
30
29
|
* `cloudtasks` and `cron` are deliberately NOT here: delivery is fire-and-forget, nobody waits for a
|
|
31
|
-
* body, and `Promise<void>` is the CONTRACT rather than an omission.
|
|
30
|
+
* body, and `Promise<void>` is the CONTRACT rather than an omission. Neither are `external-inbound`
|
|
31
|
+
* and `external-outbound` (#1339): the OUTSIDE system owns that wire — a vendor's webhook expects a
|
|
32
|
+
* bare 2xx, and a vendor's API documents ops that answer nothing — so a `Promise<void>` there
|
|
33
|
+
* describes somebody else's contract rather than choosing ours.
|
|
32
34
|
*/
|
|
33
35
|
export declare const ANSWERING_KINDS: readonly EndpointKind[];
|
|
34
36
|
/** ONE reason a declaration is refused, in the two halves every refusal prints. */
|
|
@@ -59,7 +61,7 @@ export declare class ContractLines {
|
|
|
59
61
|
* failure mode a printed cure must not have.
|
|
60
62
|
*/
|
|
61
63
|
export declare function unknownValueCure(rule: string): string;
|
|
62
|
-
/** The one cure for a `void` endpoint that somebody is WAITING on (rpc
|
|
64
|
+
/** The one cure for a `void` endpoint that somebody is WAITING on (rpc). */
|
|
63
65
|
export declare const VOID_RPC_CURE: string;
|
|
64
66
|
/**
|
|
65
67
|
* Why the resolver could not map this type, said in the author's vocabulary.
|
|
@@ -31,16 +31,18 @@ exports.RPC_KIND = 'rpc';
|
|
|
31
31
|
/**
|
|
32
32
|
* The endpoint kinds whose response a caller WAITS FOR, and which therefore must name a DTO (#1017).
|
|
33
33
|
*
|
|
34
|
-
* `rpc`
|
|
35
|
-
* back, so there IS a body and it must be a named type that can gain a field later. `void` on
|
|
36
|
-
* is a one-way door — a `void` response can never grow without breaking every generated client,
|
|
37
|
-
* an empty DTO grows additively forever.
|
|
38
|
-
* unstated at the time; it is stated now.
|
|
34
|
+
* `rpc` is synchronous request/response between our own services: somebody posts and reads what
|
|
35
|
+
* comes back, so there IS a body and it must be a named type that can gain a field later. `void` on
|
|
36
|
+
* it is a one-way door — a `void` response can never grow without breaking every generated client,
|
|
37
|
+
* where an empty DTO grows additively forever.
|
|
39
38
|
*
|
|
40
39
|
* `cloudtasks` and `cron` are deliberately NOT here: delivery is fire-and-forget, nobody waits for a
|
|
41
|
-
* body, and `Promise<void>` is the CONTRACT rather than an omission.
|
|
40
|
+
* body, and `Promise<void>` is the CONTRACT rather than an omission. Neither are `external-inbound`
|
|
41
|
+
* and `external-outbound` (#1339): the OUTSIDE system owns that wire — a vendor's webhook expects a
|
|
42
|
+
* bare 2xx, and a vendor's API documents ops that answer nothing — so a `Promise<void>` there
|
|
43
|
+
* describes somebody else's contract rather than choosing ours.
|
|
42
44
|
*/
|
|
43
|
-
exports.ANSWERING_KINDS = ['rpc'
|
|
45
|
+
exports.ANSWERING_KINDS = ['rpc'];
|
|
44
46
|
/** The scalar keywords a MIXED SCALAR union is made of. */
|
|
45
47
|
const SCALAR_KEYWORDS = new Set([
|
|
46
48
|
ts.SyntaxKind.StringKeyword,
|
|
@@ -93,13 +95,12 @@ function unknownValueCure(rule) {
|
|
|
93
95
|
'An existing no-any-unknown disable does NOT count — that rule asked whether the code is ' +
|
|
94
96
|
'type-safe, which is a different question from whether a partner is handed a shapeless field.');
|
|
95
97
|
}
|
|
96
|
-
/** The one cure for a `void` endpoint that somebody is WAITING on (rpc
|
|
98
|
+
/** The one cure for a `void` endpoint that somebody is WAITING on (rpc). */
|
|
97
99
|
exports.VOID_RPC_CURE = 'Declare a named response DTO and return Promise<That>, even when it has no fields today: an ' +
|
|
98
100
|
'empty object grows additively forever, and a void response cannot gain a field without ' +
|
|
99
|
-
'breaking every generated client.
|
|
100
|
-
'
|
|
101
|
-
'the
|
|
102
|
-
'fire-and-forget is the contract.';
|
|
101
|
+
'breaking every generated client. Promise<void> stays legal on a cloudtasks, cron, ' +
|
|
102
|
+
'EXTERNAL_INBOUND or EXTERNAL_OUTBOUND endpoint, where fire-and-forget or an outside system ' +
|
|
103
|
+
'owns the contract.';
|
|
103
104
|
/**
|
|
104
105
|
* Why the resolver could not map this type, said in the author's vocabulary.
|
|
105
106
|
*
|
|
@@ -217,8 +218,8 @@ function toolFailures(endpoint, renderer, toolNames, model) {
|
|
|
217
218
|
'two @WpMcpTool declarations.'));
|
|
218
219
|
}
|
|
219
220
|
if (endpoint.kind !== exports.RPC_KIND) {
|
|
220
|
-
found.push(new Verdict(`an MCP tool is declared on a '${endpoint.kind}' endpoint`, 'Only an RPC endpoint can be a tool — a cloudtasks, cron or external endpoint is ' +
|
|
221
|
-
'driven by something other than a caller. Make it RPC, or drop the @WpMcpTool.'));
|
|
221
|
+
found.push(new Verdict(`an MCP tool is declared on a '${endpoint.kind}' endpoint`, 'Only an RPC endpoint can be a tool — a cloudtasks, cron or external-inbound endpoint is ' +
|
|
222
|
+
'driven by something other than a caller, and an external-outbound one is an outside system\'s. Make it RPC, or drop the @WpMcpTool.'));
|
|
222
223
|
}
|
|
223
224
|
if (endpoint.auth === undefined) {
|
|
224
225
|
found.push(new Verdict('an MCP tool declares no HTTP auth', 'Put a @WpAuth... decorator on the method. The MCP server refuses to boot without ' +
|