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,46 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { isContextMethod } from './response-contract-route-analysis.mjs';
|
|
3
|
+
export function isXmlResponseCall(call) {
|
|
4
|
+
const expression = call.expression;
|
|
5
|
+
return (ts.isPropertyAccessExpression(expression) &&
|
|
6
|
+
expression.name.text === 'xml' &&
|
|
7
|
+
isContextMethod(expression.expression, 'response'));
|
|
8
|
+
}
|
|
9
|
+
export function streamingTextMediaType(call) {
|
|
10
|
+
let statement = call;
|
|
11
|
+
while (!ts.isStatement(statement))
|
|
12
|
+
statement = statement.parent;
|
|
13
|
+
const contentType = precedingContentType(statement);
|
|
14
|
+
return contentType === 'unsupported' ? undefined : contentType;
|
|
15
|
+
}
|
|
16
|
+
function precedingContentType(statement) {
|
|
17
|
+
const block = statement.parent;
|
|
18
|
+
if (!ts.isBlock(block))
|
|
19
|
+
return undefined;
|
|
20
|
+
const index = block.statements.indexOf(statement);
|
|
21
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
22
|
+
const contentType = contentTypeStatement(block.statements[i]);
|
|
23
|
+
if (contentType)
|
|
24
|
+
return contentType;
|
|
25
|
+
}
|
|
26
|
+
const enclosing = block.parent;
|
|
27
|
+
return ts.isStatement(enclosing) ? precedingContentType(enclosing) : undefined;
|
|
28
|
+
}
|
|
29
|
+
function contentTypeStatement(statement) {
|
|
30
|
+
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression))
|
|
31
|
+
return undefined;
|
|
32
|
+
const setter = statement.expression;
|
|
33
|
+
if (!isContextMethod(setter.expression, 'set'))
|
|
34
|
+
return undefined;
|
|
35
|
+
const [name, value] = setter.arguments;
|
|
36
|
+
if (!name || !ts.isStringLiteral(name) || name.text.toLowerCase() !== 'content-type')
|
|
37
|
+
return undefined;
|
|
38
|
+
if (!value || !ts.isStringLiteral(value))
|
|
39
|
+
return 'unsupported';
|
|
40
|
+
const contentType = value.text.toLowerCase();
|
|
41
|
+
if (contentType.startsWith('text/csv'))
|
|
42
|
+
return 'text/csv';
|
|
43
|
+
if (contentType.startsWith('application/xml'))
|
|
44
|
+
return 'application/xml';
|
|
45
|
+
return 'unsupported';
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import type { ExtractedResponseContract, ExtractContractSchemaOptions } from '../contract-schema/index.mts';
|
|
3
|
+
import type { BackendResponseContract } from './response-contract-types.mts';
|
|
4
|
+
export declare function responseBodyExpression(call: ts.CallExpression): ts.Expression | undefined;
|
|
5
|
+
export declare function containsResponseMarker(node: ts.Node): boolean;
|
|
6
|
+
export declare function extractBodyContract(call: ts.CallExpression, checker: ts.TypeChecker, sourceFile: ts.SourceFile, location: string, options?: ExtractContractSchemaOptions): ExtractedResponseContract;
|
|
7
|
+
export declare function registerContract(contracts: Map<string, BackendResponseContract>, key: string, contract: BackendResponseContract): void;
|
|
8
|
+
export declare function noContentContract(source: string): ExtractedResponseContract;
|
|
9
|
+
export declare function binaryContentContract(source: string): ExtractedResponseContract;
|
|
10
|
+
export declare function sourceLocation(sourceFile: ts.SourceFile, _node: ts.Node): string;
|
|
11
|
+
export declare function contractError(sourceFile: ts.SourceFile, node: ts.Node, message: string): Error;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { extractContractSchema, hashContractSchema } from '../contract-schema/index.mjs';
|
|
3
|
+
import { isContextMethod, responseMarker, visit } from './response-contract-route-analysis.mjs';
|
|
4
|
+
export function responseBodyExpression(call) {
|
|
5
|
+
if (isContextMethod(call.expression, 'json'))
|
|
6
|
+
return call.arguments[0];
|
|
7
|
+
if (!isContextMethod(call.expression, 'pipeline'))
|
|
8
|
+
return undefined;
|
|
9
|
+
const pipelineBody = call.arguments[0];
|
|
10
|
+
if (!pipelineBody ||
|
|
11
|
+
!ts.isCallExpression(pipelineBody) ||
|
|
12
|
+
!ts.isIdentifier(pipelineBody.expression) ||
|
|
13
|
+
pipelineBody.expression.text !== 'streamJsonObject') {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return pipelineBody.arguments[0];
|
|
17
|
+
}
|
|
18
|
+
export function containsResponseMarker(node) {
|
|
19
|
+
let found = false;
|
|
20
|
+
visit(node, (child) => {
|
|
21
|
+
if (ts.isCallExpression(child) && responseMarker(child.expression))
|
|
22
|
+
found = true;
|
|
23
|
+
});
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
export function extractBodyContract(call, checker, sourceFile, location, options = {}) {
|
|
27
|
+
const body = call.arguments[1];
|
|
28
|
+
if (!body)
|
|
29
|
+
throw contractError(sourceFile, call, 'apiResponse requires a response body');
|
|
30
|
+
return extractContractSchema(checker.getTypeAtLocation(body), checker, location, options);
|
|
31
|
+
}
|
|
32
|
+
export function registerContract(contracts, key, contract) {
|
|
33
|
+
const existing = contracts.get(key);
|
|
34
|
+
if (!existing) {
|
|
35
|
+
contracts.set(key, contract);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (existing.hash !== contract.hash) {
|
|
39
|
+
throw new Error(`Backend response contract "${key}" resolves to multiple backend response schemas at ${existing.source} and ${contract.source}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function noContentContract(source) {
|
|
43
|
+
const schema = { root: { type: 'null' }, definitions: {} };
|
|
44
|
+
return { source, schema, hash: hashContractSchema(schema) };
|
|
45
|
+
}
|
|
46
|
+
export function binaryContentContract(source) {
|
|
47
|
+
const schema = {
|
|
48
|
+
root: { type: 'string', format: 'binary' },
|
|
49
|
+
definitions: {},
|
|
50
|
+
};
|
|
51
|
+
return { source, schema, hash: hashContractSchema(schema) };
|
|
52
|
+
}
|
|
53
|
+
export function sourceLocation(sourceFile, _node) {
|
|
54
|
+
return normalizePath(sourceFile.fileName);
|
|
55
|
+
}
|
|
56
|
+
export function contractError(sourceFile, node, message) {
|
|
57
|
+
return new Error(`${sourceLocation(sourceFile, node)}: ${message}`);
|
|
58
|
+
}
|
|
59
|
+
function normalizePath(path) {
|
|
60
|
+
return path.replaceAll('\\', '/');
|
|
61
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { type DiscoverApiResponseContractsOptions } from './response-contract-lenient.mts';
|
|
3
|
+
import type { BackendResponseContract } from './response-contract-types.mts';
|
|
4
|
+
export type { BackendResponseContract } from './response-contract-types.mts';
|
|
5
|
+
export type { DiscoverApiResponseContractsOptions } from './response-contract-lenient.mts';
|
|
6
|
+
export declare function discoverApiResponseContracts(program: ts.Program, sourceFiles: readonly ts.SourceFile[], requestedKeys?: ReadonlySet<string>, options?: DiscoverApiResponseContractsOptions): Record<string, BackendResponseContract>;
|
|
7
|
+
export declare function enclosingResponseEmission(call: ts.CallExpression): ts.CallExpression | undefined;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { discoverImplicitContract } from './response-contract-implicit.mjs';
|
|
3
|
+
import { registerRouteContract, } from './response-contract-lenient.mjs';
|
|
4
|
+
import { binaryContentContract, contractError, extractBodyContract, noContentContract, sourceLocation, } from './response-contract-registration.mjs';
|
|
5
|
+
import { collectHandlerBindings, enclosingRouteBinding, isContextMethod, responseMarker, visit, } from './response-contract-route-analysis.mjs';
|
|
6
|
+
import { resolveEmissionStatus } from './response-contract-status.mjs';
|
|
7
|
+
export function discoverApiResponseContracts(program, sourceFiles, requestedKeys, options) {
|
|
8
|
+
const checker = program.getTypeChecker();
|
|
9
|
+
const contracts = new Map();
|
|
10
|
+
const handlerBindings = collectHandlerBindings(sourceFiles, checker);
|
|
11
|
+
for (const sourceFile of sourceFiles) {
|
|
12
|
+
visit(sourceFile, (node) => {
|
|
13
|
+
if (!ts.isCallExpression(node))
|
|
14
|
+
return;
|
|
15
|
+
const marker = responseMarker(node.expression);
|
|
16
|
+
if (!marker)
|
|
17
|
+
return;
|
|
18
|
+
const keyNode = node.arguments[0];
|
|
19
|
+
if (!keyNode || !ts.isStringLiteral(keyNode)) {
|
|
20
|
+
throw contractError(sourceFile, node, `${marker} requires a literal contract key`);
|
|
21
|
+
}
|
|
22
|
+
const binding = enclosingRouteBinding(node, checker, handlerBindings);
|
|
23
|
+
if (!binding)
|
|
24
|
+
throw contractError(sourceFile, node, `${marker} must be inside an app.route handler`);
|
|
25
|
+
const expectedPrefix = `${binding.method}:${binding.routeTemplate}`;
|
|
26
|
+
if (keyNode.text !== expectedPrefix && !keyNode.text.startsWith(`${expectedPrefix}#`)) {
|
|
27
|
+
throw contractError(sourceFile, keyNode, `Contract key "${keyNode.text}" does not match enclosing route ${expectedPrefix}`);
|
|
28
|
+
}
|
|
29
|
+
const location = sourceLocation(sourceFile, node);
|
|
30
|
+
const responseEmission = enclosingResponseEmission(node) ?? node;
|
|
31
|
+
const status = resolveEmissionStatus(responseEmission);
|
|
32
|
+
const rawMediaType = rawResponseMediaType(marker, node, sourceFile);
|
|
33
|
+
registerRouteContract(contracts, keyNode.text, binding, location, {
|
|
34
|
+
bodyKind: marker === 'apiNoContent' ? 'none' : 'content',
|
|
35
|
+
...(marker === 'apiResponse'
|
|
36
|
+
? { mediaType: 'application/json', mediaTypeKnowledge: 'known' }
|
|
37
|
+
: rawMediaType
|
|
38
|
+
? { mediaType: rawMediaType, mediaTypeKnowledge: 'known' }
|
|
39
|
+
: { mediaTypeKnowledge: 'none' }),
|
|
40
|
+
...status,
|
|
41
|
+
}, () => marker === 'apiNoContent'
|
|
42
|
+
? noContentContract(location)
|
|
43
|
+
: marker === 'apiOpenApiRawResponse'
|
|
44
|
+
? binaryContentContract(location)
|
|
45
|
+
: extractBodyContract(node, checker, sourceFile, location, options), options);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const sourceFile of sourceFiles) {
|
|
49
|
+
visit(sourceFile, (node) => {
|
|
50
|
+
if (!ts.isCallExpression(node) || responseMarker(node.expression))
|
|
51
|
+
return;
|
|
52
|
+
discoverImplicitContract(node, checker, sourceFile, contracts, handlerBindings, requestedKeys, options);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return Object.fromEntries([...contracts.entries()].toSorted(([left], [right]) => left.localeCompare(right)));
|
|
56
|
+
}
|
|
57
|
+
function rawResponseMediaType(marker, node, sourceFile) {
|
|
58
|
+
if (marker !== 'apiOpenApiRawResponse')
|
|
59
|
+
return undefined;
|
|
60
|
+
const mediaType = node.arguments[1];
|
|
61
|
+
if (!mediaType || !ts.isStringLiteral(mediaType)) {
|
|
62
|
+
throw contractError(sourceFile, node, 'apiOpenApiRawResponse requires a literal media type');
|
|
63
|
+
}
|
|
64
|
+
return mediaType.text;
|
|
65
|
+
}
|
|
66
|
+
export function enclosingResponseEmission(call) {
|
|
67
|
+
let current = call.parent;
|
|
68
|
+
while (current && !ts.isStatement(current)) {
|
|
69
|
+
if (ts.isCallExpression(current) &&
|
|
70
|
+
(isContextMethod(current.expression, 'json') ||
|
|
71
|
+
isContextMethod(current.expression, 'pipeline')))
|
|
72
|
+
return current;
|
|
73
|
+
current = current.parent;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
export { isContextMethod, isContextResponseBufferCall, isContextResponseEmptyCall, responseMarker, } from './response-contract-ctx-analysis.mts';
|
|
3
|
+
export type RouteBinding = {
|
|
4
|
+
method: string;
|
|
5
|
+
routeTemplate: string;
|
|
6
|
+
};
|
|
7
|
+
export type HandlerBindings = Map<ts.Symbol, RouteBinding>;
|
|
8
|
+
export declare function requestedKeyForBinding(binding: RouteBinding, requestedKeys?: ReadonlySet<string>): string | undefined;
|
|
9
|
+
export declare function enclosingRouteBinding(node: ts.Node, checker: ts.TypeChecker, handlerBindings: HandlerBindings): RouteBinding | undefined;
|
|
10
|
+
export declare function collectHandlerBindings(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker): HandlerBindings;
|
|
11
|
+
export declare function routeTemplateFromExpression(expression: ts.Expression): string | undefined;
|
|
12
|
+
export declare function propertyName(expression: ts.Expression): string | undefined;
|
|
13
|
+
export declare function visit(node: ts.Node, callback: (node: ts.Node) => void): void;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
export { isContextMethod, isContextResponseBufferCall, isContextResponseEmptyCall, responseMarker, } from './response-contract-ctx-analysis.mjs';
|
|
3
|
+
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
4
|
+
export function requestedKeyForBinding(binding, requestedKeys) {
|
|
5
|
+
if (!requestedKeys)
|
|
6
|
+
return `${binding.method}:${binding.routeTemplate}`;
|
|
7
|
+
const exact = `${binding.method}:${binding.routeTemplate}`;
|
|
8
|
+
if (requestedKeys.has(exact))
|
|
9
|
+
return exact;
|
|
10
|
+
const bindingShape = routeShape(binding.routeTemplate);
|
|
11
|
+
return [...requestedKeys]
|
|
12
|
+
.filter((key) => !key.includes('#'))
|
|
13
|
+
.find((key) => {
|
|
14
|
+
const separator = key.indexOf(':');
|
|
15
|
+
return (key.slice(0, separator) === binding.method &&
|
|
16
|
+
routeShape(key.slice(separator + 1)) === bindingShape);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function routeShape(routeTemplate) {
|
|
20
|
+
return routeTemplate.replace(/:[^/]+/g, ':');
|
|
21
|
+
}
|
|
22
|
+
export function enclosingRouteBinding(node, checker, handlerBindings) {
|
|
23
|
+
let current = node;
|
|
24
|
+
while (current) {
|
|
25
|
+
if (isFunctionLike(current) && ts.isCallExpression(current.parent)) {
|
|
26
|
+
const handlerCall = current.parent;
|
|
27
|
+
const method = propertyName(handlerCall.expression)?.toUpperCase();
|
|
28
|
+
if (method && HTTP_METHODS.has(method)) {
|
|
29
|
+
const routeTemplate = routeTemplateFromExpression(handlerCall.expression);
|
|
30
|
+
if (routeTemplate)
|
|
31
|
+
return { method, routeTemplate };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const handlerSymbol = functionSymbol(current, checker);
|
|
35
|
+
if (handlerSymbol) {
|
|
36
|
+
const binding = handlerBindings.get(resolveSymbol(handlerSymbol, checker));
|
|
37
|
+
if (binding)
|
|
38
|
+
return binding;
|
|
39
|
+
}
|
|
40
|
+
current = current.parent;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
export function collectHandlerBindings(sourceFiles, checker) {
|
|
45
|
+
const bindingsBySymbol = new Map();
|
|
46
|
+
for (const sourceFile of sourceFiles) {
|
|
47
|
+
visit(sourceFile, (node) => {
|
|
48
|
+
if (!ts.isCallExpression(node))
|
|
49
|
+
return;
|
|
50
|
+
const method = propertyName(node.expression)?.toUpperCase();
|
|
51
|
+
if (!method || !HTTP_METHODS.has(method))
|
|
52
|
+
return;
|
|
53
|
+
const routeTemplate = routeTemplateFromExpression(node.expression);
|
|
54
|
+
if (!routeTemplate)
|
|
55
|
+
return;
|
|
56
|
+
const binding = { method, routeTemplate };
|
|
57
|
+
for (const symbol of handlerArgumentSymbols(node, checker)) {
|
|
58
|
+
const resolved = resolveSymbol(symbol, checker);
|
|
59
|
+
const candidates = bindingsBySymbol.get(resolved) ?? [];
|
|
60
|
+
candidates.push(binding);
|
|
61
|
+
bindingsBySymbol.set(resolved, candidates);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const bindings = new Map();
|
|
66
|
+
for (const [symbol, candidates] of bindingsBySymbol) {
|
|
67
|
+
const [first] = candidates;
|
|
68
|
+
const unambiguous = candidates.every((candidate) => candidate.method === first.method && candidate.routeTemplate === first.routeTemplate);
|
|
69
|
+
// A symbol bound to more than one distinct route (a genuinely shared handler function) can't
|
|
70
|
+
// be attributed to either one — recording an arbitrary winner would misdocument whichever
|
|
71
|
+
// route lost.
|
|
72
|
+
if (unambiguous)
|
|
73
|
+
bindings.set(symbol, first);
|
|
74
|
+
}
|
|
75
|
+
return bindings;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Symbols a route registration call directly implicates: identifiers passed by reference
|
|
79
|
+
* (`app.route(...).post(handleThing)`), and — for an inline handler argument — every plain named
|
|
80
|
+
* function it calls directly, e.g. `app.route(...).post(async ctx => { await handleThing(ctx) })`.
|
|
81
|
+
* `enclosingRouteBinding`'s lexical walk can't see through the latter case on its own, since
|
|
82
|
+
* `handleThing`'s body isn't lexically nested inside the route registration at all; recording its
|
|
83
|
+
* symbol here lets the same `handlerBindings` lookup resolve it.
|
|
84
|
+
*/
|
|
85
|
+
function handlerArgumentSymbols(node, checker) {
|
|
86
|
+
const symbols = [];
|
|
87
|
+
for (const argument of node.arguments) {
|
|
88
|
+
if (ts.isIdentifier(argument)) {
|
|
89
|
+
const symbol = checker.getSymbolAtLocation(argument);
|
|
90
|
+
if (symbol)
|
|
91
|
+
symbols.push(symbol);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (isFunctionLike(argument)) {
|
|
95
|
+
visit(argument, (child) => {
|
|
96
|
+
if (!ts.isCallExpression(child) || !ts.isIdentifier(child.expression))
|
|
97
|
+
return;
|
|
98
|
+
const symbol = checker.getSymbolAtLocation(child.expression);
|
|
99
|
+
if (symbol)
|
|
100
|
+
symbols.push(symbol);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return symbols;
|
|
105
|
+
}
|
|
106
|
+
function functionSymbol(node, checker) {
|
|
107
|
+
if (ts.isFunctionDeclaration(node) && node.name)
|
|
108
|
+
return checker.getSymbolAtLocation(node.name);
|
|
109
|
+
if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
|
110
|
+
ts.isVariableDeclaration(node.parent) &&
|
|
111
|
+
ts.isIdentifier(node.parent.name)) {
|
|
112
|
+
return checker.getSymbolAtLocation(node.parent.name);
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function resolveSymbol(symbol, checker) {
|
|
117
|
+
return symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
118
|
+
}
|
|
119
|
+
export function routeTemplateFromExpression(expression) {
|
|
120
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
121
|
+
return undefined;
|
|
122
|
+
return findRouteCall(expression.expression);
|
|
123
|
+
}
|
|
124
|
+
function findRouteCall(expression) {
|
|
125
|
+
if (!ts.isCallExpression(expression))
|
|
126
|
+
return undefined;
|
|
127
|
+
if (!ts.isPropertyAccessExpression(expression.expression))
|
|
128
|
+
return undefined;
|
|
129
|
+
if (expression.expression.name.text === 'route') {
|
|
130
|
+
const route = expression.arguments[0];
|
|
131
|
+
return route && ts.isStringLiteral(route) ? route.text : undefined;
|
|
132
|
+
}
|
|
133
|
+
return findRouteCall(expression.expression.expression);
|
|
134
|
+
}
|
|
135
|
+
export function propertyName(expression) {
|
|
136
|
+
return ts.isPropertyAccessExpression(expression) ? expression.name.text : undefined;
|
|
137
|
+
}
|
|
138
|
+
function isFunctionLike(node) {
|
|
139
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
140
|
+
}
|
|
141
|
+
export function visit(node, callback) {
|
|
142
|
+
callback(node);
|
|
143
|
+
node.forEachChild((child) => visit(child, callback));
|
|
144
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import type { BackendResponseContract } from './response-contract-types.mts';
|
|
3
|
+
export declare function responseStatusCodesForContract(contract: BackendResponseContract): number[];
|
|
4
|
+
/**
|
|
5
|
+
* Maps `METHOD:/route` to the non-error static `ctx.setStatus(NNN)` literal(s) found for that
|
|
6
|
+
* route binding: a plain literal contributes one status; `ctx.setStatus(cond ? A : B)` with both
|
|
7
|
+
* branches numeric contributes both (the doc can't know which branch fires at request time, so
|
|
8
|
+
* both are valid statuses for the operation). Statuses >= 400 are dropped per-branch: they mark
|
|
9
|
+
* hand-rolled error branches (the shared `default` -> `components/responses/Error` already
|
|
10
|
+
* documents error responses), and capturing them here would misattribute an error status to the
|
|
11
|
+
* whole operation. A route absent from the map never sets a non-error status explicitly (callers
|
|
12
|
+
* fall back to 200, or 204 when the response schema's root is `null`).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the status documented for one specific `apiResponse`/`apiNoContent` marker call: the
|
|
16
|
+
* nearest preceding sibling `ctx.setStatus(NNN)` literal, walking outward through enclosing
|
|
17
|
+
* blocks (mirroring `response-contract-implicit.mts`'s `precededByErrorStatus` walk). Unlike
|
|
18
|
+
* `collectRouteStatusCodes`, this has no `>=400` filter: an explicit `#variant` marker is a
|
|
19
|
+
* deliberate author opt-in to document that exact status (e.g. 422 for a `#validation` variant),
|
|
20
|
+
* and the route-wide map (keyed only on `method:routeTemplate`) can't tell two markers in the
|
|
21
|
+
* same route apart when they're preceded by different statuses.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveEmissionStatus(call: ts.CallExpression): {
|
|
24
|
+
statusCodes?: readonly [number, ...number[]];
|
|
25
|
+
statusKnowledge: 'default' | 'explicit' | 'unknown';
|
|
26
|
+
unavailableReason?: string;
|
|
27
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { isContextMethod } from './response-contract-route-analysis.mjs';
|
|
3
|
+
export function responseStatusCodesForContract(contract) {
|
|
4
|
+
if (contract.statusKnowledge === 'unknown')
|
|
5
|
+
return [];
|
|
6
|
+
if (contract.statusCodes)
|
|
7
|
+
return [...new Set(contract.statusCodes)].toSorted((a, b) => a - b);
|
|
8
|
+
const bodyKind = contract.bodyKind ?? (contract.schema.root.type === 'null' ? 'none' : 'content');
|
|
9
|
+
return [bodyKind === 'none' ? 204 : 200];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Maps `METHOD:/route` to the non-error static `ctx.setStatus(NNN)` literal(s) found for that
|
|
13
|
+
* route binding: a plain literal contributes one status; `ctx.setStatus(cond ? A : B)` with both
|
|
14
|
+
* branches numeric contributes both (the doc can't know which branch fires at request time, so
|
|
15
|
+
* both are valid statuses for the operation). Statuses >= 400 are dropped per-branch: they mark
|
|
16
|
+
* hand-rolled error branches (the shared `default` -> `components/responses/Error` already
|
|
17
|
+
* documents error responses), and capturing them here would misattribute an error status to the
|
|
18
|
+
* whole operation. A route absent from the map never sets a non-error status explicitly (callers
|
|
19
|
+
* fall back to 200, or 204 when the response schema's root is `null`).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the status documented for one specific `apiResponse`/`apiNoContent` marker call: the
|
|
23
|
+
* nearest preceding sibling `ctx.setStatus(NNN)` literal, walking outward through enclosing
|
|
24
|
+
* blocks (mirroring `response-contract-implicit.mts`'s `precededByErrorStatus` walk). Unlike
|
|
25
|
+
* `collectRouteStatusCodes`, this has no `>=400` filter: an explicit `#variant` marker is a
|
|
26
|
+
* deliberate author opt-in to document that exact status (e.g. 422 for a `#validation` variant),
|
|
27
|
+
* and the route-wide map (keyed only on `method:routeTemplate`) can't tell two markers in the
|
|
28
|
+
* same route apart when they're preceded by different statuses.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveEmissionStatus(call) {
|
|
31
|
+
if (isContextMethod(call.expression, 'setStatus'))
|
|
32
|
+
return statusArgument(call.arguments[0]);
|
|
33
|
+
let statement = call;
|
|
34
|
+
while (!ts.isStatement(statement))
|
|
35
|
+
statement = statement.parent;
|
|
36
|
+
return precedingStatus(statement);
|
|
37
|
+
}
|
|
38
|
+
function precedingStatus(statement) {
|
|
39
|
+
const block = statement.parent;
|
|
40
|
+
if (!ts.isBlock(block))
|
|
41
|
+
return { statusKnowledge: 'default' };
|
|
42
|
+
const index = block.statements.indexOf(statement);
|
|
43
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
44
|
+
const status = statusStatement(block.statements[i]);
|
|
45
|
+
if (status)
|
|
46
|
+
return status;
|
|
47
|
+
}
|
|
48
|
+
const enclosing = block.parent;
|
|
49
|
+
return ts.isStatement(enclosing) ? precedingStatus(enclosing) : { statusKnowledge: 'default' };
|
|
50
|
+
}
|
|
51
|
+
function statusStatement(statement) {
|
|
52
|
+
if (!ts.isExpressionStatement(statement))
|
|
53
|
+
return undefined;
|
|
54
|
+
const expression = statement.expression;
|
|
55
|
+
if (!ts.isCallExpression(expression) || !isContextMethod(expression.expression, 'setStatus'))
|
|
56
|
+
return undefined;
|
|
57
|
+
return statusArgument(expression.arguments[0]);
|
|
58
|
+
}
|
|
59
|
+
/** A `ctx.setStatus` argument's possible numeric literal value(s): one for a bare literal, up to two (sorted ascending) for `cond ? A : B` when both branches are numeric literals. */
|
|
60
|
+
function statusArgument(argument) {
|
|
61
|
+
if (!argument)
|
|
62
|
+
return {
|
|
63
|
+
statusKnowledge: 'unknown',
|
|
64
|
+
unavailableReason: 'ctx.setStatus has no statically known status',
|
|
65
|
+
};
|
|
66
|
+
if (ts.isNumericLiteral(argument))
|
|
67
|
+
return { statusCodes: [Number(argument.text)], statusKnowledge: 'explicit' };
|
|
68
|
+
if (ts.isConditionalExpression(argument) &&
|
|
69
|
+
ts.isNumericLiteral(argument.whenTrue) &&
|
|
70
|
+
ts.isNumericLiteral(argument.whenFalse)) {
|
|
71
|
+
const values = [
|
|
72
|
+
...new Set([Number(argument.whenTrue.text), Number(argument.whenFalse.text)]),
|
|
73
|
+
].toSorted((a, b) => a - b);
|
|
74
|
+
return { statusCodes: values, statusKnowledge: 'explicit' };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
statusKnowledge: 'unknown',
|
|
78
|
+
unavailableReason: 'nearest ctx.setStatus value is dynamic',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExtractedResponseContract } from '../contract-schema/index.mts';
|
|
2
|
+
export type BackendResponseContract = ExtractedResponseContract & {
|
|
3
|
+
method: string;
|
|
4
|
+
routeTemplate: string;
|
|
5
|
+
/** Statuses owned by this response emission. Defaults to 200, or 204 for an unannotated empty emission. */
|
|
6
|
+
statusCodes?: readonly [number, ...number[]];
|
|
7
|
+
/** Distinguishes a known framework default from an explicitly set or genuinely unknown status. */
|
|
8
|
+
statusKnowledge?: 'default' | 'explicit' | 'unknown';
|
|
9
|
+
bodyKind?: 'content' | 'none';
|
|
10
|
+
/** Required for content emissions and absent for bodyless emissions. */
|
|
11
|
+
mediaType?: string;
|
|
12
|
+
/** Raw buffers and arbitrary streams have an unknown media type; bodyless emissions have none. */
|
|
13
|
+
mediaTypeKnowledge?: 'known' | 'none' | 'unknown';
|
|
14
|
+
/** Set when the route's response schema could not be extracted (lenient discovery mode). */
|
|
15
|
+
unavailableReason?: string;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function compareCodeUnits(left: string, right: string): number;
|
|
2
|
+
export declare function assertJsonValue(value: unknown, ancestors?: WeakSet<object>): void;
|
|
3
|
+
export declare function compare(base: unknown, companion: unknown, path: string, difference: (path: string, base: unknown, companion: unknown) => void): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function compareCodeUnits(left, right) {
|
|
2
|
+
if (left < right)
|
|
3
|
+
return -1;
|
|
4
|
+
if (left > right)
|
|
5
|
+
return 1;
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
function pointer(path, key) {
|
|
9
|
+
const segment = String(key).replaceAll('~', '~0').replaceAll('/', '~1');
|
|
10
|
+
return `${path}/${segment}`;
|
|
11
|
+
}
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
14
|
+
return false;
|
|
15
|
+
const prototype = Object.getPrototypeOf(value);
|
|
16
|
+
return prototype === null || prototype === Object.prototype;
|
|
17
|
+
}
|
|
18
|
+
export function assertJsonValue(value, ancestors = new WeakSet()) {
|
|
19
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
20
|
+
return;
|
|
21
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
22
|
+
return;
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
if (ancestors.has(value))
|
|
25
|
+
throw new Error('unsupported cyclic YAML value');
|
|
26
|
+
ancestors.add(value);
|
|
27
|
+
for (const item of value)
|
|
28
|
+
assertJsonValue(item, ancestors);
|
|
29
|
+
ancestors.delete(value);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (isRecord(value)) {
|
|
33
|
+
if (ancestors.has(value))
|
|
34
|
+
throw new Error('unsupported cyclic YAML value');
|
|
35
|
+
ancestors.add(value);
|
|
36
|
+
for (const item of Object.values(value))
|
|
37
|
+
assertJsonValue(item, ancestors);
|
|
38
|
+
ancestors.delete(value);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
throw new Error('unsupported non-JSON YAML value');
|
|
42
|
+
}
|
|
43
|
+
export function compare(base, companion, path, difference) {
|
|
44
|
+
if (Object.is(base, companion))
|
|
45
|
+
return;
|
|
46
|
+
if (Array.isArray(base) && Array.isArray(companion)) {
|
|
47
|
+
const length = Math.max(base.length, companion.length);
|
|
48
|
+
for (let index = 0; index < length; index++)
|
|
49
|
+
compare(base[index], companion[index], pointer(path, index), difference);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (isRecord(base) && isRecord(companion)) {
|
|
53
|
+
for (const key of [...new Set([...Object.keys(base), ...Object.keys(companion)])].toSorted(compareCodeUnits))
|
|
54
|
+
compare(base[key], companion[key], pointer(path, key), difference);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
difference(path, base, companion);
|
|
58
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { compareCodeUnits } from './companion-parity-compare.mts';
|
|
2
|
+
export { compareCodeUnits };
|
|
1
3
|
export interface AstGrepCompanionDifference {
|
|
2
4
|
readonly baseFile: string;
|
|
3
5
|
readonly companionFile: string;
|
|
@@ -14,6 +16,5 @@ export interface AstGrepCompanionContext {
|
|
|
14
16
|
readonly file: string;
|
|
15
17
|
readonly role: 'base' | 'companion';
|
|
16
18
|
}
|
|
17
|
-
export declare function compareCodeUnits(left: string, right: string): number;
|
|
18
19
|
/** Compares recursive `<name>.yml`/`.yaml` rules with `<name>-tsx` companions. */
|
|
19
20
|
export declare function compareAstGrepCompanions(options: AstGrepCompanionParityOptions): AstGrepCompanionDifference[];
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { parse as yamlLoad } from 'yaml';
|
|
4
|
+
import { assertJsonValue, compare, compareCodeUnits } from './companion-parity-compare.mjs';
|
|
5
|
+
export { compareCodeUnits };
|
|
4
6
|
const YAML_EXTENSION = /\.ya?ml$/u;
|
|
5
|
-
export function compareCodeUnits(left, right) {
|
|
6
|
-
if (left < right)
|
|
7
|
-
return -1;
|
|
8
|
-
if (left > right)
|
|
9
|
-
return 1;
|
|
10
|
-
return 0;
|
|
11
|
-
}
|
|
12
7
|
function assertPhysicalPath(path) {
|
|
13
8
|
let ancestor = path;
|
|
14
9
|
while (true) {
|
|
@@ -49,57 +44,6 @@ function loadDocument(rules, file) {
|
|
|
49
44
|
function stem(file) {
|
|
50
45
|
return file.replace(YAML_EXTENSION, '');
|
|
51
46
|
}
|
|
52
|
-
function pointer(path, key) {
|
|
53
|
-
const segment = String(key).replaceAll('~', '~0').replaceAll('/', '~1');
|
|
54
|
-
return `${path}/${segment}`;
|
|
55
|
-
}
|
|
56
|
-
function isRecord(value) {
|
|
57
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
58
|
-
return false;
|
|
59
|
-
const prototype = Object.getPrototypeOf(value);
|
|
60
|
-
return prototype === null || prototype === Object.prototype;
|
|
61
|
-
}
|
|
62
|
-
function assertJsonValue(value, ancestors = new WeakSet()) {
|
|
63
|
-
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
64
|
-
return;
|
|
65
|
-
if (typeof value === 'number' && Number.isFinite(value))
|
|
66
|
-
return;
|
|
67
|
-
if (Array.isArray(value)) {
|
|
68
|
-
if (ancestors.has(value))
|
|
69
|
-
throw new Error('unsupported cyclic YAML value');
|
|
70
|
-
ancestors.add(value);
|
|
71
|
-
for (const item of value)
|
|
72
|
-
assertJsonValue(item, ancestors);
|
|
73
|
-
ancestors.delete(value);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
if (isRecord(value)) {
|
|
77
|
-
if (ancestors.has(value))
|
|
78
|
-
throw new Error('unsupported cyclic YAML value');
|
|
79
|
-
ancestors.add(value);
|
|
80
|
-
for (const item of Object.values(value))
|
|
81
|
-
assertJsonValue(item, ancestors);
|
|
82
|
-
ancestors.delete(value);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
throw new Error('unsupported non-JSON YAML value');
|
|
86
|
-
}
|
|
87
|
-
function compare(base, companion, path, difference) {
|
|
88
|
-
if (Object.is(base, companion))
|
|
89
|
-
return;
|
|
90
|
-
if (Array.isArray(base) && Array.isArray(companion)) {
|
|
91
|
-
const length = Math.max(base.length, companion.length);
|
|
92
|
-
for (let index = 0; index < length; index++)
|
|
93
|
-
compare(base[index], companion[index], pointer(path, index), difference);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (isRecord(base) && isRecord(companion)) {
|
|
97
|
-
for (const key of [...new Set([...Object.keys(base), ...Object.keys(companion)])].toSorted(compareCodeUnits))
|
|
98
|
-
compare(base[key], companion[key], pointer(path, key), difference);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
difference(path, base, companion);
|
|
102
|
-
}
|
|
103
47
|
function assertSuffix(suffix) {
|
|
104
48
|
if (!suffix || /[\\/]/u.test(suffix))
|
|
105
49
|
throw new Error('companionSuffix must be a non-empty filename suffix');
|