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.
- package/README.md +73 -10
- package/dist/agent-blackboard/snapshot-partition-format.mjs +2 -72
- package/dist/agent-blackboard/snapshot-partition-guards.d.mts +3 -0
- package/dist/agent-blackboard/snapshot-partition-guards.mjs +9 -0
- package/dist/agent-blackboard/snapshot-partition-predicates.d.mts +3 -0
- package/dist/agent-blackboard/snapshot-partition-predicates.mjs +65 -0
- package/dist/api-contract-discovery/header-contract-parser.d.mts +3 -0
- package/dist/api-contract-discovery/header-contract-parser.mjs +93 -0
- package/dist/api-contract-discovery/header-contract-registry.d.mts +3 -0
- package/dist/api-contract-discovery/header-contract-registry.mjs +38 -0
- package/dist/api-contract-discovery/header-contract-types.d.mts +19 -0
- package/dist/api-contract-discovery/header-contract-types.mjs +1 -0
- package/dist/api-contract-discovery/index.d.mts +31 -0
- package/dist/api-contract-discovery/index.mjs +26 -0
- package/dist/api-contract-discovery/query-contract-extraction.d.mts +3 -0
- package/dist/api-contract-discovery/query-contract-extraction.mjs +65 -0
- package/dist/api-contract-discovery/query-contract-literals.d.mts +8 -0
- package/dist/api-contract-discovery/query-contract-literals.mjs +54 -0
- package/dist/api-contract-discovery/query-contract-registry.d.mts +3 -0
- package/dist/api-contract-discovery/query-contract-registry.mjs +68 -0
- package/dist/api-contract-discovery/query-contract-types.d.mts +8 -0
- package/dist/api-contract-discovery/query-contract-types.mjs +1 -0
- package/dist/api-contract-discovery/registered-route-catalog.d.mts +10 -0
- package/dist/api-contract-discovery/registered-route-catalog.mjs +81 -0
- package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +2 -0
- package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +74 -0
- package/dist/api-contract-discovery/request-contract-implicit.d.mts +17 -0
- package/dist/api-contract-discovery/request-contract-implicit.mjs +73 -0
- package/dist/api-contract-discovery/request-contract-lenient.d.mts +28 -0
- package/dist/api-contract-discovery/request-contract-lenient.mjs +59 -0
- package/dist/api-contract-discovery/request-contract-registry.d.mts +14 -0
- package/dist/api-contract-discovery/request-contract-registry.mjs +61 -0
- package/dist/api-contract-discovery/request-contract-route-analysis.d.mts +25 -0
- package/dist/api-contract-discovery/request-contract-route-analysis.mjs +68 -0
- package/dist/api-contract-discovery/request-contract-types.d.mts +12 -0
- package/dist/api-contract-discovery/request-contract-types.mjs +1 -0
- package/dist/api-contract-discovery/response-contract-ctx-analysis.d.mts +7 -0
- package/dist/api-contract-discovery/response-contract-ctx-analysis.mjs +28 -0
- package/dist/api-contract-discovery/response-contract-error-branch.d.mts +15 -0
- package/dist/api-contract-discovery/response-contract-error-branch.mjs +47 -0
- package/dist/api-contract-discovery/response-contract-implicit-sources.test-helpers.d.mts +145 -0
- package/dist/api-contract-discovery/response-contract-implicit-sources.test-helpers.mjs +145 -0
- package/dist/api-contract-discovery/response-contract-implicit.d.mts +5 -0
- package/dist/api-contract-discovery/response-contract-implicit.mjs +110 -0
- package/dist/api-contract-discovery/response-contract-lenient.d.mts +23 -0
- package/dist/api-contract-discovery/response-contract-lenient.mjs +52 -0
- package/dist/api-contract-discovery/response-contract-media.d.mts +3 -0
- package/dist/api-contract-discovery/response-contract-media.mjs +46 -0
- package/dist/api-contract-discovery/response-contract-registration.d.mts +11 -0
- package/dist/api-contract-discovery/response-contract-registration.mjs +61 -0
- package/dist/api-contract-discovery/response-contract-registry.d.mts +7 -0
- package/dist/api-contract-discovery/response-contract-registry.mjs +76 -0
- package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +13 -0
- package/dist/api-contract-discovery/response-contract-route-analysis.mjs +144 -0
- package/dist/api-contract-discovery/response-contract-status.d.mts +27 -0
- package/dist/api-contract-discovery/response-contract-status.mjs +80 -0
- package/dist/api-contract-discovery/response-contract-types.d.mts +16 -0
- package/dist/api-contract-discovery/response-contract-types.mjs +1 -0
- package/dist/api-contract-discovery/test-setup.test-helpers.d.mts +3 -0
- package/dist/api-contract-discovery/test-setup.test-helpers.mjs +2 -0
- package/dist/ast-grep-examples/companion-parity-compare.d.mts +3 -0
- package/dist/ast-grep-examples/companion-parity-compare.mjs +58 -0
- package/dist/ast-grep-examples/companion-parity.d.mts +2 -1
- package/dist/ast-grep-examples/companion-parity.mjs +2 -58
- package/dist/browser-session-runner/attempt.mjs +29 -48
- package/dist/browser-session-runner/termination.d.mts +11 -0
- package/dist/browser-session-runner/termination.mjs +34 -0
- package/dist/cli/parse-direct-options.d.mts +26 -0
- package/dist/cli/parse-direct-options.mjs +72 -0
- package/dist/cli/parse-forwarded-commands.d.mts +42 -0
- package/dist/cli/parse-forwarded-commands.mjs +29 -0
- package/dist/cli/parse-options.d.mts +1 -2
- package/dist/cli/parse-options.mjs +2 -72
- package/dist/cli/parse.mjs +4 -26
- package/dist/compiler-build/freshness.d.mts +27 -0
- package/dist/compiler-build/freshness.mjs +116 -0
- package/dist/compiler-build/index.d.mts +2 -0
- package/dist/compiler-build/index.mjs +2 -0
- package/dist/compiler-build/settlement.d.mts +10 -0
- package/dist/compiler-build/settlement.mjs +14 -0
- package/dist/config-inventory/call-arguments.d.mts +1 -0
- package/dist/config-inventory/call-arguments.mjs +30 -0
- package/dist/config-inventory/collect.d.mts +3 -0
- package/dist/config-inventory/collect.mjs +43 -0
- package/dist/config-inventory/dynamic-configs.d.mts +2 -0
- package/dist/config-inventory/dynamic-configs.mjs +9 -0
- package/dist/config-inventory/env-expression-readers.d.mts +3 -0
- package/dist/config-inventory/env-expression-readers.mjs +117 -0
- package/dist/config-inventory/env-prefix-readers.d.mts +2 -0
- package/dist/config-inventory/env-prefix-readers.mjs +14 -0
- package/dist/config-inventory/env-reference-matchers.d.mts +3 -0
- package/dist/config-inventory/env-reference-matchers.mjs +9 -0
- package/dist/config-inventory/env-var-accumulator.d.mts +6 -0
- package/dist/config-inventory/env-var-accumulator.mjs +82 -0
- package/dist/config-inventory/env-vars.d.mts +4 -0
- package/dist/config-inventory/env-vars.mjs +108 -0
- package/dist/config-inventory/format.d.mts +2 -0
- package/dist/config-inventory/format.mjs +69 -0
- package/dist/config-inventory/index.d.mts +3 -0
- package/dist/config-inventory/index.mjs +2 -0
- package/dist/config-inventory/package-gates.d.mts +2 -0
- package/dist/config-inventory/package-gates.mjs +54 -0
- package/dist/config-inventory/shared.d.mts +3 -0
- package/dist/config-inventory/shared.mjs +7 -0
- package/dist/config-inventory/types.d.mts +96 -0
- package/dist/config-inventory/types.mjs +1 -0
- package/dist/contract-schema/contract-schema-bounded-array.d.mts +15 -0
- package/dist/contract-schema/contract-schema-bounded-array.mjs +32 -0
- package/dist/contract-schema/contract-schema-type-extractor.d.mts +2 -8
- package/dist/contract-schema/contract-schema-type-extractor.mjs +2 -31
- package/dist/coverage-transport/control-v2-parse-object.d.mts +12 -0
- package/dist/coverage-transport/control-v2-parse-object.mjs +28 -0
- package/dist/coverage-transport/control-v2.mjs +2 -27
- package/dist/dependency-license-policy/audit-directory.d.mts +28 -0
- package/dist/dependency-license-policy/audit-directory.mjs +115 -0
- package/dist/dependency-license-policy/audit-signals.d.mts +12 -0
- package/dist/dependency-license-policy/audit-signals.mjs +44 -0
- package/dist/dependency-license-policy/audit-store.d.mts +12 -0
- package/dist/dependency-license-policy/audit-store.mjs +32 -0
- package/dist/dependency-license-policy/collect.d.mts +3 -1
- package/dist/dependency-license-policy/collect.mjs +16 -21
- package/dist/dependency-license-policy/execute-pnpm.d.mts +10 -0
- package/dist/dependency-license-policy/execute-pnpm.mjs +83 -0
- package/dist/dependency-license-policy/types.d.mts +11 -9
- package/dist/dependency-license-policy/workspace.d.mts +3 -1
- package/dist/dependency-license-policy/workspace.mjs +12 -5
- package/dist/gh-api-shell-quoting/argument-scan.d.mts +7 -0
- package/dist/gh-api-shell-quoting/argument-scan.mjs +35 -0
- package/dist/gh-api-shell-quoting/logical-line.d.mts +3 -0
- package/dist/gh-api-shell-quoting/logical-line.mjs +90 -0
- package/dist/gh-api-shell-quoting/scan.d.mts +2 -4
- package/dist/gh-api-shell-quoting/scan.mjs +1 -119
- package/dist/gh-cli/diff.d.mts +24 -4
- package/dist/gh-cli/diff.mjs +111 -4
- package/dist/gh-cli/index.d.mts +2 -1
- package/dist/gh-cli/index.mjs +1 -1
- package/dist/gha-workspace-policy/docker-options.d.mts +4 -0
- package/dist/gha-workspace-policy/docker-options.mjs +93 -0
- package/dist/gha-workspace-policy/docker-workspace-user.mjs +1 -168
- package/dist/gha-workspace-policy/shell-token-modes.d.mts +10 -0
- package/dist/gha-workspace-policy/shell-token-modes.mjs +31 -0
- package/dist/gha-workspace-policy/shell-tokens.d.mts +1 -0
- package/dist/gha-workspace-policy/shell-tokens.mjs +65 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +1 -1
- package/dist/openapi-document/contract-schema-types.d.mts +1 -1
- package/dist/openapi-document/openapi-types.d.mts +1 -1
- package/dist/pnpm-install/native-family.d.mts +3 -0
- package/dist/pnpm-install/native-family.mjs +31 -0
- package/dist/pnpm-install/native-health.d.mts +1 -4
- package/dist/pnpm-install/native-health.mjs +2 -31
- package/dist/retrospective-facts/local-merge.d.mts +2 -0
- package/dist/retrospective-facts/local-merge.mjs +21 -0
- package/dist/retrospective-facts/local.mjs +1 -21
- package/dist/retrospective-transcript/claude-blocks.d.mts +3 -0
- package/dist/retrospective-transcript/claude-blocks.mjs +38 -0
- package/dist/retrospective-transcript/claude.mjs +4 -34
- package/dist/retrospective-transcript/codex-payload.d.mts +2 -0
- package/dist/retrospective-transcript/codex-payload.mjs +51 -0
- package/dist/retrospective-transcript/codex-records.d.mts +2 -0
- package/dist/retrospective-transcript/codex-records.mjs +95 -0
- package/dist/retrospective-transcript/codex.mjs +2 -144
- package/dist/retrospective-transcript/index.d.mts +3 -16
- package/dist/retrospective-transcript/index.mjs +4 -48
- package/dist/retrospective-transcript/resolve-transcript-file.d.mts +19 -0
- package/dist/retrospective-transcript/resolve-transcript-file.mjs +50 -0
- package/dist/session-friction/command-segments.d.mts +3 -0
- package/dist/session-friction/command-segments.mjs +94 -0
- package/dist/session-friction/log-file.d.mts +7 -0
- package/dist/session-friction/log-file.mjs +83 -0
- package/dist/session-friction/log.mjs +3 -83
- package/dist/session-friction/normalize.mjs +1 -94
- package/dist/sql-scanner/comments.d.mts +1 -0
- package/dist/sql-scanner/comments.mjs +101 -0
- package/dist/sql-scanner/index.d.mts +1 -1
- package/dist/sql-scanner/index.mjs +1 -100
- package/dist/sql-scanner/literals.mjs +1 -35
- package/dist/sql-scanner/quote-decode.d.mts +2 -0
- package/dist/sql-scanner/quote-decode.mjs +35 -0
- package/dist/vitest-diagnostics/index.d.mts +7 -0
- package/dist/vitest-diagnostics/index.mjs +4 -0
- package/dist/vitest-diagnostics/vitest-fork-exit-error-detail.d.mts +7 -0
- package/dist/vitest-diagnostics/vitest-fork-exit-error-detail.mjs +26 -0
- package/dist/vitest-diagnostics/vitest-fork-exit-records.d.mts +26 -0
- package/dist/vitest-diagnostics/vitest-fork-exit-records.mjs +161 -0
- package/dist/vitest-diagnostics/vitest-fork-exit-sentinel.d.mts +14 -0
- package/dist/vitest-diagnostics/vitest-fork-exit-sentinel.mjs +131 -0
- package/dist/vitest-diagnostics/vitest-fork-leak-detection.d.mts +26 -0
- package/dist/vitest-diagnostics/vitest-fork-leak-detection.mjs +96 -0
- package/dist/vitest-diagnostics/vitest-fork-leak-diagnostics.d.mts +3 -0
- package/dist/vitest-diagnostics/vitest-fork-leak-diagnostics.mjs +14 -0
- package/dist/vitest-diagnostics/vitest-fork-leak-episode.d.mts +37 -0
- package/dist/vitest-diagnostics/vitest-fork-leak-episode.mjs +118 -0
- package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-errors.d.mts +2 -0
- package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-errors.mjs +42 -0
- package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-reporter.d.mts +12 -0
- package/dist/vitest-diagnostics/vitest-worker-exit-diagnostics-reporter.mjs +142 -0
- package/dist/workflow-policy/concurrency-scope.d.mts +4 -0
- package/dist/workflow-policy/concurrency-scope.mjs +66 -0
- package/dist/workflow-policy/concurrency.d.mts +10 -0
- package/dist/workflow-policy/concurrency.mjs +77 -0
- package/dist/workflow-policy/condition-entailment.d.mts +20 -0
- package/dist/workflow-policy/condition-entailment.mjs +132 -0
- package/dist/workflow-policy/condition-split.d.mts +15 -0
- package/dist/workflow-policy/condition-split.mjs +71 -0
- package/dist/workflow-policy/graph.d.mts +2 -0
- package/dist/workflow-policy/graph.mjs +138 -0
- package/dist/workflow-policy/index.d.mts +7 -0
- package/dist/workflow-policy/index.mjs +6 -0
- package/dist/workflow-policy/permission-mismatches.d.mts +3 -0
- package/dist/workflow-policy/permission-mismatches.mjs +52 -0
- package/dist/workflow-policy/permissions.d.mts +16 -0
- package/dist/workflow-policy/permissions.mjs +83 -0
- package/dist/workflow-policy/secret-readiness.d.mts +18 -0
- package/dist/workflow-policy/secret-readiness.mjs +163 -0
- package/dist/workflow-policy/shell-comments.d.mts +7 -0
- package/dist/workflow-policy/shell-comments.mjs +28 -0
- package/dist/workflow-policy/types.d.mts +90 -0
- package/dist/workflow-policy/types.mjs +1 -0
- package/dist/workspace-gates/manifest-version-parser.mjs +1 -52
- package/dist/workspace-gates/manifest-version-skips.d.mts +1 -0
- package/dist/workspace-gates/manifest-version-skips.mjs +52 -0
- package/docs/workflow-and-compiler.md +74 -0
- package/package.json +26 -2
- package/skills/agent-workflow/SKILL.md +4 -4
- package/skills/agent-workflow/references/review-response.md +47 -44
- package/skills/backend-vitest-test-authoring/SKILL.md +9 -6
- package/skills/blackboard/SKILL.md +8 -8
- package/skills/dependabot/SKILL.md +68 -67
- package/skills/dotnet-test-authoring/SKILL.md +11 -12
- package/skills/git-commit-checklist/SKILL.md +3 -3
- package/skills/github-actions-authoring/SKILL.md +40 -38
- package/skills/github-actions-checklist/SKILL.md +93 -85
- package/skills/github-issue/SKILL.md +82 -78
- package/skills/nextjs-vitest-test-authoring/SKILL.md +8 -6
- package/skills/npm-publishing/SKILL.md +42 -38
- package/skills/organize-github-issues/SKILL.md +18 -22
- package/skills/package-json-checklist/SKILL.md +5 -5
- package/skills/planning/SKILL.md +3 -3
- package/skills/playwright-authoring/SKILL.md +10 -9
- package/skills/postgres-node-performance-tuning/SKILL.md +10 -6
- package/skills/postgres-partitioning-uuid-v7/SKILL.md +10 -7
- package/skills/pr-description/SKILL.md +4 -3
- package/skills/retrospective/SKILL.md +14 -15
- package/skills/retrospective-distill/SKILL.md +14 -14
- package/skills/review-ci-logs/SKILL.md +18 -19
- package/skills/review-github-issue-taxonomy/SKILL.md +21 -18
- package/skills/revisit-followups/SKILL.md +3 -3
- package/skills/stacked-prs/SKILL.md +30 -61
- package/skills/static-analysis-checklist/SKILL.md +6 -4
- package/skills/storybook-authoring/SKILL.md +9 -9
- package/skills/swift-test-authoring/SKILL.md +10 -9
- package/skills/test-authoring/SKILL.md +14 -14
- package/skills/test-authoring/references/core-practice.md +12 -11
- package/skills/test-authoring/references/dependency-boundaries.md +36 -35
- package/skills/test-authoring/references/tautological-tests.md +45 -44
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|