carrick 0.3.130 → 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.130",
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.130",
117
- "@carrick-tools/cli-darwin-x64": "0.3.130",
118
- "@carrick-tools/cli-linux-arm64": "0.3.130",
119
- "@carrick-tools/cli-linux-x64": "0.3.130",
120
- "@carrick-tools/cli-win32-x64": "0.3.130"
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.130",
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
  }
@@ -2530,6 +2530,13 @@ export declare class TypeInferrer {
2530
2530
  * then substring match (preferring containing matches).
2531
2531
  */
2532
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;
2533
2540
  /**
2534
2541
  * What a locator's text names, read off its own parse (carrick#2056):
2535
2542
  *
@@ -9309,6 +9309,26 @@ export class TypeInferrer {
9309
9309
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(normalizedTarget)) {
9310
9310
  return undefined;
9311
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
+ }
9312
9332
  // Fall back to substring match
9313
9333
  // For the reverse direction (target contains node text), require a minimum node text
9314
9334
  // length to avoid matching tiny identifiers like "res" or "body" too broadly.
@@ -9375,6 +9395,18 @@ export class TypeInferrer {
9375
9395
  }
9376
9396
  return bound.size > 0 ? this.pickBestMatch([...bound], lineNumber) : undefined;
9377
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
+ }
9378
9410
  /**
9379
9411
  * What a locator's text names, read off its own parse (carrick#2056):
9380
9412
  *