vouchington-tooling 0.28.0 → 0.30.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 +64 -16
- package/dist/api-contract-discovery/header-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/protocol-body-status.d.mts +2 -0
- package/dist/api-contract-discovery/protocol-body-status.mjs +4 -0
- package/dist/api-contract-discovery/protocol-callback-argument-bindings.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-callback-argument-bindings.mjs +33 -0
- package/dist/api-contract-discovery/protocol-callback-invocation.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-callback-invocation.mjs +103 -0
- package/dist/api-contract-discovery/protocol-callback-mutations.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-callback-mutations.mjs +42 -0
- package/dist/api-contract-discovery/protocol-callback-registration.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-callback-registration.mjs +27 -0
- package/dist/api-contract-discovery/protocol-callback-values.d.mts +17 -0
- package/dist/api-contract-discovery/protocol-callback-values.mjs +114 -0
- package/dist/api-contract-discovery/protocol-context-alias.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-context-alias.mjs +20 -0
- package/dist/api-contract-discovery/protocol-contract-extraction.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-contract-extraction.mjs +87 -0
- package/dist/api-contract-discovery/protocol-contract-keys.d.mts +1 -0
- package/dist/api-contract-discovery/protocol-contract-keys.mjs +8 -0
- package/dist/api-contract-discovery/protocol-contract-registry.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-contract-registry.mjs +133 -0
- package/dist/api-contract-discovery/protocol-executable-path.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-executable-path.mjs +64 -0
- package/dist/api-contract-discovery/protocol-execution-path.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-execution-path.mjs +16 -0
- package/dist/api-contract-discovery/protocol-http-association.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-http-association.mjs +97 -0
- package/dist/api-contract-discovery/protocol-http-body-kinds.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-body-kinds.mjs +19 -0
- package/dist/api-contract-discovery/protocol-http-context-callers.d.mts +6 -0
- package/dist/api-contract-discovery/protocol-http-context-callers.mjs +34 -0
- package/dist/api-contract-discovery/protocol-http-context.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-http-context.mjs +108 -0
- package/dist/api-contract-discovery/protocol-http-emission.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-emission.mjs +76 -0
- package/dist/api-contract-discovery/protocol-http-implicit-taint.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-http-implicit-taint.mjs +26 -0
- package/dist/api-contract-discovery/protocol-http-registration.d.mts +12 -0
- package/dist/api-contract-discovery/protocol-http-registration.mjs +53 -0
- package/dist/api-contract-discovery/protocol-marker-analysis.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-marker-analysis.mjs +50 -0
- package/dist/api-contract-discovery/protocol-opaque-callback.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-opaque-callback.mjs +107 -0
- package/dist/api-contract-discovery/protocol-platform-callbacks.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-platform-callbacks.mjs +72 -0
- package/dist/api-contract-discovery/protocol-platform-mutation-targets.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-platform-mutation-targets.mjs +89 -0
- package/dist/api-contract-discovery/protocol-platform-writes.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-platform-writes.mjs +92 -0
- package/dist/api-contract-discovery/protocol-requested-keys.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-requested-keys.mjs +45 -0
- package/dist/api-contract-discovery/protocol-sse-callers.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-sse-callers.mjs +67 -0
- package/dist/api-contract-discovery/protocol-sse-emission.d.mts +11 -0
- package/dist/api-contract-discovery/protocol-sse-emission.mjs +61 -0
- package/dist/api-contract-discovery/protocol-sse-feasible-status.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-sse-feasible-status.mjs +24 -0
- package/dist/api-contract-discovery/protocol-sse-raw-writes.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-raw-writes.mjs +70 -0
- package/dist/api-contract-discovery/protocol-sse-status.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-sse-status.mjs +84 -0
- package/dist/api-contract-discovery/protocol-sse-write-access.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-write-access.mjs +42 -0
- package/dist/api-contract-discovery/protocol-sse-write-helpers.d.mts +6 -0
- package/dist/api-contract-discovery/protocol-sse-write-helpers.mjs +66 -0
- package/dist/api-contract-discovery/protocol-sse-write-mutations.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-write-mutations.mjs +36 -0
- package/dist/api-contract-discovery/protocol-sse-write-resolution.d.mts +14 -0
- package/dist/api-contract-discovery/protocol-sse-write-resolution.mjs +79 -0
- package/dist/api-contract-discovery/protocol-status-dominance.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-status-dominance.mjs +35 -0
- package/dist/api-contract-discovery/protocol-write-receiver.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-write-receiver.mjs +79 -0
- package/dist/api-contract-discovery/query-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/registered-route-binding-writes.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-binding-writes.mjs +30 -0
- package/dist/api-contract-discovery/registered-route-catalog.mjs +11 -7
- package/dist/api-contract-discovery/registered-route-factory-returns.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-factory-returns.mjs +29 -0
- package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +5 -1
- package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +79 -35
- package/dist/api-contract-discovery/registered-route-runtime-parameters.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-runtime-parameters.mjs +5 -0
- package/dist/api-contract-discovery/registered-route-terminal-error.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-terminal-error.mjs +98 -0
- package/dist/api-contract-discovery/request-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/response-contract-implicit.mjs +28 -10
- package/dist/api-contract-discovery/response-contract-registry.mjs +14 -1
- package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +1 -1
- package/dist/api-contract-discovery/response-contract-route-analysis.mjs +22 -4
- package/dist/api-contract-discovery/response-contract-status.d.mts +1 -1
- package/dist/api-contract-discovery/response-contract-status.mjs +10 -8
- package/dist/api-contract-discovery/response-contract-types.d.mts +7 -0
- package/dist/cli/index.mjs +0 -3
- package/dist/cli/parse-forwarded-commands.d.mts +0 -3
- package/dist/cli/parse-forwarded-commands.mjs +0 -2
- package/dist/cli/parse.d.mts +0 -3
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +0 -5
- package/dist/contract-schema/contract-schema-bounded-array.d.mts +1 -3
- package/dist/contract-schema/contract-schema-bounded-array.mjs +5 -0
- package/dist/contract-schema/contract-schema-object-tuple.mjs +7 -5
- package/dist/contract-schema/contract-schema-type-extractor.mjs +13 -0
- package/dist/contract-schema/contract-schema-validator.mjs +2 -2
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/openapi-document/build-openapi-catalog-sse.d.mts +4 -0
- package/dist/openapi-document/build-openapi-catalog-sse.mjs +27 -0
- package/dist/openapi-document/build-openapi-document.mjs +5 -2
- package/dist/openapi-document/build-openapi-response.d.mts +2 -2
- package/dist/openapi-document/build-openapi-response.mjs +33 -3
- package/dist/openapi-document/build-openapi-sse-events.d.mts +11 -0
- package/dist/openapi-document/build-openapi-sse-events.mjs +46 -0
- package/dist/openapi-document/contract-schema-intersection-merge.mjs +11 -11
- package/dist/openapi-document/contract-schema-to-openapi.mjs +3 -3
- package/dist/openapi-document/openapi-route-helpers.mjs +1 -3
- package/dist/openapi-document/openapi-types.d.mts +3 -0
- package/dist/openapi-document/operation-types.d.mts +7 -0
- package/package.json +3 -8
- package/skills/bounded-iteration/SKILL.md +51 -0
- package/skills/bounded-iteration/references/sweep-patterns.md +342 -0
- package/skills/manifest.json +9 -3
- package/skills/postgres-node-performance-tuning/SKILL.md +2 -1
- package/skills/postgres-schema-design/SKILL.md +2 -0
- package/dist/agent-harness-config/apply.d.mts +0 -4
- package/dist/agent-harness-config/apply.mjs +0 -72
- package/dist/agent-harness-config/cli.d.mts +0 -10
- package/dist/agent-harness-config/cli.mjs +0 -100
- package/dist/agent-harness-config/index.d.mts +0 -4
- package/dist/agent-harness-config/index.mjs +0 -3
- package/dist/agent-harness-config/merge-json.d.mts +0 -5
- package/dist/agent-harness-config/merge-json.mjs +0 -62
- package/dist/agent-harness-config/merge-toml.d.mts +0 -6
- package/dist/agent-harness-config/merge-toml.mjs +0 -132
- package/dist/agent-harness-config/paths.d.mts +0 -3
- package/dist/agent-harness-config/paths.mjs +0 -117
- package/dist/agent-harness-config/policy.d.mts +0 -14
- package/dist/agent-harness-config/policy.mjs +0 -51
- package/dist/agent-harness-config/prerequisites.d.mts +0 -2
- package/dist/agent-harness-config/prerequisites.mjs +0 -67
- package/dist/agent-harness-config/toml-value.d.mts +0 -6
- package/dist/agent-harness-config/toml-value.mjs +0 -69
- package/dist/agent-harness-config/types.d.mts +0 -90
- package/dist/agent-harness-config/types.mjs +0 -2
- package/docs/agent-harness-config.md +0 -134
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { executableProtocolPath } from './protocol-execution-path.mjs';
|
|
2
3
|
export { isContextMethod, isContextResponseBufferCall, isContextResponseEmptyCall, responseMarker, } from './response-contract-ctx-analysis.mjs';
|
|
3
4
|
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
4
5
|
export function requestedKeyForBinding(binding, requestedKeys) {
|
|
@@ -19,23 +20,38 @@ export function requestedKeyForBinding(binding, requestedKeys) {
|
|
|
19
20
|
function routeShape(routeTemplate) {
|
|
20
21
|
return routeTemplate.replace(/:[^/]+/g, ':');
|
|
21
22
|
}
|
|
22
|
-
export function enclosingRouteBinding(node, checker, handlerBindings) {
|
|
23
|
+
export function enclosingRouteBinding(node, checker, handlerBindings, proveCallbacks = true) {
|
|
23
24
|
let current = node;
|
|
25
|
+
let insideHandlerFunction = false;
|
|
24
26
|
while (current) {
|
|
27
|
+
if (ts.isFunctionLike(current))
|
|
28
|
+
insideHandlerFunction = true;
|
|
29
|
+
if (insideHandlerFunction && ts.isCallExpression(current)) {
|
|
30
|
+
const method = propertyName(current.expression)?.toUpperCase();
|
|
31
|
+
const routeTemplate = routeTemplateFromExpression(current.expression);
|
|
32
|
+
if (method && HTTP_METHODS.has(method) && routeTemplate)
|
|
33
|
+
return !proveCallbacks || executableProtocolPath(node, checker)
|
|
34
|
+
? { method, routeTemplate }
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
25
37
|
if (isFunctionLike(current) && ts.isCallExpression(current.parent)) {
|
|
26
38
|
const handlerCall = current.parent;
|
|
27
39
|
const method = propertyName(handlerCall.expression)?.toUpperCase();
|
|
28
40
|
if (method && HTTP_METHODS.has(method)) {
|
|
29
41
|
const routeTemplate = routeTemplateFromExpression(handlerCall.expression);
|
|
30
42
|
if (routeTemplate)
|
|
31
|
-
return
|
|
43
|
+
return !proveCallbacks || executableProtocolPath(node, checker)
|
|
44
|
+
? { method, routeTemplate }
|
|
45
|
+
: undefined;
|
|
32
46
|
}
|
|
33
47
|
}
|
|
34
48
|
const handlerSymbol = functionSymbol(current, checker);
|
|
35
49
|
if (handlerSymbol) {
|
|
36
50
|
const binding = handlerBindings.get(resolveSymbol(handlerSymbol, checker));
|
|
37
51
|
if (binding)
|
|
38
|
-
return
|
|
52
|
+
return !proveCallbacks || executableProtocolPath(node, checker, current)
|
|
53
|
+
? binding
|
|
54
|
+
: undefined;
|
|
39
55
|
}
|
|
40
56
|
current = current.parent;
|
|
41
57
|
}
|
|
@@ -93,7 +109,9 @@ function handlerArgumentSymbols(node, checker) {
|
|
|
93
109
|
}
|
|
94
110
|
if (isFunctionLike(argument)) {
|
|
95
111
|
visit(argument, (child) => {
|
|
96
|
-
if (!ts.isCallExpression(child) ||
|
|
112
|
+
if (!ts.isCallExpression(child) ||
|
|
113
|
+
!ts.isIdentifier(child.expression) ||
|
|
114
|
+
!executableProtocolPath(child, checker))
|
|
97
115
|
return;
|
|
98
116
|
const symbol = checker.getSymbolAtLocation(child.expression);
|
|
99
117
|
if (symbol)
|
|
@@ -20,7 +20,7 @@ export declare function responseStatusCodesForContract(contract: BackendResponse
|
|
|
20
20
|
* and the route-wide map (keyed only on `method:routeTemplate`) can't tell two markers in the
|
|
21
21
|
* same route apart when they're preceded by different statuses.
|
|
22
22
|
*/
|
|
23
|
-
export declare function resolveEmissionStatus(call: ts.CallExpression): {
|
|
23
|
+
export declare function resolveEmissionStatus(call: ts.CallExpression | ts.NewExpression, matchesContext?: (expression: ts.Expression) => boolean): {
|
|
24
24
|
statusCodes?: readonly [number, ...number[]];
|
|
25
25
|
statusKnowledge: 'default' | 'explicit' | 'unknown';
|
|
26
26
|
unavailableReason?: string;
|
|
@@ -27,32 +27,34 @@ export function responseStatusCodesForContract(contract) {
|
|
|
27
27
|
* and the route-wide map (keyed only on `method:routeTemplate`) can't tell two markers in the
|
|
28
28
|
* same route apart when they're preceded by different statuses.
|
|
29
29
|
*/
|
|
30
|
-
export function resolveEmissionStatus(call) {
|
|
31
|
-
if (
|
|
30
|
+
export function resolveEmissionStatus(call, matchesContext = (expression) => isContextMethod(expression, 'setStatus')) {
|
|
31
|
+
if (ts.isCallExpression(call) && matchesContext(call.expression))
|
|
32
32
|
return statusArgument(call.arguments[0]);
|
|
33
33
|
let statement = call;
|
|
34
34
|
while (!ts.isStatement(statement))
|
|
35
35
|
statement = statement.parent;
|
|
36
|
-
return precedingStatus(statement);
|
|
36
|
+
return precedingStatus(statement, matchesContext);
|
|
37
37
|
}
|
|
38
|
-
function precedingStatus(statement) {
|
|
38
|
+
function precedingStatus(statement, matchesContext) {
|
|
39
39
|
const block = statement.parent;
|
|
40
40
|
if (!ts.isBlock(block))
|
|
41
41
|
return { statusKnowledge: 'default' };
|
|
42
42
|
const index = block.statements.indexOf(statement);
|
|
43
43
|
for (let i = index - 1; i >= 0; i--) {
|
|
44
|
-
const status = statusStatement(block.statements[i]);
|
|
44
|
+
const status = statusStatement(block.statements[i], matchesContext);
|
|
45
45
|
if (status)
|
|
46
46
|
return status;
|
|
47
47
|
}
|
|
48
48
|
const enclosing = block.parent;
|
|
49
|
-
return ts.isStatement(enclosing)
|
|
49
|
+
return ts.isStatement(enclosing)
|
|
50
|
+
? precedingStatus(enclosing, matchesContext)
|
|
51
|
+
: { statusKnowledge: 'default' };
|
|
50
52
|
}
|
|
51
|
-
function statusStatement(statement) {
|
|
53
|
+
function statusStatement(statement, matchesContext) {
|
|
52
54
|
if (!ts.isExpressionStatement(statement))
|
|
53
55
|
return undefined;
|
|
54
56
|
const expression = statement.expression;
|
|
55
|
-
if (!ts.isCallExpression(expression) || !
|
|
57
|
+
if (!ts.isCallExpression(expression) || !matchesContext(expression.expression))
|
|
56
58
|
return undefined;
|
|
57
59
|
return statusArgument(expression.arguments[0]);
|
|
58
60
|
}
|
|
@@ -11,6 +11,13 @@ export type BackendResponseContract = ExtractedResponseContract & {
|
|
|
11
11
|
mediaType?: string;
|
|
12
12
|
/** Raw buffers and arbitrary streams have an unknown media type; bodyless emissions have none. */
|
|
13
13
|
mediaTypeKnowledge?: 'known' | 'none' | 'unknown';
|
|
14
|
+
/** Named SSE frames carried by the string stream body. */
|
|
15
|
+
sseEvents?: readonly {
|
|
16
|
+
eventName: string;
|
|
17
|
+
contract: ExtractedResponseContract;
|
|
18
|
+
}[];
|
|
19
|
+
/** A concrete error status can also be produced by the framework before dispatch. */
|
|
20
|
+
includeDefaultError?: boolean;
|
|
14
21
|
/** Set when the route's response schema could not be extracted (lenient discovery mode). */
|
|
15
22
|
unavailableReason?: string;
|
|
16
23
|
};
|
package/dist/cli/index.mjs
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { runAgentHarnessConfigCli } from '../agent-harness-config/cli.mjs';
|
|
6
5
|
import { runPostReviewCli } from '../gha-post-review/cli.mjs';
|
|
7
6
|
import { runStageReviewPayloadCli } from '../gha-review-payload/cli.mjs';
|
|
8
7
|
import { runSwiftSemanticEqualCli } from '../swift-semantic-equal/cli.mjs';
|
|
@@ -98,8 +97,6 @@ export function runCli(argv = process.argv) {
|
|
|
98
97
|
return runRunnerPortPolicy(parsed);
|
|
99
98
|
case 'with-host-lock':
|
|
100
99
|
return runWithHostLock(parsed.args);
|
|
101
|
-
case 'agent-harness-config':
|
|
102
|
-
return runAgentHarnessConfigCli(parsed.args);
|
|
103
100
|
case 'gha-runtime-audit':
|
|
104
101
|
return runGhaRuntimeAudit(parsed);
|
|
105
102
|
case 'script': {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export function parseForwardedCommand(command, args) {
|
|
2
2
|
if (command === 'with-host-lock')
|
|
3
3
|
return { kind: 'with-host-lock', args };
|
|
4
|
-
if (command === 'agent-harness-config')
|
|
5
|
-
return { kind: 'agent-harness-config', args };
|
|
6
4
|
if (command === 'pnpm-install')
|
|
7
5
|
return { kind: 'pnpm-install', args };
|
|
8
6
|
if (command === 'vitest-blob-manifest')
|
package/dist/cli/parse.d.mts
CHANGED
package/dist/cli/usage.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n
|
|
1
|
+
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n gha-collaborator-trust Classify authors as write-access collaborators or not\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n mcp Serve the agent-blackboard journal tools over stdio (MCP)\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling\n [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\ngha-collaborator-trust <owner/repo> [--allow-bot <login>]... < authors.json\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --mode interactive|autonomous --source-event-id ID --work-outcome OUTCOME --coverage-status STATUS --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601] [--coverage-source SOURCE,...] [--dropped-count N] [--outbox-directory PATH]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard journal flush --outbox-directory PATH\nInteractive journal append requires --outbox-directory PATH.\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\nmcp\n (no args) Serve journal_append, journal_entries, outbox_status, outbox_flush,\n session_ensure, snapshot_export, and session_archive over stdio.\n Needs the optional @modelcontextprotocol/sdk peer (and zod).\n Launch it from inside the repository's git worktree.\n";
|
|
2
2
|
export declare function printUsage(stream?: NodeJS.WritableStream): void;
|
|
3
3
|
export declare function commandNames(usage?: string): string[];
|
|
4
4
|
export declare function commandUsage(command: string, usage?: string): string;
|
package/dist/cli/usage.mjs
CHANGED
|
@@ -3,7 +3,6 @@ export const USAGE = `Usage: vouchington <command> [options]
|
|
|
3
3
|
Commands:
|
|
4
4
|
runner-port-policy Print or validate a runner port policy
|
|
5
5
|
with-host-lock Run a command under a host-wide lock
|
|
6
|
-
agent-harness-config Apply classifier-auto + sandbox keys to agent harnesses
|
|
7
6
|
gha-runtime-audit Audit successful GitHub Actions job runtimes
|
|
8
7
|
require-up-to-date Require HEAD to include a fetched remote branch
|
|
9
8
|
gitleaks-directory-scan Scan a directory with Gitleaks
|
|
@@ -66,10 +65,6 @@ with-host-lock
|
|
|
66
65
|
[--on-acquire-timeout fail|run-unlocked]
|
|
67
66
|
-- <command> [args...]
|
|
68
67
|
|
|
69
|
-
agent-harness-config dump
|
|
70
|
-
agent-harness-config check|apply [--global] [--repo PATH]... [--harness claude|codex|grok|cursor]...
|
|
71
|
-
[--home PATH]
|
|
72
|
-
|
|
73
68
|
gha-runtime-audit
|
|
74
69
|
[--repository owner/name] Default GITHUB_REPOSITORY
|
|
75
70
|
[--branch main]
|
|
@@ -9,7 +9,5 @@ export type ExtractionContext = {
|
|
|
9
9
|
options: ExtractContractSchemaOptions;
|
|
10
10
|
};
|
|
11
11
|
type SchemaForType = (type: ts.Type, context: ExtractionContext) => ContractSchemaNode;
|
|
12
|
-
export declare function boundedArraySchema(type: ts.Type, context: ExtractionContext, schemaForType: SchemaForType):
|
|
13
|
-
type: 'array';
|
|
14
|
-
}> | undefined;
|
|
12
|
+
export declare function boundedArraySchema(type: ts.Type, context: ExtractionContext, schemaForType: SchemaForType): ContractSchemaNode | undefined;
|
|
15
13
|
export {};
|
|
@@ -16,6 +16,11 @@ export function boundedArraySchema(type, context, schemaForType) {
|
|
|
16
16
|
if (!(uniqueItemsType.flags & ts.TypeFlags.BooleanLiteral)) {
|
|
17
17
|
throw unsupportedType(type, context.checker, `${alias} uniqueness must be literal`);
|
|
18
18
|
}
|
|
19
|
+
if (itemType.flags & ts.TypeFlags.Never) {
|
|
20
|
+
if (minItems !== 0)
|
|
21
|
+
throw unsupportedType(type, context.checker, `${alias} has impossible never element bounds`);
|
|
22
|
+
return { type: 'tuple', items: [], optionalItems: 0 };
|
|
23
|
+
}
|
|
19
24
|
return {
|
|
20
25
|
type: 'array',
|
|
21
26
|
items: schemaForType(itemType, context),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from './typescript-api.mjs';
|
|
2
2
|
import { compareSymbols } from './contract-schema-type-utils.mjs';
|
|
3
3
|
export function objectSchema(type, context, schemaForType) {
|
|
4
|
-
const properties =
|
|
4
|
+
const properties = new Map();
|
|
5
5
|
for (const property of context.checker.getPropertiesOfType(type).toSorted(compareSymbols)) {
|
|
6
6
|
const declaration = property.valueDeclaration;
|
|
7
7
|
/* v8 ignore start */
|
|
@@ -12,10 +12,10 @@ export function objectSchema(type, context, schemaForType) {
|
|
|
12
12
|
throw new Error(`Property "${property.name}" has no type`);
|
|
13
13
|
/* v8 ignore stop */
|
|
14
14
|
try {
|
|
15
|
-
properties
|
|
15
|
+
properties.set(property.name, {
|
|
16
16
|
required: !(property.flags & ts.SymbolFlags.Optional),
|
|
17
17
|
schema: schemaForType(propertyType, context),
|
|
18
|
-
};
|
|
18
|
+
});
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
21
21
|
throw new Error(`Property "${property.name}": ${error.message}`, { cause: error });
|
|
@@ -24,8 +24,10 @@ export function objectSchema(type, context, schemaForType) {
|
|
|
24
24
|
const stringIndex = context.checker.getIndexInfoOfType(type, ts.IndexKind.String);
|
|
25
25
|
return {
|
|
26
26
|
type: 'object',
|
|
27
|
-
properties,
|
|
28
|
-
additionalProperties: stringIndex
|
|
27
|
+
properties: Object.fromEntries(properties),
|
|
28
|
+
additionalProperties: stringIndex && !(stringIndex.type.flags & ts.TypeFlags.Never)
|
|
29
|
+
? schemaForType(stringIndex.type, context)
|
|
30
|
+
: false,
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
/* v8 ignore start -- checker internal used only for compiler-synthesized properties */
|
|
@@ -80,6 +80,17 @@ function schemaForType(type, context) {
|
|
|
80
80
|
return buildUnion();
|
|
81
81
|
}
|
|
82
82
|
if (type.isIntersection()) {
|
|
83
|
+
const index = checker.getIndexTypeOfType(type, ts.IndexKind.String);
|
|
84
|
+
const properties = checker.getPropertiesOfType(type);
|
|
85
|
+
if (index && index.flags & ts.TypeFlags.Never && properties.length > 0) {
|
|
86
|
+
if (properties.some((property) => !(property.flags & ts.SymbolFlags.Optional)))
|
|
87
|
+
throw unsupportedType(type, checker, 'never string index conflicts with named properties');
|
|
88
|
+
if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0)
|
|
89
|
+
throw unsupportedType(type, checker, 'callable types are not supported');
|
|
90
|
+
if (checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0)
|
|
91
|
+
throw unsupportedType(type, checker, 'constructable types are not supported');
|
|
92
|
+
return { type: 'object', properties: {}, additionalProperties: false };
|
|
93
|
+
}
|
|
83
94
|
return {
|
|
84
95
|
type: 'intersection',
|
|
85
96
|
variants: distinctNodes(type.types.map((variant) => schemaForType(variant, context))),
|
|
@@ -104,6 +115,8 @@ function schemaForType(type, context) {
|
|
|
104
115
|
/* v8 ignore next */
|
|
105
116
|
if (!typeArguments[0])
|
|
106
117
|
throw unsupportedType(type, checker, 'array element type is missing');
|
|
118
|
+
if (typeArguments[0].flags & ts.TypeFlags.Never)
|
|
119
|
+
return { type: 'tuple', items: [], optionalItems: 0 };
|
|
107
120
|
return { type: 'array', items: schemaForType(typeArguments[0], context) };
|
|
108
121
|
}
|
|
109
122
|
if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0) {
|
|
@@ -66,7 +66,7 @@ function validateObject(node, value, path, context, checkUnexpected) {
|
|
|
66
66
|
if (!isObject(value))
|
|
67
67
|
return requireType(false, 'object', value, path, context);
|
|
68
68
|
for (const [key, property] of Object.entries(node.properties)) {
|
|
69
|
-
if (!(key
|
|
69
|
+
if (!Object.hasOwn(value, key)) {
|
|
70
70
|
if (property.required) {
|
|
71
71
|
addIssue(context, propertyPath(path, key), 'missing-required', 'Required field is missing');
|
|
72
72
|
}
|
|
@@ -77,7 +77,7 @@ function validateObject(node, value, path, context, checkUnexpected) {
|
|
|
77
77
|
if (!checkUnexpected)
|
|
78
78
|
return;
|
|
79
79
|
for (const [key, nested] of Object.entries(value)) {
|
|
80
|
-
if (
|
|
80
|
+
if (Object.hasOwn(node.properties, key))
|
|
81
81
|
continue;
|
|
82
82
|
if (node.additionalProperties === false) {
|
|
83
83
|
addUnexpectedIssue(context, propertyPath(path, key));
|
package/dist/index.d.mts
CHANGED
|
@@ -15,8 +15,6 @@ export type { EphemeralListenerOptions, RunnerPortPolicy } from './runner-port-p
|
|
|
15
15
|
export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mts';
|
|
16
16
|
export type { ForeignKey, SqlCreateIndexMetadata, SqlCreateTableColumn, SqlCreateTableMetadata, SqlDropIndexMetadata, SqlIndexParam, SqlMigrationConstraintMetadata, } from './sql-ast/index.mts';
|
|
17
17
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mts';
|
|
18
|
-
export { applyHarnessConfig, checkHarnessConfig, dumpHarnessPolicy, planHarnessConfig, DEFAULT_EXTRA_WRITABLE_ROOTS, } from './agent-harness-config/index.mts';
|
|
19
|
-
export type { ApplyTarget, FilePlan, FileResult, HarnessApplyResult, HarnessCheckResult, HarnessConfigOptions, HarnessId, HarnessPlan, HarnessPolicyDump, JsonPatch, KeyDrift, TomlPatch, TomlValue, } from './agent-harness-config/index.mts';
|
|
20
18
|
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mts';
|
|
21
19
|
export type { GhApiExecutor, RuntimeAuditOptions, RuntimeAuditResult, RuntimeAuditWorkflowFilter, RuntimeJobResult, RuntimeSample, } from './gha-runtime-audit/index.mts';
|
|
22
20
|
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,6 @@ export { buildSessionFrictionReport, classifyFrictionObservation, FRICTION_LOG_M
|
|
|
8
8
|
export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mjs';
|
|
9
9
|
export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mjs';
|
|
10
10
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mjs';
|
|
11
|
-
export { applyHarnessConfig, checkHarnessConfig, dumpHarnessPolicy, planHarnessConfig, DEFAULT_EXTRA_WRITABLE_ROOTS, } from './agent-harness-config/index.mjs';
|
|
12
11
|
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mjs';
|
|
13
12
|
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mjs';
|
|
14
13
|
export { requireUpToDate } from './require-up-to-date/index.mjs';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { buildOperationResponse } from './build-openapi-response.mts';
|
|
2
|
+
import type { RegisteredRoute, ResponseContract } from './operation-types.mts';
|
|
3
|
+
/** Keeps a cataloged stream unavailable until its extracted variants describe SSE. */
|
|
4
|
+
export declare function retainCatalogSseResponse(kind: RegisteredRoute['kind'] | undefined, variants: readonly ResponseContract[], rendered: ReturnType<typeof buildOperationResponse>): ReturnType<typeof buildOperationResponse>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { catalogResponse } from './openapi-route-helpers.mjs';
|
|
2
|
+
/** Keeps a cataloged stream unavailable until its extracted variants describe SSE. */
|
|
3
|
+
export function retainCatalogSseResponse(kind, variants, rendered) {
|
|
4
|
+
if (kind !== 'sse' ||
|
|
5
|
+
variants.some((variant) => variant.mediaType === 'text/event-stream' &&
|
|
6
|
+
variant.mediaTypeKnowledge !== 'unknown' &&
|
|
7
|
+
variant.bodyKind !== 'none'))
|
|
8
|
+
return rendered;
|
|
9
|
+
const reason = [rendered.unavailableReason, catalogResponse('sse').unavailableReason]
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join('; ');
|
|
12
|
+
const existing = rendered.responses[200];
|
|
13
|
+
return {
|
|
14
|
+
responses: {
|
|
15
|
+
...rendered.responses,
|
|
16
|
+
200: {
|
|
17
|
+
...existing,
|
|
18
|
+
description: existing?.description ?? 'OK',
|
|
19
|
+
content: { ...existing?.content, 'text/event-stream': { schema: {} } },
|
|
20
|
+
'x-schema-unavailable': true,
|
|
21
|
+
'x-schema-unavailable-reason': reason,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
unavailable: true,
|
|
25
|
+
unavailableReason: reason,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { queryParameters } from './build-openapi-query.mjs';
|
|
2
2
|
import { buildOperationRequestBody } from './build-openapi-request.mjs';
|
|
3
3
|
import { buildOperationResponse } from './build-openapi-response.mjs';
|
|
4
|
+
import { retainCatalogSseResponse } from './build-openapi-catalog-sse.mjs';
|
|
4
5
|
import { createComponentRegistry } from './component-registry.mjs';
|
|
5
6
|
import { responseStatusCodesForContract, routeShape, } from './operation-types.mjs';
|
|
6
7
|
import { registerOpenApiErrorResponse } from './openapi-error-response.mjs';
|
|
@@ -112,10 +113,12 @@ function assertFixedNoContent(catalogRoute, variants, groupKey) {
|
|
|
112
113
|
throw new Error(`OpenAPI fixed-no-content metadata conflicts with extracted response variants for ${groupKey}`);
|
|
113
114
|
}
|
|
114
115
|
function renderOperation(catalogRoute, variants, registry) {
|
|
115
|
-
if (catalogRoute?.kind === '
|
|
116
|
+
if (catalogRoute?.kind === 'fixed-no-content')
|
|
116
117
|
return catalogResponse(catalogRoute.kind, catalogRoute.fixedStatus);
|
|
117
118
|
if (variants)
|
|
118
|
-
return buildOperationResponse(variants, registry);
|
|
119
|
+
return retainCatalogSseResponse(catalogRoute?.kind, variants, buildOperationResponse(variants, registry));
|
|
120
|
+
if (catalogRoute?.kind === 'sse')
|
|
121
|
+
return catalogResponse('sse');
|
|
119
122
|
if (catalogRoute?.kind === 'error-only')
|
|
120
123
|
return catalogResponse('error-only');
|
|
121
124
|
return catalogResponse('ordinary');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ComponentRegistry } from './component-registry.mts';
|
|
2
2
|
import { type ResponseContract } from './operation-types.mts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { OpenApiResponse, OpenApiSchema } from './openapi-types.mts';
|
|
4
4
|
export declare function buildOperationResponse(variants: ResponseContract[], registry: ComponentRegistry): {
|
|
5
|
-
responses: Record<number,
|
|
5
|
+
responses: Record<number, OpenApiResponse>;
|
|
6
6
|
unavailable: boolean;
|
|
7
7
|
unavailableReason?: string;
|
|
8
8
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { STATUS_CODES } from 'node:http';
|
|
2
|
+
import { collectSseEventSchemas, renderSseEvents, } from './build-openapi-sse-events.mjs';
|
|
2
3
|
import { nodeToOpenApi } from './contract-schema-to-openapi.mjs';
|
|
3
4
|
import { responseStatusCodesForContract } from './operation-types.mjs';
|
|
4
5
|
export function buildOperationResponse(variants, registry) {
|
|
@@ -7,6 +8,8 @@ export function buildOperationResponse(variants, registry) {
|
|
|
7
8
|
const operationFailures = [];
|
|
8
9
|
for (const contract of variants) {
|
|
9
10
|
const converted = convertContract(contract, registry);
|
|
11
|
+
const events = collectSseEventSchemas(contract, registry);
|
|
12
|
+
operationFailures.push(...events.failureReasons);
|
|
10
13
|
if (converted.failureReason)
|
|
11
14
|
operationFailures.push(converted.failureReason);
|
|
12
15
|
if (contract.statusKnowledge === 'unknown' && !converted.failureReason)
|
|
@@ -17,6 +20,8 @@ export function buildOperationResponse(variants, registry) {
|
|
|
17
20
|
if (converted.failureReason)
|
|
18
21
|
reasons.push(converted.failureReason);
|
|
19
22
|
bodyless.set(status, reasons);
|
|
23
|
+
if (contract.includeDefaultError)
|
|
24
|
+
addDefaultError(content, status, registry);
|
|
20
25
|
continue;
|
|
21
26
|
}
|
|
22
27
|
const mediaType = mediaTypeFor(contract);
|
|
@@ -25,12 +30,21 @@ export function buildOperationResponse(variants, registry) {
|
|
|
25
30
|
continue;
|
|
26
31
|
}
|
|
27
32
|
const byMedia = content.get(status) ?? new Map();
|
|
28
|
-
const bucket = byMedia.get(mediaType) ?? {
|
|
29
|
-
|
|
33
|
+
const bucket = byMedia.get(mediaType) ?? {
|
|
34
|
+
schemas: [],
|
|
35
|
+
failureReasons: [],
|
|
36
|
+
sseEvents: new Map(),
|
|
37
|
+
};
|
|
38
|
+
bucket.schemas.push(mediaType === 'text/event-stream' ? { type: 'string' } : (converted.schema ?? {}));
|
|
39
|
+
bucket.failureReasons.push(...events.failureReasons);
|
|
30
40
|
if (converted.failureReason)
|
|
31
41
|
bucket.failureReasons.push(converted.failureReason);
|
|
42
|
+
for (const [name, schemas] of events.schemas)
|
|
43
|
+
bucket.sseEvents.set(name, [...(bucket.sseEvents.get(name) ?? []), ...schemas]);
|
|
32
44
|
byMedia.set(mediaType, bucket);
|
|
33
45
|
content.set(status, byMedia);
|
|
46
|
+
if (contract.includeDefaultError)
|
|
47
|
+
addDefaultError(content, status, registry);
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
const responses = {};
|
|
@@ -45,6 +59,17 @@ export function buildOperationResponse(variants, registry) {
|
|
|
45
59
|
...(reasons.length > 0 ? { unavailableReason: reasons.join('; ') } : {}),
|
|
46
60
|
};
|
|
47
61
|
}
|
|
62
|
+
function addDefaultError(content, status, registry) {
|
|
63
|
+
const byMedia = content.get(status) ?? new Map();
|
|
64
|
+
const bucket = byMedia.get('application/json') ?? {
|
|
65
|
+
schemas: [],
|
|
66
|
+
failureReasons: [],
|
|
67
|
+
sseEvents: new Map(),
|
|
68
|
+
};
|
|
69
|
+
bucket.schemas.push({ $ref: `#/components/schemas/${registry.refName('ErrorBody')}` });
|
|
70
|
+
byMedia.set('application/json', bucket);
|
|
71
|
+
content.set(status, byMedia);
|
|
72
|
+
}
|
|
48
73
|
function renderStatusResponse(status, byMedia, bodyless, operationFailures) {
|
|
49
74
|
const hasBodyless = bodyless.has(status);
|
|
50
75
|
const conflictReason = byMedia && hasBodyless
|
|
@@ -67,7 +92,12 @@ function renderStatusResponse(status, byMedia, bodyless, operationFailures) {
|
|
|
67
92
|
.toSorted(([left], [right]) => left.localeCompare(right))
|
|
68
93
|
.map(([mediaType, bucket]) => [
|
|
69
94
|
mediaType,
|
|
70
|
-
{
|
|
95
|
+
{
|
|
96
|
+
schema: mergeVariantSchemas(bucket.schemas),
|
|
97
|
+
...(bucket.sseEvents.size
|
|
98
|
+
? { 'x-sse-events': renderSseEvents(bucket.sseEvents) }
|
|
99
|
+
: {}),
|
|
100
|
+
},
|
|
71
101
|
])),
|
|
72
102
|
}
|
|
73
103
|
: {}),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComponentRegistry } from './component-registry.mts';
|
|
2
|
+
import type { ResponseContract } from './operation-types.mts';
|
|
3
|
+
import type { OpenApiSchema } from './openapi-types.mts';
|
|
4
|
+
export type SseEventSchemas = Map<string, OpenApiSchema[]>;
|
|
5
|
+
export declare function collectSseEventSchemas(contract: ResponseContract, registry: ComponentRegistry): {
|
|
6
|
+
schemas: SseEventSchemas;
|
|
7
|
+
failureReasons: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function renderSseEvents(schemas: SseEventSchemas): Record<string, {
|
|
10
|
+
dataSchema: OpenApiSchema;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { nodeToOpenApi } from './contract-schema-to-openapi.mjs';
|
|
2
|
+
export function collectSseEventSchemas(contract, registry) {
|
|
3
|
+
const schemas = new Map();
|
|
4
|
+
const failureReasons = [];
|
|
5
|
+
if (contract.mediaType !== 'text/event-stream' && !contract.sseEvents)
|
|
6
|
+
return { schemas, failureReasons };
|
|
7
|
+
if (contract.mediaType !== 'text/event-stream' || contract.bodyKind === 'none')
|
|
8
|
+
return {
|
|
9
|
+
schemas,
|
|
10
|
+
failureReasons: ['SSE event metadata requires a text/event-stream response body'],
|
|
11
|
+
};
|
|
12
|
+
if (!contract.sseEvents?.length)
|
|
13
|
+
return { schemas, failureReasons: ['SSE event payload contracts are missing'] };
|
|
14
|
+
for (const { eventName, contract: event } of contract.sseEvents) {
|
|
15
|
+
if (!eventName.trim() || event.schema.root.type === 'unknown') {
|
|
16
|
+
failureReasons.push('SSE event payload or name is not statically known');
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
let schema;
|
|
20
|
+
try {
|
|
21
|
+
schema = nodeToOpenApi(event.schema.root, {
|
|
22
|
+
definitions: event.schema.definitions,
|
|
23
|
+
refName: registry.refName,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
failureReasons.push(String(error));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
registry.register(event.source, event.schema.definitions);
|
|
31
|
+
const variants = schemas.get(eventName) ?? [];
|
|
32
|
+
variants.push(schema);
|
|
33
|
+
schemas.set(eventName, variants);
|
|
34
|
+
}
|
|
35
|
+
return { schemas, failureReasons };
|
|
36
|
+
}
|
|
37
|
+
export function renderSseEvents(schemas) {
|
|
38
|
+
return Object.fromEntries([...schemas.entries()]
|
|
39
|
+
.toSorted(([left], [right]) => left.localeCompare(right))
|
|
40
|
+
.map(([name, variants]) => {
|
|
41
|
+
const unique = [
|
|
42
|
+
...new Map(variants.map((schema) => [JSON.stringify(schema), schema])).values(),
|
|
43
|
+
].toSorted((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
44
|
+
return [name, { dataSchema: unique.length === 1 ? unique[0] : { anyOf: unique } }];
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
@@ -11,10 +11,10 @@ export function intersectionToOpenApi(node, ctx, nodeToOpenApi) {
|
|
|
11
11
|
if (resolved.some((variant) => variant === undefined)) {
|
|
12
12
|
return { allOf: node.variants.map((variant) => nodeToOpenApi(variant, ctx)) };
|
|
13
13
|
}
|
|
14
|
-
const properties =
|
|
14
|
+
const properties = new Map();
|
|
15
15
|
for (const variant of resolved) {
|
|
16
16
|
for (const [key, property] of Object.entries(variant.properties)) {
|
|
17
|
-
const existing = properties
|
|
17
|
+
const existing = properties.get(key);
|
|
18
18
|
if (existing &&
|
|
19
19
|
hashSchemaNode(existing.schema, ctx) !== hashSchemaNode(property.schema, ctx)) {
|
|
20
20
|
// A member re-declaring a shared key with a strictly narrower type is a common, valid
|
|
@@ -23,33 +23,33 @@ export function intersectionToOpenApi(node, ctx, nodeToOpenApi) {
|
|
|
23
23
|
// narrower type, not a conflict. Prefer whichever side is the narrower one; only two
|
|
24
24
|
// genuinely incompatible schemas (neither a subset of the other) are a real conflict.
|
|
25
25
|
if (isNarrowerVariant(property.schema, existing.schema, ctx)) {
|
|
26
|
-
properties
|
|
26
|
+
properties.set(key, {
|
|
27
27
|
schema: property.schema,
|
|
28
28
|
required: existing.required || property.required,
|
|
29
|
-
};
|
|
29
|
+
});
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
32
|
/* v8 ignore next 6 -- symmetric of the previous branch; covered by order-swapped fixtures */
|
|
33
33
|
if (isNarrowerVariant(existing.schema, property.schema, ctx)) {
|
|
34
|
-
properties
|
|
34
|
+
properties.set(key, {
|
|
35
35
|
schema: existing.schema,
|
|
36
36
|
required: existing.required || property.required,
|
|
37
|
-
};
|
|
37
|
+
});
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
40
40
|
throw new Error(`Cannot merge intersection: property "${key}" has conflicting schemas across members`);
|
|
41
41
|
}
|
|
42
|
-
properties
|
|
42
|
+
properties.set(key, {
|
|
43
43
|
schema: property.schema,
|
|
44
44
|
required: existing?.required || property.required,
|
|
45
|
-
};
|
|
45
|
+
});
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
const sortedKeys =
|
|
49
|
-
const required = sortedKeys.filter((key) => properties
|
|
48
|
+
const sortedKeys = [...properties.keys()].toSorted();
|
|
49
|
+
const required = sortedKeys.filter((key) => properties.get(key).required);
|
|
50
50
|
return {
|
|
51
51
|
type: 'object',
|
|
52
|
-
properties: Object.fromEntries(sortedKeys.map((key) => [key, nodeToOpenApi(properties
|
|
52
|
+
properties: Object.fromEntries(sortedKeys.map((key) => [key, nodeToOpenApi(properties.get(key).schema, ctx)])),
|
|
53
53
|
...(required.length > 0 ? { required } : {}),
|
|
54
54
|
additionalProperties: false,
|
|
55
55
|
};
|