vouchington-tooling 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +69 -19
  2. package/bin/node-guard.mjs +9 -0
  3. package/bin/vouchington-mcp.mjs +20 -0
  4. package/bin/vouchington.mjs +18 -0
  5. package/dist/agent-blackboard/feedback-outbox-counts.d.mts +5 -0
  6. package/dist/agent-blackboard/feedback-outbox-counts.mjs +11 -0
  7. package/dist/agent-blackboard/feedback-outbox-directory.d.mts +2 -0
  8. package/dist/agent-blackboard/feedback-outbox-directory.mjs +16 -0
  9. package/dist/agent-blackboard/feedback-outbox.mjs +5 -7
  10. package/dist/agent-blackboard/index.d.mts +1 -0
  11. package/dist/agent-blackboard/index.mjs +1 -0
  12. package/dist/api-contract-discovery/header-contract-registry.mjs +2 -0
  13. package/dist/api-contract-discovery/protocol-body-status.d.mts +2 -0
  14. package/dist/api-contract-discovery/protocol-body-status.mjs +4 -0
  15. package/dist/api-contract-discovery/protocol-callback-argument-bindings.d.mts +7 -0
  16. package/dist/api-contract-discovery/protocol-callback-argument-bindings.mjs +33 -0
  17. package/dist/api-contract-discovery/protocol-callback-invocation.d.mts +5 -0
  18. package/dist/api-contract-discovery/protocol-callback-invocation.mjs +103 -0
  19. package/dist/api-contract-discovery/protocol-callback-mutations.d.mts +5 -0
  20. package/dist/api-contract-discovery/protocol-callback-mutations.mjs +42 -0
  21. package/dist/api-contract-discovery/protocol-callback-registration.d.mts +3 -0
  22. package/dist/api-contract-discovery/protocol-callback-registration.mjs +27 -0
  23. package/dist/api-contract-discovery/protocol-callback-values.d.mts +17 -0
  24. package/dist/api-contract-discovery/protocol-callback-values.mjs +114 -0
  25. package/dist/api-contract-discovery/protocol-context-alias.d.mts +3 -0
  26. package/dist/api-contract-discovery/protocol-context-alias.mjs +20 -0
  27. package/dist/api-contract-discovery/protocol-contract-extraction.d.mts +9 -0
  28. package/dist/api-contract-discovery/protocol-contract-extraction.mjs +87 -0
  29. package/dist/api-contract-discovery/protocol-contract-keys.d.mts +1 -0
  30. package/dist/api-contract-discovery/protocol-contract-keys.mjs +8 -0
  31. package/dist/api-contract-discovery/protocol-contract-registry.d.mts +5 -0
  32. package/dist/api-contract-discovery/protocol-contract-registry.mjs +133 -0
  33. package/dist/api-contract-discovery/protocol-executable-path.d.mts +3 -0
  34. package/dist/api-contract-discovery/protocol-executable-path.mjs +64 -0
  35. package/dist/api-contract-discovery/protocol-execution-path.d.mts +3 -0
  36. package/dist/api-contract-discovery/protocol-execution-path.mjs +16 -0
  37. package/dist/api-contract-discovery/protocol-http-association.d.mts +7 -0
  38. package/dist/api-contract-discovery/protocol-http-association.mjs +97 -0
  39. package/dist/api-contract-discovery/protocol-http-body-kinds.d.mts +3 -0
  40. package/dist/api-contract-discovery/protocol-http-body-kinds.mjs +19 -0
  41. package/dist/api-contract-discovery/protocol-http-context-callers.d.mts +6 -0
  42. package/dist/api-contract-discovery/protocol-http-context-callers.mjs +34 -0
  43. package/dist/api-contract-discovery/protocol-http-context.d.mts +9 -0
  44. package/dist/api-contract-discovery/protocol-http-context.mjs +108 -0
  45. package/dist/api-contract-discovery/protocol-http-emission.d.mts +3 -0
  46. package/dist/api-contract-discovery/protocol-http-emission.mjs +76 -0
  47. package/dist/api-contract-discovery/protocol-http-implicit-taint.d.mts +7 -0
  48. package/dist/api-contract-discovery/protocol-http-implicit-taint.mjs +26 -0
  49. package/dist/api-contract-discovery/protocol-http-registration.d.mts +12 -0
  50. package/dist/api-contract-discovery/protocol-http-registration.mjs +53 -0
  51. package/dist/api-contract-discovery/protocol-marker-analysis.d.mts +7 -0
  52. package/dist/api-contract-discovery/protocol-marker-analysis.mjs +50 -0
  53. package/dist/api-contract-discovery/protocol-opaque-callback.d.mts +3 -0
  54. package/dist/api-contract-discovery/protocol-opaque-callback.mjs +107 -0
  55. package/dist/api-contract-discovery/protocol-platform-callbacks.d.mts +5 -0
  56. package/dist/api-contract-discovery/protocol-platform-callbacks.mjs +72 -0
  57. package/dist/api-contract-discovery/protocol-platform-mutation-targets.d.mts +5 -0
  58. package/dist/api-contract-discovery/protocol-platform-mutation-targets.mjs +89 -0
  59. package/dist/api-contract-discovery/protocol-platform-writes.d.mts +3 -0
  60. package/dist/api-contract-discovery/protocol-platform-writes.mjs +92 -0
  61. package/dist/api-contract-discovery/protocol-requested-keys.d.mts +5 -0
  62. package/dist/api-contract-discovery/protocol-requested-keys.mjs +45 -0
  63. package/dist/api-contract-discovery/protocol-sse-callers.d.mts +7 -0
  64. package/dist/api-contract-discovery/protocol-sse-callers.mjs +67 -0
  65. package/dist/api-contract-discovery/protocol-sse-emission.d.mts +11 -0
  66. package/dist/api-contract-discovery/protocol-sse-emission.mjs +61 -0
  67. package/dist/api-contract-discovery/protocol-sse-feasible-status.d.mts +3 -0
  68. package/dist/api-contract-discovery/protocol-sse-feasible-status.mjs +24 -0
  69. package/dist/api-contract-discovery/protocol-sse-raw-writes.d.mts +9 -0
  70. package/dist/api-contract-discovery/protocol-sse-raw-writes.mjs +70 -0
  71. package/dist/api-contract-discovery/protocol-sse-status.d.mts +3 -0
  72. package/dist/api-contract-discovery/protocol-sse-status.mjs +84 -0
  73. package/dist/api-contract-discovery/protocol-sse-write-access.d.mts +9 -0
  74. package/dist/api-contract-discovery/protocol-sse-write-access.mjs +42 -0
  75. package/dist/api-contract-discovery/protocol-sse-write-helpers.d.mts +6 -0
  76. package/dist/api-contract-discovery/protocol-sse-write-helpers.mjs +66 -0
  77. package/dist/api-contract-discovery/protocol-sse-write-mutations.d.mts +9 -0
  78. package/dist/api-contract-discovery/protocol-sse-write-mutations.mjs +36 -0
  79. package/dist/api-contract-discovery/protocol-sse-write-resolution.d.mts +14 -0
  80. package/dist/api-contract-discovery/protocol-sse-write-resolution.mjs +79 -0
  81. package/dist/api-contract-discovery/protocol-status-dominance.d.mts +5 -0
  82. package/dist/api-contract-discovery/protocol-status-dominance.mjs +35 -0
  83. package/dist/api-contract-discovery/protocol-write-receiver.d.mts +9 -0
  84. package/dist/api-contract-discovery/protocol-write-receiver.mjs +79 -0
  85. package/dist/api-contract-discovery/query-contract-registry.mjs +2 -0
  86. package/dist/api-contract-discovery/registered-route-binding-writes.d.mts +3 -0
  87. package/dist/api-contract-discovery/registered-route-binding-writes.mjs +30 -0
  88. package/dist/api-contract-discovery/registered-route-catalog.mjs +11 -7
  89. package/dist/api-contract-discovery/registered-route-factory-returns.d.mts +3 -0
  90. package/dist/api-contract-discovery/registered-route-factory-returns.mjs +29 -0
  91. package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +5 -1
  92. package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +79 -35
  93. package/dist/api-contract-discovery/registered-route-runtime-parameters.d.mts +3 -0
  94. package/dist/api-contract-discovery/registered-route-runtime-parameters.mjs +5 -0
  95. package/dist/api-contract-discovery/registered-route-terminal-error.d.mts +3 -0
  96. package/dist/api-contract-discovery/registered-route-terminal-error.mjs +98 -0
  97. package/dist/api-contract-discovery/request-contract-registry.mjs +2 -0
  98. package/dist/api-contract-discovery/response-contract-implicit.mjs +28 -10
  99. package/dist/api-contract-discovery/response-contract-registry.mjs +14 -1
  100. package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +1 -1
  101. package/dist/api-contract-discovery/response-contract-route-analysis.mjs +22 -4
  102. package/dist/api-contract-discovery/response-contract-status.d.mts +1 -1
  103. package/dist/api-contract-discovery/response-contract-status.mjs +10 -8
  104. package/dist/api-contract-discovery/response-contract-types.d.mts +7 -0
  105. package/dist/cli/commands/agent-blackboard-flags.d.mts +4 -0
  106. package/dist/cli/commands/agent-blackboard-flags.mjs +34 -0
  107. package/dist/cli/commands/agent-blackboard.mjs +27 -35
  108. package/dist/cli/index.d.mts +2 -0
  109. package/dist/cli/index.mjs +12 -15
  110. package/dist/cli/parse-forwarded-commands.d.mts +0 -3
  111. package/dist/cli/parse-forwarded-commands.mjs +0 -2
  112. package/dist/cli/parse.d.mts +0 -3
  113. package/dist/cli/usage.d.mts +1 -1
  114. package/dist/cli/usage.mjs +2 -6
  115. package/dist/contract-schema/contract-schema-bounded-array.d.mts +1 -3
  116. package/dist/contract-schema/contract-schema-bounded-array.mjs +5 -0
  117. package/dist/contract-schema/contract-schema-object-tuple.mjs +7 -5
  118. package/dist/contract-schema/contract-schema-type-extractor.mjs +13 -0
  119. package/dist/contract-schema/contract-schema-validator.mjs +2 -2
  120. package/dist/index.d.mts +0 -2
  121. package/dist/index.mjs +0 -1
  122. package/dist/mcp-server/dispatch.d.mts +4 -1
  123. package/dist/mcp-server/dispatch.mjs +6 -4
  124. package/dist/mcp-server/handlers-outbox.mjs +2 -7
  125. package/dist/mcp-server/harness.test-helpers.d.mts +1 -0
  126. package/dist/mcp-server/harness.test-helpers.mjs +1 -1
  127. package/dist/mcp-server/main.d.mts +5 -0
  128. package/dist/mcp-server/main.mjs +8 -0
  129. package/dist/mcp-server/run.d.mts +2 -0
  130. package/dist/mcp-server/run.mjs +4 -1
  131. package/dist/mcp-server/sdk.d.mts +11 -2
  132. package/dist/mcp-server/sdk.mjs +37 -5
  133. package/dist/mcp-server/server.mjs +3 -2
  134. package/dist/mcp-server/tools.mjs +3 -2
  135. package/dist/mcp-server/worktree.d.mts +17 -10
  136. package/dist/mcp-server/worktree.mjs +48 -31
  137. package/dist/openapi-document/build-openapi-catalog-sse.d.mts +4 -0
  138. package/dist/openapi-document/build-openapi-catalog-sse.mjs +27 -0
  139. package/dist/openapi-document/build-openapi-document.mjs +5 -2
  140. package/dist/openapi-document/build-openapi-response.d.mts +2 -2
  141. package/dist/openapi-document/build-openapi-response.mjs +33 -3
  142. package/dist/openapi-document/build-openapi-sse-events.d.mts +11 -0
  143. package/dist/openapi-document/build-openapi-sse-events.mjs +46 -0
  144. package/dist/openapi-document/contract-schema-intersection-merge.mjs +11 -11
  145. package/dist/openapi-document/contract-schema-to-openapi.mjs +3 -3
  146. package/dist/openapi-document/openapi-route-helpers.mjs +1 -3
  147. package/dist/openapi-document/openapi-types.d.mts +3 -0
  148. package/dist/openapi-document/operation-types.d.mts +7 -0
  149. package/docs/mcp-server.md +83 -25
  150. package/package.json +11 -9
  151. package/scripts/build.mjs +2 -0
  152. package/skills/blackboard/SKILL.md +66 -4
  153. package/skills/manifest.json +19 -19
  154. package/dist/agent-harness-config/apply.d.mts +0 -4
  155. package/dist/agent-harness-config/apply.mjs +0 -72
  156. package/dist/agent-harness-config/cli.d.mts +0 -10
  157. package/dist/agent-harness-config/cli.mjs +0 -100
  158. package/dist/agent-harness-config/index.d.mts +0 -4
  159. package/dist/agent-harness-config/index.mjs +0 -3
  160. package/dist/agent-harness-config/merge-json.d.mts +0 -5
  161. package/dist/agent-harness-config/merge-json.mjs +0 -62
  162. package/dist/agent-harness-config/merge-toml.d.mts +0 -6
  163. package/dist/agent-harness-config/merge-toml.mjs +0 -132
  164. package/dist/agent-harness-config/paths.d.mts +0 -3
  165. package/dist/agent-harness-config/paths.mjs +0 -117
  166. package/dist/agent-harness-config/policy.d.mts +0 -14
  167. package/dist/agent-harness-config/policy.mjs +0 -51
  168. package/dist/agent-harness-config/prerequisites.d.mts +0 -2
  169. package/dist/agent-harness-config/prerequisites.mjs +0 -67
  170. package/dist/agent-harness-config/toml-value.d.mts +0 -6
  171. package/dist/agent-harness-config/toml-value.mjs +0 -69
  172. package/dist/agent-harness-config/types.d.mts +0 -90
  173. package/dist/agent-harness-config/types.mjs +0 -2
  174. package/docs/agent-harness-config.md +0 -134
@@ -0,0 +1,114 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { registeredHandler } from './protocol-callback-registration.mjs';
3
+ import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
4
+ import { hasBindingWrite } from './registered-route-binding-writes.mjs';
5
+ import { unwrapExpression } from './protocol-marker-analysis.mjs';
6
+ export function isProtocolCallbackFunction(node) {
7
+ return ts.isFunctionLike(node) && 'body' in node;
8
+ }
9
+ /** Resolves stable named uses in outer statements and directly registered inline handlers. */
10
+ export function protocolCallbackSourceCalls(fn) {
11
+ const declaration = fn.parent;
12
+ if (!ts.isFunctionDeclaration(fn) &&
13
+ (!ts.isVariableDeclaration(declaration) ||
14
+ !ts.isIdentifier(declaration.name) ||
15
+ !ts.isVariableDeclarationList(declaration.parent) ||
16
+ !(declaration.parent.flags & ts.NodeFlags.Const)))
17
+ return [];
18
+ const calls = [];
19
+ function visit(node) {
20
+ if (isProtocolCallbackFunction(node) &&
21
+ !(ts.isCallExpression(node.parent) &&
22
+ registeredHandler(node.parent) &&
23
+ node.parent.arguments.includes(node)))
24
+ return;
25
+ if (ts.isCallExpression(node))
26
+ calls.push(node);
27
+ ts.forEachChild(node, visit);
28
+ }
29
+ visit(fn.getSourceFile());
30
+ return calls;
31
+ }
32
+ function literalName(node) {
33
+ return ts.isIdentifier(node) || ts.isStringLiteral(node) ? node.text : undefined;
34
+ }
35
+ export function createProtocolCallbackValueResolver(checker) {
36
+ function symbol(node) {
37
+ const found = checker.getSymbolAtLocation(node);
38
+ return found?.flags && found.flags & ts.SymbolFlags.Alias
39
+ ? checker.getAliasedSymbol(found)
40
+ : found;
41
+ }
42
+ function property(value, name, seen) {
43
+ if (!value ||
44
+ !ts.isObjectLiteralExpression(value.node) ||
45
+ value.node.properties.some(ts.isSpreadAssignment))
46
+ return undefined;
47
+ const member = value.node.properties.find((item) => item.name && literalName(item.name) === name);
48
+ if (member && ts.isPropertyAssignment(member))
49
+ return resolve(member.initializer, value.env, seen);
50
+ if (member && ts.isShorthandPropertyAssignment(member)) {
51
+ const target = checker.getShorthandAssignmentValueSymbol(member);
52
+ return target ? declaration(target, value.env, seen) : undefined;
53
+ }
54
+ return member && isProtocolCallbackFunction(member)
55
+ ? { node: member, env: value.env }
56
+ : undefined;
57
+ }
58
+ function declaration(target, env, seen) {
59
+ const bound = env.get(target);
60
+ if (bound)
61
+ return resolve(bound.node, bound.env, seen);
62
+ const implemented = target.declarations?.find((item) => isProtocolCallbackFunction(item) && item.body);
63
+ if (implemented)
64
+ return { node: implemented, env };
65
+ const node = target.valueDeclaration;
66
+ if (!node || seen.has(node))
67
+ return undefined;
68
+ seen.add(node);
69
+ if (ts.isVariableDeclaration(node) &&
70
+ node.initializer &&
71
+ ts.isVariableDeclarationList(node.parent) &&
72
+ node.parent.flags & ts.NodeFlags.Const)
73
+ return resolve(node.initializer, env, seen);
74
+ if (ts.isBindingElement(node) && ts.isObjectBindingPattern(node.parent)) {
75
+ const owner = node.parent.parent;
76
+ const name = literalName(node.propertyName ?? node.name);
77
+ if (name &&
78
+ ts.isVariableDeclaration(owner) &&
79
+ owner.initializer &&
80
+ ts.isVariableDeclarationList(owner.parent) &&
81
+ owner.parent.flags & ts.NodeFlags.Const)
82
+ return property(resolve(owner.initializer, env, seen), name, seen);
83
+ }
84
+ return undefined;
85
+ }
86
+ function resolve(node, env, seen = new Set()) {
87
+ if (ts.isExpression(node))
88
+ node = unwrapExpression(node);
89
+ if (seen.has(node))
90
+ return undefined;
91
+ seen.add(node);
92
+ if (isProtocolCallbackFunction(node) ||
93
+ ts.isObjectLiteralExpression(node) ||
94
+ ts.isArrayLiteralExpression(node))
95
+ return { node, env };
96
+ if (ts.isIdentifier(node)) {
97
+ const target = symbol(node);
98
+ return target ? declaration(target, env, seen) : undefined;
99
+ }
100
+ if (ts.isPropertyAccessExpression(node)) {
101
+ const member = property(resolve(node.expression, env, seen), node.name.text, seen);
102
+ if (member)
103
+ return member;
104
+ const target = symbol(node.name);
105
+ return target ? declaration(target, env, seen) : undefined;
106
+ }
107
+ return undefined;
108
+ }
109
+ return { resolve, symbol, property };
110
+ }
111
+ export function protocolCallbackHasWrittenBindings(fn, checker) {
112
+ return ((ts.isFunctionDeclaration(fn) && hasBindingWrite(fn, checker)) ||
113
+ runtimeParameters(fn).some((parameter) => hasBindingWrite(parameter, checker)));
114
+ }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ export declare function unsupportedContextAlias(node: ts.VariableDeclaration, context: ts.Symbol, checker: ts.TypeChecker): boolean;
3
+ export declare function unsupportedContextAssignment(node: ts.Node, context: ts.Symbol, checker: ts.TypeChecker): boolean;
@@ -0,0 +1,20 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { expressionReceiver } from './protocol-write-receiver.mjs';
3
+ export function unsupportedContextAlias(node, context, checker) {
4
+ if (!node.initializer)
5
+ return false;
6
+ const receiver = expressionReceiver(node.initializer, checker);
7
+ return (receiver?.root === context &&
8
+ (receiver.path.length === 0 ||
9
+ (receiver.path.length === 1 && receiver.path[0] === 'response')) &&
10
+ (!ts.isIdentifier(node.name) ||
11
+ !ts.isVariableDeclarationList(node.parent) ||
12
+ !(node.parent.flags & ts.NodeFlags.Const)));
13
+ }
14
+ export function unsupportedContextAssignment(node, context, checker) {
15
+ if (!ts.isBinaryExpression(node) || node.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
16
+ return false;
17
+ const receiver = expressionReceiver(node.right, checker);
18
+ return (receiver?.root === context &&
19
+ (receiver.path.length === 0 || (receiver.path.length === 1 && receiver.path[0] === 'response')));
20
+ }
@@ -0,0 +1,9 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import { type ExtractedResponseContract } from '../contract-schema/index.mts';
3
+ import type { DiscoverApiResponseContractsOptions } from './response-contract-lenient.mts';
4
+ import type { BackendResponseContract } from './response-contract-types.mts';
5
+ export declare function extractSseEvents(type: ts.Type, checker: ts.TypeChecker, source: string, options: DiscoverApiResponseContractsOptions | undefined): {
6
+ eventName: string;
7
+ contract: ExtractedResponseContract;
8
+ }[];
9
+ export declare function extractHttpVariants(type: ts.Type, checker: ts.TypeChecker, source: string, options: DiscoverApiResponseContractsOptions | undefined, selectRow: (index: number) => boolean): Omit<BackendResponseContract, 'method' | 'routeTemplate'>[];
@@ -0,0 +1,87 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { extractContractSchema } from '../contract-schema/index.mjs';
3
+ import { propertyType, stringLiterals, typeVariants } from './protocol-marker-analysis.mjs';
4
+ import { noContentContract } from './response-contract-registration.mjs';
5
+ import { statusForbidsBody } from './protocol-body-status.mjs';
6
+ export function extractSseEvents(type, checker, source, options) {
7
+ return typeVariants(type).flatMap((variant) => {
8
+ const names = stringLiterals(propertyType(variant, 'event', checker));
9
+ const contract = concretePayload(propertyType(variant, 'data', checker), checker, source, options);
10
+ return names.map((eventName) => ({ eventName, contract }));
11
+ });
12
+ }
13
+ export function extractHttpVariants(type, checker, source, options, selectRow) {
14
+ const carrier = propertyType(type, 'apiHttpResponseVariants', checker);
15
+ const variants = typeVariants(carrier).filter((variant) => !(variant.flags & ts.TypeFlags.Undefined));
16
+ if (variants.length === 0)
17
+ throw new Error('HTTP response requires concrete variants');
18
+ let row = 0;
19
+ const selections = variants.map((variant) => {
20
+ const rawMedia = rawPropertyType(variant, 'mediaType', checker);
21
+ const rawKind = rawPropertyType(variant, 'bodyKind', checker);
22
+ const count = rawKind?.isStringLiteral() && rawKind.value === 'none'
23
+ ? 1
24
+ : rawMedia?.isUnion()
25
+ ? rawMedia.types.length
26
+ : 1;
27
+ const selected = Array.from({ length: count }, () => selectRow(row++));
28
+ return selected;
29
+ });
30
+ return variants.flatMap((variant, index) => {
31
+ const selected = selections[index];
32
+ if (!selected.some(Boolean))
33
+ return [];
34
+ const status = propertyType(variant, 'status', checker);
35
+ if (!(status.flags & ts.TypeFlags.NumberLiteral))
36
+ throw new Error('HTTP response requires a literal status');
37
+ const value = status.value;
38
+ if (!Number.isInteger(value) || value < 100 || value > 599)
39
+ throw new Error('HTTP response status is invalid');
40
+ const kinds = stringLiterals(propertyType(variant, 'bodyKind', checker));
41
+ if (kinds.length !== 1)
42
+ throw new Error('HTTP response requires one concrete body kind per variant');
43
+ const statusCodes = [value];
44
+ if (kinds[0] === 'none')
45
+ return [
46
+ {
47
+ ...noContentContract(source),
48
+ statusCodes,
49
+ statusKnowledge: 'explicit',
50
+ bodyKind: 'none',
51
+ mediaTypeKnowledge: 'none',
52
+ ...(value === 400 ? { includeDefaultError: true } : {}),
53
+ },
54
+ ];
55
+ if (kinds[0] !== 'content')
56
+ throw new Error('HTTP response body kind is invalid');
57
+ if (statusForbidsBody(value))
58
+ throw new Error(`HTTP status ${value} forbids response content`);
59
+ const contract = concretePayload(propertyType(variant, 'body', checker), checker, source, options);
60
+ return stringLiterals(propertyType(variant, 'mediaType', checker)).flatMap((mediaType, index) => selected[index]
61
+ ? [
62
+ {
63
+ ...contract,
64
+ statusCodes,
65
+ statusKnowledge: 'explicit',
66
+ bodyKind: 'content',
67
+ mediaType,
68
+ mediaTypeKnowledge: 'known',
69
+ ...(value === 400 ? { includeDefaultError: true } : {}),
70
+ },
71
+ ]
72
+ : []);
73
+ });
74
+ }
75
+ function concretePayload(type, checker, source, options) {
76
+ const contract = extractContractSchema(type, checker, source, options);
77
+ if (contract.schema.root.type === 'unknown')
78
+ throw new Error('Protocol payload must have a concrete root');
79
+ return contract;
80
+ }
81
+ function rawPropertyType(type, name, checker) {
82
+ const property = checker.getPropertyOfType(type, name);
83
+ const declaration = property?.valueDeclaration ?? property?.declarations?.[0];
84
+ return property && declaration
85
+ ? checker.getTypeOfSymbolAtLocation(property, declaration)
86
+ : undefined;
87
+ }
@@ -0,0 +1 @@
1
+ export declare function nextProtocolKey(contracts: ReadonlyMap<string, unknown>, key: string): string;
@@ -0,0 +1,8 @@
1
+ export function nextProtocolKey(contracts, key) {
2
+ if (!contracts.has(key))
3
+ return key;
4
+ let index = 2;
5
+ while (contracts.has(`${key}#protocol-${index}`))
6
+ index++;
7
+ return `${key}#protocol-${index}`;
8
+ }
@@ -0,0 +1,5 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import { type DiscoverApiResponseContractsOptions } from './response-contract-lenient.mts';
3
+ import { type HandlerBindings } from './response-contract-route-analysis.mts';
4
+ import type { BackendResponseContract } from './response-contract-types.mts';
5
+ export declare function discoverProtocolContracts(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, bindings: HandlerBindings, contracts: Map<string, BackendResponseContract>, options: DiscoverApiResponseContractsOptions | undefined, requestedKeys?: ReadonlySet<string>): Set<ts.CallExpression>;
@@ -0,0 +1,133 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { nextProtocolKey } from './protocol-contract-keys.mjs';
3
+ import { extractContractSchema } from '../contract-schema/index.mjs';
4
+ import { extractHttpVariants, extractSseEvents } from './protocol-contract-extraction.mjs';
5
+ import { extractHttpBodyKinds } from './protocol-http-body-kinds.mjs';
6
+ import { registerHttpProtocols } from './protocol-http-registration.mjs';
7
+ import { protocolBindingRequested, requestedProtocolKey } from './protocol-requested-keys.mjs';
8
+ import { protocolMarker } from './protocol-marker-analysis.mjs';
9
+ import { registerRouteContract, } from './response-contract-lenient.mjs';
10
+ import { contractError, sourceLocation } from './response-contract-registration.mjs';
11
+ import { enclosingRouteBinding, visit, } from './response-contract-route-analysis.mjs';
12
+ import { rejectRawSseWrites } from './protocol-sse-raw-writes.mjs';
13
+ import { sseEmission } from './protocol-sse-emission.mjs';
14
+ export function discoverProtocolContracts(sourceFiles, checker, bindings, contracts, options, requestedKeys) {
15
+ if (requestedKeys?.size === 0)
16
+ return new Set();
17
+ const calls = [];
18
+ for (const file of sourceFiles)
19
+ visit(file, (node) => {
20
+ if (ts.isCallExpression(node))
21
+ calls.push(node);
22
+ });
23
+ const pending = [];
24
+ const allocated = new Map();
25
+ const framedWrites = new Set();
26
+ const sseBindings = new Map();
27
+ for (const file of sourceFiles)
28
+ visit(file, (node) => {
29
+ if (!ts.isCallExpression(node))
30
+ return;
31
+ const marker = protocolMarker(node.expression, checker);
32
+ if (!marker)
33
+ return;
34
+ const binding = enclosingRouteBinding(node, checker, bindings, false);
35
+ if (!binding)
36
+ throw contractError(file, node, `${marker} must be inside an app.route handler`);
37
+ if (!protocolBindingRequested(binding, requestedKeys))
38
+ return;
39
+ const keyNode = node.arguments[0];
40
+ if (!keyNode || !ts.isStringLiteral(keyNode))
41
+ throw contractError(file, node, `${marker} requires a literal contract key`);
42
+ const prefix = `${binding.method}:${binding.routeTemplate}`;
43
+ if (keyNode.text !== prefix && !keyNode.text.startsWith(`${prefix}#`))
44
+ throw contractError(file, keyNode, `Contract key "${keyNode.text}" does not match enclosing route ${prefix}`);
45
+ const location = sourceLocation(file, node);
46
+ let errorKeys = [];
47
+ try {
48
+ if (marker === 'apiSseFrame') {
49
+ const rowKey = reserveKey(allocated, keyNode.text);
50
+ errorKeys = [rowKey];
51
+ if (!requestedProtocolKey(rowKey, binding, requestedKeys)) {
52
+ if (ts.isCallExpression(node.parent))
53
+ framedWrites.add(node.parent);
54
+ return;
55
+ }
56
+ const body = node.arguments[1];
57
+ if (!body)
58
+ throw new Error(`${marker} requires a protocol body`);
59
+ const { write, receiver, status } = sseEmission(node, checker, calls, binding, bindings);
60
+ const sseEvents = extractSseEvents(checker.getTypeAtLocation(body), checker, location, options);
61
+ contracts.set(rowKey, {
62
+ ...extractContractSchema(checker.getStringType(), checker, location, options),
63
+ ...binding,
64
+ bodyKind: 'content',
65
+ mediaType: 'text/event-stream',
66
+ mediaTypeKnowledge: 'known',
67
+ ...status,
68
+ sseEvents,
69
+ });
70
+ framedWrites.add(write);
71
+ const routeWrites = sseBindings.get(prefix) ?? { receivers: [], keys: [] };
72
+ routeWrites.receivers.push(receiver);
73
+ routeWrites.keys.push(rowKey);
74
+ sseBindings.set(prefix, routeWrites);
75
+ }
76
+ else {
77
+ const declaration = node.parent;
78
+ const symbol = ts.isVariableDeclaration(declaration) &&
79
+ ts.isIdentifier(declaration.name) &&
80
+ ts.isVariableDeclarationList(declaration.parent) &&
81
+ !!(declaration.parent.flags & ts.NodeFlags.Const)
82
+ ? checker.getSymbolAtLocation(declaration.name)
83
+ : undefined;
84
+ if (!symbol)
85
+ throw new Error('apiOpenApiHttpResponse must bind a response variable');
86
+ const keys = [];
87
+ errorKeys = keys;
88
+ const variants = extractHttpVariants(checker.getTypeAtLocation(node), checker, location, options, () => {
89
+ const rowKey = reserveKey(allocated, keyNode.text);
90
+ if (!requestedProtocolKey(rowKey, binding, requestedKeys))
91
+ return false;
92
+ keys.push(rowKey);
93
+ return true;
94
+ });
95
+ pending.push({
96
+ call: node,
97
+ symbol,
98
+ binding,
99
+ variants,
100
+ get declaredBodyKinds() {
101
+ return extractHttpBodyKinds(checker.getTypeAtLocation(node), checker);
102
+ },
103
+ keys,
104
+ });
105
+ }
106
+ }
107
+ catch (error) {
108
+ for (const key of errorKeys.length ? errorKeys : [reserveKey(allocated, keyNode.text)])
109
+ unavailable(contracts, key, binding, location, error, options);
110
+ }
111
+ });
112
+ const covered = registerHttpProtocols(pending, contracts, checker, (response, error) => {
113
+ for (const key of response.keys)
114
+ unavailable(contracts, key, response.binding, response.variants[0].source, error, options);
115
+ });
116
+ rejectRawSseWrites(sourceFiles, checker, bindings, framedWrites, sseBindings, (node, binding, keys) => {
117
+ for (const key of keys) {
118
+ contracts.delete(key);
119
+ unavailable(contracts, key, binding, sourceLocation(node.getSourceFile(), node), new Error('SSE route writes an unmarked frame'), options);
120
+ }
121
+ });
122
+ return covered;
123
+ }
124
+ function unavailable(contracts, key, binding, location, error, options) {
125
+ registerRouteContract(contracts, nextProtocolKey(contracts, key), binding, location, { bodyKind: 'content', statusKnowledge: 'unknown', mediaTypeKnowledge: 'unknown' }, () => {
126
+ throw error;
127
+ }, options);
128
+ }
129
+ function reserveKey(allocated, key) {
130
+ const row = nextProtocolKey(allocated, key);
131
+ allocated.set(row, undefined);
132
+ return row;
133
+ }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** Excludes statically dead branches without claiming that dynamic branches always run. */
3
+ export declare function potentiallyExecuted(node: ts.Node): boolean;
@@ -0,0 +1,64 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { unwrapExpression } from './protocol-marker-analysis.mjs';
3
+ /** Excludes statically dead branches without claiming that dynamic branches always run. */
4
+ export function potentiallyExecuted(node) {
5
+ let current = node;
6
+ while (current.parent && !ts.isFunctionLike(current.parent)) {
7
+ const parent = current.parent;
8
+ if (ts.isBlock(parent)) {
9
+ const index = parent.statements.indexOf(current);
10
+ if (parent.statements.slice(0, index).some(terminates))
11
+ return false;
12
+ }
13
+ if (ts.isIfStatement(parent)) {
14
+ const condition = unwrapExpression(parent.expression).kind;
15
+ if (condition === ts.SyntaxKind.FalseKeyword && parent.thenStatement === current)
16
+ return false;
17
+ if (condition === ts.SyntaxKind.TrueKeyword && parent.elseStatement === current)
18
+ return false;
19
+ }
20
+ if ((ts.isWhileStatement(parent) || ts.isForStatement(parent)) &&
21
+ parent.statement === current) {
22
+ const condition = ts.isWhileStatement(parent) ? parent.expression : parent.condition;
23
+ if (condition && unwrapExpression(condition).kind === ts.SyntaxKind.FalseKeyword)
24
+ return false;
25
+ }
26
+ if (ts.isConditionalExpression(parent)) {
27
+ const condition = unwrapExpression(parent.condition).kind;
28
+ if (condition === ts.SyntaxKind.FalseKeyword && parent.whenTrue === current)
29
+ return false;
30
+ if (condition === ts.SyntaxKind.TrueKeyword && parent.whenFalse === current)
31
+ return false;
32
+ }
33
+ if (ts.isBinaryExpression(parent) && parent.right === current) {
34
+ const left = unwrapExpression(parent.left).kind;
35
+ if (left === ts.SyntaxKind.FalseKeyword &&
36
+ parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
37
+ return false;
38
+ if (left === ts.SyntaxKind.TrueKeyword &&
39
+ parent.operatorToken.kind === ts.SyntaxKind.BarBarToken)
40
+ return false;
41
+ }
42
+ current = parent;
43
+ }
44
+ return true;
45
+ }
46
+ function terminates(statement) {
47
+ if (ts.isReturnStatement(statement) ||
48
+ ts.isThrowStatement(statement) ||
49
+ ts.isBreakStatement(statement) ||
50
+ ts.isContinueStatement(statement))
51
+ return true;
52
+ if (ts.isBlock(statement))
53
+ return statement.statements.some(terminates);
54
+ if (!ts.isIfStatement(statement))
55
+ return false;
56
+ const condition = unwrapExpression(statement.expression).kind;
57
+ if (condition === ts.SyntaxKind.TrueKeyword)
58
+ return terminates(statement.thenStatement);
59
+ if (condition === ts.SyntaxKind.FalseKeyword)
60
+ return !!statement.elseStatement && terminates(statement.elseStatement);
61
+ return (!!statement.elseStatement &&
62
+ terminates(statement.thenStatement) &&
63
+ terminates(statement.elseStatement));
64
+ }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** Proves execution up to an optional handler whose registered binding is already established. */
3
+ export declare function executableProtocolPath(node: ts.Node, checker: ts.TypeChecker, boundHandler?: ts.Node): boolean;
@@ -0,0 +1,16 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { isSupportedProtocolCallback } from './protocol-callback-invocation.mjs';
3
+ import { potentiallyExecuted } from './protocol-executable-path.mjs';
4
+ import { enclosingFunction } from './protocol-marker-analysis.mjs';
5
+ /** Proves execution up to an optional handler whose registered binding is already established. */
6
+ export function executableProtocolPath(node, checker, boundHandler) {
7
+ if (!potentiallyExecuted(node))
8
+ return false;
9
+ let fn = enclosingFunction(node);
10
+ while (fn && fn !== boundHandler) {
11
+ if (!potentiallyExecuted(fn) || !isSupportedProtocolCallback(fn, checker))
12
+ return false;
13
+ fn = enclosingFunction(fn);
14
+ }
15
+ return true;
16
+ }
@@ -0,0 +1,7 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ export declare function associateHttpResponse(call: ts.CallExpression, response: ts.Symbol, checker: ts.TypeChecker, siblings?: ReadonlySet<ts.Symbol>): Map<ts.CallExpression, 'content' | 'none' | 'status'>;
3
+ /** Opaque callbacks may emit through an independently proven handler's context. */
4
+ export declare function opaqueHttpResponse(call: ts.CallExpression, checker: ts.TypeChecker, context?: ts.Symbol): boolean;
5
+ /** Mutable wrappers on supported callback paths cannot establish a complete contract. */
6
+ export declare function unsupportedContextResponse(call: ts.CallExpression, checker: ts.TypeChecker): boolean;
7
+ export declare function supportedResponseContext(call: ts.CallExpression, checker: ts.TypeChecker): ts.Symbol | undefined;
@@ -0,0 +1,97 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { httpHandlerContext, httpContextScopes, indirectHttpResponseMethod, } from './protocol-http-context.mjs';
3
+ import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
4
+ import { httpEmissionKind, contextResponseMethod } from './protocol-http-emission.mjs';
5
+ import { enclosingFunction } from './protocol-marker-analysis.mjs';
6
+ import { executableProtocolPath } from './protocol-execution-path.mjs';
7
+ import { unsupportedContextAlias } from './protocol-context-alias.mjs';
8
+ import { expressionReceiver } from './protocol-write-receiver.mjs';
9
+ import { visit } from './response-contract-route-analysis.mjs';
10
+ import { isSupportedProtocolCallback } from './protocol-callback-invocation.mjs';
11
+ import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
12
+ const responseMethods = new Set([
13
+ 'setStatus',
14
+ 'pipeline',
15
+ 'json',
16
+ 'response.buffer',
17
+ 'response.empty',
18
+ ]);
19
+ export function associateHttpResponse(call, response, checker, siblings) {
20
+ const handler = enclosingFunction(call);
21
+ const handlerContext = handler && httpHandlerContext(handler, checker);
22
+ if (!handler || !handlerContext)
23
+ throw new Error('HTTP response must bind the handler context');
24
+ if (!executableProtocolPath(call, checker))
25
+ throw new Error('HTTP response must be inside a supported executable handler callback');
26
+ const emissions = new Map();
27
+ for (const [scope, context] of httpContextScopes(handler, checker))
28
+ visit(scope, (node) => {
29
+ if (ts.isCallExpression(node) &&
30
+ executableProtocolPath(node, checker) &&
31
+ responseMethods.has(indirectHttpResponseMethod(node, context, checker) ?? ''))
32
+ throw new Error('HTTP response has an unsupported indirect status or body emission');
33
+ if (ts.isCallExpression(node) && opaqueHttpResponse(node, checker, context))
34
+ throw new Error('HTTP response context escapes through an opaque callback');
35
+ if (ts.isBinaryExpression(node) &&
36
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
37
+ executableProtocolPath(node, checker)) {
38
+ const receiver = expressionReceiver(node.right, checker);
39
+ if (receiver?.root === context &&
40
+ (receiver.path.length === 0 ||
41
+ (receiver.path.length === 1 && receiver.path[0] === 'response')))
42
+ throw new Error('HTTP response context has an unsupported mutable alias');
43
+ }
44
+ if (ts.isVariableDeclaration(node) &&
45
+ enclosingFunction(node) === scope &&
46
+ unsupportedContextAlias(node, context, checker))
47
+ throw new Error('HTTP response context has an unsupported mutable or destructured alias');
48
+ if (!ts.isCallExpression(node) || !executableProtocolPath(node, checker))
49
+ return;
50
+ const kind = enclosingFunction(node) === handler
51
+ ? httpEmissionKind(node, response, context, checker)
52
+ : undefined;
53
+ if (kind)
54
+ emissions.set(node, kind);
55
+ else if (responseMethods.has(contextResponseMethod(node.expression, context, checker, true) ?? '') &&
56
+ (enclosingFunction(node) === scope ||
57
+ !!contextResponseMethod(node.expression, context, checker)))
58
+ if (siblings &&
59
+ [...siblings].some((symbol) => symbol !== response && !!httpEmissionKind(node, symbol, context, checker)))
60
+ return;
61
+ else
62
+ throw new Error('HTTP response has an unrelated status or body emission');
63
+ });
64
+ return emissions;
65
+ }
66
+ /** Opaque callbacks may emit through an independently proven handler's context. */
67
+ export function opaqueHttpResponse(call, checker, context) {
68
+ if (context)
69
+ return (responseMethods.has(contextResponseMethod(call.expression, context, checker, true) ??
70
+ indirectHttpResponseMethod(call, context, checker) ??
71
+ '') && opaqueProtocolCallbackPath(call, checker));
72
+ return !!supportedResponseContext(call, checker) && opaqueProtocolCallbackPath(call, checker);
73
+ }
74
+ /** Mutable wrappers on supported callback paths cannot establish a complete contract. */
75
+ export function unsupportedContextResponse(call, checker) {
76
+ if (!executableProtocolPath(call, checker))
77
+ return false;
78
+ const context = supportedResponseContext(call, checker);
79
+ return !!context && contextResponseMethod(call.expression, context, checker) === undefined;
80
+ }
81
+ export function supportedResponseContext(call, checker) {
82
+ let handler = enclosingFunction(call);
83
+ while (handler) {
84
+ const name = runtimeParameters(handler)[0]?.name;
85
+ const symbol = name && ts.isIdentifier(name) ? checker.getSymbolAtLocation(name) : undefined;
86
+ if (symbol &&
87
+ responseMethods.has(contextResponseMethod(call.expression, symbol, checker, true) ??
88
+ indirectHttpResponseMethod(call, symbol, checker) ??
89
+ '') &&
90
+ httpHandlerContext(handler, checker) === symbol &&
91
+ isSupportedProtocolCallback(handler, checker) &&
92
+ executableProtocolPath(handler, checker))
93
+ return symbol;
94
+ handler = enclosingFunction(handler);
95
+ }
96
+ return undefined;
97
+ }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** Checks complete wire body kinds without extracting unselected payload schemas. */
3
+ export declare function extractHttpBodyKinds(type: ts.Type, checker: ts.TypeChecker): ReadonlySet<'content' | 'none'>;
@@ -0,0 +1,19 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { propertyType, stringLiterals, typeVariants } from './protocol-marker-analysis.mjs';
3
+ /** Checks complete wire body kinds without extracting unselected payload schemas. */
4
+ export function extractHttpBodyKinds(type, checker) {
5
+ const carrier = propertyType(type, 'apiHttpResponseVariants', checker);
6
+ const kinds = new Set();
7
+ for (const variant of typeVariants(carrier)) {
8
+ if (variant.flags & ts.TypeFlags.Undefined)
9
+ continue;
10
+ const names = stringLiterals(propertyType(variant, 'bodyKind', checker));
11
+ if (names.length !== 1)
12
+ throw new Error('HTTP response requires one concrete body kind per variant');
13
+ const kind = names[0];
14
+ if (kind !== 'content' && kind !== 'none')
15
+ throw new Error('HTTP response body kind is invalid');
16
+ kinds.add(kind);
17
+ }
18
+ return kinds;
19
+ }
@@ -0,0 +1,6 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** Use the same actual invocation sites for context admission and caller emission coverage. */
3
+ export declare function httpContextInvocations(fn: ts.FunctionLikeDeclaration, checker: ts.TypeChecker): {
4
+ registered: boolean;
5
+ callers: ts.CallExpression[];
6
+ };