vouchington-tooling 0.20.0 → 0.22.0

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 (257) hide show
  1. package/README.md +73 -10
  2. package/dist/agent-blackboard/snapshot-partition-format.mjs +2 -72
  3. package/dist/agent-blackboard/snapshot-partition-guards.d.mts +3 -0
  4. package/dist/agent-blackboard/snapshot-partition-guards.mjs +9 -0
  5. package/dist/agent-blackboard/snapshot-partition-predicates.d.mts +3 -0
  6. package/dist/agent-blackboard/snapshot-partition-predicates.mjs +65 -0
  7. package/dist/api-contract-discovery/header-contract-parser.d.mts +3 -0
  8. package/dist/api-contract-discovery/header-contract-parser.mjs +93 -0
  9. package/dist/api-contract-discovery/header-contract-registry.d.mts +3 -0
  10. package/dist/api-contract-discovery/header-contract-registry.mjs +38 -0
  11. package/dist/api-contract-discovery/header-contract-types.d.mts +19 -0
  12. package/dist/api-contract-discovery/header-contract-types.mjs +1 -0
  13. package/dist/api-contract-discovery/index.d.mts +31 -0
  14. package/dist/api-contract-discovery/index.mjs +26 -0
  15. package/dist/api-contract-discovery/query-contract-extraction.d.mts +3 -0
  16. package/dist/api-contract-discovery/query-contract-extraction.mjs +65 -0
  17. package/dist/api-contract-discovery/query-contract-literals.d.mts +8 -0
  18. package/dist/api-contract-discovery/query-contract-literals.mjs +54 -0
  19. package/dist/api-contract-discovery/query-contract-registry.d.mts +3 -0
  20. package/dist/api-contract-discovery/query-contract-registry.mjs +68 -0
  21. package/dist/api-contract-discovery/query-contract-types.d.mts +8 -0
  22. package/dist/api-contract-discovery/query-contract-types.mjs +1 -0
  23. package/dist/api-contract-discovery/registered-route-catalog.d.mts +10 -0
  24. package/dist/api-contract-discovery/registered-route-catalog.mjs +81 -0
  25. package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +2 -0
  26. package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +74 -0
  27. package/dist/api-contract-discovery/request-contract-implicit.d.mts +17 -0
  28. package/dist/api-contract-discovery/request-contract-implicit.mjs +73 -0
  29. package/dist/api-contract-discovery/request-contract-lenient.d.mts +28 -0
  30. package/dist/api-contract-discovery/request-contract-lenient.mjs +59 -0
  31. package/dist/api-contract-discovery/request-contract-registry.d.mts +14 -0
  32. package/dist/api-contract-discovery/request-contract-registry.mjs +61 -0
  33. package/dist/api-contract-discovery/request-contract-route-analysis.d.mts +25 -0
  34. package/dist/api-contract-discovery/request-contract-route-analysis.mjs +68 -0
  35. package/dist/api-contract-discovery/request-contract-types.d.mts +12 -0
  36. package/dist/api-contract-discovery/request-contract-types.mjs +1 -0
  37. package/dist/api-contract-discovery/response-contract-ctx-analysis.d.mts +7 -0
  38. package/dist/api-contract-discovery/response-contract-ctx-analysis.mjs +28 -0
  39. package/dist/api-contract-discovery/response-contract-error-branch.d.mts +15 -0
  40. package/dist/api-contract-discovery/response-contract-error-branch.mjs +47 -0
  41. package/dist/api-contract-discovery/response-contract-implicit-sources.test-helpers.d.mts +145 -0
  42. package/dist/api-contract-discovery/response-contract-implicit-sources.test-helpers.mjs +145 -0
  43. package/dist/api-contract-discovery/response-contract-implicit.d.mts +5 -0
  44. package/dist/api-contract-discovery/response-contract-implicit.mjs +110 -0
  45. package/dist/api-contract-discovery/response-contract-lenient.d.mts +23 -0
  46. package/dist/api-contract-discovery/response-contract-lenient.mjs +52 -0
  47. package/dist/api-contract-discovery/response-contract-media.d.mts +3 -0
  48. package/dist/api-contract-discovery/response-contract-media.mjs +46 -0
  49. package/dist/api-contract-discovery/response-contract-registration.d.mts +11 -0
  50. package/dist/api-contract-discovery/response-contract-registration.mjs +61 -0
  51. package/dist/api-contract-discovery/response-contract-registry.d.mts +7 -0
  52. package/dist/api-contract-discovery/response-contract-registry.mjs +76 -0
  53. package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +13 -0
  54. package/dist/api-contract-discovery/response-contract-route-analysis.mjs +144 -0
  55. package/dist/api-contract-discovery/response-contract-status.d.mts +27 -0
  56. package/dist/api-contract-discovery/response-contract-status.mjs +80 -0
  57. package/dist/api-contract-discovery/response-contract-types.d.mts +16 -0
  58. package/dist/api-contract-discovery/response-contract-types.mjs +1 -0
  59. package/dist/api-contract-discovery/test-setup.test-helpers.d.mts +3 -0
  60. package/dist/api-contract-discovery/test-setup.test-helpers.mjs +2 -0
  61. package/dist/ast-grep-examples/companion-parity-compare.d.mts +3 -0
  62. package/dist/ast-grep-examples/companion-parity-compare.mjs +58 -0
  63. package/dist/ast-grep-examples/companion-parity.d.mts +2 -1
  64. package/dist/ast-grep-examples/companion-parity.mjs +2 -58
  65. package/dist/browser-session-runner/attempt.mjs +29 -48
  66. package/dist/browser-session-runner/termination.d.mts +11 -0
  67. package/dist/browser-session-runner/termination.mjs +34 -0
  68. package/dist/cli/parse-direct-options.d.mts +26 -0
  69. package/dist/cli/parse-direct-options.mjs +72 -0
  70. package/dist/cli/parse-forwarded-commands.d.mts +42 -0
  71. package/dist/cli/parse-forwarded-commands.mjs +29 -0
  72. package/dist/cli/parse-options.d.mts +1 -2
  73. package/dist/cli/parse-options.mjs +2 -72
  74. package/dist/cli/parse.mjs +4 -26
  75. package/dist/compiler-build/freshness.d.mts +27 -0
  76. package/dist/compiler-build/freshness.mjs +116 -0
  77. package/dist/compiler-build/index.d.mts +2 -0
  78. package/dist/compiler-build/index.mjs +2 -0
  79. package/dist/compiler-build/settlement.d.mts +10 -0
  80. package/dist/compiler-build/settlement.mjs +14 -0
  81. package/dist/config-inventory/call-arguments.d.mts +1 -0
  82. package/dist/config-inventory/call-arguments.mjs +30 -0
  83. package/dist/config-inventory/collect.d.mts +3 -0
  84. package/dist/config-inventory/collect.mjs +43 -0
  85. package/dist/config-inventory/dynamic-configs.d.mts +2 -0
  86. package/dist/config-inventory/dynamic-configs.mjs +9 -0
  87. package/dist/config-inventory/env-expression-readers.d.mts +3 -0
  88. package/dist/config-inventory/env-expression-readers.mjs +117 -0
  89. package/dist/config-inventory/env-prefix-readers.d.mts +2 -0
  90. package/dist/config-inventory/env-prefix-readers.mjs +14 -0
  91. package/dist/config-inventory/env-reference-matchers.d.mts +3 -0
  92. package/dist/config-inventory/env-reference-matchers.mjs +9 -0
  93. package/dist/config-inventory/env-var-accumulator.d.mts +6 -0
  94. package/dist/config-inventory/env-var-accumulator.mjs +82 -0
  95. package/dist/config-inventory/env-vars.d.mts +4 -0
  96. package/dist/config-inventory/env-vars.mjs +108 -0
  97. package/dist/config-inventory/format.d.mts +2 -0
  98. package/dist/config-inventory/format.mjs +69 -0
  99. package/dist/config-inventory/index.d.mts +3 -0
  100. package/dist/config-inventory/index.mjs +2 -0
  101. package/dist/config-inventory/package-gates.d.mts +2 -0
  102. package/dist/config-inventory/package-gates.mjs +54 -0
  103. package/dist/config-inventory/shared.d.mts +3 -0
  104. package/dist/config-inventory/shared.mjs +7 -0
  105. package/dist/config-inventory/types.d.mts +96 -0
  106. package/dist/config-inventory/types.mjs +1 -0
  107. package/dist/contract-schema/contract-schema-bounded-array.d.mts +15 -0
  108. package/dist/contract-schema/contract-schema-bounded-array.mjs +32 -0
  109. package/dist/contract-schema/contract-schema-type-extractor.d.mts +2 -8
  110. package/dist/contract-schema/contract-schema-type-extractor.mjs +2 -31
  111. package/dist/coverage-transport/control-v2-parse-object.d.mts +12 -0
  112. package/dist/coverage-transport/control-v2-parse-object.mjs +28 -0
  113. package/dist/coverage-transport/control-v2.mjs +2 -27
  114. package/dist/dependency-license-policy/audit-directory.d.mts +28 -0
  115. package/dist/dependency-license-policy/audit-directory.mjs +115 -0
  116. package/dist/dependency-license-policy/audit-signals.d.mts +12 -0
  117. package/dist/dependency-license-policy/audit-signals.mjs +44 -0
  118. package/dist/dependency-license-policy/audit-store.d.mts +12 -0
  119. package/dist/dependency-license-policy/audit-store.mjs +32 -0
  120. package/dist/dependency-license-policy/collect.d.mts +3 -1
  121. package/dist/dependency-license-policy/collect.mjs +16 -21
  122. package/dist/dependency-license-policy/execute-pnpm.d.mts +10 -0
  123. package/dist/dependency-license-policy/execute-pnpm.mjs +83 -0
  124. package/dist/dependency-license-policy/types.d.mts +11 -9
  125. package/dist/dependency-license-policy/workspace.d.mts +3 -1
  126. package/dist/dependency-license-policy/workspace.mjs +12 -5
  127. package/dist/gh-api-shell-quoting/argument-scan.d.mts +7 -0
  128. package/dist/gh-api-shell-quoting/argument-scan.mjs +35 -0
  129. package/dist/gh-api-shell-quoting/logical-line.d.mts +3 -0
  130. package/dist/gh-api-shell-quoting/logical-line.mjs +90 -0
  131. package/dist/gh-api-shell-quoting/scan.d.mts +2 -4
  132. package/dist/gh-api-shell-quoting/scan.mjs +1 -119
  133. package/dist/gh-cli/diff.d.mts +24 -4
  134. package/dist/gh-cli/diff.mjs +111 -4
  135. package/dist/gh-cli/index.d.mts +2 -1
  136. package/dist/gh-cli/index.mjs +1 -1
  137. package/dist/gha-workspace-policy/docker-options.d.mts +4 -0
  138. package/dist/gha-workspace-policy/docker-options.mjs +93 -0
  139. package/dist/gha-workspace-policy/docker-workspace-user.mjs +1 -168
  140. package/dist/gha-workspace-policy/shell-token-modes.d.mts +10 -0
  141. package/dist/gha-workspace-policy/shell-token-modes.mjs +31 -0
  142. package/dist/gha-workspace-policy/shell-tokens.d.mts +1 -0
  143. package/dist/gha-workspace-policy/shell-tokens.mjs +65 -0
  144. package/dist/index.d.mts +2 -1
  145. package/dist/index.mjs +1 -1
  146. package/dist/openapi-document/contract-schema-types.d.mts +1 -1
  147. package/dist/openapi-document/openapi-types.d.mts +1 -1
  148. package/dist/pnpm-install/native-family.d.mts +3 -0
  149. package/dist/pnpm-install/native-family.mjs +31 -0
  150. package/dist/pnpm-install/native-health.d.mts +1 -4
  151. package/dist/pnpm-install/native-health.mjs +2 -31
  152. package/dist/retrospective-facts/local-merge.d.mts +2 -0
  153. package/dist/retrospective-facts/local-merge.mjs +21 -0
  154. package/dist/retrospective-facts/local.mjs +1 -21
  155. package/dist/retrospective-transcript/claude-blocks.d.mts +3 -0
  156. package/dist/retrospective-transcript/claude-blocks.mjs +38 -0
  157. package/dist/retrospective-transcript/claude.mjs +4 -34
  158. package/dist/retrospective-transcript/codex-payload.d.mts +2 -0
  159. package/dist/retrospective-transcript/codex-payload.mjs +51 -0
  160. package/dist/retrospective-transcript/codex-records.d.mts +2 -0
  161. package/dist/retrospective-transcript/codex-records.mjs +95 -0
  162. package/dist/retrospective-transcript/codex.mjs +2 -144
  163. package/dist/retrospective-transcript/index.d.mts +3 -16
  164. package/dist/retrospective-transcript/index.mjs +4 -48
  165. package/dist/retrospective-transcript/resolve-transcript-file.d.mts +19 -0
  166. package/dist/retrospective-transcript/resolve-transcript-file.mjs +50 -0
  167. package/dist/session-friction/command-segments.d.mts +3 -0
  168. package/dist/session-friction/command-segments.mjs +94 -0
  169. package/dist/session-friction/log-file.d.mts +7 -0
  170. package/dist/session-friction/log-file.mjs +83 -0
  171. package/dist/session-friction/log.mjs +3 -83
  172. package/dist/session-friction/normalize.mjs +1 -94
  173. package/dist/sql-scanner/comments.d.mts +1 -0
  174. package/dist/sql-scanner/comments.mjs +101 -0
  175. package/dist/sql-scanner/index.d.mts +1 -1
  176. package/dist/sql-scanner/index.mjs +1 -100
  177. package/dist/sql-scanner/literals.mjs +1 -35
  178. package/dist/sql-scanner/quote-decode.d.mts +2 -0
  179. package/dist/sql-scanner/quote-decode.mjs +35 -0
  180. package/dist/vitest-diagnostics/index.d.mts +7 -0
  181. package/dist/vitest-diagnostics/index.mjs +4 -0
  182. package/dist/vitest-diagnostics/vitest-fork-exit-error-detail.d.mts +7 -0
  183. package/dist/vitest-diagnostics/vitest-fork-exit-error-detail.mjs +26 -0
  184. package/dist/vitest-diagnostics/vitest-fork-exit-records.d.mts +26 -0
  185. package/dist/vitest-diagnostics/vitest-fork-exit-records.mjs +161 -0
  186. package/dist/vitest-diagnostics/vitest-fork-exit-sentinel.d.mts +14 -0
  187. package/dist/vitest-diagnostics/vitest-fork-exit-sentinel.mjs +131 -0
  188. package/dist/vitest-diagnostics/vitest-fork-leak-detection.d.mts +26 -0
  189. package/dist/vitest-diagnostics/vitest-fork-leak-detection.mjs +96 -0
  190. package/dist/vitest-diagnostics/vitest-fork-leak-diagnostics.d.mts +3 -0
  191. package/dist/vitest-diagnostics/vitest-fork-leak-diagnostics.mjs +14 -0
  192. package/dist/vitest-diagnostics/vitest-fork-leak-episode.d.mts +37 -0
  193. package/dist/vitest-diagnostics/vitest-fork-leak-episode.mjs +118 -0
  194. package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-errors.d.mts +2 -0
  195. package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-errors.mjs +42 -0
  196. package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-reporter.d.mts +12 -0
  197. package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-reporter.mjs +142 -0
  198. package/dist/workflow-policy/concurrency-scope.d.mts +4 -0
  199. package/dist/workflow-policy/concurrency-scope.mjs +66 -0
  200. package/dist/workflow-policy/concurrency.d.mts +10 -0
  201. package/dist/workflow-policy/concurrency.mjs +77 -0
  202. package/dist/workflow-policy/condition-entailment.d.mts +20 -0
  203. package/dist/workflow-policy/condition-entailment.mjs +132 -0
  204. package/dist/workflow-policy/condition-split.d.mts +15 -0
  205. package/dist/workflow-policy/condition-split.mjs +71 -0
  206. package/dist/workflow-policy/graph.d.mts +2 -0
  207. package/dist/workflow-policy/graph.mjs +138 -0
  208. package/dist/workflow-policy/index.d.mts +7 -0
  209. package/dist/workflow-policy/index.mjs +6 -0
  210. package/dist/workflow-policy/permission-mismatches.d.mts +3 -0
  211. package/dist/workflow-policy/permission-mismatches.mjs +52 -0
  212. package/dist/workflow-policy/permissions.d.mts +16 -0
  213. package/dist/workflow-policy/permissions.mjs +83 -0
  214. package/dist/workflow-policy/secret-readiness.d.mts +18 -0
  215. package/dist/workflow-policy/secret-readiness.mjs +163 -0
  216. package/dist/workflow-policy/shell-comments.d.mts +7 -0
  217. package/dist/workflow-policy/shell-comments.mjs +28 -0
  218. package/dist/workflow-policy/types.d.mts +90 -0
  219. package/dist/workflow-policy/types.mjs +1 -0
  220. package/dist/workspace-gates/manifest-version-parser.mjs +1 -52
  221. package/dist/workspace-gates/manifest-version-skips.d.mts +1 -0
  222. package/dist/workspace-gates/manifest-version-skips.mjs +52 -0
  223. package/docs/workflow-and-compiler.md +74 -0
  224. package/package.json +26 -2
  225. package/skills/agent-workflow/SKILL.md +4 -4
  226. package/skills/agent-workflow/references/review-response.md +47 -44
  227. package/skills/backend-vitest-test-authoring/SKILL.md +9 -6
  228. package/skills/blackboard/SKILL.md +8 -8
  229. package/skills/dependabot/SKILL.md +68 -67
  230. package/skills/dotnet-test-authoring/SKILL.md +11 -12
  231. package/skills/git-commit-checklist/SKILL.md +3 -3
  232. package/skills/github-actions-authoring/SKILL.md +40 -38
  233. package/skills/github-actions-checklist/SKILL.md +93 -85
  234. package/skills/github-issue/SKILL.md +82 -78
  235. package/skills/nextjs-vitest-test-authoring/SKILL.md +8 -6
  236. package/skills/npm-publishing/SKILL.md +42 -38
  237. package/skills/organize-github-issues/SKILL.md +18 -22
  238. package/skills/package-json-checklist/SKILL.md +5 -5
  239. package/skills/planning/SKILL.md +3 -3
  240. package/skills/playwright-authoring/SKILL.md +10 -9
  241. package/skills/postgres-node-performance-tuning/SKILL.md +10 -6
  242. package/skills/postgres-partitioning-uuid-v7/SKILL.md +10 -7
  243. package/skills/pr-description/SKILL.md +4 -3
  244. package/skills/retrospective/SKILL.md +14 -15
  245. package/skills/retrospective-distill/SKILL.md +14 -14
  246. package/skills/review-ci-logs/SKILL.md +18 -19
  247. package/skills/review-github-issue-taxonomy/SKILL.md +21 -18
  248. package/skills/revisit-followups/SKILL.md +3 -3
  249. package/skills/stacked-prs/SKILL.md +30 -61
  250. package/skills/static-analysis-checklist/SKILL.md +6 -4
  251. package/skills/storybook-authoring/SKILL.md +9 -9
  252. package/skills/swift-test-authoring/SKILL.md +10 -9
  253. package/skills/test-authoring/SKILL.md +14 -14
  254. package/skills/test-authoring/references/core-practice.md +12 -11
  255. package/skills/test-authoring/references/dependency-boundaries.md +36 -35
  256. package/skills/test-authoring/references/tautological-tests.md +45 -44
  257. package/skills/vitest-test-authoring/SKILL.md +9 -6
@@ -0,0 +1,68 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { isContextMethod } from './response-contract-route-analysis.mjs';
3
+ /**
4
+ * Detects the request-contract markers in the version-one route convention.
5
+ * Kept out of `responseMarker` (which only recognizes `apiResponse`/`apiNoContent`) so the two
6
+ * AST walks never interfere — see `response-contract-route-analysis.mts` for the response side.
7
+ */
8
+ export function requestMarker(expression) {
9
+ if (!ts.isIdentifier(expression))
10
+ return undefined;
11
+ if (expression.text === 'apiRequest' ||
12
+ expression.text === 'apiRequestContract' ||
13
+ expression.text === 'apiNoRequestBody')
14
+ return expression.text;
15
+ return undefined;
16
+ }
17
+ /**
18
+ * Detects `ctx.request.json(...)`: a two-level nested access (`ctx.request` then `.json`) that
19
+ * `isContextMethod`'s single-level match can't see through directly — mirrors
20
+ * `isContextResponseBufferCall`'s own two-level pattern on the response side.
21
+ */
22
+ export function isContextRequestJsonCall(expression) {
23
+ if (!ts.isPropertyAccessExpression(expression) || expression.name.text !== 'json')
24
+ return false;
25
+ return isContextMethod(expression.expression, 'request');
26
+ }
27
+ /** Detects `ctx.request.buffer(...)`: a raw request body whose shape can't be statically extracted. */
28
+ export function isContextRequestBufferCall(expression) {
29
+ if (!ts.isPropertyAccessExpression(expression) || expression.name.text !== 'buffer')
30
+ return false;
31
+ return isContextMethod(expression.expression, 'request');
32
+ }
33
+ /**
34
+ * Walks up from a `ctx.request.json(...)` call expression to find the enclosing `as T` cast that
35
+ * gives it a static type, unwrapping the wrapper shapes real call sites use in between:
36
+ * `(await ctx.request.json(...)) as T` and the `.catch()`-guarded
37
+ * `(await ctx.request.json(...).catch(() => ({}))) as T`.
38
+ * Returns `undefined` when no enclosing cast exists (e.g. the read feeds directly into another
39
+ * call, `parseFoo(await ctx.request.json())`) — the caller treats that as an honest untyped
40
+ * success, not a failure.
41
+ */
42
+ export function enclosingRequestBodyCast(call) {
43
+ let current = call;
44
+ while (true) {
45
+ const parent = current.parent;
46
+ if (!parent)
47
+ return undefined;
48
+ if (ts.isParenthesizedExpression(parent) && parent.expression === current) {
49
+ current = parent;
50
+ continue;
51
+ }
52
+ if (ts.isAwaitExpression(parent) && parent.expression === current) {
53
+ current = parent;
54
+ continue;
55
+ }
56
+ if (ts.isPropertyAccessExpression(parent) &&
57
+ parent.name.text === 'catch' &&
58
+ parent.expression === current &&
59
+ ts.isCallExpression(parent.parent) &&
60
+ parent.parent.expression === parent) {
61
+ current = parent.parent;
62
+ continue;
63
+ }
64
+ if (ts.isAsExpression(parent) && parent.expression === current)
65
+ return parent;
66
+ return undefined;
67
+ }
68
+ }
@@ -0,0 +1,12 @@
1
+ import type { ExtractedResponseContract } from '../contract-schema/index.mts';
2
+ /**
3
+ * A backend route's request-body contract. Mirrors `BackendResponseContract` minus the
4
+ * status/variant concepts responses have — a request has no status code and (unlike a response)
5
+ * no `#variant` escape hatch, so every route resolves to at most one `BackendRequestContract`.
6
+ */
7
+ export type BackendRequestContract = ExtractedResponseContract & {
8
+ method: string;
9
+ routeTemplate: string;
10
+ /** Set when the route's request schema could not be extracted (lenient discovery mode), or the route reads a raw non-JSON buffer body. */
11
+ unavailableReason?: string;
12
+ };
@@ -0,0 +1,7 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ export declare function responseMarker(expression: ts.Expression): 'apiOpenApiRawResponse' | 'apiResponse' | 'apiNoContent' | undefined;
3
+ export declare function isContextMethod(expression: ts.Expression, method: string): boolean;
4
+ /** Detects the framework's explicit no-body signal independently of the response status. */
5
+ export declare function isContextResponseEmptyCall(expression: ts.Expression): boolean;
6
+ /** Detects `ctx.response.buffer(...)`: a raw pass-through body whose shape can't be statically extracted. */
7
+ export declare function isContextResponseBufferCall(expression: ts.Expression): boolean;
@@ -0,0 +1,28 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ export function responseMarker(expression) {
3
+ if (!ts.isIdentifier(expression))
4
+ return undefined;
5
+ if (expression.text === 'apiOpenApiRawResponse' ||
6
+ expression.text === 'apiResponse' ||
7
+ expression.text === 'apiNoContent')
8
+ return expression.text;
9
+ return undefined;
10
+ }
11
+ export function isContextMethod(expression, method) {
12
+ return (ts.isPropertyAccessExpression(expression) &&
13
+ ts.isIdentifier(expression.expression) &&
14
+ expression.expression.text === 'ctx' &&
15
+ expression.name.text === method);
16
+ }
17
+ /** Detects the framework's explicit no-body signal independently of the response status. */
18
+ export function isContextResponseEmptyCall(expression) {
19
+ if (!ts.isPropertyAccessExpression(expression) || expression.name.text !== 'empty')
20
+ return false;
21
+ return isContextMethod(expression.expression, 'response');
22
+ }
23
+ /** Detects `ctx.response.buffer(...)`: a raw pass-through body whose shape can't be statically extracted. */
24
+ export function isContextResponseBufferCall(expression) {
25
+ if (!ts.isPropertyAccessExpression(expression) || expression.name.text !== 'buffer')
26
+ return false;
27
+ return isContextMethod(expression.expression, 'response');
28
+ }
@@ -0,0 +1,15 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /**
3
+ * Detects a hand-rolled early-return error guard: `ctx.setStatus(NNN >= 400)` followed by a
4
+ * response body call in the same block (e.g. `auth-me.mts`'s `ctx.setStatus(401); return
5
+ * ctx.json({error})` before its `ctx.json({user})` success body). Skipping these keeps the
6
+ * bare implicit key free for the route's actual success body instead of locking onto whichever
7
+ * error branch happens to appear first in document order.
8
+ *
9
+ * Deliberately narrow: only a *direct* sibling `ExpressionStatement` counts, not a nested one
10
+ * inside an earlier sibling's own subtree (e.g. inside an earlier, unrelated `if` guard's own
11
+ * block). Recursing into nested subtrees would flag a trailing success statement that follows
12
+ * two independent, self-contained early-return guards, even though neither guard's error status
13
+ * is in effect on the path that reaches the trailing statement.
14
+ */
15
+ export declare function isInErrorBranch(call: ts.CallExpression): boolean;
@@ -0,0 +1,47 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { isContextMethod } from './response-contract-route-analysis.mjs';
3
+ /**
4
+ * Detects a hand-rolled early-return error guard: `ctx.setStatus(NNN >= 400)` followed by a
5
+ * response body call in the same block (e.g. `auth-me.mts`'s `ctx.setStatus(401); return
6
+ * ctx.json({error})` before its `ctx.json({user})` success body). Skipping these keeps the
7
+ * bare implicit key free for the route's actual success body instead of locking onto whichever
8
+ * error branch happens to appear first in document order.
9
+ *
10
+ * Deliberately narrow: only a *direct* sibling `ExpressionStatement` counts, not a nested one
11
+ * inside an earlier sibling's own subtree (e.g. inside an earlier, unrelated `if` guard's own
12
+ * block). Recursing into nested subtrees would flag a trailing success statement that follows
13
+ * two independent, self-contained early-return guards, even though neither guard's error status
14
+ * is in effect on the path that reaches the trailing statement.
15
+ */
16
+ export function isInErrorBranch(call) {
17
+ let statement = call;
18
+ while (!ts.isStatement(statement))
19
+ statement = statement.parent;
20
+ return precededByErrorStatus(statement);
21
+ }
22
+ /**
23
+ * Walks outward through enclosing blocks so a `ctx.setStatus(>=400)` set before the branch's own
24
+ * containing statement — not just a direct sibling in the same block — still marks the branch as
25
+ * an error path. Needed for a shared helper like `data-request.mts`'s `sendActiveRequestConflict`,
26
+ * which sets one error status up front, then branches into two differently-shaped `ctx.json`
27
+ * bodies nested one block deeper.
28
+ */
29
+ function precededByErrorStatus(statement) {
30
+ const block = statement.parent;
31
+ if (!ts.isBlock(block))
32
+ return false;
33
+ const index = block.statements.indexOf(statement);
34
+ if (block.statements.slice(0, index).some(isBareErrorStatusStatement))
35
+ return true;
36
+ const enclosing = block.parent;
37
+ return ts.isStatement(enclosing) ? precededByErrorStatus(enclosing) : false;
38
+ }
39
+ function isBareErrorStatusStatement(statement) {
40
+ if (!ts.isExpressionStatement(statement))
41
+ return false;
42
+ const expression = statement.expression;
43
+ if (!ts.isCallExpression(expression) || !isContextMethod(expression.expression, 'setStatus'))
44
+ return false;
45
+ const statusArgument = expression.arguments[0];
46
+ return (!!statusArgument && ts.isNumericLiteral(statusArgument) && Number(statusArgument.text) >= 400);
47
+ }
@@ -0,0 +1,145 @@
1
+ export declare const responseContractImplicitSources: {
2
+ readonly 'called-helper': `
3
+ declare const app: any
4
+ async function handleCreate(ctx: any) {
5
+ ctx.setStatus(201)
6
+ return { id: 'one' as string }
7
+ }
8
+ app.route('/api/v1/items').post(async (ctx: any) => {
9
+ ctx.json(await handleCreate(ctx))
10
+ })
11
+ `;
12
+ readonly 'shared-helper': `
13
+ declare const app: any
14
+ function markCreated(ctx: any) { ctx.setStatus(201) }
15
+ app.route('/api/v1/items').post((ctx: any) => {
16
+ markCreated(ctx)
17
+ ctx.json({ id: 'one' as string })
18
+ })
19
+ app.route('/api/v1/widgets').post((ctx: any) => {
20
+ markCreated(ctx)
21
+ ctx.json({ id: 'one' as string })
22
+ })
23
+ `;
24
+ readonly empty: `
25
+ declare const app: any
26
+ app.route('/api/v1/items/:id').delete((ctx: any) => {
27
+ ctx.setStatus(200)
28
+ ctx.response.empty()
29
+ })
30
+ `;
31
+ readonly 'error-guard': `
32
+ declare const app: any
33
+ app.route('/api/v1/auth/me').get((ctx: any) => {
34
+ if (!ctx.user) {
35
+ ctx.setStatus(401)
36
+ return ctx.json({ error: 'not authenticated' as string })
37
+ }
38
+ ctx.json({ user: { id: 'one' as string } })
39
+ })
40
+ `;
41
+ readonly 'two-guards': `
42
+ declare const app: any
43
+ app.route('/api/v1/items/:id').get((ctx: any) => {
44
+ if (!ctx.user) {
45
+ ctx.setStatus(401)
46
+ return ctx.json({ error: 'not authenticated' as string })
47
+ }
48
+ if (!ctx.item) {
49
+ ctx.setStatus(404)
50
+ return ctx.json({ error: 'not found' as string })
51
+ }
52
+ ctx.json({ item: { id: 'one' as string } })
53
+ })
54
+ `;
55
+ readonly conflict: `
56
+ declare const app: any
57
+ const sendConflict = (ctx: any, existing: any) => {
58
+ ctx.setStatus(409)
59
+ if (!existing) {
60
+ ctx.json({ error: 'conflict' as string })
61
+ return
62
+ }
63
+ ctx.json({ error: 'conflict' as string, id: 'x' as string })
64
+ }
65
+ app.route('/api/v1/items').post((ctx: any) => {
66
+ const conflict: any = {}
67
+ if (conflict.existing !== undefined) {
68
+ sendConflict(ctx, conflict.existing)
69
+ return
70
+ }
71
+ ctx.setStatus(201)
72
+ ctx.json({ id: 'one' as string, status: 'pending' as string })
73
+ })
74
+ `;
75
+ readonly ternary: `
76
+ declare const app: any
77
+ declare const created: boolean
78
+ app.route('/api/v1/items').post((ctx: any) => {
79
+ ctx.setStatus(created ? 201 : 200)
80
+ ctx.json({ id: 'one' as string })
81
+ })
82
+ `;
83
+ readonly variants: `
84
+ declare const app: any
85
+ declare function apiResponse<K extends string, T>(key: K, body: T): T
86
+ app.route('/api/v1/items').post((ctx: any) => {
87
+ ctx.setStatus(422)
88
+ apiResponse('POST:/api/v1/items#validation', { error: 'invalid' as string })
89
+ ctx.setStatus(201)
90
+ apiResponse('POST:/api/v1/items', { id: 'one' as string })
91
+ })
92
+ `;
93
+ readonly branches: `
94
+ declare const app: any
95
+ app.route('/api/v1/webhooks/:id').post((ctx: any) => {
96
+ if (ctx.body.event === 'known') {
97
+ ctx.json({ received: true as boolean })
98
+ return
99
+ }
100
+ ctx.json({ ignored: true as boolean })
101
+ })
102
+ `;
103
+ readonly 'secondary-failure': `
104
+ declare const app: any
105
+ declare function riskyLookup(): any
106
+ app.route('/api/v1/jobs/:id').get((ctx: any) => {
107
+ if (ctx.query.mode === 'basic') {
108
+ ctx.json({ ready: true as boolean })
109
+ return
110
+ }
111
+ ctx.json({ value: riskyLookup() })
112
+ })
113
+ `;
114
+ readonly 'empty-then-buffer': `
115
+ declare const app: any
116
+ declare const body: any
117
+ declare const contentType: string
118
+ app.route('/api/v1/mcp').post((ctx: any) => {
119
+ if (body.byteLength === 0) {
120
+ ctx.response.empty()
121
+ } else {
122
+ ctx.response.buffer(body, contentType)
123
+ }
124
+ })
125
+ `;
126
+ readonly 'buffer-then-empty': `
127
+ declare const app: any
128
+ declare const body: any
129
+ declare const contentType: string
130
+ app.route('/api/v1/mcp').post((ctx: any) => {
131
+ if (body.byteLength > 0) {
132
+ ctx.response.buffer(body, contentType)
133
+ } else {
134
+ ctx.response.empty()
135
+ }
136
+ })
137
+ `;
138
+ readonly 'single-failure': `
139
+ declare const app: any
140
+ declare function riskyLookup(): any
141
+ app.route('/api/v1/jobs/:id').get((ctx: any) => {
142
+ ctx.json({ value: riskyLookup() })
143
+ })
144
+ `;
145
+ };
@@ -0,0 +1,145 @@
1
+ export const responseContractImplicitSources = {
2
+ 'called-helper': `
3
+ declare const app: any
4
+ async function handleCreate(ctx: any) {
5
+ ctx.setStatus(201)
6
+ return { id: 'one' as string }
7
+ }
8
+ app.route('/api/v1/items').post(async (ctx: any) => {
9
+ ctx.json(await handleCreate(ctx))
10
+ })
11
+ `,
12
+ 'shared-helper': `
13
+ declare const app: any
14
+ function markCreated(ctx: any) { ctx.setStatus(201) }
15
+ app.route('/api/v1/items').post((ctx: any) => {
16
+ markCreated(ctx)
17
+ ctx.json({ id: 'one' as string })
18
+ })
19
+ app.route('/api/v1/widgets').post((ctx: any) => {
20
+ markCreated(ctx)
21
+ ctx.json({ id: 'one' as string })
22
+ })
23
+ `,
24
+ empty: `
25
+ declare const app: any
26
+ app.route('/api/v1/items/:id').delete((ctx: any) => {
27
+ ctx.setStatus(200)
28
+ ctx.response.empty()
29
+ })
30
+ `,
31
+ 'error-guard': `
32
+ declare const app: any
33
+ app.route('/api/v1/auth/me').get((ctx: any) => {
34
+ if (!ctx.user) {
35
+ ctx.setStatus(401)
36
+ return ctx.json({ error: 'not authenticated' as string })
37
+ }
38
+ ctx.json({ user: { id: 'one' as string } })
39
+ })
40
+ `,
41
+ 'two-guards': `
42
+ declare const app: any
43
+ app.route('/api/v1/items/:id').get((ctx: any) => {
44
+ if (!ctx.user) {
45
+ ctx.setStatus(401)
46
+ return ctx.json({ error: 'not authenticated' as string })
47
+ }
48
+ if (!ctx.item) {
49
+ ctx.setStatus(404)
50
+ return ctx.json({ error: 'not found' as string })
51
+ }
52
+ ctx.json({ item: { id: 'one' as string } })
53
+ })
54
+ `,
55
+ conflict: `
56
+ declare const app: any
57
+ const sendConflict = (ctx: any, existing: any) => {
58
+ ctx.setStatus(409)
59
+ if (!existing) {
60
+ ctx.json({ error: 'conflict' as string })
61
+ return
62
+ }
63
+ ctx.json({ error: 'conflict' as string, id: 'x' as string })
64
+ }
65
+ app.route('/api/v1/items').post((ctx: any) => {
66
+ const conflict: any = {}
67
+ if (conflict.existing !== undefined) {
68
+ sendConflict(ctx, conflict.existing)
69
+ return
70
+ }
71
+ ctx.setStatus(201)
72
+ ctx.json({ id: 'one' as string, status: 'pending' as string })
73
+ })
74
+ `,
75
+ ternary: `
76
+ declare const app: any
77
+ declare const created: boolean
78
+ app.route('/api/v1/items').post((ctx: any) => {
79
+ ctx.setStatus(created ? 201 : 200)
80
+ ctx.json({ id: 'one' as string })
81
+ })
82
+ `,
83
+ variants: `
84
+ declare const app: any
85
+ declare function apiResponse<K extends string, T>(key: K, body: T): T
86
+ app.route('/api/v1/items').post((ctx: any) => {
87
+ ctx.setStatus(422)
88
+ apiResponse('POST:/api/v1/items#validation', { error: 'invalid' as string })
89
+ ctx.setStatus(201)
90
+ apiResponse('POST:/api/v1/items', { id: 'one' as string })
91
+ })
92
+ `,
93
+ branches: `
94
+ declare const app: any
95
+ app.route('/api/v1/webhooks/:id').post((ctx: any) => {
96
+ if (ctx.body.event === 'known') {
97
+ ctx.json({ received: true as boolean })
98
+ return
99
+ }
100
+ ctx.json({ ignored: true as boolean })
101
+ })
102
+ `,
103
+ 'secondary-failure': `
104
+ declare const app: any
105
+ declare function riskyLookup(): any
106
+ app.route('/api/v1/jobs/:id').get((ctx: any) => {
107
+ if (ctx.query.mode === 'basic') {
108
+ ctx.json({ ready: true as boolean })
109
+ return
110
+ }
111
+ ctx.json({ value: riskyLookup() })
112
+ })
113
+ `,
114
+ 'empty-then-buffer': `
115
+ declare const app: any
116
+ declare const body: any
117
+ declare const contentType: string
118
+ app.route('/api/v1/mcp').post((ctx: any) => {
119
+ if (body.byteLength === 0) {
120
+ ctx.response.empty()
121
+ } else {
122
+ ctx.response.buffer(body, contentType)
123
+ }
124
+ })
125
+ `,
126
+ 'buffer-then-empty': `
127
+ declare const app: any
128
+ declare const body: any
129
+ declare const contentType: string
130
+ app.route('/api/v1/mcp').post((ctx: any) => {
131
+ if (body.byteLength > 0) {
132
+ ctx.response.buffer(body, contentType)
133
+ } else {
134
+ ctx.response.empty()
135
+ }
136
+ })
137
+ `,
138
+ 'single-failure': `
139
+ declare const app: any
140
+ declare function riskyLookup(): any
141
+ app.route('/api/v1/jobs/:id').get((ctx: any) => {
142
+ ctx.json({ value: riskyLookup() })
143
+ })
144
+ `,
145
+ };
@@ -0,0 +1,5 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import { type DiscoverApiResponseContractsOptions } from './response-contract-lenient.mts';
3
+ import { type HandlerBindings } from './response-contract-route-analysis.mts';
4
+ import type { BackendResponseContract } from './response-contract-types.mts';
5
+ export declare function discoverImplicitContract(call: ts.CallExpression, checker: ts.TypeChecker, sourceFile: ts.SourceFile, contracts: Map<string, BackendResponseContract>, handlerBindings: HandlerBindings, requestedKeys: ReadonlySet<string> | undefined, options: DiscoverApiResponseContractsOptions | undefined): void;
@@ -0,0 +1,110 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { extractContractSchema } from '../contract-schema/index.mjs';
3
+ import { containsResponseMarker, noContentContract, registerContract, responseBodyExpression, sourceLocation, } from './response-contract-registration.mjs';
4
+ import { isInErrorBranch } from './response-contract-error-branch.mjs';
5
+ import { isXmlResponseCall, streamingTextMediaType } from './response-contract-media.mjs';
6
+ import { markBufferedRouteUnavailable, registerRouteContract, } from './response-contract-lenient.mjs';
7
+ import { enclosingRouteBinding, isContextMethod, isContextResponseBufferCall, isContextResponseEmptyCall, requestedKeyForBinding, responseMarker, } from './response-contract-route-analysis.mjs';
8
+ import { resolveEmissionStatus } from './response-contract-status.mjs';
9
+ export function discoverImplicitContract(call, checker, sourceFile, contracts, handlerBindings, requestedKeys, options) {
10
+ const binding = enclosingRouteBinding(call, checker, handlerBindings);
11
+ if (!binding)
12
+ return;
13
+ const key = requestedKeyForBinding(binding, requestedKeys);
14
+ if (!key)
15
+ return;
16
+ const body = responseBodyExpression(call);
17
+ if (body) {
18
+ if (ts.isCallExpression(body) && responseMarker(body.expression))
19
+ return;
20
+ if (requestedKeys && contracts.has(key))
21
+ return;
22
+ if (isInErrorBranch(call))
23
+ return;
24
+ const location = sourceLocation(sourceFile, call);
25
+ const extract = () => extractContractSchema(checker.getTypeAtLocation(body), checker, location, options);
26
+ const status = resolveEmissionStatus(call);
27
+ const emission = {
28
+ bodyKind: 'content',
29
+ mediaType: 'application/json',
30
+ mediaTypeKnowledge: 'known',
31
+ ...status,
32
+ };
33
+ // Primary body (bare key, or requested key in subset mode) uses the lenient path, so a
34
+ // genuine single-variant route still reports a real `unavailableReason` on failure.
35
+ if (requestedKeys || !contracts.has(key)) {
36
+ registerRouteContract(contracts, key, binding, location, emission, extract, options);
37
+ return;
38
+ }
39
+ // Full-scope discovery: a route can emit more than one distinct unmarked success body across
40
+ // branches (e.g. a webhook's `{received}` vs `{ignored}`); register each under its own key —
41
+ // `groupContractsByRoute` merges by method/routeTemplate, not this key. This route already has
42
+ // a primary contract, so retain this one under a deterministic variant key. A failed secondary
43
+ // extraction is registered as unavailable so the operation never silently omits a real branch.
44
+ registerRouteContract(contracts, nextImplicitVariantKey(contracts, key), binding, location, emission, extract, options);
45
+ return;
46
+ }
47
+ if (isXmlResponseCall(call)) {
48
+ if (requestedKeys && contracts.has(key))
49
+ return;
50
+ const body = call.arguments[0];
51
+ if (!body)
52
+ return;
53
+ const location = sourceLocation(sourceFile, call);
54
+ registerRouteContract(contracts, contracts.has(key) ? nextImplicitVariantKey(contracts, key) : key, binding, location, {
55
+ bodyKind: 'content',
56
+ mediaType: 'application/xml',
57
+ mediaTypeKnowledge: 'known',
58
+ ...resolveEmissionStatus(call),
59
+ }, () => extractContractSchema(checker.getTypeAtLocation(body), checker, location, options), options);
60
+ return;
61
+ }
62
+ const streamedTextMediaType = isContextMethod(call.expression, 'pipeline')
63
+ ? streamingTextMediaType(call)
64
+ : undefined;
65
+ if (streamedTextMediaType) {
66
+ const location = sourceLocation(sourceFile, call);
67
+ registerRouteContract(contracts, contracts.has(key) ? nextImplicitVariantKey(contracts, key) : key, binding, location, {
68
+ bodyKind: 'content',
69
+ mediaType: streamedTextMediaType,
70
+ mediaTypeKnowledge: 'known',
71
+ ...resolveEmissionStatus(call),
72
+ }, () => extractContractSchema(checker.getStringType(), checker, location, options), options);
73
+ return;
74
+ }
75
+ // Empty and raw branches can coexist; AST order must not let empty hide an unmarked raw body,
76
+ // so every buffer or pipeline without an explicit response marker makes the route unavailable.
77
+ if (isContextResponseBufferCall(call.expression) ||
78
+ (isContextMethod(call.expression, 'pipeline') && !body && !containsResponseMarker(call))) {
79
+ markBufferedRouteUnavailable(contracts, key, binding, sourceLocation(sourceFile, call));
80
+ return;
81
+ }
82
+ if (requestedKeys && contracts.has(key))
83
+ return;
84
+ const emissionStatus = resolveEmissionStatus(call);
85
+ const isExplicitNoContentCall = (isContextMethod(call.expression, 'setStatus') &&
86
+ call.arguments[0] &&
87
+ ts.isNumericLiteral(call.arguments[0]) &&
88
+ (call.arguments[0].text === '204' || call.arguments[0].text === '205')) ||
89
+ isContextResponseEmptyCall(call.expression);
90
+ if (isExplicitNoContentCall) {
91
+ registerContract(contracts, contracts.has(key) ? nextImplicitVariantKey(contracts, key) : key, {
92
+ ...noContentContract(sourceLocation(sourceFile, call)),
93
+ ...binding,
94
+ bodyKind: 'none',
95
+ mediaTypeKnowledge: 'none',
96
+ ...(emissionStatus.statusCodes ? { statusCodes: emissionStatus.statusCodes } : {}),
97
+ statusKnowledge: emissionStatus.statusKnowledge,
98
+ ...(emissionStatus.unavailableReason
99
+ ? { unavailableReason: emissionStatus.unavailableReason }
100
+ : {}),
101
+ });
102
+ }
103
+ }
104
+ /** Picks the next deterministic registration key for another implicit response variant. */
105
+ function nextImplicitVariantKey(contracts, key) {
106
+ let suffix = 2;
107
+ while (contracts.has(`${key}#implicit-${suffix}`))
108
+ suffix++;
109
+ return `${key}#implicit-${suffix}`;
110
+ }
@@ -0,0 +1,23 @@
1
+ import type { ExtractedResponseContract, ExtractContractSchemaOptions } from '../contract-schema/index.mts';
2
+ import type { RouteBinding } from './response-contract-route-analysis.mts';
3
+ import type { BackendResponseContract } from './response-contract-types.mts';
4
+ export type RouteDiscoveryError = {
5
+ method: string;
6
+ routeTemplate: string;
7
+ source: string;
8
+ reason: string;
9
+ };
10
+ export type DiscoverApiResponseContractsOptions = ExtractContractSchemaOptions & {
11
+ onRouteError?: (error: RouteDiscoveryError) => void;
12
+ };
13
+ /** Marks `key` unavailable regardless of AST visit order, for a route that can send a raw buffered body (e.g. a proxied MCP response) alongside a no-content branch. */
14
+ export declare function markBufferedRouteUnavailable(contracts: Map<string, BackendResponseContract>, key: string, binding: RouteBinding, source: string): void;
15
+ /**
16
+ * Extracts a route's response contract and registers it. When `options.onRouteError` is
17
+ * set, an extraction failure is caught, reported, and replaced with an `unavailable`
18
+ * placeholder instead of aborting discovery for every other route. With `options` absent
19
+ * (or `onRouteError` unset), behavior is unchanged: extraction failures throw.
20
+ */
21
+ export declare function registerRouteContract(contracts: Map<string, BackendResponseContract>, key: string, binding: RouteBinding, location: string, emission: Pick<BackendResponseContract, 'bodyKind' | 'mediaType' | 'mediaTypeKnowledge' | 'statusCodes' | 'statusKnowledge'> & {
22
+ unavailableReason?: string;
23
+ }, extract: () => ExtractedResponseContract, options: DiscoverApiResponseContractsOptions | undefined): void;
@@ -0,0 +1,52 @@
1
+ import { hashContractSchema } from '../contract-schema/index.mjs';
2
+ import { registerContract } from './response-contract-registration.mjs';
3
+ /** Placeholder contract for a route whose response type could not be extracted. */
4
+ function unavailableContract(source, reason) {
5
+ const schema = { root: { type: 'unknown' }, definitions: {} };
6
+ return { source, schema, hash: hashContractSchema(schema), unavailableReason: reason };
7
+ }
8
+ /** Marks `key` unavailable regardless of AST visit order, for a route that can send a raw buffered body (e.g. a proxied MCP response) alongside a no-content branch. */
9
+ export function markBufferedRouteUnavailable(contracts, key, binding, source) {
10
+ contracts.set(key, {
11
+ ...unavailableContract(source, 'route can send a raw buffered response body (ctx.response.buffer) whose shape is not statically determinable'),
12
+ ...binding,
13
+ bodyKind: 'content',
14
+ statusKnowledge: 'unknown',
15
+ mediaTypeKnowledge: 'unknown',
16
+ });
17
+ }
18
+ /**
19
+ * Extracts a route's response contract and registers it. When `options.onRouteError` is
20
+ * set, an extraction failure is caught, reported, and replaced with an `unavailable`
21
+ * placeholder instead of aborting discovery for every other route. With `options` absent
22
+ * (or `onRouteError` unset), behavior is unchanged: extraction failures throw.
23
+ */
24
+ export function registerRouteContract(contracts, key, binding, location, emission, extract, options) {
25
+ let extracted;
26
+ try {
27
+ if (emission.unavailableReason)
28
+ throw new Error(emission.unavailableReason);
29
+ extracted = extract();
30
+ }
31
+ catch (error) {
32
+ if (!options?.onRouteError)
33
+ throw error;
34
+ const reason = error instanceof Error ? error.message : String(error);
35
+ options.onRouteError({
36
+ method: binding.method,
37
+ routeTemplate: binding.routeTemplate,
38
+ source: location,
39
+ reason,
40
+ });
41
+ extracted = unavailableContract(location, reason);
42
+ }
43
+ registerContract(contracts, key, {
44
+ ...extracted,
45
+ ...binding,
46
+ ...(emission.bodyKind ? { bodyKind: emission.bodyKind } : {}),
47
+ ...(emission.mediaType ? { mediaType: emission.mediaType } : {}),
48
+ ...(emission.mediaTypeKnowledge ? { mediaTypeKnowledge: emission.mediaTypeKnowledge } : {}),
49
+ ...(emission.statusCodes ? { statusCodes: emission.statusCodes } : {}),
50
+ ...(emission.statusKnowledge ? { statusKnowledge: emission.statusKnowledge } : {}),
51
+ });
52
+ }