carrick 0.3.90 → 0.3.91

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 (37) hide show
  1. package/dist/auth/oauth.d.ts +8 -0
  2. package/dist/auth/oauth.js +65 -11
  3. package/dist/auth/oauth.js.map +1 -1
  4. package/dist/init/connect.d.ts +42 -3
  5. package/dist/init/connect.js +172 -113
  6. package/dist/init/connect.js.map +1 -1
  7. package/dist/init/mcp.d.ts +11 -0
  8. package/dist/init/mcp.js +14 -8
  9. package/dist/init/mcp.js.map +1 -1
  10. package/dist/init/output.d.ts +2 -0
  11. package/dist/init/output.js +33 -2
  12. package/dist/init/output.js.map +1 -1
  13. package/dist/init/projects.d.ts +56 -5
  14. package/dist/init/projects.js +124 -17
  15. package/dist/init/projects.js.map +1 -1
  16. package/dist/init/repos.d.ts +33 -0
  17. package/dist/init/repos.js +8 -1
  18. package/dist/init/repos.js.map +1 -1
  19. package/dist/init/run.d.ts +64 -36
  20. package/dist/init/run.js +263 -163
  21. package/dist/init/run.js.map +1 -1
  22. package/package.json +6 -6
  23. package/sidecar/dist/src/capture/api.d.ts +13 -1
  24. package/sidecar/dist/src/capture/check-classify.js +12 -7
  25. package/sidecar/dist/src/capture/check-probe.d.ts +10 -0
  26. package/sidecar/dist/src/capture/check-probe.js +21 -3
  27. package/sidecar/dist/src/capture/check.js +1 -0
  28. package/sidecar/dist/src/capture/index.d.ts +1 -0
  29. package/sidecar/dist/src/capture/index.js +1 -0
  30. package/sidecar/dist/src/index.js +37 -9
  31. package/sidecar/dist/src/retype.d.ts +67 -0
  32. package/sidecar/dist/src/retype.js +813 -0
  33. package/sidecar/dist/src/type-inferrer.d.ts +85 -1
  34. package/sidecar/dist/src/type-inferrer.js +384 -1
  35. package/sidecar/dist/src/types.d.ts +58 -2
  36. package/sidecar/dist/src/validators.d.ts +153 -20
  37. package/sidecar/dist/src/validators.js +17 -0
@@ -17,7 +17,7 @@
17
17
  * (redirects, 204s), the LLM emits null and we fall back to the containing
18
18
  * function's return type.
19
19
  */
20
- import { Project } from 'ts-morph';
20
+ import { Project, SourceFile, type CallExpression } from 'ts-morph';
21
21
  import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js';
22
22
  /**
23
23
  * Strongly-discriminating member names of HTTP transport machinery — the
@@ -89,6 +89,16 @@ export declare class TypeInferrer {
89
89
  * Infer a single type from a request
90
90
  */
91
91
  private inferSingle;
92
+ /**
93
+ * The call a `call_result` locator names, located exactly as `infer` locates
94
+ * it (span, else expression text near a line). The retype check
95
+ * (carrick#1491) rewrites this node, so it must be the node the consumer's
96
+ * published type came from, not a guess at the line.
97
+ */
98
+ locateCall(request: InferRequestItem): {
99
+ sourceFile: SourceFile;
100
+ call: CallExpression;
101
+ } | undefined;
92
102
  /**
93
103
  * Get or add a source file to the project
94
104
  */
@@ -1088,6 +1098,80 @@ export declare class TypeInferrer {
1088
1098
  * the route declares its request nowhere we can read.
1089
1099
  */
1090
1100
  private requestContractFromRegistration;
1101
+ /**
1102
+ * carrick-cloud#1366, shape 1: the request contract of a parameter a KEYED
1103
+ * parameter decorator binds to one field of the body (`@Body('key') x: T`).
1104
+ *
1105
+ * The decorator is recognised by shape, not by name: a call decorator whose
1106
+ * first argument is a string literal. Every parameter of the same handler
1107
+ * that the SAME decorator binds by key contributes one member, so two keyed
1108
+ * reads of one body publish one object. A sibling the same decorator binds
1109
+ * WITHOUT a key (no argument, or a non-string one such as a pipe) receives
1110
+ * the whole body, so the contract is that sibling's type intersected with
1111
+ * the keyed members.
1112
+ *
1113
+ * Returns null when the located node is not a keyed parameter, so every
1114
+ * other request path is untouched.
1115
+ */
1116
+ private keyedBodyParamContract;
1117
+ /**
1118
+ * The handler parameter a request locator names: the parameter itself, its
1119
+ * name, a decorator on it, or an identifier whose declaration is one. A
1120
+ * member read of a parameter (`x.length`) is not the parameter and is left
1121
+ * to the expression path.
1122
+ */
1123
+ private parameterNamedBy;
1124
+ /**
1125
+ * Every call decorator on the parameter, with the string key its first
1126
+ * argument names (`@Body('key')` -> `{ name: 'Body', key: 'key' }`). A call
1127
+ * decorator with no argument, or a first argument that is not a string (a
1128
+ * pipe instance), binds without a key.
1129
+ */
1130
+ private callDecoratorBindings;
1131
+ /**
1132
+ * carrick-cloud#1366, shapes 2 and 3: a request locator that names the
1133
+ * request CALL, or a request CONFIG argument, instead of the body.
1134
+ *
1135
+ * The body is found from the callee's DECLARED signature:
1136
+ *
1137
+ * - a body slot is a parameter named as a body (`data`, `body`, `json`)
1138
+ * and typed by one of the signature's own type parameters (`data?: D`);
1139
+ * - a config slot is a parameter typed by a generic config type whose
1140
+ * body-named member is typed by the config's own type parameter
1141
+ * (`config?: Config<D>` with `data?: D`). A generic config with no such
1142
+ * member (its type parameter types something else, a response mode say)
1143
+ * is opaque: nothing can be said about its body.
1144
+ *
1145
+ * A located CALL is taken as the request call only when its first argument
1146
+ * is string-typed (the URL) and one argument supplies a body through a slot;
1147
+ * anything else is left alone, so an awaited payload builder keeps its own
1148
+ * type. A located argument in a config slot yields the payload member: from
1149
+ * an object literal directly, from a variable through its type. A literal
1150
+ * that does not set the member means the call sends no body; a variable
1151
+ * whose type does not resolve the member, or an opaque config, abstains.
1152
+ */
1153
+ private requestBodyInCall;
1154
+ /**
1155
+ * The body a config-slot argument supplies through `member`: an object
1156
+ * literal's own member, or a variable's member read off its type.
1157
+ */
1158
+ private configArgumentBody;
1159
+ /**
1160
+ * Per parameter position of the call's resolved declaration: a body slot, a
1161
+ * config slot (with the member that carries the body), an opaque generic
1162
+ * config, or neither (undefined). Empty when the callee has no declaration.
1163
+ */
1164
+ private requestBodySlots;
1165
+ /**
1166
+ * A generic config type's slot: `config` with the body-named member typed by
1167
+ * the config's own type parameter (`interface Config<D> { data?: D }`), or
1168
+ * `opaque_config` when the config is generic but no body-named member is
1169
+ * (`interface Options<R> { responseType?: R }`). Undefined for a type that
1170
+ * is not a generic config, and for two candidate members (ambiguous).
1171
+ */
1172
+ private configSlot;
1173
+ /** The value an object literal gives `name`: an assignment's initializer or a shorthand's identifier. */
1174
+ private objectLiteralMemberValue;
1091
1175
  /**
1092
1176
  * An explicit request `InferredType` for a contract the route declares,
1093
1177
  * carrying the provenance the reading recorded.
@@ -17,6 +17,7 @@
17
17
  * (redirects, 204s), the LLM emits null and we fall back to the containing
18
18
  * function's return type.
19
19
  */
20
+ import * as path from 'node:path';
20
21
  import { Node, SyntaxKind, ts, } from 'ts-morph';
21
22
  import { validateInferRequestItem } from './validators.js';
22
23
  import { isExternalOrigin } from './origin.js';
@@ -206,6 +207,17 @@ function classifyStatusCodes(codes) {
206
207
  * request contract.
207
208
  */
208
209
  const REQUEST_BODY_PARTS = new Set(['json', 'form', 'body']);
210
+ /**
211
+ * A contract a route declares, as printed text, plus the provenance the reading
212
+ * recorded (a coerced request member, carrick#1101). Provenance is absent, not
213
+ * empty, when there is nothing to say.
214
+ */
215
+ /**
216
+ * carrick-cloud#1366: the parameter and config-member names a client takes a
217
+ * request body under. A generic slot is read as the body only under one of
218
+ * these names; a type parameter alone also types response modes and fallbacks.
219
+ */
220
+ const BODY_MEMBER_NAMES = new Set(['data', 'body', 'json']);
209
221
  /**
210
222
  * Print a `Type` to its string form WITHOUT the compiler's default truncation.
211
223
  *
@@ -325,6 +337,22 @@ export class TypeInferrer {
325
337
  return null;
326
338
  }
327
339
  }
340
+ /**
341
+ * The call a `call_result` locator names, located exactly as `infer` locates
342
+ * it (span, else expression text near a line). The retype check
343
+ * (carrick#1491) rewrites this node, so it must be the node the consumer's
344
+ * published type came from, not a guess at the line.
345
+ */
346
+ locateCall(request) {
347
+ const filePath = path.isAbsolute(request.file_path)
348
+ ? request.file_path
349
+ : path.join(this.repoRoot, request.file_path);
350
+ const sourceFile = this.getSourceFile(filePath);
351
+ if (!sourceFile)
352
+ return undefined;
353
+ const call = this.resolveTargetCallExpression(sourceFile, request);
354
+ return call ? { sourceFile, call } : undefined;
355
+ }
328
356
  /**
329
357
  * Get or add a source file to the project
330
358
  */
@@ -1042,6 +1070,21 @@ export class TypeInferrer {
1042
1070
  // LLM's `primary_type_symbol` schema contract extracts, and the Rust
1043
1071
  // depth-copy's symbol-agreement guard makes a mismatched fallback inert.
1044
1072
  // Multi-generic calls are ambiguous and anchor nothing here.
1073
+ //
1074
+ // carrick#1491: why a typed call like `client.post<{ x: number }>(url)`
1075
+ // publishes no comparable type. Two facts, both pinned by
1076
+ // test/infer-inline-call-generic.test.ts:
1077
+ // - when the client resolves and no wrapper rule unwrapped its envelope,
1078
+ // the anchor above is the ENVELOPE's own symbol, so this fallback never
1079
+ // runs and the text stays the whole envelope
1080
+ // (`ClientResponse<{ x: number; }, any>`), whose defaulted request-data
1081
+ // parameter is `any`;
1082
+ // - when the client does not resolve, this fallback runs, and it anchors
1083
+ // a NAMED generic only: an inline literal has no symbol, so
1084
+ // `primaryTypeSymbol` returns nothing and no anchor is taken.
1085
+ // Either way the text carries `any`, cannot anchor a literal capture, and
1086
+ // the pair is left unverified; the check phase's retype of the consumer
1087
+ // call is what judges such a call today.
1045
1088
  if (!anchor || !this.primaryTypeSymbol(anchor.element)) {
1046
1089
  const typeArgs = callExpr.getTypeArguments();
1047
1090
  if (typeArgs.length === 1) {
@@ -1192,9 +1235,55 @@ export class TypeInferrer {
1192
1235
  const parent = located.getParent();
1193
1236
  located = parent.getInitializer() ?? located;
1194
1237
  }
1238
+ // carrick-cloud#1366: the locator named a handler parameter that a keyed
1239
+ // parameter decorator binds to ONE field of the body (`@Body('key') x: T`).
1240
+ // The parameter's type is that field's, so publishing it states the whole
1241
+ // body is a `T`. The body the handler reads is an object with one member
1242
+ // per keyed parameter the same decorator binds.
1243
+ const keyedBody = this.keyedBodyParamContract(located);
1244
+ if (keyedBody) {
1245
+ this.log(`Request locator at ${request.file_path}:${request.line_number} names a parameter bound ` +
1246
+ 'to one body field by a keyed decorator; publishing the body those fields make up');
1247
+ return this.declaredRequestInferredType(request, keyedBody, this.getNodeLocation(located));
1248
+ }
1195
1249
  // Mirror inferCallResult: strip `await`/`as`/parens/`!` so the inner
1196
1250
  // expression's type (not the surrounding `Promise<any>`) is read.
1197
- const unwrapped = this.unwrapExpressionNode(located);
1251
+ let unwrapped = this.unwrapExpressionNode(located);
1252
+ // carrick-cloud#1366: the locator landed on the request CALL, or on the
1253
+ // request CONFIG the call takes, rather than on the body. The call's type
1254
+ // is its response, and a config's type is the config, so either one read
1255
+ // as the body publishes the wrong contract. Follow the call's signature to
1256
+ // the argument it sends as the body.
1257
+ const inCall = this.requestBodyInCall(unwrapped);
1258
+ if (inCall.kind === 'body') {
1259
+ this.log(`Request locator at ${request.file_path}:${request.line_number} names ${inCall.from}; ` +
1260
+ 'reading the body argument its signature declares');
1261
+ unwrapped = this.unwrapExpressionNode(inCall.node);
1262
+ }
1263
+ else if (inCall.kind === 'body_type') {
1264
+ this.log(`Request locator at ${request.file_path}:${request.line_number} names ${inCall.from}; ` +
1265
+ "reading the body off the config variable's type");
1266
+ return this.createInferredType(request, this.expandResolvedTypeStructural(inCall.type, typeText(inCall.type, inCall.at)), false, this.getNodeLocation(inCall.at));
1267
+ }
1268
+ else if (inCall.kind === 'abstain') {
1269
+ this.log(`Request locator at ${request.file_path}:${request.line_number}: ${inCall.why}; leaving unresolved`);
1270
+ return null;
1271
+ }
1272
+ else if (inCall.kind === 'no_body') {
1273
+ this.log(`Request locator at ${request.file_path}:${request.line_number} is a request config ` +
1274
+ 'that carries no body member; the call sends no request body');
1275
+ const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(unwrapped));
1276
+ abstain.any_provenance = [
1277
+ {
1278
+ path: '',
1279
+ kind: 'unknown',
1280
+ reason: 'no_request_body',
1281
+ detail: `the located argument is the call's request config, and it sets no '${inCall.member}' ` +
1282
+ 'member, which is where the call takes its body, so the call sends no request body',
1283
+ },
1284
+ ];
1285
+ return abstain;
1286
+ }
1198
1287
  // A `fetch` body is almost always `JSON.stringify(payload)`, whose own type
1199
1288
  // is the useless `string`. Drill to the serialized argument so the consumer
1200
1289
  // request shape is the payload's type, not `string`. General: any
@@ -3946,6 +4035,300 @@ export class TypeInferrer {
3946
4035
  const read = this.inferRequestReadFromHandler(handler);
3947
4036
  return read ? { text: read } : null;
3948
4037
  }
4038
+ /**
4039
+ * carrick-cloud#1366, shape 1: the request contract of a parameter a KEYED
4040
+ * parameter decorator binds to one field of the body (`@Body('key') x: T`).
4041
+ *
4042
+ * The decorator is recognised by shape, not by name: a call decorator whose
4043
+ * first argument is a string literal. Every parameter of the same handler
4044
+ * that the SAME decorator binds by key contributes one member, so two keyed
4045
+ * reads of one body publish one object. A sibling the same decorator binds
4046
+ * WITHOUT a key (no argument, or a non-string one such as a pipe) receives
4047
+ * the whole body, so the contract is that sibling's type intersected with
4048
+ * the keyed members.
4049
+ *
4050
+ * Returns null when the located node is not a keyed parameter, so every
4051
+ * other request path is untouched.
4052
+ */
4053
+ keyedBodyParamContract(located) {
4054
+ const param = this.parameterNamedBy(located);
4055
+ if (!param)
4056
+ return null;
4057
+ const binding = this.callDecoratorBindings(param).find((b) => b.key !== undefined);
4058
+ if (!binding)
4059
+ return null;
4060
+ const func = param.getParent();
4061
+ if (!func || !('getParameters' in func))
4062
+ return null;
4063
+ const siblings = func.getParameters();
4064
+ const members = [];
4065
+ const seen = new Set();
4066
+ let wholeBody;
4067
+ for (const sibling of siblings) {
4068
+ const same = this.callDecoratorBindings(sibling).filter((b) => b.name === binding.name);
4069
+ if (same.length === 0)
4070
+ continue;
4071
+ const keyed = same.find((b) => b.key !== undefined);
4072
+ if (!keyed || keyed.key === undefined) {
4073
+ wholeBody ??= sibling;
4074
+ continue;
4075
+ }
4076
+ if (seen.has(keyed.key))
4077
+ continue;
4078
+ seen.add(keyed.key);
4079
+ const optional = sibling.hasQuestionToken() || sibling.hasInitializer();
4080
+ let type = sibling.getType();
4081
+ if (optional)
4082
+ type = type.getNonNullableType();
4083
+ const text = this.expandResolvedTypeStructural(type, typeText(type, sibling));
4084
+ const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(keyed.key)
4085
+ ? keyed.key
4086
+ : JSON.stringify(keyed.key);
4087
+ members.push(`${key}${optional ? '?' : ''}: ${text};`);
4088
+ }
4089
+ if (members.length === 0)
4090
+ return null;
4091
+ const keyedText = `{ ${members.join(' ')} }`;
4092
+ if (!wholeBody)
4093
+ return { text: keyedText };
4094
+ const wholeType = wholeBody.getType();
4095
+ const wholeText = this.expandResolvedTypeStructural(wholeType, typeText(wholeType, wholeBody));
4096
+ const operand = /^\{[\s\S]*\}$/.test(wholeText) ? wholeText : `(${wholeText})`;
4097
+ return { text: `${operand} & ${keyedText}` };
4098
+ }
4099
+ /**
4100
+ * The handler parameter a request locator names: the parameter itself, its
4101
+ * name, a decorator on it, or an identifier whose declaration is one. A
4102
+ * member read of a parameter (`x.length`) is not the parameter and is left
4103
+ * to the expression path.
4104
+ */
4105
+ parameterNamedBy(located) {
4106
+ if (Node.isParameterDeclaration(located))
4107
+ return located;
4108
+ const decoratedParam = located.getFirstAncestor((n) => Node.isParameterDeclaration(n) &&
4109
+ n.getDecorators().some((d) => d === located || d.containsRange(located.getPos(), located.getEnd())));
4110
+ if (decoratedParam)
4111
+ return decoratedParam;
4112
+ if (!Node.isIdentifier(located))
4113
+ return undefined;
4114
+ const parent = located.getParent();
4115
+ if (Node.isParameterDeclaration(parent) && parent.getNameNode() === located) {
4116
+ return parent;
4117
+ }
4118
+ for (const def of located.getDefinitionNodes()) {
4119
+ if (Node.isParameterDeclaration(def))
4120
+ return def;
4121
+ }
4122
+ return undefined;
4123
+ }
4124
+ /**
4125
+ * Every call decorator on the parameter, with the string key its first
4126
+ * argument names (`@Body('key')` -> `{ name: 'Body', key: 'key' }`). A call
4127
+ * decorator with no argument, or a first argument that is not a string (a
4128
+ * pipe instance), binds without a key.
4129
+ */
4130
+ callDecoratorBindings(param) {
4131
+ const bindings = [];
4132
+ for (const decorator of param.getDecorators()) {
4133
+ const call = decorator.getCallExpression();
4134
+ if (!call)
4135
+ continue;
4136
+ const first = call.getArguments()[0];
4137
+ if (first && (Node.isStringLiteral(first) || Node.isNoSubstitutionTemplateLiteral(first))) {
4138
+ bindings.push({ name: decorator.getName(), key: first.getLiteralValue() });
4139
+ }
4140
+ else {
4141
+ bindings.push({ name: decorator.getName() });
4142
+ }
4143
+ }
4144
+ return bindings;
4145
+ }
4146
+ /**
4147
+ * carrick-cloud#1366, shapes 2 and 3: a request locator that names the
4148
+ * request CALL, or a request CONFIG argument, instead of the body.
4149
+ *
4150
+ * The body is found from the callee's DECLARED signature:
4151
+ *
4152
+ * - a body slot is a parameter named as a body (`data`, `body`, `json`)
4153
+ * and typed by one of the signature's own type parameters (`data?: D`);
4154
+ * - a config slot is a parameter typed by a generic config type whose
4155
+ * body-named member is typed by the config's own type parameter
4156
+ * (`config?: Config<D>` with `data?: D`). A generic config with no such
4157
+ * member (its type parameter types something else, a response mode say)
4158
+ * is opaque: nothing can be said about its body.
4159
+ *
4160
+ * A located CALL is taken as the request call only when its first argument
4161
+ * is string-typed (the URL) and one argument supplies a body through a slot;
4162
+ * anything else is left alone, so an awaited payload builder keeps its own
4163
+ * type. A located argument in a config slot yields the payload member: from
4164
+ * an object literal directly, from a variable through its type. A literal
4165
+ * that does not set the member means the call sends no body; a variable
4166
+ * whose type does not resolve the member, or an opaque config, abstains.
4167
+ */
4168
+ requestBodyInCall(node) {
4169
+ if (Node.isCallExpression(node)) {
4170
+ const args = node.getArguments();
4171
+ if (args.length < 2)
4172
+ return { kind: 'none' };
4173
+ const first = args[0].getType();
4174
+ const stringLike = first.isString() ||
4175
+ first.isStringLiteral() ||
4176
+ first.isTemplateLiteral() ||
4177
+ (first.isUnion() && first.getUnionTypes().every((t) => t.isString() || t.isStringLiteral()));
4178
+ if (!stringLike)
4179
+ return { kind: 'none' };
4180
+ const slots = this.requestBodySlots(node);
4181
+ for (let i = 1; i < args.length; i++) {
4182
+ const slot = slots[i];
4183
+ if (!slot || slot.kind === 'opaque_config')
4184
+ continue;
4185
+ if (slot.kind === 'body') {
4186
+ return { kind: 'body', node: args[i], from: 'the request call' };
4187
+ }
4188
+ const fromConfig = this.configArgumentBody(args[i], slot.member, 'the request call');
4189
+ if (fromConfig.kind === 'body' || fromConfig.kind === 'body_type')
4190
+ return fromConfig;
4191
+ }
4192
+ return { kind: 'none' };
4193
+ }
4194
+ if (Node.isObjectLiteralExpression(node) || Node.isIdentifier(node)) {
4195
+ const call = node.getParent();
4196
+ if (!call || !Node.isCallExpression(call))
4197
+ return { kind: 'none' };
4198
+ const index = call.getArguments().indexOf(node);
4199
+ if (index < 1)
4200
+ return { kind: 'none' };
4201
+ const slot = this.requestBodySlots(call)[index];
4202
+ if (!slot || slot.kind === 'body')
4203
+ return { kind: 'none' };
4204
+ if (slot.kind === 'opaque_config') {
4205
+ return {
4206
+ kind: 'abstain',
4207
+ why: "the located argument is a request config whose declared type names no body member",
4208
+ };
4209
+ }
4210
+ return this.configArgumentBody(node, slot.member, "the call's request config");
4211
+ }
4212
+ return { kind: 'none' };
4213
+ }
4214
+ /**
4215
+ * The body a config-slot argument supplies through `member`: an object
4216
+ * literal's own member, or a variable's member read off its type.
4217
+ */
4218
+ configArgumentBody(arg, member, from) {
4219
+ const unwrapped = this.unwrapExpressionNode(arg);
4220
+ if (Node.isObjectLiteralExpression(unwrapped)) {
4221
+ const value = this.objectLiteralMemberValue(unwrapped, member);
4222
+ if (value)
4223
+ return { kind: 'body', node: value, from };
4224
+ return { kind: 'no_body', member };
4225
+ }
4226
+ const property = unwrapped.getType().getProperty(member);
4227
+ if (property) {
4228
+ const type = property.getTypeAtLocation(unwrapped);
4229
+ // Top types first: the non-nullable form of `unknown` is `{}`.
4230
+ const bare = type.isAny() || type.isUnknown() ? type : type.getNonNullableType();
4231
+ if (!bare.isAny() && !bare.isUnknown() && !bare.isTypeParameter()) {
4232
+ return { kind: 'body_type', type: bare, at: unwrapped, from };
4233
+ }
4234
+ }
4235
+ return {
4236
+ kind: 'abstain',
4237
+ why: `the located request config's type does not resolve its '${member}' member`,
4238
+ };
4239
+ }
4240
+ /**
4241
+ * Per parameter position of the call's resolved declaration: a body slot, a
4242
+ * config slot (with the member that carries the body), an opaque generic
4243
+ * config, or neither (undefined). Empty when the callee has no declaration.
4244
+ */
4245
+ requestBodySlots(call) {
4246
+ const checker = this.project.getTypeChecker().compilerObject;
4247
+ let declaration;
4248
+ try {
4249
+ declaration = checker.getResolvedSignature(call.compilerNode)?.getDeclaration();
4250
+ }
4251
+ catch {
4252
+ return [];
4253
+ }
4254
+ if (!declaration || !('parameters' in declaration))
4255
+ return [];
4256
+ const ownTypeParams = new Set((declaration.typeParameters ?? []).map((tp) => tp.name.text));
4257
+ return declaration.parameters.map((param) => {
4258
+ if (param.dotDotDotToken)
4259
+ return undefined;
4260
+ const typeNode = param.type;
4261
+ if (!typeNode || !ts.isTypeReferenceNode(typeNode))
4262
+ return undefined;
4263
+ if (ts.isIdentifier(typeNode.typeName) &&
4264
+ !typeNode.typeArguments &&
4265
+ ownTypeParams.has(typeNode.typeName.text)) {
4266
+ return ts.isIdentifier(param.name) && BODY_MEMBER_NAMES.has(param.name.text)
4267
+ ? { kind: 'body' }
4268
+ : undefined;
4269
+ }
4270
+ return this.configSlot(checker, typeNode);
4271
+ });
4272
+ }
4273
+ /**
4274
+ * A generic config type's slot: `config` with the body-named member typed by
4275
+ * the config's own type parameter (`interface Config<D> { data?: D }`), or
4276
+ * `opaque_config` when the config is generic but no body-named member is
4277
+ * (`interface Options<R> { responseType?: R }`). Undefined for a type that
4278
+ * is not a generic config, and for two candidate members (ambiguous).
4279
+ */
4280
+ configSlot(checker, typeNode) {
4281
+ let symbol = checker.getSymbolAtLocation(typeNode.typeName);
4282
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
4283
+ symbol = checker.getAliasedSymbol(symbol);
4284
+ }
4285
+ let generic = false;
4286
+ const found = new Set();
4287
+ for (const decl of symbol?.getDeclarations() ?? []) {
4288
+ if (!ts.isInterfaceDeclaration(decl) && !ts.isTypeAliasDeclaration(decl))
4289
+ continue;
4290
+ const own = new Set((decl.typeParameters ?? []).map((tp) => tp.name.text));
4291
+ if (own.size === 0)
4292
+ continue;
4293
+ const members = ts.isInterfaceDeclaration(decl)
4294
+ ? decl.members
4295
+ : ts.isTypeLiteralNode(decl.type)
4296
+ ? decl.type.members
4297
+ : undefined;
4298
+ if (!members)
4299
+ continue;
4300
+ generic = true;
4301
+ for (const member of members) {
4302
+ if (!ts.isPropertySignature(member) || !member.type || !member.name)
4303
+ continue;
4304
+ if (!ts.isIdentifier(member.name) && !ts.isStringLiteral(member.name))
4305
+ continue;
4306
+ if (!BODY_MEMBER_NAMES.has(member.name.text))
4307
+ continue;
4308
+ const t = member.type;
4309
+ if (ts.isTypeReferenceNode(t) &&
4310
+ ts.isIdentifier(t.typeName) &&
4311
+ !t.typeArguments &&
4312
+ own.has(t.typeName.text)) {
4313
+ found.add(member.name.text);
4314
+ }
4315
+ }
4316
+ }
4317
+ if (found.size === 1)
4318
+ return { kind: 'config', member: [...found][0] };
4319
+ return generic && found.size === 0 ? { kind: 'opaque_config' } : undefined;
4320
+ }
4321
+ /** The value an object literal gives `name`: an assignment's initializer or a shorthand's identifier. */
4322
+ objectLiteralMemberValue(literal, name) {
4323
+ const property = literal.getProperty(name);
4324
+ if (!property)
4325
+ return undefined;
4326
+ if (Node.isPropertyAssignment(property))
4327
+ return property.getInitializer();
4328
+ if (Node.isShorthandPropertyAssignment(property))
4329
+ return property.getNameNode();
4330
+ return undefined;
4331
+ }
3949
4332
  /**
3950
4333
  * An explicit request `InferredType` for a contract the route declares,
3951
4334
  * carrying the provenance the reading recorded.
@@ -283,10 +283,41 @@ export interface ResolveDefinitionsRequest extends BaseRequest {
283
283
  /** Surface type alias names to resolve */
284
284
  aliases: string[];
285
285
  }
286
+ /**
287
+ * One consumer call to retype with the producer's response type (carrick#1491).
288
+ * The locator is the one the consumer's `call_result` inference used: a span,
289
+ * or expression text near a line.
290
+ */
291
+ export interface RetypeItem {
292
+ /** Caller key, echoed on the outcome. */
293
+ item_id: string;
294
+ /** Absolute path, or relative to the init'd root. */
295
+ file_path: string;
296
+ line_number: number;
297
+ span_start?: number;
298
+ span_end?: number;
299
+ expression_text?: string;
300
+ expression_line?: number;
301
+ /** The producer's response type as TypeScript text, fully inlined. */
302
+ producer_type: string;
303
+ /** Judge the form JSON puts on the wire (an `http` response). */
304
+ wire: boolean;
305
+ }
306
+ /**
307
+ * Retype consumer calls with the producer's response type and report what the
308
+ * consumer file's own type-check says about it. Needs `init`: it runs in the
309
+ * consumer's real program.
310
+ */
311
+ export interface RetypeCheckRequest extends BaseRequest {
312
+ action: 'retype_check';
313
+ items: RetypeItem[];
314
+ /** Time the request may spend; items it does not reach abstain. */
315
+ budget_ms?: number;
316
+ }
286
317
  /**
287
318
  * Union type for all possible sidecar requests
288
319
  */
289
- export type SidecarRequest = InitRequest | BundleRequest | EmitSurfaceRequest | CaptureV2Request | CheckV2Request | InferRequest | BuildWorkspaceRequest | CheckCompatibilityRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
320
+ export type SidecarRequest = RetypeCheckRequest | InitRequest | BundleRequest | EmitSurfaceRequest | CaptureV2Request | CheckV2Request | InferRequest | BuildWorkspaceRequest | CheckCompatibilityRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
290
321
  /**
291
322
  * Request for a specific symbol to be bundled
292
323
  */
@@ -467,10 +498,35 @@ export interface ErrorResponse extends BaseResponse {
467
498
  status: 'error';
468
499
  errors: string[];
469
500
  }
501
+ /** A diagnostic the retype ADDED to the consumer file, at its original line. */
502
+ export interface RetypeDiagnostic {
503
+ line: number;
504
+ code: number;
505
+ message: string;
506
+ }
507
+ /**
508
+ * What the consumer file's type-check said once the call stated the
509
+ * producer's response type.
510
+ *
511
+ * - `mismatch`: the rewrite added diagnostics; each is a place the consumer
512
+ * uses something the producer's response does not provide.
513
+ * - `agrees`: it added none.
514
+ * - `abstain`: the check could not be made; `reason` says why.
515
+ */
516
+ export interface RetypeOutcome {
517
+ item_id: string;
518
+ outcome: 'mismatch' | 'agrees' | 'abstain';
519
+ diagnostics: RetypeDiagnostic[];
520
+ reason?: string;
521
+ }
522
+ export interface RetypeCheckResponse extends BaseResponse {
523
+ outcomes?: RetypeOutcome[];
524
+ errors?: string[];
525
+ }
470
526
  /**
471
527
  * Union type for all possible sidecar responses
472
528
  */
473
- export type SidecarResponse = InitResponse | BundleResponse | EmitSurfaceResponse | CaptureV2Response | CheckV2Response | InferResponse | BuildWorkspaceResponse | CheckCompatibilityResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
529
+ export type SidecarResponse = RetypeCheckResponse | InitResponse | BundleResponse | EmitSurfaceResponse | CaptureV2Response | CheckV2Response | InferResponse | BuildWorkspaceResponse | CheckCompatibilityResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
474
530
  /**
475
531
  * An entry in the type manifest
476
532
  */