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,54 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ export function requiredPropertyType(type, name, checker, fail) {
3
+ const property = type.getProperty(name);
4
+ if (!property || property.flags & ts.SymbolFlags.Optional)
5
+ return fail(`requires literal ${name}`);
6
+ const location = property.valueDeclaration ?? property.declarations?.[0];
7
+ if (!location)
8
+ return fail(`requires declared ${name}`);
9
+ return checker.getTypeOfSymbolAtLocation(property, location);
10
+ }
11
+ export function requiredStringLiteral(type, name, checker, fail) {
12
+ const value = requiredPropertyType(type, name, checker, fail);
13
+ return value.isStringLiteral() ? value.value : fail(`requires literal ${name}`);
14
+ }
15
+ export function requiredNumberLiteral(type, name, checker, fail) {
16
+ const value = requiredPropertyType(type, name, checker, fail);
17
+ return value.isNumberLiteral() ? value.value : fail(`requires literal ${name}`);
18
+ }
19
+ export function requiredBooleanLiteral(type, name, checker, fail) {
20
+ const value = requiredPropertyType(type, name, checker, fail);
21
+ if (value.flags & ts.TypeFlags.BooleanLiteral)
22
+ return checker.typeToString(value) === 'true';
23
+ return fail(`requires literal ${name}`);
24
+ }
25
+ export function optionalStringLiteral(type, name, checker, node, fail) {
26
+ const property = type.getProperty(name);
27
+ if (!property)
28
+ return undefined;
29
+ if (property.flags & ts.SymbolFlags.Optional)
30
+ return fail(`${name} must be literal when present`);
31
+ const location = property.valueDeclaration ?? property.declarations?.[0] ?? node;
32
+ const value = checker.getTypeOfSymbolAtLocation(property, location);
33
+ return value.isStringLiteral() ? value.value : fail(`requires literal ${name}`);
34
+ }
35
+ export function optionalNumberLiteral(type, name, checker, node, fail) {
36
+ const property = type.getProperty(name);
37
+ if (!property)
38
+ return undefined;
39
+ if (property.flags & ts.SymbolFlags.Optional)
40
+ return fail(`${name} must be literal when present`);
41
+ const location = property.valueDeclaration ?? property.declarations?.[0] ?? node;
42
+ const value = checker.getTypeOfSymbolAtLocation(property, location);
43
+ return value.isNumberLiteral() ? value.value : fail(`requires literal ${name}`);
44
+ }
45
+ export function stringTuple(type, name, checker, fail) {
46
+ const value = requiredPropertyType(type, name, checker, fail);
47
+ if (!checker.isTupleType(value))
48
+ return fail(`${name} must be a literal tuple`);
49
+ const values = checker.getTypeArguments(value);
50
+ if (values.length === 0 || values.some((item) => !item.isStringLiteral())) {
51
+ return fail(`${name} must contain string literals`);
52
+ }
53
+ return values.map((item) => item.value);
54
+ }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import type { BackendQueryContractRegistry } from './query-contract-types.mts';
3
+ export declare function discoverApiQueryContracts(program: ts.Program, sourceFiles: readonly ts.SourceFile[], knownResponseRoutes: ReadonlySet<string>): BackendQueryContractRegistry;
@@ -0,0 +1,68 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { extractQueryParameterDescriptor } from './query-contract-extraction.mjs';
3
+ import { contractError } from './response-contract-registration.mjs';
4
+ import { collectHandlerBindings, enclosingRouteBinding, visit, } from './response-contract-route-analysis.mjs';
5
+ export function discoverApiQueryContracts(program, sourceFiles, knownResponseRoutes) {
6
+ const checker = program.getTypeChecker();
7
+ const handlerBindings = collectHandlerBindings(sourceFiles, checker);
8
+ const contracts = new Map();
9
+ const knownOperations = new Set([...knownResponseRoutes].map((key) => key.split('#')[0]));
10
+ for (const sourceFile of sourceFiles) {
11
+ visit(sourceFile, (node) => {
12
+ if (!ts.isCallExpression(node) || !isApiQueryMarker(node.expression))
13
+ return;
14
+ const keyNode = node.arguments[0];
15
+ if (!keyNode || !ts.isStringLiteral(keyNode)) {
16
+ throw contractError(sourceFile, node, 'apiQuery requires a literal operation key');
17
+ }
18
+ const binding = enclosingRouteBinding(node, checker, handlerBindings);
19
+ if (!binding)
20
+ throw contractError(sourceFile, node, 'apiQuery must be inside an app.route handler');
21
+ const expectedKey = `${binding.method}:${binding.routeTemplate}`;
22
+ if (keyNode.text !== expectedKey) {
23
+ throw contractError(sourceFile, keyNode, `Query operation key "${keyNode.text}" does not match enclosing route ${expectedKey}`);
24
+ }
25
+ if (!knownOperations.has(expectedKey)) {
26
+ throw contractError(sourceFile, keyNode, `apiQuery references unknown response route ${expectedKey}`);
27
+ }
28
+ if (contracts.has(expectedKey)) {
29
+ throw contractError(sourceFile, keyNode, `Duplicate apiQuery marker for ${expectedKey}`);
30
+ }
31
+ if (node.arguments.length < 2) {
32
+ throw contractError(sourceFile, node, 'apiQuery requires at least one query contract carrier');
33
+ }
34
+ const parameters = Object.create(null);
35
+ for (const carrier of node.arguments.slice(1)) {
36
+ const carrierType = checker.getTypeAtLocation(carrier);
37
+ const queryContractSymbol = carrierType.getProperty('queryContract');
38
+ if (!queryContractSymbol || queryContractSymbol.flags & ts.SymbolFlags.Optional) {
39
+ throw contractError(sourceFile, carrier, 'apiQuery carrier requires a queryContract property');
40
+ }
41
+ const queryContractType = checker.getTypeOfSymbolAtLocation(queryContractSymbol, carrier);
42
+ if (queryContractType.getStringIndexType()) {
43
+ throw contractError(sourceFile, carrier, 'apiQuery queryContract keys must be literal names');
44
+ }
45
+ const properties = queryContractType.getProperties();
46
+ if (properties.length === 0) {
47
+ throw contractError(sourceFile, carrier, 'apiQuery queryContract must define parameters');
48
+ }
49
+ for (const property of properties) {
50
+ const name = property.getName();
51
+ if (property.flags & ts.SymbolFlags.Optional) {
52
+ throw contractError(sourceFile, carrier, `Query parameter "${name}" must not be optional`);
53
+ }
54
+ if (Object.hasOwn(parameters, name)) {
55
+ throw contractError(sourceFile, carrier, `Duplicate query parameter contract: ${name}`);
56
+ }
57
+ const descriptorType = checker.getTypeOfSymbolAtLocation(property, carrier);
58
+ parameters[name] = extractQueryParameterDescriptor(descriptorType, checker, sourceFile, carrier, name);
59
+ }
60
+ }
61
+ contracts.set(expectedKey, { ...binding, parameters });
62
+ });
63
+ }
64
+ return Object.fromEntries([...contracts.entries()].toSorted(([left], [right]) => left.localeCompare(right)));
65
+ }
66
+ function isApiQueryMarker(expression) {
67
+ return ts.isIdentifier(expression) && expression.text === 'apiQuery';
68
+ }
@@ -0,0 +1,8 @@
1
+ import type { OpenApiQueryContract, OpenApiQueryParameter } from '../openapi-document/index.mts';
2
+ export type QueryParameterContract = OpenApiQueryParameter;
3
+ export type BackendQueryContract = {
4
+ method: string;
5
+ routeTemplate: string;
6
+ parameters: OpenApiQueryContract;
7
+ };
8
+ export type BackendQueryContractRegistry = Readonly<Record<string, BackendQueryContract>>;
@@ -0,0 +1,10 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ export type RegisteredRoute = {
3
+ method: string;
4
+ routeTemplate: string;
5
+ kind: 'ordinary' | 'sse' | 'error-only' | 'fixed-no-content';
6
+ fixedStatus?: number;
7
+ source: string;
8
+ };
9
+ export declare const routeShape: (routeTemplate: string) => string;
10
+ export declare function discoverRegisteredRoutes(program: ts.Program, sourceFiles: readonly ts.SourceFile[]): RegisteredRoute[];
@@ -0,0 +1,81 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { handlerNodes } from './registered-route-handler-analysis.mjs';
3
+ import { propertyName, routeTemplateFromExpression, visit, } from './response-contract-route-analysis.mjs';
4
+ export const routeShape = (routeTemplate) => routeTemplate.replace(/:[^/]+/g, ':');
5
+ export function discoverRegisteredRoutes(program, sourceFiles) {
6
+ const checker = program.getTypeChecker();
7
+ const routes = [];
8
+ const registrations = new Map();
9
+ sourceFiles.forEach((sourceFile) => {
10
+ visit(sourceFile, (node) => {
11
+ if (!ts.isCallExpression(node))
12
+ return;
13
+ const method = propertyName(node.expression)?.toUpperCase();
14
+ if (!method || !['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method))
15
+ return;
16
+ const routeTemplate = routeTemplateFromExpression(node.expression);
17
+ if (!routeTemplate)
18
+ return;
19
+ const key = `${method}:${routeShape(routeTemplate)}`;
20
+ const source = `${sourceFile.fileName}:${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}`;
21
+ const classification = classifyHandler(node, checker, method, routeTemplate);
22
+ const route = {
23
+ method,
24
+ routeTemplate,
25
+ ...classification,
26
+ source,
27
+ };
28
+ const existing = registrations.get(key);
29
+ if (existing)
30
+ throw new Error(`Duplicate registered API route ${key} at ${existing.source} and ${source}`);
31
+ registrations.set(key, route);
32
+ routes.push(route);
33
+ });
34
+ });
35
+ return routes.toSorted((left, right) => `${left.method}:${left.routeTemplate}`.localeCompare(`${right.method}:${right.routeTemplate}`));
36
+ }
37
+ function classifyHandler(call, checker, method, routeTemplate) {
38
+ const nodes = call.arguments.flatMap((argument) => handlerNodes(argument, checker));
39
+ if (nodes.length === 0)
40
+ throw new Error(`Cannot inspect registered route handler ${method}:${routeTemplate}`);
41
+ let sse = false;
42
+ let error405 = false;
43
+ let fixedStatus;
44
+ nodes.forEach((node) => {
45
+ visit(node, (child) => {
46
+ if (!ts.isCallExpression(child))
47
+ return;
48
+ if (ts.isIdentifier(child.expression) && child.expression.text === 'apiOpenApiNoContent') {
49
+ const [key, status] = child.arguments;
50
+ if (!key || !ts.isStringLiteral(key) || key.text !== `${method}:${routeTemplate}`)
51
+ throw new Error(`apiOpenApiNoContent key must match registered route ${method}:${routeTemplate}`);
52
+ if (!status || !ts.isNumericLiteral(status))
53
+ throw new Error('apiOpenApiNoContent status requires a numeric literal');
54
+ const value = Number(status.text);
55
+ if (fixedStatus !== undefined && fixedStatus !== value)
56
+ throw new Error(`Conflicting apiOpenApiNoContent statuses for ${method}:${routeTemplate}`);
57
+ fixedStatus = value;
58
+ }
59
+ if (ts.isIdentifier(child.expression) && child.expression.text === 'startSSE')
60
+ sse = true;
61
+ if (ts.isPropertyAccessExpression(child.expression) &&
62
+ child.expression.name.text === 'setType' &&
63
+ child.arguments[0] &&
64
+ ts.isStringLiteral(child.arguments[0]) &&
65
+ child.arguments[0].text.toLowerCase() === 'text/event-stream')
66
+ sse = true;
67
+ if (ts.isPropertyAccessExpression(child.expression) &&
68
+ child.expression.name.text === 'throw' &&
69
+ child.arguments[0] &&
70
+ ts.isNumericLiteral(child.arguments[0]) &&
71
+ child.arguments[0].text === '405')
72
+ error405 = true;
73
+ });
74
+ });
75
+ if (fixedStatus !== undefined) {
76
+ if (sse)
77
+ throw new Error(`apiOpenApiNoContent conflicts with SSE response handling for ${method}:${routeTemplate}`);
78
+ return { kind: 'fixed-no-content', fixedStatus };
79
+ }
80
+ return { kind: sse ? 'sse' : error405 ? 'error-only' : 'ordinary' };
81
+ }
@@ -0,0 +1,2 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ export declare function handlerNodes(argument: ts.Expression, checker: ts.TypeChecker, parameterBindings?: Map<ts.Symbol, ts.Expression>): ts.Node[];
@@ -0,0 +1,74 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ export function handlerNodes(argument, checker, parameterBindings = new Map()) {
3
+ if (ts.isIdentifier(argument)) {
4
+ const symbol = checker.getSymbolAtLocation(argument);
5
+ const boundArgument = symbol && parameterBindings.get(symbol);
6
+ if (boundArgument)
7
+ return handlerNodes(boundArgument, checker, parameterBindings);
8
+ }
9
+ if (ts.isArrowFunction(argument) || ts.isFunctionExpression(argument))
10
+ return [argument];
11
+ if (ts.isCallExpression(argument)) {
12
+ return callableImplementations(argument.expression, checker).flatMap((implementation) => {
13
+ const bindings = new Map(parameterBindings);
14
+ implementation.parameters.forEach((parameter, index) => {
15
+ const callArgument = argument.arguments[index];
16
+ const symbol = checker.getSymbolAtLocation(parameter.name);
17
+ if (callArgument && symbol)
18
+ bindings.set(symbol, callArgument);
19
+ });
20
+ return returnedExpressions(implementation).flatMap((returnedExpression) => handlerNodes(returnedExpression, checker, bindings));
21
+ });
22
+ }
23
+ return declarationImplementations(argument, checker);
24
+ }
25
+ function callableImplementations(node, checker) {
26
+ const symbol = checker.getSymbolAtLocation(node);
27
+ if (!symbol)
28
+ return [];
29
+ const resolved = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
30
+ const implementations = [];
31
+ for (const declaration of resolved.declarations ?? []) {
32
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
33
+ const initializer = declaration.initializer;
34
+ if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))
35
+ implementations.push(initializer);
36
+ continue;
37
+ }
38
+ if ((ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration)) &&
39
+ declaration.body)
40
+ implementations.push(declaration);
41
+ }
42
+ return implementations;
43
+ }
44
+ function returnedExpressions(declaration) {
45
+ const body = declaration.body;
46
+ if (!ts.isBlock(body))
47
+ return [body];
48
+ const returned = [];
49
+ const collect = (node) => {
50
+ if (node !== declaration && ts.isFunctionLike(node))
51
+ return;
52
+ if (ts.isReturnStatement(node)) {
53
+ if (node.expression)
54
+ returned.push(node.expression);
55
+ return;
56
+ }
57
+ node.forEachChild(collect);
58
+ };
59
+ body.forEachChild(collect);
60
+ return returned;
61
+ }
62
+ function declarationImplementations(node, checker) {
63
+ const symbol = checker.getSymbolAtLocation(node);
64
+ if (!symbol)
65
+ return [];
66
+ const resolved = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
67
+ return (resolved.declarations ?? []).flatMap((declaration) => {
68
+ if (ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration))
69
+ return declaration.body ? [declaration] : [];
70
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer)
71
+ return [declaration, ...handlerNodes(declaration.initializer, checker)];
72
+ return [];
73
+ });
74
+ }
@@ -0,0 +1,17 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import { type DiscoverApiRequestContractsOptions } from './request-contract-lenient.mts';
3
+ import { type HandlerBindings } from './response-contract-route-analysis.mts';
4
+ import type { BackendRequestContract } from './request-contract-types.mts';
5
+ /**
6
+ * Phase 2 (implicit harvest) dispatch for one call expression. Requests have no `#variant` escape
7
+ * hatch, so precedence is stricter than the response side's: an explicit `apiRequest`/
8
+ * `apiNoRequestBody` marker registered in Phase 1 (`markerKeys`) always wins outright — a harvest
9
+ * never even attempts to register over one. A second implicit body-read for a route an earlier
10
+ * implicit call already covered attempts registration but swallows any failure (extraction error
11
+ * or hash mismatch), keeping the first-registered contract untouched — mirrors
12
+ * `response-contract-implicit.mts`'s secondary-variant handling, minus the variant key (requests
13
+ * get at most one contract per route). A `ctx.request.buffer(...)` read always overrides,
14
+ * regardless of visit order or what's already registered, mirroring the response side's own
15
+ * buffer-detection override.
16
+ */
17
+ export declare function discoverImplicitRequestContract(call: ts.CallExpression, checker: ts.TypeChecker, sourceFile: ts.SourceFile, contracts: Map<string, BackendRequestContract>, markerKeys: ReadonlySet<string>, handlerBindings: HandlerBindings, requestedKeys: ReadonlySet<string> | undefined, options: DiscoverApiRequestContractsOptions | undefined): void;
@@ -0,0 +1,73 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { extractContractSchema } from '../contract-schema/index.mjs';
3
+ import { markBufferedRequestRouteUnavailable, registerRequestContract, registerRequestRouteContract, } from './request-contract-lenient.mjs';
4
+ import { enclosingRequestBodyCast, isContextRequestBufferCall, isContextRequestJsonCall, } from './request-contract-route-analysis.mjs';
5
+ import { sourceLocation } from './response-contract-registration.mjs';
6
+ import { enclosingRouteBinding, requestedKeyForBinding, } from './response-contract-route-analysis.mjs';
7
+ /**
8
+ * Resolves the effective static type of a body-read call, or `undefined` when `call` isn't a
9
+ * body-read pattern this harvester recognizes at all (most call expressions in a route file
10
+ * aren't). `parseJsonBody<T>(ctx)`'s own call-expression type is `Promise<T>` (or `Promise<unknown>`
11
+ * with no type argument) — `extractContractSchema` auto-unwraps the `Promise` either way, so no
12
+ * special-casing is needed. `ctx.request.json(...)` has no such generic in practice; its effective
13
+ * type comes from the enclosing `as T` cast, when one exists (`enclosingRequestBodyCast`) — absent
14
+ * one, the call's own type is `Promise<unknown>`, which resolves to an honest `{type:'unknown'}`
15
+ * rather than a failure (see `enclosingRequestBodyCast`'s docstring).
16
+ */
17
+ function resolveRequestBodyType(call, checker) {
18
+ if (ts.isIdentifier(call.expression) && call.expression.text === 'parseJsonBody') {
19
+ return checker.getTypeAtLocation(call);
20
+ }
21
+ if (!isContextRequestJsonCall(call.expression))
22
+ return undefined;
23
+ const cast = enclosingRequestBodyCast(call);
24
+ return checker.getTypeAtLocation(cast ?? call);
25
+ }
26
+ /**
27
+ * Phase 2 (implicit harvest) dispatch for one call expression. Requests have no `#variant` escape
28
+ * hatch, so precedence is stricter than the response side's: an explicit `apiRequest`/
29
+ * `apiNoRequestBody` marker registered in Phase 1 (`markerKeys`) always wins outright — a harvest
30
+ * never even attempts to register over one. A second implicit body-read for a route an earlier
31
+ * implicit call already covered attempts registration but swallows any failure (extraction error
32
+ * or hash mismatch), keeping the first-registered contract untouched — mirrors
33
+ * `response-contract-implicit.mts`'s secondary-variant handling, minus the variant key (requests
34
+ * get at most one contract per route). A `ctx.request.buffer(...)` read always overrides,
35
+ * regardless of visit order or what's already registered, mirroring the response side's own
36
+ * buffer-detection override.
37
+ */
38
+ export function discoverImplicitRequestContract(call, checker, sourceFile, contracts, markerKeys, handlerBindings, requestedKeys, options) {
39
+ const binding = enclosingRouteBinding(call, checker, handlerBindings);
40
+ if (!binding)
41
+ return;
42
+ const key = requestedKeyForBinding(binding, requestedKeys);
43
+ if (!key)
44
+ return;
45
+ if (isContextRequestBufferCall(call.expression)) {
46
+ markBufferedRequestRouteUnavailable(contracts, key, binding, sourceLocation(sourceFile, call));
47
+ return;
48
+ }
49
+ if (markerKeys.has(key))
50
+ return; // An explicit Phase 1 marker always wins; never harvest over it.
51
+ const bodyType = resolveRequestBodyType(call, checker);
52
+ if (!bodyType)
53
+ return;
54
+ const location = sourceLocation(sourceFile, call);
55
+ const extract = () => extractContractSchema(bodyType, checker, location, options);
56
+ if (!contracts.has(key)) {
57
+ registerRequestRouteContract(contracts, key, binding, location, extract, options);
58
+ return;
59
+ }
60
+ // A second body-read call for a route an earlier implicit harvest already covered — attempt
61
+ // registration but silently drop it on a hash-mismatch collision, keeping the
62
+ // first-registered contract. Anything else (a type-checker crash, an AST invariant
63
+ // violation) is unexpected and worth surfacing rather than swallowing.
64
+ try {
65
+ const extracted = extract();
66
+ registerRequestContract(contracts, key, { ...extracted, ...binding });
67
+ }
68
+ catch (error) {
69
+ if (error instanceof Error && !error.message.includes('resolves to multiple')) {
70
+ console.warn(`Second body-read extraction for "${key}" failed:`, error);
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,28 @@
1
+ import type { ExtractedResponseContract, ExtractContractSchemaOptions } from '../contract-schema/index.mts';
2
+ import type { RouteBinding } from './response-contract-route-analysis.mts';
3
+ import type { RouteDiscoveryError } from './response-contract-lenient.mts';
4
+ import type { BackendRequestContract } from './request-contract-types.mts';
5
+ export type DiscoverApiRequestContractsOptions = ExtractContractSchemaOptions & {
6
+ onRouteError?: (error: RouteDiscoveryError) => void;
7
+ };
8
+ /**
9
+ * Marks `key` unavailable regardless of AST visit order or what else is already registered for
10
+ * this route (an explicit marker, or an earlier implicit harvest) — a raw buffered body's shape
11
+ * is never statically determinable, so it always wins. Mirrors the response side's
12
+ * `markBufferedRouteUnavailable` for `ctx.response.buffer(...)`.
13
+ */
14
+ export declare function markBufferedRequestRouteUnavailable(contracts: Map<string, BackendRequestContract>, key: string, binding: RouteBinding, source: string): void;
15
+ /**
16
+ * Inserts `contract` at `key`, throwing if `key` already resolves to a different schema. Requests
17
+ * have no `#variant` escape hatch, so — unlike the response side, where a genuine collision is
18
+ * always a real author bug worth surfacing — callers here must choose deliberately whether a
19
+ * mismatch should throw (an explicit marker collision) or be caught (an implicit harvest
20
+ * collision); see `request-contract-implicit.mts`.
21
+ */
22
+ export declare function registerRequestContract(contracts: Map<string, BackendRequestContract>, key: string, contract: BackendRequestContract): void;
23
+ /**
24
+ * Extracts a route's request contract and registers it. When `options.onRouteError` is set, an
25
+ * extraction failure is caught, reported, and replaced with an `unavailable` placeholder instead
26
+ * of aborting discovery for every other route — mirrors the response side's `registerRouteContract`.
27
+ */
28
+ export declare function registerRequestRouteContract(contracts: Map<string, BackendRequestContract>, key: string, binding: RouteBinding, location: string, extract: () => ExtractedResponseContract, options: DiscoverApiRequestContractsOptions | undefined): void;
@@ -0,0 +1,59 @@
1
+ import { hashContractSchema } from '../contract-schema/index.mjs';
2
+ /** Placeholder contract for a route whose request body could not be extracted. */
3
+ function unavailableRequestContract(source, reason) {
4
+ const schema = { root: { type: 'unknown' }, definitions: {} };
5
+ return { source, schema, hash: hashContractSchema(schema), unavailableReason: reason };
6
+ }
7
+ /**
8
+ * Marks `key` unavailable regardless of AST visit order or what else is already registered for
9
+ * this route (an explicit marker, or an earlier implicit harvest) — a raw buffered body's shape
10
+ * is never statically determinable, so it always wins. Mirrors the response side's
11
+ * `markBufferedRouteUnavailable` for `ctx.response.buffer(...)`.
12
+ */
13
+ export function markBufferedRequestRouteUnavailable(contracts, key, binding, source) {
14
+ contracts.set(key, {
15
+ ...unavailableRequestContract(source, 'route reads a raw buffered request body (ctx.request.buffer) whose shape is not statically determinable'),
16
+ ...binding,
17
+ });
18
+ }
19
+ /**
20
+ * Inserts `contract` at `key`, throwing if `key` already resolves to a different schema. Requests
21
+ * have no `#variant` escape hatch, so — unlike the response side, where a genuine collision is
22
+ * always a real author bug worth surfacing — callers here must choose deliberately whether a
23
+ * mismatch should throw (an explicit marker collision) or be caught (an implicit harvest
24
+ * collision); see `request-contract-implicit.mts`.
25
+ */
26
+ export function registerRequestContract(contracts, key, contract) {
27
+ const existing = contracts.get(key);
28
+ if (!existing) {
29
+ contracts.set(key, contract);
30
+ return;
31
+ }
32
+ if (existing.hash !== contract.hash) {
33
+ throw new Error(`Backend request contract "${key}" resolves to multiple backend request schemas at ${existing.source} and ${contract.source}`);
34
+ }
35
+ }
36
+ /**
37
+ * Extracts a route's request contract and registers it. When `options.onRouteError` is set, an
38
+ * extraction failure is caught, reported, and replaced with an `unavailable` placeholder instead
39
+ * of aborting discovery for every other route — mirrors the response side's `registerRouteContract`.
40
+ */
41
+ export function registerRequestRouteContract(contracts, key, binding, location, extract, options) {
42
+ let extracted;
43
+ try {
44
+ extracted = extract();
45
+ }
46
+ catch (error) {
47
+ if (!options?.onRouteError)
48
+ throw error;
49
+ const reason = error instanceof Error ? error.message : String(error);
50
+ options.onRouteError({
51
+ method: binding.method,
52
+ routeTemplate: binding.routeTemplate,
53
+ source: location,
54
+ reason,
55
+ });
56
+ extracted = unavailableRequestContract(location, reason);
57
+ }
58
+ registerRequestContract(contracts, key, { ...extracted, ...binding });
59
+ }
@@ -0,0 +1,14 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import { type DiscoverApiRequestContractsOptions } from './request-contract-lenient.mts';
3
+ import type { BackendRequestContract } from './request-contract-types.mts';
4
+ export type { BackendRequestContract } from './request-contract-types.mts';
5
+ export type { DiscoverApiRequestContractsOptions } from './request-contract-lenient.mts';
6
+ /**
7
+ * Two full passes over every route file, mirroring `discoverApiResponseContracts`: Phase 1
8
+ * (explicit request-contract markers) completes for every file before Phase 2
9
+ * (implicit harvest of `parseJsonBody`/`ctx.request.json(...) as T`/`ctx.request.buffer(...)`)
10
+ * begins, so a marker anywhere always wins over a harvest anywhere regardless of visit order.
11
+ * `markerKeys` records every key an explicit marker claimed (a body, or a deliberate
12
+ * `apiNoRequestBody` suppression) so Phase 2 knows to never harvest over it.
13
+ */
14
+ export declare function discoverApiRequestContracts(program: ts.Program, sourceFiles: readonly ts.SourceFile[], requestedKeys?: ReadonlySet<string>, options?: DiscoverApiRequestContractsOptions): Record<string, BackendRequestContract>;
@@ -0,0 +1,61 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { extractContractSchema } from '../contract-schema/index.mjs';
3
+ import { registerRequestRouteContract, } from './request-contract-lenient.mjs';
4
+ import { discoverImplicitRequestContract } from './request-contract-implicit.mjs';
5
+ import { requestMarker } from './request-contract-route-analysis.mjs';
6
+ import { contractError, sourceLocation } from './response-contract-registration.mjs';
7
+ import { collectHandlerBindings, enclosingRouteBinding, visit, } from './response-contract-route-analysis.mjs';
8
+ /**
9
+ * Two full passes over every route file, mirroring `discoverApiResponseContracts`: Phase 1
10
+ * (explicit request-contract markers) completes for every file before Phase 2
11
+ * (implicit harvest of `parseJsonBody`/`ctx.request.json(...) as T`/`ctx.request.buffer(...)`)
12
+ * begins, so a marker anywhere always wins over a harvest anywhere regardless of visit order.
13
+ * `markerKeys` records every key an explicit marker claimed (a body, or a deliberate
14
+ * `apiNoRequestBody` suppression) so Phase 2 knows to never harvest over it.
15
+ */
16
+ export function discoverApiRequestContracts(program, sourceFiles, requestedKeys, options) {
17
+ const checker = program.getTypeChecker();
18
+ const contracts = new Map();
19
+ const markerKeys = new Set();
20
+ const handlerBindings = collectHandlerBindings(sourceFiles, checker);
21
+ for (const sourceFile of sourceFiles) {
22
+ visit(sourceFile, (node) => {
23
+ if (!ts.isCallExpression(node))
24
+ return;
25
+ const marker = requestMarker(node.expression);
26
+ if (!marker)
27
+ return;
28
+ const keyNode = node.arguments[0];
29
+ if (!keyNode || !ts.isStringLiteral(keyNode)) {
30
+ throw contractError(sourceFile, node, `${marker} requires a literal contract key`);
31
+ }
32
+ const binding = enclosingRouteBinding(node, checker, handlerBindings);
33
+ if (!binding)
34
+ throw contractError(sourceFile, node, `${marker} must be inside an app.route handler`);
35
+ const expectedKey = `${binding.method}:${binding.routeTemplate}`;
36
+ if (keyNode.text !== expectedKey) {
37
+ // Requests have no `#variant` escape hatch (unlike responses) — the key must match exactly.
38
+ throw contractError(sourceFile, keyNode, `Contract key "${keyNode.text}" does not match enclosing route ${expectedKey}`);
39
+ }
40
+ markerKeys.add(keyNode.text);
41
+ if (marker === 'apiNoRequestBody')
42
+ return; // Deliberate suppression: no contract, harvest skips this key.
43
+ const bodyNode = node.arguments[1];
44
+ const typeNode = node.typeArguments?.[1];
45
+ if (marker === 'apiRequest' && !bodyNode)
46
+ throw contractError(sourceFile, node, 'apiRequest requires a request body');
47
+ if (marker === 'apiRequestContract' && !typeNode)
48
+ throw contractError(sourceFile, node, 'apiRequestContract requires a request body type');
49
+ const location = sourceLocation(sourceFile, node);
50
+ registerRequestRouteContract(contracts, keyNode.text, binding, location, () => extractContractSchema(typeNode ? checker.getTypeFromTypeNode(typeNode) : checker.getTypeAtLocation(bodyNode), checker, location, options), options);
51
+ });
52
+ }
53
+ for (const sourceFile of sourceFiles) {
54
+ visit(sourceFile, (node) => {
55
+ if (!ts.isCallExpression(node) || requestMarker(node.expression))
56
+ return;
57
+ discoverImplicitRequestContract(node, checker, sourceFile, contracts, markerKeys, handlerBindings, requestedKeys, options);
58
+ });
59
+ }
60
+ return Object.fromEntries([...contracts.entries()].toSorted(([left], [right]) => left.localeCompare(right)));
61
+ }
@@ -0,0 +1,25 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /**
3
+ * Detects the request-contract markers in the version-one route convention.
4
+ * Kept out of `responseMarker` (which only recognizes `apiResponse`/`apiNoContent`) so the two
5
+ * AST walks never interfere — see `response-contract-route-analysis.mts` for the response side.
6
+ */
7
+ export declare function requestMarker(expression: ts.Expression): 'apiRequest' | 'apiRequestContract' | 'apiNoRequestBody' | undefined;
8
+ /**
9
+ * Detects `ctx.request.json(...)`: a two-level nested access (`ctx.request` then `.json`) that
10
+ * `isContextMethod`'s single-level match can't see through directly — mirrors
11
+ * `isContextResponseBufferCall`'s own two-level pattern on the response side.
12
+ */
13
+ export declare function isContextRequestJsonCall(expression: ts.Expression): boolean;
14
+ /** Detects `ctx.request.buffer(...)`: a raw request body whose shape can't be statically extracted. */
15
+ export declare function isContextRequestBufferCall(expression: ts.Expression): boolean;
16
+ /**
17
+ * Walks up from a `ctx.request.json(...)` call expression to find the enclosing `as T` cast that
18
+ * gives it a static type, unwrapping the wrapper shapes real call sites use in between:
19
+ * `(await ctx.request.json(...)) as T` and the `.catch()`-guarded
20
+ * `(await ctx.request.json(...).catch(() => ({}))) as T`.
21
+ * Returns `undefined` when no enclosing cast exists (e.g. the read feeds directly into another
22
+ * call, `parseFoo(await ctx.request.json())`) — the caller treats that as an honest untyped
23
+ * success, not a failure.
24
+ */
25
+ export declare function enclosingRequestBodyCast(call: ts.CallExpression): ts.AsExpression | undefined;