@webpieces/nx-webpieces-rules 0.4.956 → 0.4.958

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +7 -7
  2. package/src/lib/api-usage/api-ast.d.ts +11 -3
  3. package/src/lib/api-usage/api-ast.js +109 -14
  4. package/src/lib/api-usage/api-ast.js.map +1 -1
  5. package/src/lib/api-usage/api-contract-errors.d.ts +6 -1
  6. package/src/lib/api-usage/api-contract-errors.js +24 -6
  7. package/src/lib/api-usage/api-contract-errors.js.map +1 -1
  8. package/src/lib/api-usage/api-contract-evidence.js +1 -0
  9. package/src/lib/api-usage/api-contract-evidence.js.map +1 -1
  10. package/src/lib/api-usage/api-doc-rules-scan.d.ts +8 -8
  11. package/src/lib/api-usage/api-doc-rules-scan.js +8 -8
  12. package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -1
  13. package/src/lib/api-usage/api-doc-rules-verdicts.d.ts +9 -7
  14. package/src/lib/api-usage/api-doc-rules-verdicts.js +15 -14
  15. package/src/lib/api-usage/api-doc-rules-verdicts.js.map +1 -1
  16. package/src/lib/api-usage/api-relations.d.ts +33 -8
  17. package/src/lib/api-usage/api-relations.js +32 -1
  18. package/src/lib/api-usage/api-relations.js.map +1 -1
  19. package/src/lib/api-usage/api-scanner.d.ts +3 -1
  20. package/src/lib/api-usage/api-scanner.js +7 -2
  21. package/src/lib/api-usage/api-scanner.js.map +1 -1
  22. package/src/lib/runtime-graph-model.d.ts +2 -2
  23. package/src/lib/runtime-graph-model.js.map +1 -1
  24. package/src/lib/runtime-graph.js +6 -4
  25. package/src/lib/runtime-graph.js.map +1 -1
  26. package/src/lib/runtime-wiring/service-targets.js +6 -0
  27. package/src/lib/runtime-wiring/service-targets.js.map +1 -1
  28. package/src/lib/runtime-wiring/source-extractor.d.ts +5 -0
  29. package/src/lib/runtime-wiring/source-extractor.js +20 -0
  30. package/src/lib/runtime-wiring/source-extractor.js.map +1 -1
  31. package/src/lib/runtime-wiring/source-values.d.ts +6 -1
  32. package/src/lib/runtime-wiring/source-values.js +21 -0
  33. package/src/lib/runtime-wiring/source-values.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"api-doc-rules-verdicts.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules-verdicts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The pure VERDICTS the api-contract rules print, split out of `api-doc-rules-scan.ts`, which owns\n * the walk and is at its file-size limit.\n *\n * Nothing here touches the filesystem or the project graph. Each function answers one question about\n * ONE declaration the extractor or the renderer has already ruled on, and turns it into the two\n * halves every webpieces refusal carries — what is wrong, and what to do instead. The DECISION is\n * never made here: `model.unmapped` is the extractor's and a render failure is the renderer's. What\n * this file adds is the WORDING, which is the part a generic \"no model representation for this type\n * form\" cannot give an author.\n */\n\nimport * as ts from 'typescript';\nimport {\n ApiDocModel,\n ApiDocExtractor,\n DocumentedEndpoint,\n McpRenderError,\n McpSchemaRenderer,\n TypeRef,\n UnmappedType,\n} from '@webpieces/api-doc-model';\nimport { EndpointKind } from './api-relations';\n\n/**\n * The one trigger kind an MCP tool may have. A LITERAL, for the same reason `EndpointKind` beside it\n * is one: this package deliberately does not depend on `@webpieces/core-util` at runtime, and the\n * set the literal belongs to is pinned by that type.\n */\nexport const RPC_KIND: EndpointKind = 'rpc';\n\n/**\n * The endpoint kinds whose response a caller WAITS FOR, and which therefore must name a DTO (#1017).\n *\n * `rpc` and `external` are both synchronous request/response: somebody posts and reads what comes\n * back, so there IS a body and it must be a named type that can gain a field later. `void` on either\n * is a one-way door — a `void` response can never grow without breaking every generated client, where\n * an empty DTO grows additively forever. #1016 refused it on `rpc` only, because `external` was\n * unstated at the time; it is stated now.\n *\n * `cloudtasks` and `cron` are deliberately NOT here: delivery is fire-and-forget, nobody waits for a\n * body, and `Promise<void>` is the CONTRACT rather than an omission.\n */\nexport const ANSWERING_KINDS: readonly EndpointKind[] = ['rpc', 'external'];\n\n/** The scalar keywords a MIXED SCALAR union is made of. */\nconst SCALAR_KEYWORDS = new Set<ts.SyntaxKind>([\n ts.SyntaxKind.StringKeyword,\n ts.SyntaxKind.NumberKeyword,\n ts.SyntaxKind.BooleanKeyword,\n]);\n\n/** ONE reason a declaration is refused, in the two halves every refusal prints. */\nexport class Verdict {\n constructor(\n public readonly what: string,\n public readonly cure: string,\n ) {}\n}\n\n\n/** Method name -> the line its declaration starts on, for one contract class. */\nexport class ContractLines {\n constructor(\n public readonly classLine: number,\n public readonly byMethod: ReadonlyMap<string, number>,\n /** Methods carrying `@WpMcpTool` that do NOT also carry `@Endpoint`. */\n public readonly toolsWithoutEndpoint: readonly string[],\n ) {}\n\n lineOf(methodName: string): number {\n return this.byMethod.get(methodName) ?? this.classLine;\n }\n}\n\n\n/**\n * The cure for an `unknown` value, for whichever rule is REPORTING it.\n *\n * It takes the rule name rather than naming one, because this defect is shared: it blocks the\n * OpenAPI document, so `DefectSink` routes it to `api-rules-for-openapi` when that rule is running\n * and to `api-rules-for-mcp` when it is the only one. A hard-coded rule name would then hand an\n * author, on the mcp-only configuration, a `// webpieces-disable api-rules-for-openapi` line that\n * the collector reading the site does not look for — a cure that does not cure, which is the one\n * failure mode a printed cure must not have.\n */\n// webpieces-disable no-function-outside-class -- pure message builder, beside the verdicts that print it\nexport function unknownValueCure(rule: string): string {\n return (\n 'Name the value type — a DTO, an array of one, a string-literal union, or ' +\n 'Record<string, ThatDto>. If the shape really is unknowable (a transport envelope, ' +\n 'arbitrary SQL rows), write the per-site disable and say so: ' +\n `// webpieces-disable ${rule} -- <why this field is published with no shape>. ` +\n 'An existing no-any-unknown disable does NOT count — that rule asked whether the code is ' +\n 'type-safe, which is a different question from whether a partner is handed a shapeless field.'\n );\n}\n\n/** The one cure for a `void` endpoint that somebody is WAITING on (rpc or external). */\nexport const VOID_RPC_CURE =\n 'Declare a named response DTO and return Promise<That>, even when it has no fields today: an ' +\n 'empty object grows additively forever, and a void response cannot gain a field without ' +\n 'breaking every generated client. An external endpoint is a synchronous call an outside caller ' +\n 'waits on, so it has a body too — an empty one serializes as {} and the failure detail rides on ' +\n 'the thrown error. Promise<void> stays legal on a cloudtasks or cron endpoint, where ' +\n 'fire-and-forget is the contract.';\n\n/**\n * Why the resolver could not map this type, said in the author's vocabulary.\n *\n * The DECISION to refuse is the extractor's and is not second-guessed here — every entry in\n * `model.unmapped` becomes a defect. What this adds is the MESSAGE: an open enum and a mixed scalar\n * union are the two shapes a real repo actually hits (measured: 6 of 98 endpoints, from exactly\n * these two), and \"no model representation for this type form\" tells their author nothing.\n */\n// webpieces-disable no-function-outside-class -- pure classifier over the extractor's own verdict\nexport function classifyUnmapped(unmapped: UnmappedType): Verdict {\n const branches = unionBranchesOf(unmapped.typeText);\n if (isOpenEnum(branches)) {\n return new Verdict(\n `'${unmapped.typeText}' is an OPEN ENUM — literals unioned with the wide type`,\n 'Use an enum OR a string, not both. TypeScript COLLAPSES this union to the wide type, ' +\n 'so the literals buy no compile-time safety and a document cannot state them as a ' +\n \"closed set. Either drop the `| string` (`'a' | 'b'` publishes as a real enum), or \" +\n 'drop the literals and list the known values in the JSDoc.',\n );\n }\n if (isMixedScalarUnion(branches)) {\n return new Verdict(\n `'${unmapped.typeText}' is a MIXED SCALAR union`,\n 'Give the field ONE type. JSON Schema can spell {\"type\": [\"string\",\"number\"]}, but ' +\n 'this framework\\'s ApiJsonSchema helpers read a type array as \"one real type plus ' +\n 'maybe null\" (baseTypeOf returns the first non-null member), so publishing it ' +\n 'would VALIDATE WRONGLY — which is worse than refusing it. Pick the type, or wrap ' +\n 'the alternatives in a discriminated union of named objects.',\n );\n }\n return new Verdict(\n `'${unmapped.typeText}' has no shape a document can state — ${unmapped.reason}`,\n 'Give it a type a document can carry: a named DTO, an array of one, a string-literal ' +\n 'union, a Record<string, ThatDto>, or a primitive. A field with no shape publishes as ' +\n '\"anything\".',\n );\n}\n\n/** The branches of `typeText` when it parses as a union, else an empty list. */\n// webpieces-disable no-function-outside-class -- pure parser used by classifyUnmapped alone\nfunction unionBranchesOf(typeText: string): readonly ts.TypeNode[] {\n const parsed = ts.createSourceFile(\n '__unmapped.ts',\n `type __X = ${typeText};`,\n ts.ScriptTarget.Latest,\n true,\n );\n const alias = parsed.statements[0];\n if (alias === undefined || !ts.isTypeAliasDeclaration(alias)) return [];\n if (!ts.isUnionTypeNode(alias.type)) return [];\n return alias.type.types.filter((branch: ts.TypeNode) => !isNullishBranch(branch));\n}\n\n/** `'a' | 'b' | string`, and the numeric equivalent. See #1010 and the decision on #1011. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isOpenEnum(branches: readonly ts.TypeNode[]): boolean {\n const wide = branches.some(\n (branch: ts.TypeNode) =>\n branch.kind === ts.SyntaxKind.StringKeyword ||\n branch.kind === ts.SyntaxKind.NumberKeyword,\n );\n const literals = branches.some((branch: ts.TypeNode) => ts.isLiteralTypeNode(branch));\n return wide && literals;\n}\n\n/** `string | number`, `string | boolean | null` — two or more DIFFERENT scalar keywords. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isMixedScalarUnion(branches: readonly ts.TypeNode[]): boolean {\n if (branches.length < 2) return false;\n const kinds = new Set<ts.SyntaxKind>();\n for (const branch of branches) {\n if (!SCALAR_KEYWORDS.has(branch.kind)) return false;\n kinds.add(branch.kind);\n }\n return kinds.size >= 2;\n}\n\n/** `null` / `undefined` branches — nullability, not composition, exactly as the resolver reads it. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isNullishBranch(node: ts.TypeNode): boolean {\n if (node.kind === ts.SyntaxKind.UndefinedKeyword || node.kind === ts.SyntaxKind.NullKeyword) {\n return true;\n }\n return ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword;\n}\n\n/**\n * True when this type PUBLISHES an `unknown` value.\n *\n * It looks at the VALUE TYPE wherever it sits — the item of an array, the value of an open map, the\n * field itself — and never at the `Record<>` spelling, because the real cases in the wild are not\n * all Records: `params?: unknown[]` is the same defect written differently.\n *\n * It does NOT descend through a `ref`: a named DTO is its own entry in the model and its own fields\n * are judged there, so descending would report the same field once per type that points at it.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over one TypeRef\nexport function carriesUnknown(ref: TypeRef): boolean {\n if (ref.kind === 'primitive') return ref.primitive === 'unknown';\n if (ref.kind === 'array') return carriesUnknown(ref.items!);\n if (ref.kind === 'openMap') return carriesUnknown(ref.values!);\n return false;\n}\n\n/**\n * `void`, `unknown` and `any` all reach the model as the same primitive — the resolver maps the\n * three keywords onto one, because to a document they say the identical thing: nothing.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over one TypeRef\nexport function isVoidLike(ref: TypeRef): boolean {\n return ref.kind === 'primitive' && ref.primitive === 'unknown';\n}\n\n/**\n * Every reason this tool could not be served, in the order `McpToolRegistry` asks them at boot —\n * unique name, trigger kind, HTTP auth, MCP auth — and then the renderer's own verdict.\n *\n * The registry's four are restated from the model rather than driven, because they are decorator\n * PRESENCE facts the model already carries verbatim; the SCHEMA question is the one with a real\n * implementation behind it, and that one is driven.\n */\n// webpieces-disable no-function-outside-class, max-lines-new-methods -- pure judge over one endpoint, and the four boot checks read as one list\nexport function toolFailures(\n endpoint: DocumentedEndpoint,\n renderer: McpSchemaRenderer,\n toolNames: Map<string, string>,\n model: ApiDocModel,\n): readonly Verdict[] {\n const found: Verdict[] = [];\n const toolName = endpoint.mcpTool!.name;\n const claimed = toolNames.get(toolName);\n if (claimed === undefined) {\n toolNames.set(toolName, `${model.contractName}.${endpoint.methodName}`);\n } else {\n found.push(\n new Verdict(\n `tool name '${toolName}' is already declared by ${claimed}`,\n 'Tool names are the protocol identity and are globally unique — rename one of the ' +\n 'two @WpMcpTool declarations.',\n ),\n );\n }\n if (endpoint.kind !== RPC_KIND) {\n found.push(\n new Verdict(\n `an MCP tool is declared on a '${endpoint.kind}' endpoint`,\n 'Only an RPC endpoint can be a tool — a cloudtasks, cron or external endpoint is ' +\n 'driven by something other than a caller. Make it RPC, or drop the @WpMcpTool.',\n ),\n );\n }\n if (endpoint.auth === undefined) {\n found.push(\n new Verdict(\n 'an MCP tool declares no HTTP auth',\n 'Put a @WpAuth... decorator on the method. The MCP server refuses to boot without ' +\n 'one, because a tool with no credential is an unauthenticated endpoint an ' +\n 'agent can call.',\n ),\n );\n }\n if (endpoint.authorizationText === undefined) {\n found.push(\n new Verdict(\n 'an MCP tool does not declare @WpAuthorization(...)',\n 'Add @WpAuthorization(...) beside @WpAuth. Common operation authorization is rechecked ' +\n 'before tool/schema lookup and at the receiving endpoint.',\n ),\n );\n }\n const rendered = renderFailure(renderer, endpoint);\n if (rendered !== undefined) found.push(rendered);\n return found;\n}\n\n/**\n * The renderer's own refusal for one tool, or undefined when it renders.\n *\n * This is the whole acceptance contract for the MCP half: \"passes the rule\" and \"renders a tool\" are\n * the SAME statement, because the rule asks the renderer. It covers the method's missing JSDoc,\n * every reachable DTO field's missing JSDoc, a request or response that is not a named object, a\n * root-level union, a recursive DTO, an `unknown` leaf and a malformed `@mcpHeader` — none of which\n * is restated here.\n *\n * One verdict per tool, because the renderer stops at the first thing it cannot draw. A tool blocked\n * by several undocumented fields is therefore fixed a round at a time, which is the price of having\n * exactly one definition of renderable rather than a second walk that could disagree with it.\n */\n// webpieces-disable no-function-outside-class -- pure judge beside toolFailures\nfunction renderFailure(\n renderer: McpSchemaRenderer,\n endpoint: DocumentedEndpoint,\n): Verdict | undefined {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the render refusal IS the verdict\n try {\n renderer.tool(endpoint);\n return undefined;\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof McpRenderError)) throw err;\n return new Verdict(`${err.message} — no MCP schema could be built`, err.cure);\n }\n}\n\n/** Every `@ApiPath` class name declared at the top level of a file, in declaration order. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nexport function contractNamesIn(source: ts.SourceFile): string[] {\n const names: string[] = [];\n for (const statement of source.statements) {\n if (!ts.isClassDeclaration(statement) || statement.name === undefined) continue;\n if (hasDecoratorNamed(statement, 'ApiPath')) names.push(statement.name.text);\n }\n return names;\n}\n\n/**\n * Where each method of one contract starts, and which of them carry `@WpMcpTool` without\n * `@Endpoint`.\n *\n * The line numbers exist because the MCP failures come from the RENDERER, whose location is a NAME\n * (`Order.window`) rather than a file offset — it publishes a schema, not a diagnostic. Anchoring\n * every tool failure at its METHOD is also where an author would write the disable: \"this tool is\n * deliberately not renderable\" is a decision about the method, not about a field three DTOs down.\n */\n// webpieces-disable no-function-outside-class -- pure AST reader beside contractNamesIn\nexport function contractLinesOf(source: ts.SourceFile, contractName: string): ContractLines {\n for (const statement of source.statements) {\n if (!ts.isClassDeclaration(statement) || statement.name?.text !== contractName) continue;\n const byMethod = new Map<string, number>();\n const orphanTools: string[] = [];\n for (const member of statement.members) {\n if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue;\n byMethod.set(member.name.text, lineOf(source, member));\n if (hasDecoratorNamed(member, 'WpMcpTool') && !hasDecoratorNamed(member, 'Endpoint')) {\n orphanTools.push(member.name.text);\n }\n }\n return new ContractLines(lineOf(source, statement), byMethod, orphanTools);\n }\n return new ContractLines(1, new Map<string, number>(), []);\n}\n\n/** 1-based line of a node's first token, decorators excluded. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nfunction lineOf(source: ts.SourceFile, node: ts.Node): number {\n return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;\n}\n\n/** Match canonical imports, including aliases and namespace calls. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nfunction hasDecoratorNamed(node: ts.Node, name: string): boolean {\n const decorators = ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];\n for (const decorator of decorators) {\n const call = decorator.expression;\n if (\n ts.isCallExpression(call) &&\n ApiDocExtractor.canonicalCallName(call.expression) === name\n ) {\n return true;\n }\n }\n return false;\n}\n"]}
1
+ {"version":3,"file":"api-doc-rules-verdicts.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules-verdicts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The pure VERDICTS the api-contract rules print, split out of `api-doc-rules-scan.ts`, which owns\n * the walk and is at its file-size limit.\n *\n * Nothing here touches the filesystem or the project graph. Each function answers one question about\n * ONE declaration the extractor or the renderer has already ruled on, and turns it into the two\n * halves every webpieces refusal carries — what is wrong, and what to do instead. The DECISION is\n * never made here: `model.unmapped` is the extractor's and a render failure is the renderer's. What\n * this file adds is the WORDING, which is the part a generic \"no model representation for this type\n * form\" cannot give an author.\n */\n\nimport * as ts from 'typescript';\nimport {\n ApiDocModel,\n ApiDocExtractor,\n DocumentedEndpoint,\n McpRenderError,\n McpSchemaRenderer,\n TypeRef,\n UnmappedType,\n} from '@webpieces/api-doc-model';\nimport { EndpointKind } from './api-relations';\n\n/**\n * The one trigger kind an MCP tool may have. A LITERAL, for the same reason `EndpointKind` beside it\n * is one: this package deliberately does not depend on `@webpieces/core-util` at runtime, and the\n * set the literal belongs to is pinned by that type.\n */\nexport const RPC_KIND: EndpointKind = 'rpc';\n\n/**\n * The endpoint kinds whose response a caller WAITS FOR, and which therefore must name a DTO (#1017).\n *\n * `rpc` is synchronous request/response between our own services: somebody posts and reads what\n * comes back, so there IS a body and it must be a named type that can gain a field later. `void` on\n * it is a one-way door — a `void` response can never grow without breaking every generated client,\n * where an empty DTO grows additively forever.\n *\n * `cloudtasks` and `cron` are deliberately NOT here: delivery is fire-and-forget, nobody waits for a\n * body, and `Promise<void>` is the CONTRACT rather than an omission. Neither are `external-inbound`\n * and `external-outbound` (#1339): the OUTSIDE system owns that wire — a vendor's webhook expects a\n * bare 2xx, and a vendor's API documents ops that answer nothing — so a `Promise<void>` there\n * describes somebody else's contract rather than choosing ours.\n */\nexport const ANSWERING_KINDS: readonly EndpointKind[] = ['rpc'];\n\n/** The scalar keywords a MIXED SCALAR union is made of. */\nconst SCALAR_KEYWORDS = new Set<ts.SyntaxKind>([\n ts.SyntaxKind.StringKeyword,\n ts.SyntaxKind.NumberKeyword,\n ts.SyntaxKind.BooleanKeyword,\n]);\n\n/** ONE reason a declaration is refused, in the two halves every refusal prints. */\nexport class Verdict {\n constructor(\n public readonly what: string,\n public readonly cure: string,\n ) {}\n}\n\n\n/** Method name -> the line its declaration starts on, for one contract class. */\nexport class ContractLines {\n constructor(\n public readonly classLine: number,\n public readonly byMethod: ReadonlyMap<string, number>,\n /** Methods carrying `@WpMcpTool` that do NOT also carry `@Endpoint`. */\n public readonly toolsWithoutEndpoint: readonly string[],\n ) {}\n\n lineOf(methodName: string): number {\n return this.byMethod.get(methodName) ?? this.classLine;\n }\n}\n\n\n/**\n * The cure for an `unknown` value, for whichever rule is REPORTING it.\n *\n * It takes the rule name rather than naming one, because this defect is shared: it blocks the\n * OpenAPI document, so `DefectSink` routes it to `api-rules-for-openapi` when that rule is running\n * and to `api-rules-for-mcp` when it is the only one. A hard-coded rule name would then hand an\n * author, on the mcp-only configuration, a `// webpieces-disable api-rules-for-openapi` line that\n * the collector reading the site does not look for — a cure that does not cure, which is the one\n * failure mode a printed cure must not have.\n */\n// webpieces-disable no-function-outside-class -- pure message builder, beside the verdicts that print it\nexport function unknownValueCure(rule: string): string {\n return (\n 'Name the value type — a DTO, an array of one, a string-literal union, or ' +\n 'Record<string, ThatDto>. If the shape really is unknowable (a transport envelope, ' +\n 'arbitrary SQL rows), write the per-site disable and say so: ' +\n `// webpieces-disable ${rule} -- <why this field is published with no shape>. ` +\n 'An existing no-any-unknown disable does NOT count — that rule asked whether the code is ' +\n 'type-safe, which is a different question from whether a partner is handed a shapeless field.'\n );\n}\n\n/** The one cure for a `void` endpoint that somebody is WAITING on (rpc). */\nexport const VOID_RPC_CURE =\n 'Declare a named response DTO and return Promise<That>, even when it has no fields today: an ' +\n 'empty object grows additively forever, and a void response cannot gain a field without ' +\n 'breaking every generated client. Promise<void> stays legal on a cloudtasks, cron, ' +\n 'EXTERNAL_INBOUND or EXTERNAL_OUTBOUND endpoint, where fire-and-forget or an outside system ' +\n 'owns the contract.';\n\n/**\n * Why the resolver could not map this type, said in the author's vocabulary.\n *\n * The DECISION to refuse is the extractor's and is not second-guessed here — every entry in\n * `model.unmapped` becomes a defect. What this adds is the MESSAGE: an open enum and a mixed scalar\n * union are the two shapes a real repo actually hits (measured: 6 of 98 endpoints, from exactly\n * these two), and \"no model representation for this type form\" tells their author nothing.\n */\n// webpieces-disable no-function-outside-class -- pure classifier over the extractor's own verdict\nexport function classifyUnmapped(unmapped: UnmappedType): Verdict {\n const branches = unionBranchesOf(unmapped.typeText);\n if (isOpenEnum(branches)) {\n return new Verdict(\n `'${unmapped.typeText}' is an OPEN ENUM — literals unioned with the wide type`,\n 'Use an enum OR a string, not both. TypeScript COLLAPSES this union to the wide type, ' +\n 'so the literals buy no compile-time safety and a document cannot state them as a ' +\n \"closed set. Either drop the `| string` (`'a' | 'b'` publishes as a real enum), or \" +\n 'drop the literals and list the known values in the JSDoc.',\n );\n }\n if (isMixedScalarUnion(branches)) {\n return new Verdict(\n `'${unmapped.typeText}' is a MIXED SCALAR union`,\n 'Give the field ONE type. JSON Schema can spell {\"type\": [\"string\",\"number\"]}, but ' +\n 'this framework\\'s ApiJsonSchema helpers read a type array as \"one real type plus ' +\n 'maybe null\" (baseTypeOf returns the first non-null member), so publishing it ' +\n 'would VALIDATE WRONGLY — which is worse than refusing it. Pick the type, or wrap ' +\n 'the alternatives in a discriminated union of named objects.',\n );\n }\n return new Verdict(\n `'${unmapped.typeText}' has no shape a document can state — ${unmapped.reason}`,\n 'Give it a type a document can carry: a named DTO, an array of one, a string-literal ' +\n 'union, a Record<string, ThatDto>, or a primitive. A field with no shape publishes as ' +\n '\"anything\".',\n );\n}\n\n/** The branches of `typeText` when it parses as a union, else an empty list. */\n// webpieces-disable no-function-outside-class -- pure parser used by classifyUnmapped alone\nfunction unionBranchesOf(typeText: string): readonly ts.TypeNode[] {\n const parsed = ts.createSourceFile(\n '__unmapped.ts',\n `type __X = ${typeText};`,\n ts.ScriptTarget.Latest,\n true,\n );\n const alias = parsed.statements[0];\n if (alias === undefined || !ts.isTypeAliasDeclaration(alias)) return [];\n if (!ts.isUnionTypeNode(alias.type)) return [];\n return alias.type.types.filter((branch: ts.TypeNode) => !isNullishBranch(branch));\n}\n\n/** `'a' | 'b' | string`, and the numeric equivalent. See #1010 and the decision on #1011. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isOpenEnum(branches: readonly ts.TypeNode[]): boolean {\n const wide = branches.some(\n (branch: ts.TypeNode) =>\n branch.kind === ts.SyntaxKind.StringKeyword ||\n branch.kind === ts.SyntaxKind.NumberKeyword,\n );\n const literals = branches.some((branch: ts.TypeNode) => ts.isLiteralTypeNode(branch));\n return wide && literals;\n}\n\n/** `string | number`, `string | boolean | null` — two or more DIFFERENT scalar keywords. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isMixedScalarUnion(branches: readonly ts.TypeNode[]): boolean {\n if (branches.length < 2) return false;\n const kinds = new Set<ts.SyntaxKind>();\n for (const branch of branches) {\n if (!SCALAR_KEYWORDS.has(branch.kind)) return false;\n kinds.add(branch.kind);\n }\n return kinds.size >= 2;\n}\n\n/** `null` / `undefined` branches — nullability, not composition, exactly as the resolver reads it. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isNullishBranch(node: ts.TypeNode): boolean {\n if (node.kind === ts.SyntaxKind.UndefinedKeyword || node.kind === ts.SyntaxKind.NullKeyword) {\n return true;\n }\n return ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword;\n}\n\n/**\n * True when this type PUBLISHES an `unknown` value.\n *\n * It looks at the VALUE TYPE wherever it sits — the item of an array, the value of an open map, the\n * field itself — and never at the `Record<>` spelling, because the real cases in the wild are not\n * all Records: `params?: unknown[]` is the same defect written differently.\n *\n * It does NOT descend through a `ref`: a named DTO is its own entry in the model and its own fields\n * are judged there, so descending would report the same field once per type that points at it.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over one TypeRef\nexport function carriesUnknown(ref: TypeRef): boolean {\n if (ref.kind === 'primitive') return ref.primitive === 'unknown';\n if (ref.kind === 'array') return carriesUnknown(ref.items!);\n if (ref.kind === 'openMap') return carriesUnknown(ref.values!);\n return false;\n}\n\n/**\n * `void`, `unknown` and `any` all reach the model as the same primitive — the resolver maps the\n * three keywords onto one, because to a document they say the identical thing: nothing.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over one TypeRef\nexport function isVoidLike(ref: TypeRef): boolean {\n return ref.kind === 'primitive' && ref.primitive === 'unknown';\n}\n\n/**\n * Every reason this tool could not be served, in the order `McpToolRegistry` asks them at boot —\n * unique name, trigger kind, HTTP auth, MCP auth — and then the renderer's own verdict.\n *\n * The registry's four are restated from the model rather than driven, because they are decorator\n * PRESENCE facts the model already carries verbatim; the SCHEMA question is the one with a real\n * implementation behind it, and that one is driven.\n */\n// webpieces-disable no-function-outside-class, max-lines-new-methods -- pure judge over one endpoint, and the four boot checks read as one list\nexport function toolFailures(\n endpoint: DocumentedEndpoint,\n renderer: McpSchemaRenderer,\n toolNames: Map<string, string>,\n model: ApiDocModel,\n): readonly Verdict[] {\n const found: Verdict[] = [];\n const toolName = endpoint.mcpTool!.name;\n const claimed = toolNames.get(toolName);\n if (claimed === undefined) {\n toolNames.set(toolName, `${model.contractName}.${endpoint.methodName}`);\n } else {\n found.push(\n new Verdict(\n `tool name '${toolName}' is already declared by ${claimed}`,\n 'Tool names are the protocol identity and are globally unique — rename one of the ' +\n 'two @WpMcpTool declarations.',\n ),\n );\n }\n if (endpoint.kind !== RPC_KIND) {\n found.push(\n new Verdict(\n `an MCP tool is declared on a '${endpoint.kind}' endpoint`,\n 'Only an RPC endpoint can be a tool — a cloudtasks, cron or external-inbound endpoint is ' +\n 'driven by something other than a caller, and an external-outbound one is an outside system\\'s. Make it RPC, or drop the @WpMcpTool.',\n ),\n );\n }\n if (endpoint.auth === undefined) {\n found.push(\n new Verdict(\n 'an MCP tool declares no HTTP auth',\n 'Put a @WpAuth... decorator on the method. The MCP server refuses to boot without ' +\n 'one, because a tool with no credential is an unauthenticated endpoint an ' +\n 'agent can call.',\n ),\n );\n }\n if (endpoint.authorizationText === undefined) {\n found.push(\n new Verdict(\n 'an MCP tool does not declare @WpAuthorization(...)',\n 'Add @WpAuthorization(...) beside @WpAuth. Common operation authorization is rechecked ' +\n 'before tool/schema lookup and at the receiving endpoint.',\n ),\n );\n }\n const rendered = renderFailure(renderer, endpoint);\n if (rendered !== undefined) found.push(rendered);\n return found;\n}\n\n/**\n * The renderer's own refusal for one tool, or undefined when it renders.\n *\n * This is the whole acceptance contract for the MCP half: \"passes the rule\" and \"renders a tool\" are\n * the SAME statement, because the rule asks the renderer. It covers the method's missing JSDoc,\n * every reachable DTO field's missing JSDoc, a request or response that is not a named object, a\n * root-level union, a recursive DTO, an `unknown` leaf and a malformed `@mcpHeader` — none of which\n * is restated here.\n *\n * One verdict per tool, because the renderer stops at the first thing it cannot draw. A tool blocked\n * by several undocumented fields is therefore fixed a round at a time, which is the price of having\n * exactly one definition of renderable rather than a second walk that could disagree with it.\n */\n// webpieces-disable no-function-outside-class -- pure judge beside toolFailures\nfunction renderFailure(\n renderer: McpSchemaRenderer,\n endpoint: DocumentedEndpoint,\n): Verdict | undefined {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the render refusal IS the verdict\n try {\n renderer.tool(endpoint);\n return undefined;\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof McpRenderError)) throw err;\n return new Verdict(`${err.message} — no MCP schema could be built`, err.cure);\n }\n}\n\n/** Every `@ApiPath` class name declared at the top level of a file, in declaration order. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nexport function contractNamesIn(source: ts.SourceFile): string[] {\n const names: string[] = [];\n for (const statement of source.statements) {\n if (!ts.isClassDeclaration(statement) || statement.name === undefined) continue;\n if (hasDecoratorNamed(statement, 'ApiPath')) names.push(statement.name.text);\n }\n return names;\n}\n\n/**\n * Where each method of one contract starts, and which of them carry `@WpMcpTool` without\n * `@Endpoint`.\n *\n * The line numbers exist because the MCP failures come from the RENDERER, whose location is a NAME\n * (`Order.window`) rather than a file offset — it publishes a schema, not a diagnostic. Anchoring\n * every tool failure at its METHOD is also where an author would write the disable: \"this tool is\n * deliberately not renderable\" is a decision about the method, not about a field three DTOs down.\n */\n// webpieces-disable no-function-outside-class -- pure AST reader beside contractNamesIn\nexport function contractLinesOf(source: ts.SourceFile, contractName: string): ContractLines {\n for (const statement of source.statements) {\n if (!ts.isClassDeclaration(statement) || statement.name?.text !== contractName) continue;\n const byMethod = new Map<string, number>();\n const orphanTools: string[] = [];\n for (const member of statement.members) {\n if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue;\n byMethod.set(member.name.text, lineOf(source, member));\n if (hasDecoratorNamed(member, 'WpMcpTool') && !hasDecoratorNamed(member, 'Endpoint')) {\n orphanTools.push(member.name.text);\n }\n }\n return new ContractLines(lineOf(source, statement), byMethod, orphanTools);\n }\n return new ContractLines(1, new Map<string, number>(), []);\n}\n\n/** 1-based line of a node's first token, decorators excluded. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nfunction lineOf(source: ts.SourceFile, node: ts.Node): number {\n return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;\n}\n\n/** Match canonical imports, including aliases and namespace calls. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nfunction hasDecoratorNamed(node: ts.Node, name: string): boolean {\n const decorators = ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];\n for (const decorator of decorators) {\n const call = decorator.expression;\n if (\n ts.isCallExpression(call) &&\n ApiDocExtractor.canonicalCallName(call.expression) === name\n ) {\n return true;\n }\n }\n return false;\n}\n"]}
@@ -104,16 +104,20 @@ export type ProjectApiRelations = Record<string, ApiRelation>;
104
104
  * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a
105
105
  * different @webpieces version than the tooling itself).
106
106
  */
107
- export type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';
107
+ export type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external-inbound' | 'external-outbound';
108
108
  /** Side-effect semantics copied from the required third `@Endpoint` argument. */
109
109
  export type EndpointOperation = 'read' | 'write-idempotent' | 'write';
110
110
  /** HTTP verbs understood by the generated contract runtime. */
111
- export type ContractHttpMethod = 'GET' | 'POST';
111
+ export type ContractHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
112
+ /** Every verb `@Endpoint` accepts, mirroring core-util's `HTTP_METHODS`. */
113
+ export declare const CONTRACT_HTTP_METHODS: readonly ContractHttpMethod[];
114
+ /** Mirrors core-util's `verbCarriesBody`: POST, PUT and PATCH carry the one unannotated body argument. */
115
+ export declare function contractVerbCarriesBody(method: ContractHttpMethod): boolean;
112
116
  /** One path/query/body mapping emitted into architecture/dependencies.json. */
113
117
  export interface ApiParameterMeta {
114
118
  index: number;
115
- source: 'path' | 'query' | 'body';
116
- /** Path/query wire name; a JSON/form body occupies the whole entity and has no key. */
119
+ source: 'path' | 'query' | 'query-map' | 'body';
120
+ /** Path/query wire name; a JSON/form body and a `@QueryString()` multimap have no key. */
117
121
  wireName?: string;
118
122
  }
119
123
  /**
@@ -133,20 +137,22 @@ export interface ApiMethodMeta {
133
137
  parameters?: ApiParameterMeta[];
134
138
  /** Present when callers receive the transport-neutral full response. */
135
139
  responseType?: 'full';
140
+ /** Present when `@Endpoint(..., { responseBody: NONE })`: a `Promise<void>` method, 0-byte success. */
141
+ responseBody?: 'none';
136
142
  /**
137
143
  * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.
138
144
  *
139
145
  * ONLY on a `cloudtasks` or `cron` method — those are the kinds actually delivered through a
140
146
  * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an
141
- * inbound `external`) endpoint has no queue and needs none; emitting a plausible-looking name for
147
+ * `external-inbound`) endpoint has no queue and needs none; emitting a plausible-looking name for
142
148
  * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being
143
149
  * provisioned as a queue.
144
150
  */
145
151
  queueName?: string;
146
152
  /**
147
- * WHO outside this repo drives this endpoint, from `@Endpoint(POST, p, WRITE, EXTERNAL, { calledBy })`.
153
+ * WHO outside this repo drives this endpoint, from `@Endpoint(POST, p, WRITE, EXTERNAL_INBOUND, { calledBy })`.
148
154
  *
149
- * ONLY on an `external` method, the same way `queueName` is only on the kinds that HAVE a queue.
155
+ * ONLY on an `external-inbound` method, the same way `queueName` is only on the kinds that HAVE a queue.
150
156
  *
151
157
  * Deliberately the SAME {@link ExternalSystemDeclaration} the OUTBOUND `@externalSystem` tag
152
158
  * resolves to, not a parallel inbound-only type: an inbound `saas twilio` and an outbound
@@ -154,7 +160,7 @@ export interface ApiMethodMeta {
154
160
  * converge on ONE node instead of drawing twilio twice facing opposite directions.
155
161
  *
156
162
  * Optional in the TYPE only for graphs generated before the caller was required — generation
157
- * FAILS on an `external` method whose caller cannot be read (UndeclaredExternalCallerError).
163
+ * FAILS on an `external-inbound` method whose caller cannot be read (UndeclaredExternalCallerError).
158
164
  */
159
165
  caller?: ExternalSystemDeclaration;
160
166
  }
@@ -325,6 +331,25 @@ export declare class UndeclaredExternalCaller {
325
331
  at: string);
326
332
  }
327
333
  /** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */
334
+ /**
335
+ * An `@Endpoint` whose `responseBody: NONE` disagrees with its declared return type: NONE on a method
336
+ * that does not return `Promise<void>`, or a `Promise<void>` method without NONE. The runtime cannot
337
+ * see `void` (`design:returntype` is just `Promise`), so the option is the only thing a server and a
338
+ * client act on, and it must not drift from the signature.
339
+ */
340
+ export declare class VoidResponseMismatch {
341
+ readonly api: string;
342
+ readonly method: string;
343
+ /** True when the method declares NONE but does not return `Promise<void>`. */
344
+ readonly declaresNone: boolean;
345
+ /** `path/to/file.ts:LINE`, workspace-relative. */
346
+ readonly at: string;
347
+ constructor(api: string, method: string,
348
+ /** True when the method declares NONE but does not return `Promise<void>`. */
349
+ declaresNone: boolean,
350
+ /** `path/to/file.ts:LINE`, workspace-relative. */
351
+ at: string);
352
+ }
328
353
  export declare class UndeclaredEndpointOperation {
329
354
  /** The contract class the method is declared on. */
330
355
  readonly api: string;
@@ -15,9 +15,10 @@
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.UndeclaredExternalCaller = exports.UnresolvedEndpointPath = exports.NonLiteralDecoratorArg = exports.UnresolvedApiCall = exports.EXTERNAL_SYSTEM_KINDS = void 0;
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;
19
19
  exports.isExternalSystemKind = isExternalSystemKind;
20
20
  exports.apiRefKey = apiRefKey;
21
+ exports.contractVerbCarriesBody = contractVerbCarriesBody;
21
22
  exports.deriveApiRelationKind = deriveApiRelationKind;
22
23
  exports.sortApiRefs = sortApiRefs;
23
24
  /**
@@ -63,6 +64,13 @@ function isExternalSystemKind(value) {
63
64
  function apiRefKey(ref) {
64
65
  return `${ref.api} ${ref.targetService ?? ''}`;
65
66
  }
67
+ /** Every verb `@Endpoint` accepts, mirroring core-util's `HTTP_METHODS`. */
68
+ exports.CONTRACT_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
69
+ /** Mirrors core-util's `verbCarriesBody`: POST, PUT and PATCH carry the one unannotated body argument. */
70
+ // webpieces-disable no-function-outside-class -- pure predicate mirroring the framework rule
71
+ function contractVerbCarriesBody(method) {
72
+ return method === 'POST' || method === 'PUT' || method === 'PATCH';
73
+ }
66
74
  /**
67
75
  * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an
68
76
  * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken
@@ -186,6 +194,29 @@ class UndeclaredExternalCaller {
186
194
  }
187
195
  exports.UndeclaredExternalCaller = UndeclaredExternalCaller;
188
196
  /** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */
197
+ /**
198
+ * An `@Endpoint` whose `responseBody: NONE` disagrees with its declared return type: NONE on a method
199
+ * that does not return `Promise<void>`, or a `Promise<void>` method without NONE. The runtime cannot
200
+ * see `void` (`design:returntype` is just `Promise`), so the option is the only thing a server and a
201
+ * client act on, and it must not drift from the signature.
202
+ */
203
+ class VoidResponseMismatch {
204
+ api;
205
+ method;
206
+ declaresNone;
207
+ at;
208
+ constructor(api, method,
209
+ /** True when the method declares NONE but does not return `Promise<void>`. */
210
+ declaresNone,
211
+ /** `path/to/file.ts:LINE`, workspace-relative. */
212
+ at) {
213
+ this.api = api;
214
+ this.method = method;
215
+ this.declaresNone = declaresNone;
216
+ this.at = at;
217
+ }
218
+ }
219
+ exports.VoidResponseMismatch = VoidResponseMismatch;
189
220
  class UndeclaredEndpointOperation {
190
221
  api;
191
222
  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';\n\n/** Side-effect semantics copied from the required third `@Endpoint` argument. */\nexport type EndpointOperation = 'read' | 'write-idempotent' | 'write';\n\n/** HTTP verbs understood by the generated contract runtime. */\nexport type ContractHttpMethod = 'GET' | 'POST';\n\n/** One path/query/body mapping emitted into architecture/dependencies.json. */\nexport interface ApiParameterMeta {\n index: number;\n source: 'path' | 'query' | 'body';\n /** Path/query wire name; a JSON/form body occupies the whole entity and has no key. */\n wireName?: string;\n}\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /** Retry/idempotency contract; independent of the HTTP verb. */\n operation: EndpointOperation;\n /** Required in newly scanned source; optional only when reading older committed graph data. */\n httpMethod?: ContractHttpMethod;\n /** Explicit parameter mappings; absent only when the method has none. */\n parameters?: ApiParameterMeta[];\n /** Present when callers receive the transport-neutral full response. */\n responseType?: 'full';\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method — those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * inbound `external`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n /**\n * WHO outside this repo drives this endpoint, from `@Endpoint(POST, p, WRITE, EXTERNAL, { calledBy })`.\n *\n * ONLY on an `external` method, the same way `queueName` is only on the kinds that HAVE a queue.\n *\n * Deliberately the SAME {@link ExternalSystemDeclaration} the OUTBOUND `@externalSystem` tag\n * resolves to, not a parallel inbound-only type: an inbound `saas twilio` and an outbound\n * `saas twilio` are the same vendor, so sharing the type makes them share an IDENTITY and\n * converge on ONE node instead of drawing twilio twice facing opposite directions.\n *\n * Optional in the TYPE only for graphs generated before the caller was required — generation\n * FAILS on an `external` method whose caller cannot be read (UndeclaredExternalCallerError).\n */\n caller?: ExternalSystemDeclaration;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints — it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — a vendor seam\n * declaring WHAT it is a seam to. JSDoc rather than a decorator because these seams are plain TS\n * `interface`s, which cannot carry one.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\n/** The `(kind, label)` pair a single declaration resolves to. */\nexport interface ExternalSystemDeclaration {\n kind: ExternalSystemKind;\n label: string;\n}\n\n/**\n * The committed, per-contract view written to `architecture/apis/<ApiName>.json` (one file per API;\n * dependencies.json only links to it under `apiContractFiles` — see ApiContractFiles).\n *\n * The runtime graph is derived from the committed files so generate and validate can never\n * diverge — which means anything the runtime graph needs must be COMMITTED, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it — every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as one `architecture/apis/<ApiName>.json` each. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an\n * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken\n * scan (a real api-lib whose source we never indexed), never a \"this isn't an API\" argument —\n * so it is reported loudly instead of collapsing into a silent `return null`.\n */\nexport class UnresolvedApiCall {\n constructor(\n /** The project whose source makes the call. */\n public readonly project: string,\n /** The contract class name as written at the call site. */\n public readonly api: string,\n /** `path/to/file.ts:LINE` of the call site, workspace-relative. */\n public readonly at: string,\n /** The declaration file the checker resolved to (where decorators are erased). */\n public readonly declaredIn: string,\n ) {}\n}\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class — with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.\n *\n * Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because\n * this one is FATAL. Upstream components need the URL: an http client builds its request as\n * `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata — the same\n * reasoning that already makes basePath required. Skipping the method instead used to delete it, and\n * a class whose every path was a constant lost every method and vanished from the contract table with\n * nothing printed anywhere.\n */\nexport class UnresolvedEndpointPath {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** The path argument exactly as written, e.g. `PROCESS_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `external` `@Endpoint` whose CALLER could not be read from the source.\n *\n * Fatal for the same reason {@link UnresolvedEndpointPath} is. The inbound box exists to say who is\n * calling us from outside; with no caller it can only restate our own contract name, which is the\n * exact bug this diagnostic exists to make impossible to reintroduce. `@Endpoint`'s TS overloads\n * already require `calledBy`, so anything reaching here is a JS caller, an `as any`, a cross-module\n * constant the parser-only scan cannot fold, or an unknown `callerKind`.\n */\nexport class UndeclaredExternalCaller {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, `SOME_CONST`, `callerKind: 'vendor'`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** ONE `@Endpoint` whose required side-effect declaration could not be read from source. */\nexport class UndeclaredEndpointOperation {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** What was wrong, as written — `<missing>`, a constant name, or an invalid literal. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * A contract class that DECLARED `@Endpoint` methods and kept none of them.\n *\n * The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method\n * class (correctly — a vendor seam has no routes), so a class gutted by unreadable decorator\n * arguments left through the same door as a legitimately routeless one. A class that declared\n * endpoints and produced none is never legitimate, so it is named instead.\n */\nexport class EmptiedApiContract {\n constructor(\n /** The contract class name. */\n public readonly api: string,\n /** How many `@Endpoint` decorators were written on it. */\n public readonly declared: number,\n /** `path/to/file.ts:LINE` of the class, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(\n implementsRefs: ApiRef[],\n usesRefs: ApiRef[],\n): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) ||\n (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
1
+ {"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model —\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` — synchronous request/response over HTTP\n * - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a service→service edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * The kinds an external system can be DECLARED as. Each draws its own shape in the runtime viz, so\n * a datastore stops looking like an HTTP service.\n *\n * Lives here rather than beside the runtime graph model because that model already imports from this\n * file; putting it there and importing back would close a module cycle.\n */\nexport const EXTERNAL_SYSTEM_KINDS = [\n 'database',\n 'cache',\n 'queue',\n 'storage',\n 'saas',\n 'system',\n /**\n * A destination whose ADDRESS is supplied at runtime — a URL a partner registered, an OAuth\n * callback, a per-tenant host. Unlike every other kind it does not name one vendor: it names the\n * PLACE in our own system where somebody else's address is dialled, which is the fact a security\n * review is looking for.\n *\n * Declared like every other kind, on the CONTRACT: `@externalSystem runtime partner-webhooks`.\n * On the contract rather than at a `createRpcClient` call site deliberately — \"the far end of\n * this contract is outside our estate\" is a property of the CONTRACT, true for every caller of\n * it, so putting it there means one declaration however many services deliver over it, and\n * nothing to keep in step when a second one appears. It also means this kind rides the exact\n * same declare → resolve → draw pipeline `saas` and `database` already ride, rather than a\n * second mechanism that reads construction sites and can disagree with the first.\n */\n 'runtime',\n] as const;\n\nexport type ExternalSystemKind = (typeof EXTERNAL_SYSTEM_KINDS)[number];\n\n/** True for a string that names one of {@link EXTERNAL_SYSTEM_KINDS}. */\n// webpieces-disable no-function-outside-class -- type guard beside the type it guards, matching this file's DTO style\nexport function isExternalSystemKind(value: string): value is ExternalSystemKind {\n return (EXTERNAL_SYSTEM_KINDS as readonly string[]).includes(value);\n}\n\n/**\n * ONE declared external system, keyed in {@link ExternalSystemDecls} by its IDENTITY.\n *\n * Identity, not display text: two projects each tagged `external:database:postgres` name the same\n * `postgres` node and converge on it with one arrow apiece, instead of drawing a database each.\n *\n * The two arrays are the two declaration sites, and a system may legitimately have both — a repo can\n * wrap a datastore behind a contract in one service and open it directly in another.\n */\nexport interface ExternalSystemDecl {\n kind: ExternalSystemKind;\n label: string;\n /** Contracts declaring it with an `@externalSystem` JSDoc tag; every user of one gets an arrow. */\n apis: string[];\n /** Projects declaring it with an `external:<kind>:<identity>` nx tag; each gets its OWN arrow. */\n projects: string[];\n}\n\n/** identity -> its declaration. Serialized as the `externalSystems` key of dependencies.json. */\nexport type ExternalSystemDecls = Record<string, ExternalSystemDecl>;\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` — it serves the api (a controller extends it)\n * - `uses` — it calls the api (generates a client)\n * - `uses-implements` — it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` → `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api — which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" — the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /** Qualified selected library export that contributed this relationship. */\n declaredVia?: string;\n conditional?: string;\n}\n\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"]}
@@ -29,7 +29,7 @@
29
29
  */
30
30
  import type { EnhancedGraph } from '../graph-sorter';
31
31
  import { ProjectInfo } from '../project-info';
32
- import { ApiClassInfo, ApiContracts, EmptiedApiContract, NonLiteralDecoratorArg, ProjectApiRelations, UndeclaredExternalCaller, UndeclaredEndpointOperation, UnresolvedApiCall, UnresolvedEndpointPath } from './api-relations';
32
+ import { ApiClassInfo, ApiContracts, EmptiedApiContract, NonLiteralDecoratorArg, ProjectApiRelations, UndeclaredExternalCaller, UndeclaredEndpointOperation, VoidResponseMismatch, UnresolvedApiCall, UnresolvedEndpointPath } from './api-relations';
33
33
  import { RootUnionFindings, RootUnionRule } from './root-union-scan';
34
34
  import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
35
35
  import { WireClosureRule } from './wire-closure';
@@ -77,6 +77,8 @@ 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 `responseBody: NONE` disagrees with a `Promise<void>` signature. Fatal. */
81
+ voidResponseMismatches: VoidResponseMismatch[];
80
82
  /** `no-root-union-api-type`'s findings. Fatal in buildApiContracts — see root-union-scan.ts. */
81
83
  rootUnions: RootUnionFindings;
82
84
  /**
@@ -263,6 +263,7 @@ class ApiUsageScanner {
263
263
  emptiedApiContracts: this.decoratorArgDiagnostics.emptiedContracts(),
264
264
  undeclaredExternalCallers: this.decoratorArgDiagnostics.undeclaredExternalCallers(),
265
265
  undeclaredEndpointOperations: this.decoratorArgDiagnostics.undeclaredEndpointOperations(),
266
+ voidResponseMismatches: this.decoratorArgDiagnostics.voidResponseMismatches(),
266
267
  rootUnions: new root_union_scan_1.RootUnionScan(this.workspaceRoot, this.projectInfos, this.rootUnionRule).run(),
267
268
  apiDocRules: new api_doc_rules_scan_1.ApiDocRulesScan(this.workspaceRoot, this.projectInfos, this.openApiRule, this.mcpRule, this.wireClosureRule).run(),
268
269
  };
@@ -448,6 +449,9 @@ function buildApiContracts(scan) {
448
449
  if (scan.undeclaredEndpointOperations.length > 0) {
449
450
  throw new api_contract_errors_1.UndeclaredEndpointOperationError(scan.undeclaredEndpointOperations);
450
451
  }
452
+ if ((scan.voidResponseMismatches ?? []).length > 0) {
453
+ throw new api_contract_errors_1.VoidResponseMismatchError(scan.voidResponseMismatches);
454
+ }
451
455
  // A shape no function-calling API will accept, read perfectly well — unlike the four above, which
452
456
  // are contracts the scan could not READ at all.
453
457
  if (!scan.rootUnions.isEmpty())
@@ -519,8 +523,9 @@ function describeNonLiteralDecoratorArgs(args) {
519
523
  // webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls
520
524
  function describeMismatchedEndpointKinds(contracts) {
521
525
  const allowedByKind = {
522
- rpc: ['rpc', 'external'],
523
- pubsub: ['cloudtasks', 'cron', 'external'],
526
+ rpc: ['rpc', 'external-inbound', 'external-outbound'],
527
+ pubsub: ['cloudtasks', 'cron', 'external-inbound'],
528
+ external: ['external-outbound'],
524
529
  };
525
530
  const problems = [];
526
531
  for (const api of Object.keys(contracts)) {