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 +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/check-classify.d.ts +5 -1
- package/sidecar/dist/src/capture/check-classify.js +20 -1
- package/sidecar/dist/src/retype.js +42 -0
- package/sidecar/dist/src/type-inferrer.d.ts +7 -0
- package/sidecar/dist/src/type-inferrer.js +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
117
|
-
"@carrick-tools/cli-darwin-x64": "0.3.
|
|
118
|
-
"@carrick-tools/cli-linux-arm64": "0.3.
|
|
119
|
-
"@carrick-tools/cli-linux-x64": "0.3.
|
|
120
|
-
"@carrick-tools/cli-win32-x64": "0.3.
|
|
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.
|
|
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) ->
|
|
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) ->
|
|
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
|
*
|