vouchington-tooling 0.21.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 +59 -1
- 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/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
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Libraries and the `vouchington` CLI.
|
|
4
4
|
|
|
5
|
+
The [workflow and compiler primitives](docs/workflow-and-compiler.md) provide consumer supplied
|
|
6
|
+
GitHub Actions policy checks and compiler host freshness with bounded build settlement.
|
|
7
|
+
|
|
8
|
+
[`config-inventory`](../../docs/config-inventory.md) collects environment, dynamic-configuration,
|
|
9
|
+
and package-manager evidence using caller-owned file roles and policies.
|
|
10
|
+
|
|
5
11
|
```bash
|
|
6
12
|
npm install vouchington-tooling
|
|
7
13
|
# optional, only if you import vouchington-tooling/sql-ast
|
|
@@ -9,6 +15,8 @@ npm install @libpg-query/parser
|
|
|
9
15
|
# optional, only if you import vouchington-tooling/contract-schema
|
|
10
16
|
# (classic compiler API; typescript@7's package root is version-only)
|
|
11
17
|
npm install @typescript/typescript6
|
|
18
|
+
# optional, only if you use the Vitest reporter export
|
|
19
|
+
npm install vitest
|
|
12
20
|
# optional, only for vouchington-tooling/agent-blackboard and agent-blackboard CLI commands
|
|
13
21
|
npm install agent-blackboard@^0.6.0
|
|
14
22
|
```
|
|
@@ -227,6 +235,7 @@ import {
|
|
|
227
235
|
validateFixtureContracts,
|
|
228
236
|
writeGeneratedFiles,
|
|
229
237
|
} from 'vouchington-tooling/api-fixtures'
|
|
238
|
+
import { discoverAppRouteCtxContractsV1 } from 'vouchington-tooling/api-contract-discovery'
|
|
230
239
|
import { decide, deriveRetryAttempt } from 'vouchington-tooling/transient-retry'
|
|
231
240
|
import { parseCsvRows, streamCsvRows } from 'vouchington-tooling/csv'
|
|
232
241
|
import {
|
|
@@ -268,20 +277,40 @@ import { normalizeSwiftSource } from 'vouchington-tooling/swift-semantic-equal'
|
|
|
268
277
|
import { parseUniqueSwiftBinaryTargetChecksum } from 'vouchington-tooling/swift-source-offset'
|
|
269
278
|
import { validateResolvedPinDelta } from 'vouchington-tooling/swift-resolved-pin-delta'
|
|
270
279
|
import {
|
|
280
|
+
createForkLeakDetector,
|
|
281
|
+
createVitestWorkerExitDiagnosticsReporter,
|
|
271
282
|
formatDiagnosticReportSummaries,
|
|
272
283
|
readDiagnosticReportSummaries,
|
|
284
|
+
registerForkExitSentinel,
|
|
273
285
|
} from 'vouchington-tooling/vitest-diagnostics'
|
|
274
286
|
import { runRetrospectiveTranscript } from 'vouchington-tooling/retrospective-transcript'
|
|
275
287
|
import { appendJournal, probeBlackboard } from 'vouchington-tooling/agent-blackboard'
|
|
276
288
|
import { buildSessionFrictionReport, recordFriction } from 'vouchington-tooling/session-friction'
|
|
277
289
|
import { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from 'vouchington-tooling'
|
|
278
|
-
import { createPullRequest,
|
|
290
|
+
import { createPullRequest, processDiffCommand, runGh, runGit } from 'vouchington-tooling/gh-cli'
|
|
279
291
|
import {
|
|
280
292
|
shellScriptViolations,
|
|
281
293
|
workflowYamlViolations,
|
|
282
294
|
} from 'vouchington-tooling/gh-api-shell-quoting'
|
|
283
295
|
```
|
|
284
296
|
|
|
297
|
+
`processDiffCommand` runs a caller-supplied `git diff` or `gh pr diff` executable and argv without
|
|
298
|
+
a shell, emitting complete unified-diff file blocks without accumulating the whole patch. It keeps
|
|
299
|
+
at most one file block (which can itself be large), preserves preambles and line content, and drains
|
|
300
|
+
a bounded stderr tail for typed command failures. Blocks are provisional until the returned promise
|
|
301
|
+
resolves: callers must discard accumulated results if the command later exits nonzero or is signaled.
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
let changedFileCount = 0
|
|
305
|
+
await processDiffCommand({ executable: 'git', args: ['diff', 'origin/main...HEAD'] }, async () => {
|
|
306
|
+
changedFileCount += 1
|
|
307
|
+
})
|
|
308
|
+
// Use changedFileCount only after processDiffCommand resolves.
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
This breaking minor removes `getDiffAgainstBase`; migrate callers to `processDiffCommand` and pass
|
|
312
|
+
the command they need to run.
|
|
313
|
+
|
|
285
314
|
`checkSccComplexity` keeps its single repository-wide scan when `scopes` is omitted. Consumers
|
|
286
315
|
that need separately ratcheted areas may provide named scopes with positional `includePaths`; SCC
|
|
287
316
|
runs once per scope and reports the scope in each diagnostic. Parse a consumer-owned JSON baseline
|
|
@@ -383,6 +412,35 @@ token-source router remains deprecated for one release line.
|
|
|
383
412
|
filenames, tolerates partial files, returns only a bounded field allowlist, and never emits raw
|
|
384
413
|
native frame symbols. Both structured reads and text rendering have hard report-count limits.
|
|
385
414
|
|
|
415
|
+
`api-contract-discovery` is the version-one adapter for `app.route('/path').get/post/...` and
|
|
416
|
+
`ctx` response conventions. Pass a classic TypeScript `program` and its route `sourceFiles` to
|
|
417
|
+
`discoverAppRouteCtxContractsV1`. The result contains registered routes and response, request,
|
|
418
|
+
query, and header contracts. The caller owns program construction, route-file selection, and
|
|
419
|
+
generation. Pass `options.formatAliases` and `options.boundedArrayAlias` for application type
|
|
420
|
+
aliases; `options.onRouteError` enables lenient response and request extraction. The adapter
|
|
421
|
+
does not cache programs or depend on the caller's repository layout. OpenAPI document assembly
|
|
422
|
+
remains a separate `openapi-document` call.
|
|
423
|
+
|
|
424
|
+
Version one recognizes literal-key `apiResponse`, `apiNoContent`, `apiOpenApiRawResponse`,
|
|
425
|
+
`apiRequest`, `apiRequestContract`, `apiNoRequestBody`, `apiQuery`, and `apiHeaders` markers inside
|
|
426
|
+
`app.route(...).get/post/put/patch/delete(...)` handlers. It also inspects unmarked `ctx.json`,
|
|
427
|
+
`ctx.pipeline`, `ctx.response.empty/buffer/xml`, `ctx.request.json/buffer`, `parseJsonBody`,
|
|
428
|
+
`ctx.setStatus`, and static `Content-Type` setters. The input source files and TypeScript checker
|
|
429
|
+
are the source representation; the adapter does not parse or discover a repository on its own.
|
|
430
|
+
|
|
431
|
+
Fork diagnostics use a caller-selected `recordDirectory` in both the worker setup file and the
|
|
432
|
+
main-process reporter. Call `registerForkExitSentinel({ recordDirectory })` once per fork, then
|
|
433
|
+
update `setCurrentForkExitModule` and `setCurrentForkExitProject` around tests. Construct
|
|
434
|
+
`createVitestWorkerExitDiagnosticsReporter({ recordDirectory })` in the Vitest reporter list
|
|
435
|
+
before workers start. The reporter clears stale records at construction, preserves them across
|
|
436
|
+
watch reruns, and prints an attribution summary only for abnormal worker exits. The worker writes
|
|
437
|
+
a synchronous stderr line and one JSONL file per PID; a start record without an exit record
|
|
438
|
+
identifies a fork killed before its handler could run. `createForkLeakDetector` accepts tracked
|
|
439
|
+
resource names and warmup, threshold, and sustained-growth settings; the caller supplies each
|
|
440
|
+
checkpoint's resource counts. Use a caller-owned run directory: the reporter clears only regular
|
|
441
|
+
numeric PID `.jsonl` files there at construction and leaves the directory and other files alone.
|
|
442
|
+
Application metrics and CI artifact handling stay with the caller.
|
|
443
|
+
|
|
386
444
|
`browser-session-runner` supervises caller-created browser-test processes. Callers supply command
|
|
387
445
|
construction, line classification, retry/outcome policy, and budgets; the library owns process-group
|
|
388
446
|
termination, output line buffering, shared deadlines, progress watchdogs, diagnostics, and parent signals.
|
|
@@ -1,76 +1,6 @@
|
|
|
1
1
|
import { assertSessionId } from './session-id.mjs';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
function isTimestamp(value) {
|
|
6
|
-
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
7
|
-
}
|
|
8
|
-
function isCount(value) {
|
|
9
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
10
|
-
}
|
|
11
|
-
function isSelection(value) {
|
|
12
|
-
if (!isObject(value) || value.archived !== false)
|
|
13
|
-
return false;
|
|
14
|
-
if (value.agent !== undefined && typeof value.agent !== 'string')
|
|
15
|
-
return false;
|
|
16
|
-
if (value.version !== undefined && typeof value.version !== 'string')
|
|
17
|
-
return false;
|
|
18
|
-
if (value.parentSessionId !== undefined &&
|
|
19
|
-
value.parentSessionId !== null &&
|
|
20
|
-
typeof value.parentSessionId !== 'string')
|
|
21
|
-
return false;
|
|
22
|
-
if (value.data !== undefined && !isObject(value.data))
|
|
23
|
-
return false;
|
|
24
|
-
if (value.dataArrayContains !== undefined &&
|
|
25
|
-
(!isObject(value.dataArrayContains) ||
|
|
26
|
-
Object.keys(value.dataArrayContains).length === 0 ||
|
|
27
|
-
Object.entries(value.dataArrayContains).some(([key, member]) => key.length === 0 || typeof member !== 'string' || member.length === 0)))
|
|
28
|
-
return false;
|
|
29
|
-
return (value.inactiveForHours === undefined ||
|
|
30
|
-
(typeof value.inactiveForHours === 'number' &&
|
|
31
|
-
Number.isFinite(value.inactiveForHours) &&
|
|
32
|
-
value.inactiveForHours > 0));
|
|
33
|
-
}
|
|
34
|
-
function isManifest(value) {
|
|
35
|
-
if (!isObject(value) || value.schemaVersion !== 1 || value.status !== 'complete')
|
|
36
|
-
return false;
|
|
37
|
-
if (!isTimestamp(value.createdAt) ||
|
|
38
|
-
!isTimestamp(value.completedAt) ||
|
|
39
|
-
!isSelection(value.selection))
|
|
40
|
-
return false;
|
|
41
|
-
if (!isObject(value.counts) ||
|
|
42
|
-
!isCount(value.counts.sessions) ||
|
|
43
|
-
!isCount(value.counts.entries) ||
|
|
44
|
-
!isCount(value.counts.records))
|
|
45
|
-
return false;
|
|
46
|
-
return (isObject(value.ordering) &&
|
|
47
|
-
value.ordering.sessions === 'createdAt ascending' &&
|
|
48
|
-
value.ordering.entries === 'createdAt ascending within session' &&
|
|
49
|
-
value.consistency === 'best-effort');
|
|
50
|
-
}
|
|
51
|
-
function isSession(value) {
|
|
52
|
-
if (isObject(value) &&
|
|
53
|
-
typeof value.id === 'string' &&
|
|
54
|
-
value.id.length > 0 &&
|
|
55
|
-
(value.parentSessionId === null || typeof value.parentSessionId === 'string') &&
|
|
56
|
-
typeof value.agent === 'string' &&
|
|
57
|
-
typeof value.version === 'string' &&
|
|
58
|
-
isTimestamp(value.createdAt) &&
|
|
59
|
-
(value.lastEntryAt === null || isTimestamp(value.lastEntryAt)) &&
|
|
60
|
-
value.archivedAt === null &&
|
|
61
|
-
isObject(value.data)) {
|
|
62
|
-
try {
|
|
63
|
-
assertSessionId(value.id);
|
|
64
|
-
if (value.parentSessionId !== null)
|
|
65
|
-
assertSessionId(value.parentSessionId, 'parent session id');
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
2
|
+
import { isManifest, isSession } from './snapshot-partition-predicates.mjs';
|
|
3
|
+
import { isObject, isTimestamp } from './snapshot-partition-guards.mjs';
|
|
74
4
|
function isEntry(value) {
|
|
75
5
|
if (isObject(value) &&
|
|
76
6
|
typeof value.sessionId === 'string' &&
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function isObject(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function isTimestamp(value) {
|
|
5
|
+
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
6
|
+
}
|
|
7
|
+
export function isCount(value) {
|
|
8
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
9
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { assertSessionId } from './session-id.mjs';
|
|
2
|
+
import { isCount, isObject, isTimestamp } from './snapshot-partition-guards.mjs';
|
|
3
|
+
function isSelection(value) {
|
|
4
|
+
if (!isObject(value) || value.archived !== false)
|
|
5
|
+
return false;
|
|
6
|
+
if (value.agent !== undefined && typeof value.agent !== 'string')
|
|
7
|
+
return false;
|
|
8
|
+
if (value.version !== undefined && typeof value.version !== 'string')
|
|
9
|
+
return false;
|
|
10
|
+
if (value.parentSessionId !== undefined &&
|
|
11
|
+
value.parentSessionId !== null &&
|
|
12
|
+
typeof value.parentSessionId !== 'string')
|
|
13
|
+
return false;
|
|
14
|
+
if (value.data !== undefined && !isObject(value.data))
|
|
15
|
+
return false;
|
|
16
|
+
if (value.dataArrayContains !== undefined &&
|
|
17
|
+
(!isObject(value.dataArrayContains) ||
|
|
18
|
+
Object.keys(value.dataArrayContains).length === 0 ||
|
|
19
|
+
Object.entries(value.dataArrayContains).some(([key, member]) => key.length === 0 || typeof member !== 'string' || member.length === 0)))
|
|
20
|
+
return false;
|
|
21
|
+
return (value.inactiveForHours === undefined ||
|
|
22
|
+
(typeof value.inactiveForHours === 'number' &&
|
|
23
|
+
Number.isFinite(value.inactiveForHours) &&
|
|
24
|
+
value.inactiveForHours > 0));
|
|
25
|
+
}
|
|
26
|
+
export function isManifest(value) {
|
|
27
|
+
if (!isObject(value) || value.schemaVersion !== 1 || value.status !== 'complete')
|
|
28
|
+
return false;
|
|
29
|
+
if (!isTimestamp(value.createdAt) ||
|
|
30
|
+
!isTimestamp(value.completedAt) ||
|
|
31
|
+
!isSelection(value.selection))
|
|
32
|
+
return false;
|
|
33
|
+
if (!isObject(value.counts) ||
|
|
34
|
+
!isCount(value.counts.sessions) ||
|
|
35
|
+
!isCount(value.counts.entries) ||
|
|
36
|
+
!isCount(value.counts.records))
|
|
37
|
+
return false;
|
|
38
|
+
return (isObject(value.ordering) &&
|
|
39
|
+
value.ordering.sessions === 'createdAt ascending' &&
|
|
40
|
+
value.ordering.entries === 'createdAt ascending within session' &&
|
|
41
|
+
value.consistency === 'best-effort');
|
|
42
|
+
}
|
|
43
|
+
export function isSession(value) {
|
|
44
|
+
if (isObject(value) &&
|
|
45
|
+
typeof value.id === 'string' &&
|
|
46
|
+
value.id.length > 0 &&
|
|
47
|
+
(value.parentSessionId === null || typeof value.parentSessionId === 'string') &&
|
|
48
|
+
typeof value.agent === 'string' &&
|
|
49
|
+
typeof value.version === 'string' &&
|
|
50
|
+
isTimestamp(value.createdAt) &&
|
|
51
|
+
(value.lastEntryAt === null || isTimestamp(value.lastEntryAt)) &&
|
|
52
|
+
value.archivedAt === null &&
|
|
53
|
+
isObject(value.data)) {
|
|
54
|
+
try {
|
|
55
|
+
assertSessionId(value.id);
|
|
56
|
+
if (value.parentSessionId !== null)
|
|
57
|
+
assertSessionId(value.parentSessionId, 'parent session id');
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { contractError } from './response-contract-registration.mjs';
|
|
3
|
+
export function parseHeaderContract(sourceFile, node) {
|
|
4
|
+
const request = propertyObject(sourceFile, node, 'request');
|
|
5
|
+
const responses = propertyObject(sourceFile, node, 'responses');
|
|
6
|
+
return {
|
|
7
|
+
requestHeaders: request ? parseHeaders(sourceFile, request) : {},
|
|
8
|
+
responseHeaders: responses ? parseResponses(sourceFile, responses) : {},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function parseResponses(sourceFile, node) {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const property of node.properties) {
|
|
14
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
15
|
+
!ts.isNumericLiteral(property.name) ||
|
|
16
|
+
!ts.isObjectLiteralExpression(property.initializer))
|
|
17
|
+
throw contractError(sourceFile, property, 'apiHeaders response status must be a numeric object property');
|
|
18
|
+
const status = Number(property.name.text);
|
|
19
|
+
const errors = property.initializer.properties.find((item) => ts.isPropertyAssignment(item) && item.name.getText(sourceFile) === 'errors');
|
|
20
|
+
const headers = propertyObject(sourceFile, property.initializer, 'headers');
|
|
21
|
+
const description = stringProperty(property.initializer, 'description');
|
|
22
|
+
result[status] = {
|
|
23
|
+
...(description ? { description } : {}),
|
|
24
|
+
...(errors ? { errors: parseErrors(sourceFile, property.initializer) } : {}),
|
|
25
|
+
headers: headers ? parseHeaders(sourceFile, headers) : {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
function parseErrors(sourceFile, node) {
|
|
31
|
+
const property = findProperty(node, 'errors', sourceFile);
|
|
32
|
+
if (!property || !ts.isArrayLiteralExpression(property.initializer))
|
|
33
|
+
throw contractError(sourceFile, node, 'apiHeaders errors must be an array');
|
|
34
|
+
return property.initializer.elements.map((element) => {
|
|
35
|
+
if (!ts.isObjectLiteralExpression(element))
|
|
36
|
+
throw contractError(sourceFile, element, 'apiHeaders errors entries must be objects');
|
|
37
|
+
return {
|
|
38
|
+
code: requiredString(sourceFile, element, 'code'),
|
|
39
|
+
message: requiredString(sourceFile, element, 'message'),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function parseHeaders(sourceFile, node) {
|
|
44
|
+
const result = Object.create(null);
|
|
45
|
+
for (const property of node.properties) {
|
|
46
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
47
|
+
!ts.isStringLiteral(property.name) ||
|
|
48
|
+
!ts.isObjectLiteralExpression(property.initializer))
|
|
49
|
+
throw contractError(sourceFile, property, 'apiHeaders header must be a string-keyed object property');
|
|
50
|
+
const description = stringProperty(property.initializer, 'description');
|
|
51
|
+
const format = stringProperty(property.initializer, 'format');
|
|
52
|
+
result[property.name.text] = {
|
|
53
|
+
type: requiredString(sourceFile, property.initializer, 'type'),
|
|
54
|
+
required: booleanProperty(property.initializer, 'required') ?? false,
|
|
55
|
+
...(format ? { format: format } : {}),
|
|
56
|
+
...(description ? { description } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function propertyObject(sourceFile, node, name) {
|
|
62
|
+
const property = findProperty(node, name, sourceFile);
|
|
63
|
+
if (!property)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (!ts.isObjectLiteralExpression(property.initializer))
|
|
66
|
+
throw contractError(sourceFile, property, `apiHeaders ${name} must be an object`);
|
|
67
|
+
return property.initializer;
|
|
68
|
+
}
|
|
69
|
+
function stringProperty(node, name) {
|
|
70
|
+
const property = findProperty(node, name);
|
|
71
|
+
return property && ts.isStringLiteral(property.initializer)
|
|
72
|
+
? property.initializer.text
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
function requiredString(sourceFile, node, name) {
|
|
76
|
+
const value = stringProperty(node, name);
|
|
77
|
+
if (!value)
|
|
78
|
+
throw contractError(sourceFile, node, `apiHeaders requires string ${name}`);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function booleanProperty(node, name) {
|
|
82
|
+
const property = findProperty(node, name);
|
|
83
|
+
if (!property)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (property.initializer.kind === ts.SyntaxKind.TrueKeyword)
|
|
86
|
+
return true;
|
|
87
|
+
if (property.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
88
|
+
return false;
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
function findProperty(node, name, sourceFile) {
|
|
92
|
+
return node.properties.find((item) => ts.isPropertyAssignment(item) && item.name.getText(sourceFile) === name);
|
|
93
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import type { HeaderContractRegistry } from './header-contract-types.mts';
|
|
3
|
+
export declare function discoverApiHeaderContracts(program: ts.Program, sourceFiles: readonly ts.SourceFile[], knownResponseRoutes: ReadonlySet<string>): HeaderContractRegistry;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { contractError } from './response-contract-registration.mjs';
|
|
3
|
+
import { parseHeaderContract } from './header-contract-parser.mjs';
|
|
4
|
+
import { collectHandlerBindings, enclosingRouteBinding, visit, } from './response-contract-route-analysis.mjs';
|
|
5
|
+
export function discoverApiHeaderContracts(program, sourceFiles, knownResponseRoutes) {
|
|
6
|
+
const checker = program.getTypeChecker();
|
|
7
|
+
const bindings = collectHandlerBindings(sourceFiles, checker);
|
|
8
|
+
const contracts = new Map();
|
|
9
|
+
const knownOperations = new Set([...knownResponseRoutes].map((key) => key.split('#')[0]));
|
|
10
|
+
for (const sourceFile of sourceFiles) {
|
|
11
|
+
visit(sourceFile, (node) => {
|
|
12
|
+
if (!ts.isCallExpression(node) || !isMarker(node.expression))
|
|
13
|
+
return;
|
|
14
|
+
const [keyNode, contractNode] = node.arguments;
|
|
15
|
+
if (!keyNode ||
|
|
16
|
+
!ts.isStringLiteral(keyNode) ||
|
|
17
|
+
!contractNode ||
|
|
18
|
+
!ts.isObjectLiteralExpression(contractNode)) {
|
|
19
|
+
throw contractError(sourceFile, node, 'apiHeaders requires a literal operation key and object contract');
|
|
20
|
+
}
|
|
21
|
+
const binding = enclosingRouteBinding(node, checker, bindings);
|
|
22
|
+
if (!binding)
|
|
23
|
+
throw contractError(sourceFile, node, 'apiHeaders must be inside an app.route handler');
|
|
24
|
+
const key = `${binding.method}:${binding.routeTemplate}`;
|
|
25
|
+
if (keyNode.text !== key)
|
|
26
|
+
throw contractError(sourceFile, keyNode, `Header operation key "${keyNode.text}" does not match enclosing route ${key}`);
|
|
27
|
+
if (!knownOperations.has(key))
|
|
28
|
+
throw contractError(sourceFile, keyNode, `apiHeaders references unknown response route ${key}`);
|
|
29
|
+
if (contracts.has(key))
|
|
30
|
+
throw contractError(sourceFile, keyNode, `Duplicate apiHeaders marker for ${key}`);
|
|
31
|
+
contracts.set(key, parseHeaderContract(sourceFile, contractNode));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return Object.fromEntries([...contracts.entries()].toSorted(([a], [b]) => a.localeCompare(b)));
|
|
35
|
+
}
|
|
36
|
+
function isMarker(expression) {
|
|
37
|
+
return ts.isIdentifier(expression) && expression.text === 'apiHeaders';
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type HeaderContract = {
|
|
2
|
+
requestHeaders: Readonly<Record<string, HeaderDescriptor>>;
|
|
3
|
+
responseHeaders: Readonly<Record<number, ResponseHeaderDescriptor>>;
|
|
4
|
+
};
|
|
5
|
+
export type HeaderDescriptor = {
|
|
6
|
+
description?: string;
|
|
7
|
+
format?: 'uuid';
|
|
8
|
+
required: boolean;
|
|
9
|
+
type: 'integer' | 'string';
|
|
10
|
+
};
|
|
11
|
+
export type ResponseHeaderDescriptor = {
|
|
12
|
+
description?: string;
|
|
13
|
+
errors?: readonly {
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}[];
|
|
17
|
+
headers: Readonly<Record<string, HeaderDescriptor>>;
|
|
18
|
+
};
|
|
19
|
+
export type HeaderContractRegistry = Readonly<Record<string, HeaderContract>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import type { DiscoverApiResponseContractsOptions } from './response-contract-lenient.mts';
|
|
3
|
+
export declare const APP_ROUTE_CTX_ADAPTER_VERSION: 1;
|
|
4
|
+
export type AppRouteCtxDiscoveryInput = {
|
|
5
|
+
program: ts.Program;
|
|
6
|
+
sourceFiles: readonly ts.SourceFile[];
|
|
7
|
+
requestedKeys?: ReadonlySet<string>;
|
|
8
|
+
options?: DiscoverApiResponseContractsOptions;
|
|
9
|
+
};
|
|
10
|
+
/** Discovers the version-one `app.route(...).method(handler)` and `ctx` convention. */
|
|
11
|
+
export declare function discoverAppRouteCtxContractsV1(input: AppRouteCtxDiscoveryInput): {
|
|
12
|
+
adapterVersion: 1;
|
|
13
|
+
routes: import("./registered-route-catalog.mts").RegisteredRoute[];
|
|
14
|
+
responses: Record<string, import("./response-contract-types.mts").BackendResponseContract>;
|
|
15
|
+
requests: Record<string, import("./request-contract-types.mts").BackendRequestContract>;
|
|
16
|
+
queries: Readonly<Record<string, import("./query-contract-types.mts").BackendQueryContract>>;
|
|
17
|
+
headers: Readonly<Record<string, import("./header-contract-types.mts").HeaderContract>>;
|
|
18
|
+
};
|
|
19
|
+
export { discoverApiResponseContracts } from './response-contract-registry.mts';
|
|
20
|
+
export { discoverApiRequestContracts } from './request-contract-registry.mts';
|
|
21
|
+
export { discoverApiQueryContracts } from './query-contract-registry.mts';
|
|
22
|
+
export { discoverApiHeaderContracts } from './header-contract-registry.mts';
|
|
23
|
+
export { discoverRegisteredRoutes, routeShape } from './registered-route-catalog.mts';
|
|
24
|
+
export { responseStatusCodesForContract } from './response-contract-status.mts';
|
|
25
|
+
export type { BackendResponseContract } from './response-contract-types.mts';
|
|
26
|
+
export type { BackendRequestContract } from './request-contract-types.mts';
|
|
27
|
+
export type { BackendQueryContract, BackendQueryContractRegistry } from './query-contract-types.mts';
|
|
28
|
+
export type { HeaderContract, HeaderContractRegistry } from './header-contract-types.mts';
|
|
29
|
+
export type { RegisteredRoute } from './registered-route-catalog.mts';
|
|
30
|
+
export type { RouteDiscoveryError, DiscoverApiResponseContractsOptions, } from './response-contract-lenient.mts';
|
|
31
|
+
export type { DiscoverApiRequestContractsOptions } from './request-contract-lenient.mts';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { discoverApiHeaderContracts } from './header-contract-registry.mjs';
|
|
2
|
+
import { discoverApiQueryContracts } from './query-contract-registry.mjs';
|
|
3
|
+
import { discoverRegisteredRoutes } from './registered-route-catalog.mjs';
|
|
4
|
+
import { discoverApiRequestContracts } from './request-contract-registry.mjs';
|
|
5
|
+
import { discoverApiResponseContracts } from './response-contract-registry.mjs';
|
|
6
|
+
export const APP_ROUTE_CTX_ADAPTER_VERSION = 1;
|
|
7
|
+
/** Discovers the version-one `app.route(...).method(handler)` and `ctx` convention. */
|
|
8
|
+
export function discoverAppRouteCtxContractsV1(input) {
|
|
9
|
+
const { program, sourceFiles, requestedKeys, options } = input;
|
|
10
|
+
const responses = discoverApiResponseContracts(program, sourceFiles, requestedKeys, options);
|
|
11
|
+
const knownResponseRoutes = new Set(Object.keys(responses));
|
|
12
|
+
return {
|
|
13
|
+
adapterVersion: APP_ROUTE_CTX_ADAPTER_VERSION,
|
|
14
|
+
routes: discoverRegisteredRoutes(program, sourceFiles),
|
|
15
|
+
responses,
|
|
16
|
+
requests: discoverApiRequestContracts(program, sourceFiles, requestedKeys, options),
|
|
17
|
+
queries: discoverApiQueryContracts(program, sourceFiles, knownResponseRoutes),
|
|
18
|
+
headers: discoverApiHeaderContracts(program, sourceFiles, knownResponseRoutes),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export { discoverApiResponseContracts } from './response-contract-registry.mjs';
|
|
22
|
+
export { discoverApiRequestContracts } from './request-contract-registry.mjs';
|
|
23
|
+
export { discoverApiQueryContracts } from './query-contract-registry.mjs';
|
|
24
|
+
export { discoverApiHeaderContracts } from './header-contract-registry.mjs';
|
|
25
|
+
export { discoverRegisteredRoutes, routeShape } from './registered-route-catalog.mjs';
|
|
26
|
+
export { responseStatusCodesForContract } from './response-contract-status.mjs';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { QueryParameterContract } from './query-contract-types.mts';
|
|
2
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
3
|
+
export declare function extractQueryParameterDescriptor(type: ts.Type, checker: ts.TypeChecker, sourceFile: ts.SourceFile, node: ts.Node, parameterName: string): QueryParameterContract;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { contractError } from './response-contract-registration.mjs';
|
|
3
|
+
import { optionalNumberLiteral, optionalStringLiteral, requiredBooleanLiteral, requiredNumberLiteral, requiredPropertyType, requiredStringLiteral, stringTuple, } from './query-contract-literals.mjs';
|
|
4
|
+
export function extractQueryParameterDescriptor(type, checker, sourceFile, node, parameterName) {
|
|
5
|
+
const fail = (detail) => {
|
|
6
|
+
throw contractError(sourceFile, node, `Malformed query parameter "${parameterName}": ${detail}`);
|
|
7
|
+
};
|
|
8
|
+
const kind = requiredStringLiteral(type, 'kind', checker, fail);
|
|
9
|
+
const description = optionalStringLiteral(type, 'description', checker, node, fail);
|
|
10
|
+
const options = description === undefined ? {} : { description };
|
|
11
|
+
if (kind === 'string') {
|
|
12
|
+
const format = optionalStringLiteral(type, 'format', checker, node, fail);
|
|
13
|
+
if (format === 'uuid')
|
|
14
|
+
return { kind, format, ...options };
|
|
15
|
+
if (format === 'uri')
|
|
16
|
+
return { kind, format, ...options };
|
|
17
|
+
if (format !== undefined)
|
|
18
|
+
fail('unsupported format');
|
|
19
|
+
return { kind, ...options };
|
|
20
|
+
}
|
|
21
|
+
if (kind === 'uuid-or-uri' ||
|
|
22
|
+
kind === 'boolean' ||
|
|
23
|
+
kind === 'nullable-boolean' ||
|
|
24
|
+
kind === 'number') {
|
|
25
|
+
return { kind, ...options };
|
|
26
|
+
}
|
|
27
|
+
if (kind === 'integer') {
|
|
28
|
+
const minimum = requiredNumberLiteral(type, 'minimum', checker, fail);
|
|
29
|
+
const maximum = requiredNumberLiteral(type, 'maximum', checker, fail);
|
|
30
|
+
const defaultValue = optionalNumberLiteral(type, 'default', checker, node, fail);
|
|
31
|
+
return {
|
|
32
|
+
kind,
|
|
33
|
+
minimum,
|
|
34
|
+
maximum,
|
|
35
|
+
...(defaultValue === undefined ? {} : { default: defaultValue }),
|
|
36
|
+
...options,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (kind === 'enum') {
|
|
40
|
+
const values = stringTuple(type, 'values', checker, fail);
|
|
41
|
+
const defaultValue = optionalStringLiteral(type, 'default', checker, node, fail);
|
|
42
|
+
if (defaultValue !== undefined && !values.includes(defaultValue)) {
|
|
43
|
+
fail('default must be one of values');
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
kind,
|
|
47
|
+
values,
|
|
48
|
+
...(defaultValue === undefined ? {} : { default: defaultValue }),
|
|
49
|
+
...options,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (kind === 'csv-array') {
|
|
53
|
+
if (requiredStringLiteral(type, 'style', checker, fail) !== 'form')
|
|
54
|
+
fail('style must be form');
|
|
55
|
+
if (requiredBooleanLiteral(type, 'explode', checker, fail) !== false)
|
|
56
|
+
fail('explode must be false');
|
|
57
|
+
const itemType = requiredPropertyType(type, 'items', checker, fail);
|
|
58
|
+
const items = extractQueryParameterDescriptor(itemType, checker, sourceFile, node, `${parameterName}[]`);
|
|
59
|
+
if (items.kind === 'string' || items.kind === 'enum') {
|
|
60
|
+
return { kind, items, style: 'form', explode: false, ...options };
|
|
61
|
+
}
|
|
62
|
+
return fail('array items must be string or enum');
|
|
63
|
+
}
|
|
64
|
+
return fail(`unsupported kind "${kind}"`);
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
export declare function requiredPropertyType(type: ts.Type, name: string, checker: ts.TypeChecker, fail: (detail: string) => never): ts.Type;
|
|
3
|
+
export declare function requiredStringLiteral(type: ts.Type, name: string, checker: ts.TypeChecker, fail: (detail: string) => never): string;
|
|
4
|
+
export declare function requiredNumberLiteral(type: ts.Type, name: string, checker: ts.TypeChecker, fail: (detail: string) => never): number;
|
|
5
|
+
export declare function requiredBooleanLiteral(type: ts.Type, name: string, checker: ts.TypeChecker, fail: (detail: string) => never): boolean;
|
|
6
|
+
export declare function optionalStringLiteral(type: ts.Type, name: string, checker: ts.TypeChecker, node: ts.Node, fail: (detail: string) => never): string | undefined;
|
|
7
|
+
export declare function optionalNumberLiteral(type: ts.Type, name: string, checker: ts.TypeChecker, node: ts.Node, fail: (detail: string) => never): number | undefined;
|
|
8
|
+
export declare function stringTuple(type: ts.Type, name: string, checker: ts.TypeChecker, fail: (detail: string) => never): readonly string[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
export function requiredPropertyType(type, name, checker, fail) {
|
|
3
|
+
const property = type.getProperty(name);
|
|
4
|
+
if (!property || property.flags & ts.SymbolFlags.Optional)
|
|
5
|
+
return fail(`requires literal ${name}`);
|
|
6
|
+
const location = property.valueDeclaration ?? property.declarations?.[0];
|
|
7
|
+
if (!location)
|
|
8
|
+
return fail(`requires declared ${name}`);
|
|
9
|
+
return checker.getTypeOfSymbolAtLocation(property, location);
|
|
10
|
+
}
|
|
11
|
+
export function requiredStringLiteral(type, name, checker, fail) {
|
|
12
|
+
const value = requiredPropertyType(type, name, checker, fail);
|
|
13
|
+
return value.isStringLiteral() ? value.value : fail(`requires literal ${name}`);
|
|
14
|
+
}
|
|
15
|
+
export function requiredNumberLiteral(type, name, checker, fail) {
|
|
16
|
+
const value = requiredPropertyType(type, name, checker, fail);
|
|
17
|
+
return value.isNumberLiteral() ? value.value : fail(`requires literal ${name}`);
|
|
18
|
+
}
|
|
19
|
+
export function requiredBooleanLiteral(type, name, checker, fail) {
|
|
20
|
+
const value = requiredPropertyType(type, name, checker, fail);
|
|
21
|
+
if (value.flags & ts.TypeFlags.BooleanLiteral)
|
|
22
|
+
return checker.typeToString(value) === 'true';
|
|
23
|
+
return fail(`requires literal ${name}`);
|
|
24
|
+
}
|
|
25
|
+
export function optionalStringLiteral(type, name, checker, node, fail) {
|
|
26
|
+
const property = type.getProperty(name);
|
|
27
|
+
if (!property)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (property.flags & ts.SymbolFlags.Optional)
|
|
30
|
+
return fail(`${name} must be literal when present`);
|
|
31
|
+
const location = property.valueDeclaration ?? property.declarations?.[0] ?? node;
|
|
32
|
+
const value = checker.getTypeOfSymbolAtLocation(property, location);
|
|
33
|
+
return value.isStringLiteral() ? value.value : fail(`requires literal ${name}`);
|
|
34
|
+
}
|
|
35
|
+
export function optionalNumberLiteral(type, name, checker, node, fail) {
|
|
36
|
+
const property = type.getProperty(name);
|
|
37
|
+
if (!property)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (property.flags & ts.SymbolFlags.Optional)
|
|
40
|
+
return fail(`${name} must be literal when present`);
|
|
41
|
+
const location = property.valueDeclaration ?? property.declarations?.[0] ?? node;
|
|
42
|
+
const value = checker.getTypeOfSymbolAtLocation(property, location);
|
|
43
|
+
return value.isNumberLiteral() ? value.value : fail(`requires literal ${name}`);
|
|
44
|
+
}
|
|
45
|
+
export function stringTuple(type, name, checker, fail) {
|
|
46
|
+
const value = requiredPropertyType(type, name, checker, fail);
|
|
47
|
+
if (!checker.isTupleType(value))
|
|
48
|
+
return fail(`${name} must be a literal tuple`);
|
|
49
|
+
const values = checker.getTypeArguments(value);
|
|
50
|
+
if (values.length === 0 || values.some((item) => !item.isStringLiteral())) {
|
|
51
|
+
return fail(`${name} must contain string literals`);
|
|
52
|
+
}
|
|
53
|
+
return values.map((item) => item.value);
|
|
54
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import type { BackendQueryContractRegistry } from './query-contract-types.mts';
|
|
3
|
+
export declare function discoverApiQueryContracts(program: ts.Program, sourceFiles: readonly ts.SourceFile[], knownResponseRoutes: ReadonlySet<string>): BackendQueryContractRegistry;
|