carrick 0.3.127 → 0.3.129

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.127",
3
+ "version": "0.3.129",
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.127",
117
- "@carrick-tools/cli-darwin-x64": "0.3.127",
118
- "@carrick-tools/cli-linux-arm64": "0.3.127",
119
- "@carrick-tools/cli-linux-x64": "0.3.127",
120
- "@carrick-tools/cli-win32-x64": "0.3.127"
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"
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.127",
4
+ "version": "0.3.129",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -1093,9 +1093,13 @@ function paramLocatorHints(request) {
1093
1093
  * node unchanged. The mirror of the v1 inferrer's `unwrapJsonStringifyArg`:
1094
1094
  * the global serialiser is identified by the standard `JSON` object, the one
1095
1095
  * name every runtime shares.
1096
+ *
1097
+ * carrick#2010: a property whose value is the serialiser call (`body:
1098
+ * JSON.stringify(...)`, the node the line fallback prefers on that line) is
1099
+ * read through to the call, and so to its argument.
1096
1100
  */
1097
1101
  function serialisedArgument(node) {
1098
- let current = node;
1102
+ let current = ts.isPropertyAssignment(node) ? node.initializer : node;
1099
1103
  while (ts.isParenthesizedExpression(current))
1100
1104
  current = current.expression;
1101
1105
  if (ts.isCallExpression(current) &&
@@ -17,7 +17,9 @@
17
17
  * assignment error, sent union taken in part by a weak expected type
18
18
  * -> unverifiable (carrick#1995)
19
19
  * assignment error on the published type that the sent side's unwidened
20
- * reading does not repeat -> producer_wider (carrick#1538)
20
+ * reading does not repeat -> producer_wider (carrick#1538),
21
+ * or compatible with a note when
22
+ * the consumer sends (carrick#2011)
21
23
  * assignment-class error -> incompatible
22
24
  * no diagnostics -> compatible [lowest precedence]
23
25
  *
@@ -87,3 +89,8 @@ export interface ClassifyInput {
87
89
  }
88
90
  /** Classify one pair into exactly one bucket, honouring the precedence order. */
89
91
  export declare function classifyPair(input: ClassifyInput): CheckVerdict;
92
+ /**
93
+ * The note on a request the consumer's unwidened reading cleared
94
+ * (carrick#2011).
95
+ */
96
+ export declare const CONSUMER_WIDER_NOTE: string;
@@ -17,7 +17,9 @@
17
17
  * assignment error, sent union taken in part by a weak expected type
18
18
  * -> unverifiable (carrick#1995)
19
19
  * assignment error on the published type that the sent side's unwidened
20
- * reading does not repeat -> producer_wider (carrick#1538)
20
+ * reading does not repeat -> producer_wider (carrick#1538),
21
+ * or compatible with a note when
22
+ * the consumer sends (carrick#2011)
21
23
  * assignment-class error -> incompatible
22
24
  * no diagnostics -> compatible [lowest precedence]
23
25
  *
@@ -277,7 +279,19 @@ export function classifyPair(input) {
277
279
  // return read with no literal widened fits what the other side expects,
278
280
  // so what travels agrees and only the published type is wider. Its own
279
281
  // bucket, which carries no diagnostic: a reason is read as a mismatch.
282
+ // carrick#2011: the same for a request body, where the consumer sends
283
+ // and its body was read with the literals the call writes kept. A
284
+ // consumer has no class of its own on the wire, so the pair is the
285
+ // compatible it is, and a note says the published type is wider.
280
286
  if (assignDiag && unwidenedReadingFits(input)) {
287
+ if (plan.direction.sent === 'consumer') {
288
+ return {
289
+ ...base,
290
+ bucket: 'compatible',
291
+ notes: [...base.notes, CONSUMER_WIDER_NOTE],
292
+ ...factness(input),
293
+ };
294
+ }
281
295
  return { ...base, bucket: 'producer_wider', ...factness(input) };
282
296
  }
283
297
  if (assignDiag) {
@@ -337,6 +351,12 @@ export function classifyPair(input) {
337
351
  // 6. No diagnostics -> compatible.
338
352
  return { ...base, bucket: 'compatible', ...factness(input) };
339
353
  }
354
+ /**
355
+ * The note on a request the consumer's unwidened reading cleared
356
+ * (carrick#2011).
357
+ */
358
+ export const CONSUMER_WIDER_NOTE = "the consumer's published request type is wider than what the call sends: " +
359
+ 'it widened a literal the call writes, and the values sent fit the producer';
340
360
  function onUnwidenedLines(plan, diagnostic) {
341
361
  return plan.unwidenedFromLine !== undefined && diagnostic.line >= plan.unwidenedFromLine;
342
362
  }
@@ -136,11 +136,11 @@ export declare function decisiveAssignmentLine(plan: ProbePlan): number;
136
136
  * the classifier never depends on hard-coded offsets. `packageOf` maps a
137
137
  * service name to its workspace package specifier (e.g. `@carrick/orders`).
138
138
  *
139
- * `unwidenedAliasFor` answers, for a producer that is the sending side, the
140
- * surface export that carries its unwidened reading (carrick#1538), when its
141
- * stub ships one. The probe of an `http` response then ends with a second
142
- * assignment of that reading, in the wire form. Without an answer the
143
- * probe is what it always was, byte for byte. A consumer that sends (a request
144
- * body) is never asked: the reading is of a handler's return.
139
+ * `unwidenedAliasFor` answers, for the sending side, the surface export that
140
+ * carries its unwidened reading, when its stub ships one: a producer's
141
+ * response (carrick#1538) or a consumer's request body (carrick#2011). The
142
+ * probe of an `http` pair then ends with a second assignment of that reading,
143
+ * in the wire form. Without an answer the probe is what it always was, byte
144
+ * for byte.
145
145
  */
146
- export declare function buildProbe(spec: CheckPairSpec, packageOf: (serviceName: string) => string, unwidenedAliasFor?: (sentProducer: CheckPairEndpoint) => string | undefined): ProbePlan;
146
+ export declare function buildProbe(spec: CheckPairSpec, packageOf: (serviceName: string) => string, unwidenedAliasFor?: (sent: CheckPairEndpoint) => string | undefined): ProbePlan;
@@ -126,12 +126,12 @@ export function decisiveAssignmentLine(plan) {
126
126
  * the classifier never depends on hard-coded offsets. `packageOf` maps a
127
127
  * service name to its workspace package specifier (e.g. `@carrick/orders`).
128
128
  *
129
- * `unwidenedAliasFor` answers, for a producer that is the sending side, the
130
- * surface export that carries its unwidened reading (carrick#1538), when its
131
- * stub ships one. The probe of an `http` response then ends with a second
132
- * assignment of that reading, in the wire form. Without an answer the
133
- * probe is what it always was, byte for byte. A consumer that sends (a request
134
- * body) is never asked: the reading is of a handler's return.
129
+ * `unwidenedAliasFor` answers, for the sending side, the surface export that
130
+ * carries its unwidened reading, when its stub ships one: a producer's
131
+ * response (carrick#1538) or a consumer's request body (carrick#2011). The
132
+ * probe of an `http` pair then ends with a second assignment of that reading,
133
+ * in the wire form. Without an answer the probe is what it always was, byte
134
+ * for byte.
135
135
  */
136
136
  export function buildProbe(spec, packageOf, unwidenedAliasFor) {
137
137
  const id = pairId(spec);
@@ -319,15 +319,14 @@ export function buildProbe(spec, packageOf, unwidenedAliasFor) {
319
319
  // type or a mismatch here says only that the second reading did not clear
320
320
  // the pair, and the verdict on the published type stands.
321
321
  //
322
- // Only an `http` response gets one. It is the one pair a producer sends on
323
- // whose decisive assignment a second reading can mirror, here in the wire
324
- // form. A GraphQL pair compares an unwrapped comparand (above), which a
325
- // plain assignment of the reading would not mirror, and on every other
326
- // protocol the consumer is the sender (the direction table).
322
+ // Only an `http` pair gets one: a response the producer sends, or a request
323
+ // body the consumer sends (carrick#2011), each judged on a decisive
324
+ // assignment the second reading mirrors, here in the wire form. A GraphQL
325
+ // pair compares an unwrapped comparand (above), which a plain assignment of
326
+ // the reading would not mirror. Socket and pub/sub payloads are not read
327
+ // again.
327
328
  let unwidenedFromLine;
328
- const sentUnwidenedAlias = direction.sent === 'producer' && spec.protocol === 'http'
329
- ? unwidenedAliasFor?.(sentEndpoint)
330
- : undefined;
329
+ const sentUnwidenedAlias = spec.protocol === 'http' ? unwidenedAliasFor?.(sentEndpoint) : undefined;
331
330
  if (sentUnwidenedAlias !== undefined) {
332
331
  const local = UNWIDENED_LOCAL_NAME;
333
332
  unwidenedFromLine = push(`import type { ${sentUnwidenedAlias} as ${local} } from '${sentPkg}';`);
@@ -388,17 +388,17 @@ function readStubAliasRecords(stubs) {
388
388
  return byService;
389
389
  }
390
390
  /**
391
- * The unwidened reading a producer's stub ships beside one of its aliases
392
- * (carrick#1538), as `buildProbe` asks for it. Read from the same manifest the
391
+ * The unwidened reading a sending side's stub ships beside one of its aliases
392
+ * (carrick#1538, carrick#2011), as `buildProbe` asks for it. Read from the same manifest the
393
393
  * pre-gate reads. A reading the capture already found a member-level
394
394
  * `any`/`unknown` in is not offered: it would assign to whatever the other
395
395
  * side declares.
396
396
  */
397
397
  function unwidenedAliasIn(aliasRecords) {
398
- return (sentProducer) => {
398
+ return (sent) => {
399
399
  const record = aliasRecords
400
- .get(sentProducer.service_name)
401
- ?.get(unwidenedAliasOf(sentProducer.alias));
400
+ .get(sent.service_name)
401
+ ?.get(unwidenedAliasOf(sent.alias));
402
402
  if (!record || record.any_provenance?.length)
403
403
  return undefined;
404
404
  return record.alias;
@@ -26,6 +26,7 @@ import { DefinitionResolver } from './definition-resolver.js';
26
26
  import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
27
27
  import { Retyper } from './retype.js';
28
28
  import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
29
+ import { readServerLabels } from './server-labels.js';
29
30
  import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
30
31
  import { mergeInferTimings } from './infer-timing.js';
31
32
  import { addProgramFiles, listProgramFiles } from './program-files.js';
@@ -108,6 +109,7 @@ function projectComponents(key = '') {
108
109
  packageDirectoryOf: (file) => loader.packageDirectoryOf(file),
109
110
  resolveModule: (name, from, options) => loader.resolveThroughGraph(name, from, options),
110
111
  }),
112
+ labelledMembers: new Map(),
111
113
  };
112
114
  components.set(key, built);
113
115
  }
@@ -337,10 +339,13 @@ function handleInfer(request) {
337
339
  // One count for the request, whichever programs its items span.
338
340
  let done = 0;
339
341
  const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'infer', `${done} of ${request.requests.length}`));
340
- const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => projectComponents(key).typeInferrer.infer(items, request.extraction_config, () => {
341
- done += 1;
342
- report();
343
- }, request.model_symbols));
342
+ const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => {
343
+ const built = projectComponents(key);
344
+ return built.typeInferrer.infer(items, request.extraction_config, () => {
345
+ done += 1;
346
+ report();
347
+ }, request.model_symbols, labelledMembersFor(built, key, items, request.server_claims));
348
+ });
344
349
  const result = results.length === 1
345
350
  ? results[0]
346
351
  : (() => {
@@ -372,6 +377,40 @@ function handleInfer(request) {
372
377
  };
373
378
  }
374
379
  }
380
+ /**
381
+ * How long reading one program's server-framework labels may take: as
382
+ * long as a verify_library_claims request, since the reading builds a
383
+ * program of its own. Labels it does not reach are not read.
384
+ */
385
+ const SERVER_CLAIMS_BUDGET_MS = 600_000;
386
+ /** The infer kinds a route row asks: only these read a handler's parameters (carrick#2674). */
387
+ const ROUTE_ROW_KINDS = new Set(['response_body', 'request_body']);
388
+ /**
389
+ * carrick#2674: the members an `infer` request's server-framework claims
390
+ * name, verified again in a program of their own beside the inferrer's for
391
+ * `key` (`readServerLabels`). Undefined when the request carries none, or
392
+ * the group asks no route row, so the inferrer reads nothing new. Read once
393
+ * per program and claims.
394
+ */
395
+ function labelledMembersFor(built, key, items, claims) {
396
+ if (!claims || claims.checks.length === 0)
397
+ return undefined;
398
+ if (!items.some((item) => ROUTE_ROW_KINDS.has(item.infer_kind)))
399
+ return undefined;
400
+ const cacheKey = JSON.stringify(claims);
401
+ let labels = built.labelledMembers.get(cacheKey);
402
+ if (!labels) {
403
+ const loader = initializedLoader();
404
+ labels = readServerLabels({
405
+ getRepoRoot: () => loader.getRepoRoot(),
406
+ scriptProjectFor: (owner) => loader.scriptProjectFor(owner),
407
+ packageDirectoryOf: (file) => loader.packageDirectoryOf(file),
408
+ resolveModule: (name, from, options) => loader.resolveThroughGraph(name, from, options),
409
+ }, key, claims, SERVER_CLAIMS_BUDGET_MS, log);
410
+ built.labelledMembers.set(cacheKey, labels);
411
+ }
412
+ return labels;
413
+ }
375
414
  /**
376
415
  * How long one retype_check request may spend before the items it has not
377
416
  * reached abstain: well inside the scanner's 900s read deadline.
@@ -37,7 +37,25 @@
37
37
  */
38
38
  import { ts, type Project } from 'ts-morph';
39
39
  import { type PackageDirectoryOf } from './position-rows.js';
40
- import type { LibraryCheck, LibrarySurface, SemanticsCheck, SemanticsModule, SemanticsResult } from './types.js';
40
+ import type { ClaimSlot, LibraryCheck, LibrarySurface, SemanticsCheck, SemanticsModule, SemanticsDeclaration, SemanticsResult } from './types.js';
41
+ /** How one check came out, before it is stamped with its claim and receiver. */
42
+ type Outcome = {
43
+ verdict: 'verified';
44
+ declarations?: DeclaredAt[];
45
+ /**
46
+ * In-process only, never stamped onto a verdict: the declarations of
47
+ * the member a verified op calls, and, for a receive op whose
48
+ * `request` names a `body`, of the member that path ends on
49
+ * (carrick#2674). Read by `labelledMembers`.
50
+ */
51
+ members?: readonly ts.Declaration[];
52
+ bodyMembers?: readonly ts.Declaration[];
53
+ } | {
54
+ verdict: 'failed' | 'unchecked';
55
+ reason: string;
56
+ };
57
+ /** Where a member is declared, spelled as a position row's `decl_file` and `decl_line`. */
58
+ type DeclaredAt = SemanticsDeclaration;
41
59
  export interface VerifyResult {
42
60
  semantics: SemanticsResult[];
43
61
  modules: SemanticsModule[];
@@ -78,7 +96,15 @@ export declare class LibraryClaimsVerifier {
78
96
  * A check on an instance or a scope reads the maker or scope claims of the
79
97
  * same request first, whatever their place in it.
80
98
  */
81
- run(fromDir: string, checks: LibraryCheck[], budgetMs: number, scanRoot?: string): VerifyResult;
99
+ run(fromDir: string, checks: LibraryCheck[], budgetMs: number, scanRoot?: string, options?: RunOptions): VerifyResult;
100
+ /**
101
+ * The members a service's server-framework labels name, read with the
102
+ * verifier itself so only a label that verifies in the service's own
103
+ * program counts (carrick#2674): each verified `send` op that carries a
104
+ * `payload` slot, and each verified `receive` op whose `request` names a
105
+ * `body` path. Other checks are not judged.
106
+ */
107
+ labelledMembers(fromDir: string, checks: LibraryCheck[], budgetMs: number, scanRoot?: string, typePackages?: Readonly<Record<string, readonly string[]>>): LabelledMembers;
82
108
  /**
83
109
  * Each specifier's declared surface, read with the verifier's predicates
84
110
  * (carrick#1660): every value export (or only those `only` names for the
@@ -114,7 +140,41 @@ export interface ListedSurface {
114
140
  * array of `[package, exports]` for every surface that listed at least one
115
141
  * export, sorted by `package` (code unit order). A specifier that lists
116
142
  * nothing (unresolved, local, no value exports) is left out, so asking for a
117
- * subpath a version does not have changes nothing. Type text already names
143
+ * subpath a version does not have changes nothing. Handler members are not
144
+ * hashed. Type text already names
118
145
  * the request's directory as `<root>`.
119
146
  */
120
147
  export declare function fullSurfaceSha256(surfaces: readonly LibrarySurface[]): string;
148
+ /** What `run` reads beyond the checks; none of it is part of a verdict. */
149
+ interface RunOptions {
150
+ /** Called with each check's outcome, in request order. */
151
+ observe?: (check: LibraryCheck, outcome: Outcome) => void;
152
+ /** Read server `send` ops as replies (`SERVER_REPLY_RULES`). */
153
+ readReplies?: boolean;
154
+ /**
155
+ * Per package specifier, the type-only packages the store says its surface
156
+ * re-exports (carrick#2674): a member one of them declares is the
157
+ * package's own, as the store reads it.
158
+ */
159
+ typePackages?: Readonly<Record<string, readonly string[]>>;
160
+ }
161
+ /**
162
+ * A declaration's key within one process: its file as the compiler names it
163
+ * and its start. Two programs of one project name a declaration alike, so a
164
+ * label read in the verifier's probe program joins a member read in the
165
+ * inferrer's (carrick#2674).
166
+ */
167
+ export declare function declarationKey(declaration: ts.Node): string;
168
+ /**
169
+ * The members a service's verified server-framework labels name, by
170
+ * {@link declarationKey} (carrick#2674): each `send` op member with the slot
171
+ * its payload sits in, and each member a receive op's `request.body` path
172
+ * ends on.
173
+ */
174
+ export interface LabelledMembers {
175
+ sends: ReadonlyMap<string, ClaimSlot>;
176
+ bodies: ReadonlySet<string>;
177
+ /** How each label read came out (`verified`, or the reason it was not), for the log. */
178
+ outcomes?: Readonly<Record<string, number>>;
179
+ }
180
+ export {};