@webpieces/nx-webpieces-rules 0.4.961 → 0.4.963
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 +3 -3
- package/src/lib/api-usage/api-ast.js +41 -8
- 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 +32 -7
- package/src/lib/api-usage/api-contract-errors.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +31 -10
- package/src/lib/api-usage/api-relations.js +29 -10
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +1 -1
- package/src/lib/api-usage/api-scanner.js.map +1 -1
|
@@ -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 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"]}
|
|
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 VoidResponseProblem,\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/**\n * Endpoints whose response-body declaration disagrees with their kind or their signature (#1339,\n * #1348): CLOUDTASKS / CRON are void BY KIND, EXTERNAL_OUTBOUND is the vendor's to shape, and RPC /\n * EXTERNAL_INBOUND declare `responseBody: NONE`.\n */\nexport class VoidResponseMismatchError extends Error {\n constructor(public readonly endpoints: readonly VoidResponseMismatch[]) {\n super(\n `${endpoints.length} @Endpoint(s) disagree with their kind or return type about a response body:\\n` +\n endpoints\n .map((e: VoidResponseMismatch) => ` • ${VoidResponseMismatchError.line(e)} — at ${e.at}`)\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). A CLOUDTASKS or CRON endpoint is fire-and-forget, so its\\n` +\n ` KIND already means no response body: it returns Promise<void> and writes no responseBody option.\\n` +\n ` An EXTERNAL_OUTBOUND endpoint is served by the vendor, which shapes its own responses, so it writes\\n` +\n ` no responseBody option either (its client accepts any 2xx). RPC and EXTERNAL_INBOUND add\\n` +\n ` { responseBody: NONE } to @Endpoint's options on every Promise<void> method, and only on those.`,\n );\n this.name = 'VoidResponseMismatchError';\n }\n\n // webpieces-disable no-function-outside-class -- builds the super() message, which runs before `this` exists (parameter property)\n private static line(e: VoidResponseMismatch): string {\n const kind = e.kind.toUpperCase();\n switch (e.problem) {\n case VoidResponseProblem.NONE_WITHOUT_VOID:\n return `${e.api}.${e.method} declares responseBody: NONE but does not return Promise<void>`;\n case VoidResponseProblem.VOID_WITHOUT_NONE:\n return `${e.api}.${e.method} returns Promise<void> but does not declare responseBody: NONE`;\n case VoidResponseProblem.REDUNDANT_NONE:\n return (\n `${e.api}.${e.method} is a ${kind} endpoint and declares responseBody: NONE, which is redundant: ` +\n `the ${kind} kind already means no response body — delete responseBody: NONE`\n );\n case VoidResponseProblem.OUTBOUND_NONE:\n return (\n `${e.api}.${e.method} is an EXTERNAL_OUTBOUND endpoint and declares responseBody: NONE, which does not apply: ` +\n `NONE describes what OUR server sends, and the vendor shapes an outbound response — delete responseBody: NONE`\n );\n case VoidResponseProblem.TRIGGER_NOT_VOID:\n return (\n `${e.api}.${e.method} is a ${kind} endpoint but does not return Promise<void>: ` +\n `a ${kind} endpoint is fire-and-forget and its caller reads only the status — return Promise<void>`\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-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"]}
|
|
@@ -137,7 +137,10 @@ export interface ApiMethodMeta {
|
|
|
137
137
|
parameters?: ApiParameterMeta[];
|
|
138
138
|
/** Present when callers receive the transport-neutral full response. */
|
|
139
139
|
responseType?: 'full';
|
|
140
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Present when `@Endpoint(..., { responseBody: NONE })`: a `Promise<void>` method, 0-byte success.
|
|
142
|
+
* Absent on a CLOUDTASKS / CRON method, whose KIND already means no response body (#1348).
|
|
143
|
+
*/
|
|
141
144
|
responseBody?: 'none';
|
|
142
145
|
/**
|
|
143
146
|
* `@Queue(...)` override, else `${ApiClassName}-${methodName}`.
|
|
@@ -330,26 +333,44 @@ export declare class UndeclaredExternalCaller {
|
|
|
330
333
|
/** `path/to/file.ts:LINE`, workspace-relative. */
|
|
331
334
|
at: string);
|
|
332
335
|
}
|
|
333
|
-
/**
|
|
336
|
+
/** What is wrong with one endpoint's response-body declaration (#1339, #1348). */
|
|
337
|
+
export declare enum VoidResponseProblem {
|
|
338
|
+
/** RPC / EXTERNAL_INBOUND: declares `responseBody: NONE` but does not return `Promise<void>`. */
|
|
339
|
+
NONE_WITHOUT_VOID = "none-without-void",
|
|
340
|
+
/** RPC / EXTERNAL_INBOUND: returns `Promise<void>` but does not declare `responseBody: NONE`. */
|
|
341
|
+
VOID_WITHOUT_NONE = "void-without-none",
|
|
342
|
+
/** EXTERNAL_OUTBOUND: declares `responseBody: NONE`, but the VENDOR shapes an outbound response. */
|
|
343
|
+
OUTBOUND_NONE = "outbound-none",
|
|
344
|
+
/** CLOUDTASKS / CRON: declares `responseBody: NONE`, which the kind already implies. */
|
|
345
|
+
REDUNDANT_NONE = "redundant-none",
|
|
346
|
+
/** CLOUDTASKS / CRON: does not return `Promise<void>`, but the kind is fire-and-forget. */
|
|
347
|
+
TRIGGER_NOT_VOID = "trigger-not-void"
|
|
348
|
+
}
|
|
334
349
|
/**
|
|
335
|
-
* An `@Endpoint` whose
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* client act on, and it must not drift from the signature.
|
|
350
|
+
* An `@Endpoint` whose response-body declaration disagrees with its kind or its declared return
|
|
351
|
+
* type. The runtime cannot see `void` (`design:returntype` is just `Promise`), so the kind (CLOUDTASKS
|
|
352
|
+
* and CRON imply no body, #1348) or the `responseBody: NONE` option (RPC, EXTERNAL_INBOUND) is the
|
|
353
|
+
* only thing a server and a client act on, and it must not drift from the signature. EXTERNAL_OUTBOUND
|
|
354
|
+
* takes no option at all: the vendor shapes its responses and the client accepts any 2xx.
|
|
339
355
|
*/
|
|
340
356
|
export declare class VoidResponseMismatch {
|
|
341
357
|
readonly api: string;
|
|
342
358
|
readonly method: string;
|
|
343
|
-
/**
|
|
344
|
-
readonly
|
|
359
|
+
/** Which of the five disagreements this is. */
|
|
360
|
+
readonly problem: VoidResponseProblem;
|
|
361
|
+
/** The endpoint kind as written in the graph (`rpc`, `cloudtasks`, ...). */
|
|
362
|
+
readonly kind: EndpointKind;
|
|
345
363
|
/** `path/to/file.ts:LINE`, workspace-relative. */
|
|
346
364
|
readonly at: string;
|
|
347
365
|
constructor(api: string, method: string,
|
|
348
|
-
/**
|
|
349
|
-
|
|
366
|
+
/** Which of the five disagreements this is. */
|
|
367
|
+
problem: VoidResponseProblem,
|
|
368
|
+
/** The endpoint kind as written in the graph (`rpc`, `cloudtasks`, ...). */
|
|
369
|
+
kind: EndpointKind,
|
|
350
370
|
/** `path/to/file.ts:LINE`, workspace-relative. */
|
|
351
371
|
at: string);
|
|
352
372
|
}
|
|
373
|
+
/** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */
|
|
353
374
|
export declare class UndeclaredEndpointOperation {
|
|
354
375
|
/** The contract class the method is declared on. */
|
|
355
376
|
readonly api: string;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* only as binding identifiers, not as member names).
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.EmptiedApiContract = exports.UndeclaredEndpointOperation = exports.VoidResponseMismatch = exports.UndeclaredExternalCaller = exports.UnresolvedEndpointPath = exports.NonLiteralDecoratorArg = exports.UnresolvedApiCall = exports.CONTRACT_HTTP_METHODS = exports.EXTERNAL_SYSTEM_KINDS = void 0;
|
|
18
|
+
exports.EmptiedApiContract = exports.UndeclaredEndpointOperation = exports.VoidResponseMismatch = exports.VoidResponseProblem = exports.UndeclaredExternalCaller = exports.UnresolvedEndpointPath = exports.NonLiteralDecoratorArg = exports.UnresolvedApiCall = exports.CONTRACT_HTTP_METHODS = exports.EXTERNAL_SYSTEM_KINDS = void 0;
|
|
19
19
|
exports.isExternalSystemKind = isExternalSystemKind;
|
|
20
20
|
exports.apiRefKey = apiRefKey;
|
|
21
21
|
exports.contractVerbCarriesBody = contractVerbCarriesBody;
|
|
@@ -193,30 +193,49 @@ class UndeclaredExternalCaller {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
exports.UndeclaredExternalCaller = UndeclaredExternalCaller;
|
|
196
|
-
/**
|
|
196
|
+
/** What is wrong with one endpoint's response-body declaration (#1339, #1348). */
|
|
197
|
+
var VoidResponseProblem;
|
|
198
|
+
(function (VoidResponseProblem) {
|
|
199
|
+
/** RPC / EXTERNAL_INBOUND: declares `responseBody: NONE` but does not return `Promise<void>`. */
|
|
200
|
+
VoidResponseProblem["NONE_WITHOUT_VOID"] = "none-without-void";
|
|
201
|
+
/** RPC / EXTERNAL_INBOUND: returns `Promise<void>` but does not declare `responseBody: NONE`. */
|
|
202
|
+
VoidResponseProblem["VOID_WITHOUT_NONE"] = "void-without-none";
|
|
203
|
+
/** EXTERNAL_OUTBOUND: declares `responseBody: NONE`, but the VENDOR shapes an outbound response. */
|
|
204
|
+
VoidResponseProblem["OUTBOUND_NONE"] = "outbound-none";
|
|
205
|
+
/** CLOUDTASKS / CRON: declares `responseBody: NONE`, which the kind already implies. */
|
|
206
|
+
VoidResponseProblem["REDUNDANT_NONE"] = "redundant-none";
|
|
207
|
+
/** CLOUDTASKS / CRON: does not return `Promise<void>`, but the kind is fire-and-forget. */
|
|
208
|
+
VoidResponseProblem["TRIGGER_NOT_VOID"] = "trigger-not-void";
|
|
209
|
+
})(VoidResponseProblem || (exports.VoidResponseProblem = VoidResponseProblem = {}));
|
|
197
210
|
/**
|
|
198
|
-
* An `@Endpoint` whose
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* client act on, and it must not drift from the signature.
|
|
211
|
+
* An `@Endpoint` whose response-body declaration disagrees with its kind or its declared return
|
|
212
|
+
* type. The runtime cannot see `void` (`design:returntype` is just `Promise`), so the kind (CLOUDTASKS
|
|
213
|
+
* and CRON imply no body, #1348) or the `responseBody: NONE` option (RPC, EXTERNAL_INBOUND) is the
|
|
214
|
+
* only thing a server and a client act on, and it must not drift from the signature. EXTERNAL_OUTBOUND
|
|
215
|
+
* takes no option at all: the vendor shapes its responses and the client accepts any 2xx.
|
|
202
216
|
*/
|
|
203
217
|
class VoidResponseMismatch {
|
|
204
218
|
api;
|
|
205
219
|
method;
|
|
206
|
-
|
|
220
|
+
problem;
|
|
221
|
+
kind;
|
|
207
222
|
at;
|
|
208
223
|
constructor(api, method,
|
|
209
|
-
/**
|
|
210
|
-
|
|
224
|
+
/** Which of the five disagreements this is. */
|
|
225
|
+
problem,
|
|
226
|
+
/** The endpoint kind as written in the graph (`rpc`, `cloudtasks`, ...). */
|
|
227
|
+
kind,
|
|
211
228
|
/** `path/to/file.ts:LINE`, workspace-relative. */
|
|
212
229
|
at) {
|
|
213
230
|
this.api = api;
|
|
214
231
|
this.method = method;
|
|
215
|
-
this.
|
|
232
|
+
this.problem = problem;
|
|
233
|
+
this.kind = kind;
|
|
216
234
|
this.at = at;
|
|
217
235
|
}
|
|
218
236
|
}
|
|
219
237
|
exports.VoidResponseMismatch = VoidResponseMismatch;
|
|
238
|
+
/** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */
|
|
220
239
|
class UndeclaredEndpointOperation {
|
|
221
240
|
api;
|
|
222
241
|
method;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model —\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` — synchronous request/response over HTTP\n * - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a service→service edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * The kinds an external system can be DECLARED as. Each draws its own shape in the runtime viz, so\n * a datastore stops looking like an HTTP service.\n *\n * Lives here rather than beside the runtime graph model because that model already imports from this\n * file; putting it there and importing back would close a module cycle.\n */\nexport const EXTERNAL_SYSTEM_KINDS = [\n 'database',\n 'cache',\n 'queue',\n 'storage',\n 'saas',\n 'system',\n /**\n * A destination whose ADDRESS is supplied at runtime — a URL a partner registered, an OAuth\n * callback, a per-tenant host. Unlike every other kind it does not name one vendor: it names the\n * PLACE in our own system where somebody else's address is dialled, which is the fact a security\n * review is looking for.\n *\n * Declared like every other kind, on the CONTRACT: `@externalSystem runtime partner-webhooks`.\n * On the contract rather than at a `createRpcClient` call site deliberately — \"the far end of\n * this contract is outside our estate\" is a property of the CONTRACT, true for every caller of\n * it, so putting it there means one declaration however many services deliver over it, and\n * nothing to keep in step when a second one appears. It also means this kind rides the exact\n * same declare → resolve → draw pipeline `saas` and `database` already ride, rather than a\n * second mechanism that reads construction sites and can disagree with the first.\n */\n 'runtime',\n] as const;\n\nexport type ExternalSystemKind = (typeof EXTERNAL_SYSTEM_KINDS)[number];\n\n/** True for a string that names one of {@link EXTERNAL_SYSTEM_KINDS}. */\n// webpieces-disable no-function-outside-class -- type guard beside the type it guards, matching this file's DTO style\nexport function isExternalSystemKind(value: string): value is ExternalSystemKind {\n return (EXTERNAL_SYSTEM_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * ONE declared external system, keyed in {@link ExternalSystemDecls} by its IDENTITY.\n *\n * Identity, not display text: two projects each tagged `external:database:postgres` name the same\n * `postgres` node and converge on it with one arrow apiece, instead of drawing a database each.\n *\n * The two arrays are the two declaration sites, and a system may legitimately have both — a repo can\n * wrap a datastore behind a contract in one service and open it directly in another.\n */\nexport interface ExternalSystemDecl {\n kind: ExternalSystemKind;\n label: string;\n /** Contracts declaring it with an `@externalSystem` JSDoc tag; every user of one gets an arrow. */\n apis: string[];\n /** Projects declaring it with an `external:<kind>:<identity>` nx tag; each gets its OWN arrow. */\n projects: string[];\n}\n\n/** identity -> its declaration. Serialized as the `externalSystems` key of dependencies.json. */\nexport type ExternalSystemDecls = Record<string, ExternalSystemDecl>;\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` — it serves the api (a controller extends it)\n * - `uses` — it calls the api (generates a client)\n * - `uses-implements` — it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` → `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api — which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" — the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /** Qualified selected library export that contributed this relationship. */\n declaredVia?: string;\n conditional?: string;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external-inbound' | 'external-outbound';\n\n/** Side-effect semantics copied from the required third `@Endpoint` argument. */\nexport type EndpointOperation = 'read' | 'write-idempotent' | 'write';\n\n/** HTTP verbs understood by the generated contract runtime. */\nexport type ContractHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/** Every verb `@Endpoint` accepts, mirroring core-util's `HTTP_METHODS`. */\nexport const CONTRACT_HTTP_METHODS: readonly ContractHttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];\n\n/** Mirrors core-util's `verbCarriesBody`: POST, PUT and PATCH carry the one unannotated body argument. */\n// webpieces-disable no-function-outside-class -- pure predicate mirroring the framework rule\nexport function contractVerbCarriesBody(method: ContractHttpMethod): boolean {\n return method === 'POST' || method === 'PUT' || method === 'PATCH';\n}\n\n/** One path/query/body mapping emitted into architecture/dependencies.json. */\nexport interface ApiParameterMeta {\n index: number;\n source: 'path' | 'query' | 'query-map' | 'body';\n /** Path/query wire name; a JSON/form body and a `@QueryString()` multimap have no key. */\n wireName?: string;\n}\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /** Retry/idempotency contract; independent of the HTTP verb. */\n operation: EndpointOperation;\n /** Required in newly scanned source; optional only when reading older committed graph data. */\n httpMethod?: ContractHttpMethod;\n /** Explicit parameter mappings; absent only when the method has none. */\n parameters?: ApiParameterMeta[];\n /** Present when callers receive the transport-neutral full response. */\n responseType?: 'full';\n /** Present when `@Endpoint(..., { responseBody: NONE })`: a `Promise<void>` method, 0-byte success. */\n responseBody?: 'none';\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method — those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * `external-inbound`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n /**\n * WHO outside this repo drives this endpoint, from `@Endpoint(POST, p, WRITE, EXTERNAL_INBOUND, { calledBy })`.\n *\n * ONLY on an `external-inbound` method, the same way `queueName` is only on the kinds that HAVE a queue.\n *\n * Deliberately the SAME {@link ExternalSystemDeclaration} the OUTBOUND `@externalSystem` tag\n * resolves to, not a parallel inbound-only type: an inbound `saas twilio` and an outbound\n * `saas twilio` are the same vendor, so sharing the type makes them share an IDENTITY and\n * converge on ONE node instead of drawing twilio twice facing opposite directions.\n *\n * Optional in the TYPE only for graphs generated before the caller was required — generation\n * FAILS on an `external-inbound` method whose caller cannot be read (UndeclaredExternalCallerError).\n */\n caller?: ExternalSystemDeclaration;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints — it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — a vendor seam\n * declaring WHAT it is a seam to. JSDoc rather than a decorator because these seams are plain TS\n * `interface`s, which cannot carry one.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\n/** The `(kind, label)` pair a single declaration resolves to. */\nexport interface ExternalSystemDeclaration {\n kind: ExternalSystemKind;\n label: string;\n}\n\n/**\n * The committed, per-contract view written to `architecture/apis/<ApiName>.json` (one file per API;\n * dependencies.json only links to it under `apiContractFiles` — see ApiContractFiles).\n *\n * The runtime graph is derived from the committed files so generate and validate can never\n * diverge — which means anything the runtime graph needs must be COMMITTED, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it — every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as one `architecture/apis/<ApiName>.json` each. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an\n * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken\n * scan (a real api-lib whose source we never indexed), never a \"this isn't an API\" argument —\n * so it is reported loudly instead of collapsing into a silent `return null`.\n */\nexport class UnresolvedApiCall {\n constructor(\n /** The project whose source makes the call. */\n public readonly project: string,\n /** The contract class name as written at the call site. */\n public readonly api: string,\n /** `path/to/file.ts:LINE` of the call site, workspace-relative. */\n public readonly at: string,\n /** The declaration file the checker resolved to (where decorators are erased). */\n public readonly declaredIn: string,\n ) {}\n}\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class — with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.\n *\n * Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because\n * this one is FATAL. Upstream components need the URL: an http client builds its request as\n * `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata — the same\n * reasoning that already makes basePath required. Skipping the method instead used to delete it, and\n * a class whose every path was a constant lost every method and vanished from the contract table with\n * nothing printed anywhere.\n */\nexport class UnresolvedEndpointPath {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** The path argument exactly as written, e.g. `PROCESS_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `external` `@Endpoint` whose CALLER could not be read from the source.\n *\n * Fatal for the same reason {@link UnresolvedEndpointPath} is. The inbound box exists to say who is\n * calling us from outside; with no caller it can only restate our own contract name, which is the\n * exact bug this diagnostic exists to make impossible to reintroduce. `@Endpoint`'s TS overloads\n * already require `calledBy`, so anything reaching here is a JS caller, an `as any`, a cross-module\n * constant the parser-only scan cannot fold, or an unknown `callerKind`.\n */\nexport class UndeclaredExternalCaller {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, `SOME_CONST`, `callerKind: 'vendor'`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */\n/**\n * An `@Endpoint` whose `responseBody: NONE` disagrees with its declared return type: NONE on a method\n * that does not return `Promise<void>`, or a `Promise<void>` method without NONE. The runtime cannot\n * see `void` (`design:returntype` is just `Promise`), so the option is the only thing a server and a\n * client act on, and it must not drift from the signature.\n */\nexport class VoidResponseMismatch {\n constructor(\n public readonly api: string,\n public readonly method: string,\n /** True when the method declares NONE but does not return `Promise<void>`. */\n public readonly declaresNone: boolean,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\nexport class UndeclaredEndpointOperation {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, a constant name, or an invalid literal. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * A contract class that DECLARED `@Endpoint` methods and kept none of them.\n *\n * The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method\n * class (correctly — a vendor seam has no routes), so a class gutted by unreadable decorator\n * arguments left through the same door as a legitimately routeless one. A class that declared\n * endpoints and produced none is never legitimate, so it is named instead.\n */\nexport class EmptiedApiContract {\n constructor(\n /** The contract class name. */\n public readonly api: string,\n /** How many `@Endpoint` decorators were written on it. */\n public readonly declared: number,\n /** `path/to/file.ts:LINE` of the class, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(\n implementsRefs: ApiRef[],\n usesRefs: ApiRef[],\n): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) ||\n (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model —\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` — synchronous request/response over HTTP\n * - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a service→service edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * The kinds an external system can be DECLARED as. Each draws its own shape in the runtime viz, so\n * a datastore stops looking like an HTTP service.\n *\n * Lives here rather than beside the runtime graph model because that model already imports from this\n * file; putting it there and importing back would close a module cycle.\n */\nexport const EXTERNAL_SYSTEM_KINDS = [\n 'database',\n 'cache',\n 'queue',\n 'storage',\n 'saas',\n 'system',\n /**\n * A destination whose ADDRESS is supplied at runtime — a URL a partner registered, an OAuth\n * callback, a per-tenant host. Unlike every other kind it does not name one vendor: it names the\n * PLACE in our own system where somebody else's address is dialled, which is the fact a security\n * review is looking for.\n *\n * Declared like every other kind, on the CONTRACT: `@externalSystem runtime partner-webhooks`.\n * On the contract rather than at a `createRpcClient` call site deliberately — \"the far end of\n * this contract is outside our estate\" is a property of the CONTRACT, true for every caller of\n * it, so putting it there means one declaration however many services deliver over it, and\n * nothing to keep in step when a second one appears. It also means this kind rides the exact\n * same declare → resolve → draw pipeline `saas` and `database` already ride, rather than a\n * second mechanism that reads construction sites and can disagree with the first.\n */\n 'runtime',\n] as const;\n\nexport type ExternalSystemKind = (typeof EXTERNAL_SYSTEM_KINDS)[number];\n\n/** True for a string that names one of {@link EXTERNAL_SYSTEM_KINDS}. */\n// webpieces-disable no-function-outside-class -- type guard beside the type it guards, matching this file's DTO style\nexport function isExternalSystemKind(value: string): value is ExternalSystemKind {\n return (EXTERNAL_SYSTEM_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * ONE declared external system, keyed in {@link ExternalSystemDecls} by its IDENTITY.\n *\n * Identity, not display text: two projects each tagged `external:database:postgres` name the same\n * `postgres` node and converge on it with one arrow apiece, instead of drawing a database each.\n *\n * The two arrays are the two declaration sites, and a system may legitimately have both — a repo can\n * wrap a datastore behind a contract in one service and open it directly in another.\n */\nexport interface ExternalSystemDecl {\n kind: ExternalSystemKind;\n label: string;\n /** Contracts declaring it with an `@externalSystem` JSDoc tag; every user of one gets an arrow. */\n apis: string[];\n /** Projects declaring it with an `external:<kind>:<identity>` nx tag; each gets its OWN arrow. */\n projects: string[];\n}\n\n/** identity -> its declaration. Serialized as the `externalSystems` key of dependencies.json. */\nexport type ExternalSystemDecls = Record<string, ExternalSystemDecl>;\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` — it serves the api (a controller extends it)\n * - `uses` — it calls the api (generates a client)\n * - `uses-implements` — it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` → `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api — which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" — the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /** Qualified selected library export that contributed this relationship. */\n declaredVia?: string;\n conditional?: string;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external-inbound' | 'external-outbound';\n\n/** Side-effect semantics copied from the required third `@Endpoint` argument. */\nexport type EndpointOperation = 'read' | 'write-idempotent' | 'write';\n\n/** HTTP verbs understood by the generated contract runtime. */\nexport type ContractHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/** Every verb `@Endpoint` accepts, mirroring core-util's `HTTP_METHODS`. */\nexport const CONTRACT_HTTP_METHODS: readonly ContractHttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];\n\n/** Mirrors core-util's `verbCarriesBody`: POST, PUT and PATCH carry the one unannotated body argument. */\n// webpieces-disable no-function-outside-class -- pure predicate mirroring the framework rule\nexport function contractVerbCarriesBody(method: ContractHttpMethod): boolean {\n return method === 'POST' || method === 'PUT' || method === 'PATCH';\n}\n\n/** One path/query/body mapping emitted into architecture/dependencies.json. */\nexport interface ApiParameterMeta {\n index: number;\n source: 'path' | 'query' | 'query-map' | 'body';\n /** Path/query wire name; a JSON/form body and a `@QueryString()` multimap have no key. */\n wireName?: string;\n}\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /** Retry/idempotency contract; independent of the HTTP verb. */\n operation: EndpointOperation;\n /** Required in newly scanned source; optional only when reading older committed graph data. */\n httpMethod?: ContractHttpMethod;\n /** Explicit parameter mappings; absent only when the method has none. */\n parameters?: ApiParameterMeta[];\n /** Present when callers receive the transport-neutral full response. */\n responseType?: 'full';\n /**\n * Present when `@Endpoint(..., { responseBody: NONE })`: a `Promise<void>` method, 0-byte success.\n * Absent on a CLOUDTASKS / CRON method, whose KIND already means no response body (#1348).\n */\n responseBody?: 'none';\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method — those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * `external-inbound`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n /**\n * WHO outside this repo drives this endpoint, from `@Endpoint(POST, p, WRITE, EXTERNAL_INBOUND, { calledBy })`.\n *\n * ONLY on an `external-inbound` method, the same way `queueName` is only on the kinds that HAVE a queue.\n *\n * Deliberately the SAME {@link ExternalSystemDeclaration} the OUTBOUND `@externalSystem` tag\n * resolves to, not a parallel inbound-only type: an inbound `saas twilio` and an outbound\n * `saas twilio` are the same vendor, so sharing the type makes them share an IDENTITY and\n * converge on ONE node instead of drawing twilio twice facing opposite directions.\n *\n * Optional in the TYPE only for graphs generated before the caller was required — generation\n * FAILS on an `external-inbound` method whose caller cannot be read (UndeclaredExternalCallerError).\n */\n caller?: ExternalSystemDeclaration;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints — it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — a vendor seam\n * declaring WHAT it is a seam to. JSDoc rather than a decorator because these seams are plain TS\n * `interface`s, which cannot carry one.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\n/** The `(kind, label)` pair a single declaration resolves to. */\nexport interface ExternalSystemDeclaration {\n kind: ExternalSystemKind;\n label: string;\n}\n\n/**\n * The committed, per-contract view written to `architecture/apis/<ApiName>.json` (one file per API;\n * dependencies.json only links to it under `apiContractFiles` — see ApiContractFiles).\n *\n * The runtime graph is derived from the committed files so generate and validate can never\n * diverge — which means anything the runtime graph needs must be COMMITTED, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it — every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as one `architecture/apis/<ApiName>.json` each. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an\n * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken\n * scan (a real api-lib whose source we never indexed), never a \"this isn't an API\" argument —\n * so it is reported loudly instead of collapsing into a silent `return null`.\n */\nexport class UnresolvedApiCall {\n constructor(\n /** The project whose source makes the call. */\n public readonly project: string,\n /** The contract class name as written at the call site. */\n public readonly api: string,\n /** `path/to/file.ts:LINE` of the call site, workspace-relative. */\n public readonly at: string,\n /** The declaration file the checker resolved to (where decorators are erased). */\n public readonly declaredIn: string,\n ) {}\n}\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class — with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.\n *\n * Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because\n * this one is FATAL. Upstream components need the URL: an http client builds its request as\n * `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata — the same\n * reasoning that already makes basePath required. Skipping the method instead used to delete it, and\n * a class whose every path was a constant lost every method and vanished from the contract table with\n * nothing printed anywhere.\n */\nexport class UnresolvedEndpointPath {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** The path argument exactly as written, e.g. `PROCESS_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `external` `@Endpoint` whose CALLER could not be read from the source.\n *\n * Fatal for the same reason {@link UnresolvedEndpointPath} is. The inbound box exists to say who is\n * calling us from outside; with no caller it can only restate our own contract name, which is the\n * exact bug this diagnostic exists to make impossible to reintroduce. `@Endpoint`'s TS overloads\n * already require `calledBy`, so anything reaching here is a JS caller, an `as any`, a cross-module\n * constant the parser-only scan cannot fold, or an unknown `callerKind`.\n */\nexport class UndeclaredExternalCaller {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, `SOME_CONST`, `callerKind: 'vendor'`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** What is wrong with one endpoint's response-body declaration (#1339, #1348). */\nexport enum VoidResponseProblem {\n /** RPC / EXTERNAL_INBOUND: declares `responseBody: NONE` but does not return `Promise<void>`. */\n NONE_WITHOUT_VOID = 'none-without-void',\n /** RPC / EXTERNAL_INBOUND: returns `Promise<void>` but does not declare `responseBody: NONE`. */\n VOID_WITHOUT_NONE = 'void-without-none',\n /** EXTERNAL_OUTBOUND: declares `responseBody: NONE`, but the VENDOR shapes an outbound response. */\n OUTBOUND_NONE = 'outbound-none',\n /** CLOUDTASKS / CRON: declares `responseBody: NONE`, which the kind already implies. */\n REDUNDANT_NONE = 'redundant-none',\n /** CLOUDTASKS / CRON: does not return `Promise<void>`, but the kind is fire-and-forget. */\n TRIGGER_NOT_VOID = 'trigger-not-void',\n}\n\n/**\n * An `@Endpoint` whose response-body declaration disagrees with its kind or its declared return\n * type. The runtime cannot see `void` (`design:returntype` is just `Promise`), so the kind (CLOUDTASKS\n * and CRON imply no body, #1348) or the `responseBody: NONE` option (RPC, EXTERNAL_INBOUND) is the\n * only thing a server and a client act on, and it must not drift from the signature. EXTERNAL_OUTBOUND\n * takes no option at all: the vendor shapes its responses and the client accepts any 2xx.\n */\nexport class VoidResponseMismatch {\n constructor(\n public readonly api: string,\n public readonly method: string,\n /** Which of the five disagreements this is. */\n public readonly problem: VoidResponseProblem,\n /** The endpoint kind as written in the graph (`rpc`, `cloudtasks`, ...). */\n public readonly kind: EndpointKind,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */\nexport class UndeclaredEndpointOperation {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, a constant name, or an invalid literal. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * A contract class that DECLARED `@Endpoint` methods and kept none of them.\n *\n * The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method\n * class (correctly — a vendor seam has no routes), so a class gutted by unreadable decorator\n * arguments left through the same door as a legitimately routeless one. A class that declared\n * endpoints and produced none is never legitimate, so it is named instead.\n */\nexport class EmptiedApiContract {\n constructor(\n /** The contract class name. */\n public readonly api: string,\n /** How many `@Endpoint` decorators were written on it. */\n public readonly declared: number,\n /** `path/to/file.ts:LINE` of the class, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(\n implementsRefs: ApiRef[],\n usesRefs: ApiRef[],\n): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) ||\n (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
|
|
@@ -77,7 +77,7 @@ export interface ApiScanResult {
|
|
|
77
77
|
undeclaredExternalCallers: UndeclaredExternalCaller[];
|
|
78
78
|
/** Endpoints lacking the explicit side-effect contract used for retry safety and MCP hints. */
|
|
79
79
|
undeclaredEndpointOperations: UndeclaredEndpointOperation[];
|
|
80
|
-
/** Endpoints whose
|
|
80
|
+
/** Endpoints whose response-body declaration disagrees with their kind or signature. Fatal. */
|
|
81
81
|
voidResponseMismatches: VoidResponseMismatch[];
|
|
82
82
|
/** `no-root-union-api-type`'s findings. Fatal in buildApiContracts — see root-union-scan.ts. */
|
|
83
83
|
rootUnions: RootUnionFindings;
|