carrick 0.3.129 → 0.3.131

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.129",
3
+ "version": "0.3.131",
4
4
  "carrickCache": {
5
5
  "cache_version": 22,
6
6
  "intent_cache_version": 2
@@ -113,11 +113,11 @@
113
113
  "zod": "^3.23.0"
114
114
  },
115
115
  "optionalDependencies": {
116
- "@carrick-tools/cli-darwin-arm64": "0.3.129",
117
- "@carrick-tools/cli-darwin-x64": "0.3.129",
118
- "@carrick-tools/cli-linux-arm64": "0.3.129",
119
- "@carrick-tools/cli-linux-x64": "0.3.129",
120
- "@carrick-tools/cli-win32-x64": "0.3.129"
116
+ "@carrick-tools/cli-darwin-arm64": "0.3.131",
117
+ "@carrick-tools/cli-darwin-x64": "0.3.131",
118
+ "@carrick-tools/cli-linux-arm64": "0.3.131",
119
+ "@carrick-tools/cli-linux-x64": "0.3.131",
120
+ "@carrick-tools/cli-win32-x64": "0.3.131"
121
121
  },
122
122
  "devDependencies": {
123
123
  "@types/node": "^24.13.3"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "carrick",
3
3
  "description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
4
- "version": "0.3.129",
4
+ "version": "0.3.131",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -10,7 +10,9 @@
10
10
  * surface import error (probe lines 1-2)-> unverifiable
11
11
  * IsAny gate fired (TS2344) -> gate_caught_baked_any
12
12
  * IsUnknown/IsNever gate fired (TS2344) -> unverifiable
13
- * IsVoid gate fired (TS2344) -> unverifiable (no body read)
13
+ * IsVoid gate fired (TS2344) -> compatible with a note when the
14
+ * consumer reads no response body;
15
+ * else unverifiable (no body read)
14
16
  * IsFormBody gate fired (TS2344) -> unverifiable (form-encoded body)
15
17
  * IsByteBody gate fired (TS2344) -> unverifiable (bytes, even agreeing)
16
18
  * raw-text read marked on a side -> unverifiable (text, even agreeing)
@@ -94,3 +96,5 @@ export declare function classifyPair(input: ClassifyInput): CheckVerdict;
94
96
  * (carrick#2011).
95
97
  */
96
98
  export declare const CONSUMER_WIDER_NOTE: string;
99
+ /** The note on a pair whose consumer reads no response body (carrick#1162). */
100
+ export declare const CONSUMER_READS_NO_BODY_NOTE = "the consumer reads no response body (its expected response is void), so the producer response is not compared with a read";
@@ -10,7 +10,9 @@
10
10
  * surface import error (probe lines 1-2)-> unverifiable
11
11
  * IsAny gate fired (TS2344) -> gate_caught_baked_any
12
12
  * IsUnknown/IsNever gate fired (TS2344) -> unverifiable
13
- * IsVoid gate fired (TS2344) -> unverifiable (no body read)
13
+ * IsVoid gate fired (TS2344) -> compatible with a note when the
14
+ * consumer reads no response body;
15
+ * else unverifiable (no body read)
14
16
  * IsFormBody gate fired (TS2344) -> unverifiable (form-encoded body)
15
17
  * IsByteBody gate fired (TS2344) -> unverifiable (bytes, even agreeing)
16
18
  * raw-text read marked on a side -> unverifiable (text, even agreeing)
@@ -167,6 +169,21 @@ export function classifyPair(input) {
167
169
  .find((name) => name.endsWith(':void') || name.endsWith(':form'));
168
170
  if (shapeGate) {
169
171
  const { side, kind } = sideForGate(shapeGate, plan);
172
+ // A call site that reads no response body is compatible with whatever the
173
+ // producer answers: nothing it does depends on the body. Only the
174
+ // response direction (http, graphql), where the consumer is the expected
175
+ // side; a void producer, or a consumer that sends void (a request, a
176
+ // socket or pub/sub payload), still states no contract. The producer's
177
+ // own top-type gates fired above, and `factness` still reads the producer
178
+ // side, so a producer with an `any` inside stays not a fact.
179
+ if (kind === 'void' && side === 'consumer' && plan.direction.expected === 'consumer') {
180
+ return {
181
+ ...base,
182
+ bucket: 'compatible',
183
+ notes: [...base.notes, CONSUMER_READS_NO_BODY_NOTE],
184
+ ...factness(input),
185
+ };
186
+ }
170
187
  if (kind === 'void') {
171
188
  return {
172
189
  ...base,
@@ -357,6 +374,8 @@ export function classifyPair(input) {
357
374
  */
358
375
  export const CONSUMER_WIDER_NOTE = "the consumer's published request type is wider than what the call sends: " +
359
376
  'it widened a literal the call writes, and the values sent fit the producer';
377
+ /** The note on a pair whose consumer reads no response body (carrick#1162). */
378
+ export const CONSUMER_READS_NO_BODY_NOTE = 'the consumer reads no response body (its expected response is void), so the producer response is not compared with a read';
360
379
  function onUnwidenedLines(plan, diagnostic) {
361
380
  return plan.unwidenedFromLine !== undefined && diagnostic.line >= plan.unwidenedFromLine;
362
381
  }
@@ -195,6 +195,12 @@ export class Retyper {
195
195
  };
196
196
  }
197
197
  if (declaresTypeParameters(call)) {
198
+ const inferred = inferredTypeParameterOf(call);
199
+ if (inferred) {
200
+ return (`the callee declares several type parameters and '${inferred}' is ` +
201
+ `inferred from the arguments, so writing one type argument would turn ` +
202
+ `its inference off`);
203
+ }
198
204
  const open = call.getFirstChildByKind(SyntaxKind.OpenParenToken);
199
205
  if (!open)
200
206
  return 'the consumer call has no argument list to retype';
@@ -948,6 +954,42 @@ function bodyReadEdit(read) {
948
954
  { start: end, end, text: ` as Promise<${stated}>)`, statedAt: ' as Promise<'.length },
949
955
  ];
950
956
  }
957
+ /**
958
+ * The name of a type parameter after the first that the call's arguments
959
+ * infer and whose default would change the call if inference were off, or
960
+ * `undefined`.
961
+ *
962
+ * Writing one type argument makes every other parameter take its default
963
+ * instead of its inferred type. A parameter the argument list mentions
964
+ * (`TVariables` in `options: Options<TData, TError, TVariables>`) and whose
965
+ * default is not `any` or `unknown` then types the argument against the
966
+ * default (`void`), and the call reports a diagnostic about the written
967
+ * argument, not about anything the consumer reads. A parameter nothing in the
968
+ * argument list mentions, or one defaulting to `any`, is unaffected.
969
+ */
970
+ function inferredTypeParameterOf(call) {
971
+ const declaration = resolvedDeclaration(call);
972
+ const typeParameters = declaration?.typeParameters;
973
+ if (!declaration || !typeParameters || typeParameters.length < 2)
974
+ return undefined;
975
+ for (const parameter of typeParameters.slice(1)) {
976
+ const fallback = parameter.default;
977
+ if (fallback &&
978
+ (fallback.kind === ts.SyntaxKind.AnyKeyword || fallback.kind === ts.SyntaxKind.UnknownKeyword)) {
979
+ continue;
980
+ }
981
+ const name = parameter.name.text;
982
+ if (declaration.parameters.some((p) => p.type && mentionsTypeName(p.type, name)))
983
+ return name;
984
+ }
985
+ return undefined;
986
+ }
987
+ function mentionsTypeName(node, name) {
988
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === name) {
989
+ return true;
990
+ }
991
+ return ts.forEachChild(node, (child) => (mentionsTypeName(child, name) ? true : undefined)) ?? false;
992
+ }
951
993
  function declaresTypeParameters(call) {
952
994
  return (resolvedDeclaration(call)?.typeParameters?.length ?? 0) > 0;
953
995
  }
@@ -495,6 +495,15 @@ export declare class TypeInferrer {
495
495
  private inferExpression;
496
496
  private inferRequestBody;
497
497
  private resolveCallResultTerminalNode;
498
+ /**
499
+ * carrick#2756: what a `catch` clause does with the response describes a
500
+ * body that failed to read (`catch (e) { log(await res.text()); throw e }`),
501
+ * not the payload, yet it comes after the success read and would win as the
502
+ * last whole read. Where the source uses the tracked value anywhere outside
503
+ * a `catch` clause that does not hold the call, the clause's uses are left
504
+ * out of the walk. Where every use is in such a clause the walk is as it was.
505
+ */
506
+ private catchReadsDeferred;
498
507
  /**
499
508
  * Where the walk ends on the call's own destructuring binding (`const {
500
509
  * tasks } = await api.list()`), the declaration has no type of its own: the
@@ -1234,6 +1243,17 @@ export declare class TypeInferrer {
1234
1243
  * contract, and neither the envelope nor its `string` body is any of it.
1235
1244
  */
1236
1245
  private inferSerialisedBodyEnvelope;
1246
+ /**
1247
+ * carrick#2674: the locator names a request call's options object literal
1248
+ * and the member that carries the call's body. The call sends no request
1249
+ * body when the literal can write no such member: no spread, no computed
1250
+ * key, and no member of that name in any form (assignment, shorthand,
1251
+ * method, accessor, quoted key), nor any other body-named member
1252
+ * (`BODY_MEMBER_NAMES`, `REQUEST_BODY_PARTS`). Anything else is not decided here, so the
1253
+ * answer is null. `configArgumentBody` stays the reader of the member
1254
+ * itself; the checks above it are this kind's own.
1255
+ */
1256
+ private inferRequestConfig;
1237
1257
  /** An `unknown` answer the inferrer decided, carrying its reason at the root. */
1238
1258
  private decidedAbstain;
1239
1259
  /**
@@ -2134,6 +2154,21 @@ export declare class TypeInferrer {
2134
2154
  * literal's own member, or a variable's member read off its type.
2135
2155
  */
2136
2156
  private configArgumentBody;
2157
+ /** A property whose name is computed, so the literal's written keys do not name it. */
2158
+ private isComputedMember;
2159
+ /**
2160
+ * A config literal that spreads another object, or writes a computed key,
2161
+ * names no `body` itself, so the literal's own type is read: the call sends
2162
+ * no body only when every branch of that type declares no `body` member, or
2163
+ * one typed `undefined` or `never`. The literal and each spread are first
2164
+ * read for a type that rules nothing out: one not known (a spread of an
2165
+ * untyped object), a generic one (a type parameter, or an intersection with
2166
+ * one), or one with a string index signature (which the compiler drops
2167
+ * from a spread's result). Any of those abstains.
2168
+ */
2169
+ private spreadLiteralBody;
2170
+ /** Why a spread-side type rules out no member, or undefined when it can be read. */
2171
+ private spreadTypeUnread;
2137
2172
  /**
2138
2173
  * Per parameter position of the call's resolved declaration: a body slot, a
2139
2174
  * config slot (with the member that carries the body), an opaque generic
@@ -2150,6 +2185,12 @@ export declare class TypeInferrer {
2150
2185
  private configSlot;
2151
2186
  /** The value an object literal gives `name`: an assignment's initializer or a shorthand's identifier. */
2152
2187
  private objectLiteralMemberValue;
2188
+ /**
2189
+ * The last member an object literal writes under `name`, in any form
2190
+ * (assignment, shorthand, method, accessor), with a quoted or numeric key
2191
+ * read by its text. Spreads and computed keys name nothing here.
2192
+ */
2193
+ private literalMemberNamed;
2153
2194
  /**
2154
2195
  * An explicit request `InferredType` for a contract the route declares,
2155
2196
  * carrying the provenance the reading recorded.
@@ -2489,6 +2530,13 @@ export declare class TypeInferrer {
2489
2530
  * then substring match (preferring containing matches).
2490
2531
  */
2491
2532
  private matchByText;
2533
+ /**
2534
+ * The text a locator opens with before an elision, or `undefined` when the
2535
+ * locator has none. An elision is `...` followed only by closing brackets
2536
+ * (`{ a: x, ... }`); `...rest` is a spread and source text. An opening too
2537
+ * short to tell nodes apart is judged by the caller.
2538
+ */
2539
+ private elidedLocatorOpening;
2492
2540
  /**
2493
2541
  * What a locator's text names, read off its own parse (carrick#2056):
2494
2542
  *
@@ -890,6 +890,8 @@ export class TypeInferrer {
890
890
  return this.inferFunctionParam(sourceFile, request);
891
891
  case 'receiver_type':
892
892
  return this.inferReceiverType(sourceFile, request);
893
+ case 'request_config':
894
+ return this.inferRequestConfig(sourceFile, request);
893
895
  default:
894
896
  this.logError(`Unknown infer kind: ${request.infer_kind}`);
895
897
  return null;
@@ -2970,7 +2972,7 @@ export class TypeInferrer {
2970
2972
  // `response.json()` is one in place.
2971
2973
  const callValueIsTransport = this.typeIsOrContainsResponseMachinery(callValue);
2972
2974
  const startPos = callExpr.getStart();
2973
- const candidates = this.collectDefUseNodes(func);
2975
+ const candidates = this.catchReadsDeferred(this.collectDefUseNodes(func), func, startPos, currentNames);
2974
2976
  for (const expr of candidates) {
2975
2977
  if (expr.getStart() <= startPos)
2976
2978
  continue;
@@ -3080,6 +3082,26 @@ export class TypeInferrer {
3080
3082
  }
3081
3083
  return { terminal: callExpr, projectionOnly: false, projections: [] };
3082
3084
  }
3085
+ /**
3086
+ * carrick#2756: what a `catch` clause does with the response describes a
3087
+ * body that failed to read (`catch (e) { log(await res.text()); throw e }`),
3088
+ * not the payload, yet it comes after the success read and would win as the
3089
+ * last whole read. Where the source uses the tracked value anywhere outside
3090
+ * a `catch` clause that does not hold the call, the clause's uses are left
3091
+ * out of the walk. Where every use is in such a clause the walk is as it was.
3092
+ */
3093
+ catchReadsDeferred(candidates, func, startPos, names) {
3094
+ const inLaterCatch = (node) => node
3095
+ .getAncestors()
3096
+ .some((a) => Node.isCatchClause(a) &&
3097
+ a.getStart() > startPos &&
3098
+ a.getAncestors().includes(func));
3099
+ const usesOutside = candidates.some((c) => c.getStart() > startPos &&
3100
+ Node.isIdentifier(c) &&
3101
+ this.isIdentifierUsage(c, names) &&
3102
+ !inLaterCatch(c));
3103
+ return usesOutside ? candidates.filter((c) => !inLaterCatch(c)) : candidates;
3104
+ }
3083
3105
  /**
3084
3106
  * Where the walk ends on the call's own destructuring binding (`const {
3085
3107
  * tasks } = await api.list()`), the declaration has no type of its own: the
@@ -5383,6 +5405,59 @@ export class TypeInferrer {
5383
5405
  }
5384
5406
  return this.inferredFromRecoveredPayload(request, recovered);
5385
5407
  }
5408
+ /**
5409
+ * carrick#2674: the locator names a request call's options object literal
5410
+ * and the member that carries the call's body. The call sends no request
5411
+ * body when the literal can write no such member: no spread, no computed
5412
+ * key, and no member of that name in any form (assignment, shorthand,
5413
+ * method, accessor, quoted key), nor any other body-named member
5414
+ * (`BODY_MEMBER_NAMES`, `REQUEST_BODY_PARTS`). Anything else is not decided here, so the
5415
+ * answer is null. `configArgumentBody` stays the reader of the member
5416
+ * itself; the checks above it are this kind's own.
5417
+ */
5418
+ inferRequestConfig(sourceFile, request) {
5419
+ const member = request.member;
5420
+ if (!member)
5421
+ return null;
5422
+ const located = this.resolveTargetNode(sourceFile, request);
5423
+ if (!located)
5424
+ return null;
5425
+ const literal = this.unwrapExpressionNode(located);
5426
+ if (!Node.isObjectLiteralExpression(literal))
5427
+ return null;
5428
+ for (const property of literal.getProperties()) {
5429
+ // A spread has no name node, so it falls out here with anything else
5430
+ // unnamed: the literal may write the member through it.
5431
+ const nameNode = Node.isShorthandPropertyAssignment(property)
5432
+ ? property.getNameNode()
5433
+ : property.getNameNode?.();
5434
+ if (!nameNode)
5435
+ return null;
5436
+ if (Node.isComputedPropertyName(nameNode))
5437
+ return null;
5438
+ const name = Node.isStringLiteral(nameNode) || Node.isNoSubstitutionTemplateLiteral(nameNode)
5439
+ ? nameNode.getLiteralText()
5440
+ : nameNode.getText();
5441
+ // A client may take its body through a member other than the one the
5442
+ // locator names (`json`, `form`, `data`), so any body-named key refuses,
5443
+ // the same rule `requestObjectBody` keeps.
5444
+ if (name === member || BODY_MEMBER_NAMES.has(name) || REQUEST_BODY_PARTS.has(name)) {
5445
+ return null;
5446
+ }
5447
+ }
5448
+ const read = this.configArgumentBody(literal, member, "the call's request config");
5449
+ if (read.kind !== 'no_body')
5450
+ return null;
5451
+ this.log(`Request config at ${request.file_path}:${request.line_number} writes no '${member}' member; ` +
5452
+ 'the call sends no request body');
5453
+ return this.decidedAbstain(request, this.getNodeLocation(literal), {
5454
+ path: '',
5455
+ kind: 'unknown',
5456
+ reason: 'no_request_body',
5457
+ detail: `the call's request config sets no '${read.member}' member, which is where the call ` +
5458
+ 'takes its body, so the call sends no request body',
5459
+ });
5460
+ }
5386
5461
  /** An `unknown` answer the inferrer decided, carrying its reason at the root. */
5387
5462
  decidedAbstain(request, location, reason) {
5388
5463
  const abstain = this.createInferredType(request, 'unknown', false, location);
@@ -7849,6 +7924,17 @@ export class TypeInferrer {
7849
7924
  const value = this.objectLiteralMemberValue(unwrapped, member);
7850
7925
  if (value)
7851
7926
  return { kind: 'body', node: value, from };
7927
+ if (this.literalMemberNamed(unwrapped, member)) {
7928
+ return {
7929
+ kind: 'abstain',
7930
+ why: `the request config writes its '${member}' member as a method or accessor, so its value is unread`,
7931
+ };
7932
+ }
7933
+ if (unwrapped
7934
+ .getProperties()
7935
+ .some((p) => Node.isSpreadAssignment(p) || this.isComputedMember(p))) {
7936
+ return this.spreadLiteralBody(unwrapped, member);
7937
+ }
7852
7938
  return { kind: 'no_body', member };
7853
7939
  }
7854
7940
  const property = unwrapped.getType().getProperty(member);
@@ -7865,6 +7951,73 @@ export class TypeInferrer {
7865
7951
  why: `the located request config's type does not resolve its '${member}' member`,
7866
7952
  };
7867
7953
  }
7954
+ /** A property whose name is computed, so the literal's written keys do not name it. */
7955
+ isComputedMember(property) {
7956
+ return ((Node.isPropertyAssignment(property) ||
7957
+ Node.isShorthandPropertyAssignment(property) ||
7958
+ Node.isMethodDeclaration(property)) &&
7959
+ Node.isComputedPropertyName(property.getNameNode()));
7960
+ }
7961
+ /**
7962
+ * A config literal that spreads another object, or writes a computed key,
7963
+ * names no `body` itself, so the literal's own type is read: the call sends
7964
+ * no body only when every branch of that type declares no `body` member, or
7965
+ * one typed `undefined` or `never`. The literal and each spread are first
7966
+ * read for a type that rules nothing out: one not known (a spread of an
7967
+ * untyped object), a generic one (a type parameter, or an intersection with
7968
+ * one), or one with a string index signature (which the compiler drops
7969
+ * from a spread's result). Any of those abstains.
7970
+ */
7971
+ spreadLiteralBody(literal, member) {
7972
+ const literalType = literal.getType();
7973
+ const read = [
7974
+ literalType,
7975
+ ...literal
7976
+ .getProperties()
7977
+ .filter(Node.isSpreadAssignment)
7978
+ .map((spread) => spread.getExpression().getType()),
7979
+ ];
7980
+ for (const type of read) {
7981
+ const unread = this.spreadTypeUnread(type);
7982
+ if (unread) {
7983
+ return {
7984
+ kind: 'abstain',
7985
+ why: `the request config spreads an object whose type ${unread}, so its '${member}' member is unread`,
7986
+ };
7987
+ }
7988
+ }
7989
+ const branches = literalType.isUnion() ? literalType.getUnionTypes() : [literalType];
7990
+ for (const branch of branches) {
7991
+ const property = branch.getProperty(member);
7992
+ if (!property)
7993
+ continue;
7994
+ const type = property.getTypeAtLocation(literal);
7995
+ const bare = type.isAny() || type.isUnknown() ? type : type.getNonNullableType();
7996
+ if (bare.isNever() || bare.isUndefined() || bare.isVoid())
7997
+ continue;
7998
+ return {
7999
+ kind: 'abstain',
8000
+ why: `the request config spreads an object whose '${member}' member may carry a body`,
8001
+ };
8002
+ }
8003
+ return { kind: 'no_body', member };
8004
+ }
8005
+ /** Why a spread-side type rules out no member, or undefined when it can be read. */
8006
+ spreadTypeUnread(type) {
8007
+ const branches = type.isUnion() ? type.getUnionTypes() : [type];
8008
+ for (const branch of branches) {
8009
+ const parts = branch.isIntersection() ? branch.getIntersectionTypes() : [branch];
8010
+ for (const part of parts) {
8011
+ if (part.isAny() || part.isUnknown())
8012
+ return 'is not known';
8013
+ if ((part.compilerType.flags & ts.TypeFlags.Instantiable) !== 0)
8014
+ return 'is generic';
8015
+ }
8016
+ if (branch.getStringIndexType())
8017
+ return 'has an index signature';
8018
+ }
8019
+ return undefined;
8020
+ }
7868
8021
  /**
7869
8022
  * Per parameter position of the call's resolved declaration: a body slot, a
7870
8023
  * config slot (with the member that carries the body), an opaque generic
@@ -7948,7 +8101,7 @@ export class TypeInferrer {
7948
8101
  }
7949
8102
  /** The value an object literal gives `name`: an assignment's initializer or a shorthand's identifier. */
7950
8103
  objectLiteralMemberValue(literal, name) {
7951
- const property = literal.getProperty(name);
8104
+ const property = this.literalMemberNamed(literal, name);
7952
8105
  if (!property)
7953
8106
  return undefined;
7954
8107
  if (Node.isPropertyAssignment(property))
@@ -7957,6 +8110,29 @@ export class TypeInferrer {
7957
8110
  return property.getNameNode();
7958
8111
  return undefined;
7959
8112
  }
8113
+ /**
8114
+ * The last member an object literal writes under `name`, in any form
8115
+ * (assignment, shorthand, method, accessor), with a quoted or numeric key
8116
+ * read by its text. Spreads and computed keys name nothing here.
8117
+ */
8118
+ literalMemberNamed(literal, name) {
8119
+ let found;
8120
+ for (const property of literal.getProperties()) {
8121
+ if (Node.isSpreadAssignment(property))
8122
+ continue;
8123
+ const nameNode = property.getNameNode();
8124
+ if (Node.isComputedPropertyName(nameNode))
8125
+ continue;
8126
+ const text = Node.isStringLiteral(nameNode) ||
8127
+ Node.isNoSubstitutionTemplateLiteral(nameNode) ||
8128
+ Node.isNumericLiteral(nameNode)
8129
+ ? nameNode.getLiteralText()
8130
+ : nameNode.getText();
8131
+ if (text === name)
8132
+ found = property;
8133
+ }
8134
+ return found;
8135
+ }
7960
8136
  /**
7961
8137
  * An explicit request `InferredType` for a contract the route declares,
7962
8138
  * carrying the provenance the reading recorded.
@@ -9133,6 +9309,26 @@ export class TypeInferrer {
9133
9309
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(normalizedTarget)) {
9134
9310
  return undefined;
9135
9311
  }
9312
+ // A locator that elides members (`body: { a: x, ... }`) stands for the
9313
+ // whole node it opens: the text before the elision is where the source
9314
+ // opens. Never a node the text merely contains, which is one member of the
9315
+ // payload it names.
9316
+ // An opening shorter than this (`{`) names no node in particular.
9317
+ const MIN_ELIDED_OPENING_LEN = 8;
9318
+ const elidedOpening = this.elidedLocatorOpening(normalizedTarget);
9319
+ if (elidedOpening !== undefined) {
9320
+ if (elidedOpening.length < MIN_ELIDED_OPENING_LEN)
9321
+ return undefined;
9322
+ const opened = candidates.filter((c) => c.text.startsWith(elidedOpening));
9323
+ if (opened.length === 0)
9324
+ return undefined;
9325
+ if (lineNumber !== undefined) {
9326
+ const distance = (c) => Math.abs(startLineOf(c.node) - lineNumber);
9327
+ const nearest = Math.min(...opened.map(distance));
9328
+ return this.pickBestMatch(opened.filter((c) => distance(c) === nearest).map((c) => c.node), lineNumber);
9329
+ }
9330
+ return this.pickBestMatch(opened.map((c) => c.node), lineNumber);
9331
+ }
9136
9332
  // Fall back to substring match
9137
9333
  // For the reverse direction (target contains node text), require a minimum node text
9138
9334
  // length to avoid matching tiny identifiers like "res" or "body" too broadly.
@@ -9199,6 +9395,18 @@ export class TypeInferrer {
9199
9395
  }
9200
9396
  return bound.size > 0 ? this.pickBestMatch([...bound], lineNumber) : undefined;
9201
9397
  }
9398
+ /**
9399
+ * The text a locator opens with before an elision, or `undefined` when the
9400
+ * locator has none. An elision is `...` followed only by closing brackets
9401
+ * (`{ a: x, ... }`); `...rest` is a spread and source text. An opening too
9402
+ * short to tell nodes apart is judged by the caller.
9403
+ */
9404
+ elidedLocatorOpening(normalizedTarget) {
9405
+ const match = /^(.*?)[\s,]*\.\.\.[\s,)}\]]*$/s.exec(normalizedTarget);
9406
+ if (!match)
9407
+ return undefined;
9408
+ return match[1];
9409
+ }
9202
9410
  /**
9203
9411
  * What a locator's text names, read off its own parse (carrick#2056):
9204
9412
  *
@@ -9567,6 +9775,8 @@ export class TypeInferrer {
9567
9775
  return 'SigReturn';
9568
9776
  case 'function_param':
9569
9777
  return 'Param';
9778
+ case 'request_config':
9779
+ return 'RequestConfig';
9570
9780
  default:
9571
9781
  return 'Type';
9572
9782
  }
@@ -6,7 +6,7 @@ import type { ResponseModes } from './response-modes.js';
6
6
  /**
7
7
  * The kind of type inference to perform
8
8
  */
9
- export type InferKind = 'function_return' | 'expression' | 'call_result' | 'variable' | 'response_body' | 'request_body' | 'signature_return' | 'function_param' | 'receiver_type';
9
+ export type InferKind = 'function_return' | 'expression' | 'call_result' | 'variable' | 'response_body' | 'request_body' | 'signature_return' | 'function_param' | 'receiver_type' | 'request_config';
10
10
  /**
11
11
  * A rule for unwrapping machinery/wrapper types to extract payload types.
12
12
  *
@@ -711,6 +711,8 @@ export interface InferRequestItem {
711
711
  alias?: string;
712
712
  /** Target parameter name for `function_param` inference. */
713
713
  param_name?: string;
714
+ /** The member of a request options literal that carries the body (`request_config`). */
715
+ member?: string;
714
716
  }
715
717
  /**
716
718
  * Response status
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { z } from 'zod';
6
6
  import type { SidecarRequest, InferRequestItem, LibraryCheck } from './types.js';
7
- export declare const InferKindSchema: z.ZodEnum<["function_return", "expression", "call_result", "variable", "response_body", "request_body", "signature_return", "function_param", "receiver_type"]>;
7
+ export declare const InferKindSchema: z.ZodEnum<["function_return", "expression", "call_result", "variable", "response_body", "request_body", "signature_return", "function_param", "receiver_type", "request_config"]>;
8
8
  export declare const SymbolRequestSchema: z.ZodObject<{
9
9
  symbol_name: z.ZodString;
10
10
  source_file: z.ZodString;
@@ -31,13 +31,15 @@ export declare const InferRequestItemSchema: z.ZodObject<{
31
31
  span_end: z.ZodOptional<z.ZodNumber>;
32
32
  expression_text: z.ZodOptional<z.ZodString>;
33
33
  expression_line: z.ZodOptional<z.ZodNumber>;
34
- infer_kind: z.ZodEnum<["function_return", "expression", "call_result", "variable", "response_body", "request_body", "signature_return", "function_param", "receiver_type"]>;
34
+ infer_kind: z.ZodEnum<["function_return", "expression", "call_result", "variable", "response_body", "request_body", "signature_return", "function_param", "receiver_type", "request_config"]>;
35
35
  alias: z.ZodOptional<z.ZodString>;
36
36
  param_name: z.ZodOptional<z.ZodString>;
37
+ member: z.ZodOptional<z.ZodString>;
37
38
  }, "strip", z.ZodTypeAny, {
38
39
  file_path: string;
39
40
  line_number: number;
40
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
41
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
42
+ member?: string | undefined;
41
43
  param_name?: string | undefined;
42
44
  alias?: string | undefined;
43
45
  span_start?: number | undefined;
@@ -47,7 +49,8 @@ export declare const InferRequestItemSchema: z.ZodObject<{
47
49
  }, {
48
50
  file_path: string;
49
51
  line_number: number;
50
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
52
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
53
+ member?: string | undefined;
51
54
  param_name?: string | undefined;
52
55
  alias?: string | undefined;
53
56
  span_start?: number | undefined;
@@ -633,13 +636,15 @@ export declare const InferRequestSchema: z.ZodObject<{
633
636
  span_end: z.ZodOptional<z.ZodNumber>;
634
637
  expression_text: z.ZodOptional<z.ZodString>;
635
638
  expression_line: z.ZodOptional<z.ZodNumber>;
636
- infer_kind: z.ZodEnum<["function_return", "expression", "call_result", "variable", "response_body", "request_body", "signature_return", "function_param", "receiver_type"]>;
639
+ infer_kind: z.ZodEnum<["function_return", "expression", "call_result", "variable", "response_body", "request_body", "signature_return", "function_param", "receiver_type", "request_config"]>;
637
640
  alias: z.ZodOptional<z.ZodString>;
638
641
  param_name: z.ZodOptional<z.ZodString>;
642
+ member: z.ZodOptional<z.ZodString>;
639
643
  }, "strip", z.ZodTypeAny, {
640
644
  file_path: string;
641
645
  line_number: number;
642
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
646
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
647
+ member?: string | undefined;
643
648
  param_name?: string | undefined;
644
649
  alias?: string | undefined;
645
650
  span_start?: number | undefined;
@@ -649,7 +654,8 @@ export declare const InferRequestSchema: z.ZodObject<{
649
654
  }, {
650
655
  file_path: string;
651
656
  line_number: number;
652
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
657
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
658
+ member?: string | undefined;
653
659
  param_name?: string | undefined;
654
660
  alias?: string | undefined;
655
661
  span_start?: number | undefined;
@@ -739,7 +745,8 @@ export declare const InferRequestSchema: z.ZodObject<{
739
745
  requests: {
740
746
  file_path: string;
741
747
  line_number: number;
742
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
748
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
749
+ member?: string | undefined;
743
750
  param_name?: string | undefined;
744
751
  alias?: string | undefined;
745
752
  span_start?: number | undefined;
@@ -775,7 +782,8 @@ export declare const InferRequestSchema: z.ZodObject<{
775
782
  requests: {
776
783
  file_path: string;
777
784
  line_number: number;
778
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
785
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
786
+ member?: string | undefined;
779
787
  param_name?: string | undefined;
780
788
  alias?: string | undefined;
781
789
  span_start?: number | undefined;
@@ -2984,13 +2992,15 @@ export declare const SidecarRequestSchema: z.ZodDiscriminatedUnion<"action", [z.
2984
2992
  span_end: z.ZodOptional<z.ZodNumber>;
2985
2993
  expression_text: z.ZodOptional<z.ZodString>;
2986
2994
  expression_line: z.ZodOptional<z.ZodNumber>;
2987
- infer_kind: z.ZodEnum<["function_return", "expression", "call_result", "variable", "response_body", "request_body", "signature_return", "function_param", "receiver_type"]>;
2995
+ infer_kind: z.ZodEnum<["function_return", "expression", "call_result", "variable", "response_body", "request_body", "signature_return", "function_param", "receiver_type", "request_config"]>;
2988
2996
  alias: z.ZodOptional<z.ZodString>;
2989
2997
  param_name: z.ZodOptional<z.ZodString>;
2998
+ member: z.ZodOptional<z.ZodString>;
2990
2999
  }, "strip", z.ZodTypeAny, {
2991
3000
  file_path: string;
2992
3001
  line_number: number;
2993
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
3002
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
3003
+ member?: string | undefined;
2994
3004
  param_name?: string | undefined;
2995
3005
  alias?: string | undefined;
2996
3006
  span_start?: number | undefined;
@@ -3000,7 +3010,8 @@ export declare const SidecarRequestSchema: z.ZodDiscriminatedUnion<"action", [z.
3000
3010
  }, {
3001
3011
  file_path: string;
3002
3012
  line_number: number;
3003
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
3013
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
3014
+ member?: string | undefined;
3004
3015
  param_name?: string | undefined;
3005
3016
  alias?: string | undefined;
3006
3017
  span_start?: number | undefined;
@@ -3090,7 +3101,8 @@ export declare const SidecarRequestSchema: z.ZodDiscriminatedUnion<"action", [z.
3090
3101
  requests: {
3091
3102
  file_path: string;
3092
3103
  line_number: number;
3093
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
3104
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
3105
+ member?: string | undefined;
3094
3106
  param_name?: string | undefined;
3095
3107
  alias?: string | undefined;
3096
3108
  span_start?: number | undefined;
@@ -3126,7 +3138,8 @@ export declare const SidecarRequestSchema: z.ZodDiscriminatedUnion<"action", [z.
3126
3138
  requests: {
3127
3139
  file_path: string;
3128
3140
  line_number: number;
3129
- infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type";
3141
+ infer_kind: "function_return" | "expression" | "call_result" | "variable" | "response_body" | "request_body" | "signature_return" | "function_param" | "receiver_type" | "request_config";
3142
+ member?: string | undefined;
3130
3143
  param_name?: string | undefined;
3131
3144
  alias?: string | undefined;
3132
3145
  span_start?: number | undefined;
@@ -16,6 +16,7 @@ export const InferKindSchema = z.enum([
16
16
  'signature_return',
17
17
  'function_param',
18
18
  'receiver_type',
19
+ 'request_config',
19
20
  ]);
20
21
  // ============================================================================
21
22
  // Extraction Config Schemas (New)
@@ -92,6 +93,8 @@ export const InferRequestItemSchema = z.object({
92
93
  infer_kind: InferKindSchema,
93
94
  alias: z.string().optional(),
94
95
  param_name: z.string().optional(),
96
+ // The body-carrying member a `request_config` item asks about.
97
+ member: z.string().optional(),
95
98
  });
96
99
  /**
97
100
  * Per-item locator validation for infer requests.