carrick 0.3.123 → 0.3.125

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.123",
3
+ "version": "0.3.125",
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.123",
117
- "@carrick-tools/cli-darwin-x64": "0.3.123",
118
- "@carrick-tools/cli-linux-arm64": "0.3.123",
119
- "@carrick-tools/cli-linux-x64": "0.3.123",
120
- "@carrick-tools/cli-win32-x64": "0.3.123"
116
+ "@carrick-tools/cli-darwin-arm64": "0.3.125",
117
+ "@carrick-tools/cli-darwin-x64": "0.3.125",
118
+ "@carrick-tools/cli-linux-arm64": "0.3.125",
119
+ "@carrick-tools/cli-linux-x64": "0.3.125",
120
+ "@carrick-tools/cli-win32-x64": "0.3.125"
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.123",
4
+ "version": "0.3.125",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import { decisiveAssignmentLine, unwidenedAliasOf, } from './check-probe.js';
31
31
  import { scrubDiagnostic, scrubPaths } from './check-scrub.js';
32
+ import { onlyDeclaredOpenAny } from './deep-walk.js';
32
33
  import { describeFieldReport, fieldReportNotes, } from './check-fields.js';
33
34
  const PRIMARY_RE = /^(?<file>(?:[a-zA-Z]:)?[^(]*?)\((?<line>\d+),(?<col>\d+)\): error TS(?<code>\d+): (?<msg>.*)$/;
34
35
  /** Parse `tsc --pretty false` output into structured diagnostics. */
@@ -350,9 +351,10 @@ function onUnwidenedLines(plan, diagnostic) {
350
351
  * - its own declarations typecheck (the alias is not poisoned);
351
352
  * - the member-level walk ran over it and found no `any`/`unknown`, which
352
353
  * would assign to whatever the other side declares;
353
- * - the same walk found none on the two published sides either. A mismatch
354
- * that is not a fact stays the mismatch it was: the reading would be
355
- * clearing it against a member that accepts anything.
354
+ * - the same walk found none on the two published sides either, an open
355
+ * member included (carrick#2591). A mismatch that is not a fact stays the
356
+ * mismatch it was: the reading would be clearing it against a member that
357
+ * accepts anything.
356
358
  */
357
359
  function unwidenedReadingFits(input) {
358
360
  const { plan, probeDiags, poisonReason } = input;
@@ -364,7 +366,7 @@ function unwidenedReadingFits(input) {
364
366
  if (poisonReason(sent.service_name, unwidenedAliasOf(sent.alias)))
365
367
  return false;
366
368
  const walked = input.deepFindings?.sentUnwidened;
367
- return walked !== undefined && walked.length === 0 && factness(input).resolved;
369
+ return walked !== undefined && walked.length === 0 && factness(input, true).resolved;
368
370
  }
369
371
  /**
370
372
  * The field-level sentence appended to a mismatch diagnostic
@@ -410,7 +412,7 @@ function namedFields(input, scrubCtx) {
410
412
  * empty. A walk that could not run leaves the verdict not-a-fact: absence of
411
413
  * findings is not evidence.
412
414
  */
413
- function factness(input) {
415
+ function factness(input, strict = false) {
414
416
  const findings = input.deepFindings;
415
417
  if (!findings) {
416
418
  return {
@@ -419,6 +421,10 @@ function factness(input) {
419
421
  };
420
422
  }
421
423
  for (const side of ['sent', 'expected']) {
424
+ // Open members only (carrick#2591): the closed members were judged and an
425
+ // open one is never a mismatch, so the verdict is a fact about the type.
426
+ if (!strict && onlyDeclaredOpenAny(findings[side]))
427
+ continue;
422
428
  const first = findings[side][0];
423
429
  if (!first)
424
430
  continue;
@@ -25,6 +25,7 @@ import { scrubPaths } from './check-scrub.js';
25
25
  import { assembleWorkspace, writeProbes, } from './check-workspace.js';
26
26
  import { buildPoisonIndexes } from './check-poison.js';
27
27
  import { openProbeProgram, probeDeepFindings } from './check-deep.js';
28
+ import { onlyDeclaredOpenAny } from './deep-walk.js';
28
29
  import { pairFieldReports } from './check-fields.js';
29
30
  import { pairDispatchUnions } from './check-union.js';
30
31
  function runProcess(command, args, cwd, env) {
@@ -465,7 +466,13 @@ function deepDecayOf(plan, aliasRecords) {
465
466
  const record = aliasRecords.get(endpoint.service_name)?.get(endpoint.alias);
466
467
  // The capture emits every deep finding it made; the FIRST is the one the
467
468
  // pre-gate reads, exactly as when the walk stopped at the first hit.
468
- const finding = record?.any_provenance?.[0];
469
+ const findings = record?.any_provenance ?? [];
470
+ // An `any` the source declares under a named member is an open member
471
+ // (carrick#2591): the pair is probed, and the compiler judges the closed
472
+ // members. Anything else on the record keeps the pair out of the probe.
473
+ if (onlyDeclaredOpenAny(findings))
474
+ continue;
475
+ const finding = findings[0];
469
476
  if (finding) {
470
477
  return { side, kind: finding.kind, path: finding.path };
471
478
  }
@@ -20,6 +20,14 @@ import type { TypeProvenance } from './api.js';
20
20
  * within its depth/node budget, treated as unverifiable (fail closed) rather
21
21
  * than silently clean. */
22
22
  export type DeepTopType = Pick<TypeProvenance, 'kind' | 'path'>;
23
+ /** Cap on findings reported per alias. The FIRST one is what the check phase
24
+ * pre-gates on, so verdicts never depend on this number; the rest are there to
25
+ * tell a reader which fields are `any` (carrick#376), and a type with more
26
+ * than this many is already better described by "this type is not typed".
27
+ * The scanner mirrors it (`CAPTURE_FINDINGS_CAP` in
28
+ * `src/engine/type_compat_v2.rs`): a record this long may have stopped
29
+ * listing, so it cannot vouch for every position (carrick#1752). */
30
+ export declare const MAX_DEEP_FINDINGS = 32;
23
31
  /**
24
32
  * Depth-bounded structural walk for a disqualifying top type at ANY depth:
25
33
  * a member, array element, index signature, type argument, or callable RETURN
@@ -76,6 +84,29 @@ export declare function findUncarriedPositions(root: ts.Type, program: ts.Progra
76
84
  /** TypeScript's unresolved-reference placeholder: `TypeFlags.Any` with the
77
85
  * internal `intrinsicName === 'error'` (stable since TS 1.x; see `anchors.ts`). */
78
86
  export declare function isErrorPlaceholder(t: ts.Type): boolean;
87
+ /**
88
+ * True when a deep-walk path passes through a named member (the same rule as
89
+ * `path_names_a_member` in `src/engine/type_compat_v2.rs`). The walk writes a
90
+ * member as its name, an element or type argument as `<n>`, an index signature
91
+ * as `[index]` and a callable return as `()`; a path made of the last three
92
+ * alone sits at the root of the type or inside a container, never in a field.
93
+ */
94
+ export declare function pathNamesAMember(path: string): boolean;
95
+ /**
96
+ * True when every finding is an `any` the SOURCE declares under a named
97
+ * member (carrick#2591): an OPEN member. The policy: an open member is never
98
+ * a mismatch, and the closed members are judged as for a fully typed pair.
99
+ * `any` accepts and supplies every shape in the assignment the probe makes, so
100
+ * the compiler already judges only the closed members.
101
+ *
102
+ * Nothing else qualifies. A callable's return (`getData()`) is no data
103
+ * member. An `unknown` member rejects a narrower counterparty
104
+ * under assignment, so it would read as a mismatch the policy forbids; a
105
+ * `budget_exhausted` sentinel, a root top type and an `any` an unresolved
106
+ * import left say nothing about the author's intent. A list as long as the
107
+ * walk's cap may have stopped listing, so it cannot vouch for every position.
108
+ */
109
+ export declare function onlyDeclaredOpenAny(findings: readonly TypeProvenance[]): boolean;
79
110
  /** What an anchor's SOURCE program could not resolve (carrick#1164). */
80
111
  export interface UnresolvedAtAnchor {
81
112
  /** Member paths holding the unresolved-reference placeholder. */
@@ -22,7 +22,7 @@ import { memberPath } from './member-name.js';
22
22
  * The scanner mirrors it (`CAPTURE_FINDINGS_CAP` in
23
23
  * `src/engine/type_compat_v2.rs`): a record this long may have stopped
24
24
  * listing, so it cannot vouch for every position (carrick#1752). */
25
- const MAX_DEEP_FINDINGS = 32;
25
+ export const MAX_DEEP_FINDINGS = 32;
26
26
  /**
27
27
  * Depth-bounded structural walk for a disqualifying top type at ANY depth:
28
28
  * a member, array element, index signature, type argument, or callable RETURN
@@ -269,6 +269,48 @@ function walkTopTypes(root, program, checker, location, flagOf) {
269
269
  rest.sort((a, b) => a.path.localeCompare(b.path));
270
270
  return [head, ...rest];
271
271
  }
272
+ /**
273
+ * True when a deep-walk path passes through a named member (the same rule as
274
+ * `path_names_a_member` in `src/engine/type_compat_v2.rs`). The walk writes a
275
+ * member as its name, an element or type argument as `<n>`, an index signature
276
+ * as `[index]` and a callable return as `()`; a path made of the last three
277
+ * alone sits at the root of the type or inside a container, never in a field.
278
+ */
279
+ export function pathNamesAMember(path) {
280
+ const rest = path.replaceAll('[index]', '').replaceAll('()', '');
281
+ let inArgument = false;
282
+ for (const c of rest) {
283
+ if (c === '<')
284
+ inArgument = true;
285
+ else if (c === '>')
286
+ inArgument = false;
287
+ else if (c !== '.' && !inArgument)
288
+ return true;
289
+ }
290
+ return false;
291
+ }
292
+ /**
293
+ * True when every finding is an `any` the SOURCE declares under a named
294
+ * member (carrick#2591): an OPEN member. The policy: an open member is never
295
+ * a mismatch, and the closed members are judged as for a fully typed pair.
296
+ * `any` accepts and supplies every shape in the assignment the probe makes, so
297
+ * the compiler already judges only the closed members.
298
+ *
299
+ * Nothing else qualifies. A callable's return (`getData()`) is no data
300
+ * member. An `unknown` member rejects a narrower counterparty
301
+ * under assignment, so it would read as a mismatch the policy forbids; a
302
+ * `budget_exhausted` sentinel, a root top type and an `any` an unresolved
303
+ * import left say nothing about the author's intent. A list as long as the
304
+ * walk's cap may have stopped listing, so it cannot vouch for every position.
305
+ */
306
+ export function onlyDeclaredOpenAny(findings) {
307
+ return (findings.length > 0 &&
308
+ findings.length < MAX_DEEP_FINDINGS &&
309
+ findings.every((f) => f.kind === 'any' &&
310
+ f.reason === 'declared' &&
311
+ pathNamesAMember(f.path) &&
312
+ !f.path.includes('()')));
313
+ }
272
314
  /** The disqualifier the self-check and the check phase gate on (see
273
315
  * `findDisqualifyingTopTypes`). */
274
316
  function disqualifyingFlag(t) {
@@ -34,7 +34,7 @@ export { sidecarCompilerOptions } from './compiler-options.js';
34
34
  export { isInsideCheckout, untrustedReadBound, UNTRUSTED_TREE_ENV, type ReadBound } from './checkout.js';
35
35
  export { serviceConfigPath } from './project-references.js';
36
36
  export { referenceRedirectOf } from './reference-redirect.js';
37
- export { findServiceTsconfig } from './service-config.js';
37
+ export { findServiceTsconfig, fileConfigPath } from './service-config.js';
38
38
  export { WriteGuard } from './guarded-fs.js';
39
39
  export { installedPackageDirectory } from './installed-package-directory.js';
40
40
  export { isDeclarationFileName, packageNameOf } from './specifiers.js';
@@ -52,7 +52,7 @@ export { sidecarCompilerOptions } from './compiler-options.js';
52
52
  export { isInsideCheckout, untrustedReadBound, UNTRUSTED_TREE_ENV } from './checkout.js';
53
53
  export { serviceConfigPath } from './project-references.js';
54
54
  export { referenceRedirectOf } from './reference-redirect.js';
55
- export { findServiceTsconfig } from './service-config.js';
55
+ export { findServiceTsconfig, fileConfigPath } from './service-config.js';
56
56
  // carrick#2405: the per-position rows write through the guard, and name an
57
57
  // installed declaring file the way the capture's resolution walk does.
58
58
  export { WriteGuard } from './guarded-fs.js';
@@ -39,6 +39,12 @@ export const MACHINERY_MEMBER_INDICATORS = new Set([
39
39
  'clone',
40
40
  'json',
41
41
  'statusText',
42
+ // FROZEN (old path, 2026-10-21): the reply-object half below only (the
43
+ // fetch half above and the threshold stay). Deletions and safety fixes
44
+ // only. Fixes for wrong or missing types go in labelled reply objects
45
+ // (carrick#2472 C4) and the language library rows (carrick#2445). Delete
46
+ // when a delete-and-replay on the four repos moves no row wrongly, after
47
+ // carrick-cloud#2379 and carrick#2445 (carrick#2472, C6).
42
48
  // Node http ServerResponse / reply-object surface
43
49
  'statusCode',
44
50
  'statusMessage',
@@ -19,3 +19,14 @@
19
19
  export declare function findServiceTsconfig(serviceRoot: string, scanRoot?: string): string | undefined;
20
20
  /** The path with every symlink resolved; the path as given when it does not exist. */
21
21
  export declare function realPath(p: string): string;
22
+ /**
23
+ * The config an editor types `file` under, for a service with none at its
24
+ * root (carrick#1604): the nearest `tsconfig.json`, then `jsconfig.json`, in
25
+ * each directory from the file's up to the service root, inclusive. The
26
+ * climb stops after a directory named `node_modules`, as tsserver's does. A file
27
+ * outside the service root, or with no config on its way up, has none.
28
+ *
29
+ * `known` caches the answer per directory; pass one map for one service so
30
+ * its files share the climb.
31
+ */
32
+ export declare function fileConfigPath(file: string, serviceRoot: string, known?: Map<string, string | undefined>): string | undefined;
@@ -55,3 +55,43 @@ export function realPath(p) {
55
55
  return path.resolve(p);
56
56
  }
57
57
  }
58
+ /** The names an editor looks for in a file's directory, in order. */
59
+ const FILE_CONFIG_NAMES = ['tsconfig.json', 'jsconfig.json'];
60
+ /**
61
+ * The config an editor types `file` under, for a service with none at its
62
+ * root (carrick#1604): the nearest `tsconfig.json`, then `jsconfig.json`, in
63
+ * each directory from the file's up to the service root, inclusive. The
64
+ * climb stops after a directory named `node_modules`, as tsserver's does. A file
65
+ * outside the service root, or with no config on its way up, has none.
66
+ *
67
+ * `known` caches the answer per directory; pass one map for one service so
68
+ * its files share the climb.
69
+ */
70
+ export function fileConfigPath(file, serviceRoot, known = new Map()) {
71
+ const root = path.resolve(serviceRoot);
72
+ const climbed = [];
73
+ let found;
74
+ let dir = path.dirname(path.resolve(file));
75
+ for (;;) {
76
+ const from = path.relative(root, dir);
77
+ if (from.startsWith('..') || path.isAbsolute(from))
78
+ break;
79
+ if (known.has(dir)) {
80
+ found = known.get(dir);
81
+ break;
82
+ }
83
+ climbed.push(dir);
84
+ const name = FILE_CONFIG_NAMES.find((n) => fs.existsSync(path.join(dir, n)));
85
+ if (name !== undefined) {
86
+ found = path.join(dir, name);
87
+ break;
88
+ }
89
+ // tsserver looks in a `node_modules` directory itself, then stops.
90
+ if (dir === root || path.basename(dir) === 'node_modules')
91
+ break;
92
+ dir = path.dirname(dir);
93
+ }
94
+ for (const d of climbed)
95
+ known.set(d, found);
96
+ return found;
97
+ }
@@ -442,7 +442,7 @@ function handleVerifyLibraryClaims(request) {
442
442
  writeProgress(request.request_id, 'verify_library_claims', 'program ready');
443
443
  const fromDir = path.resolve(projectLoader.getRepoRoot(), request.from_dir);
444
444
  log(`Verifying ${request.checks.length} library claim(s) from ${fromDir}`);
445
- const { semantics, modules } = claimsVerifier.run(fromDir, request.checks, request.budget_ms ?? SEMANTICS_BUDGET_MS);
445
+ const { semantics, modules } = claimsVerifier.run(fromDir, request.checks, request.budget_ms ?? SEMANTICS_BUDGET_MS, request.scan_root);
446
446
  return {
447
447
  request_id: request.request_id,
448
448
  status: 'success',
@@ -63,7 +63,7 @@ export declare class LibraryClaimsVerifier {
63
63
  * A check on an instance or a scope reads the maker or scope claims of the
64
64
  * same request first, whatever their place in it.
65
65
  */
66
- run(fromDir: string, checks: LibraryCheck[], budgetMs: number): VerifyResult;
66
+ run(fromDir: string, checks: LibraryCheck[], budgetMs: number, scanRoot?: string): VerifyResult;
67
67
  /**
68
68
  * Each specifier's declared surface, read with the verifier's predicates
69
69
  * (carrick#1660): every value export (or only those `only` names for the
@@ -39,6 +39,7 @@ import * as crypto from 'node:crypto';
39
39
  import * as fs from 'node:fs';
40
40
  import * as path from 'node:path';
41
41
  import { ts } from 'ts-morph';
42
+ import { PathNames, declaringFileName } from './position-rows.js';
42
43
  /** The methods a `verb` claim may name, and a method key may accept. */
43
44
  const HTTP_METHODS = new Set([
44
45
  'GET',
@@ -130,8 +131,9 @@ const HTTP_V2_RULES = { http: false, message: true, ops: new Set(['request']), m
130
131
  const SERVER_RULES = { http: false, message: true, ops: new Set(['receive']), methods: ROUTE_METHODS };
131
132
  /**
132
133
  * The role table: the role alone picks the checks. A role this build does
133
- * not read yet (GraphQL executors, server mounts) answers `role_unsupported`,
134
- * and `none` has nothing on the wire to check.
134
+ * not read yet (GraphQL executors) answers `role_unsupported`, and `none`
135
+ * has nothing on the wire to check. A server's mounts are scope claims
136
+ * (`mounts`, #2474 R0).
135
137
  */
136
138
  const ROLE_TABLE = {
137
139
  http_client: HTTP_RULES,
@@ -238,6 +240,13 @@ export function httpCheck(check) {
238
240
  }
239
241
  }
240
242
  }
243
+ /** Declarations in a fixed order, each once. */
244
+ function sortedDeclarations(declarations) {
245
+ const seen = new Map();
246
+ for (const each of declarations)
247
+ seen.set(`${each.decl_file}:${each.decl_line}`, each);
248
+ return [...seen.values()].sort((a, b) => (a.decl_file < b.decl_file ? -1 : a.decl_file > b.decl_file ? 1 : a.decl_line - b.decl_line));
249
+ }
241
250
  let probeSequence = 0;
242
251
  export class LibraryClaimsVerifier {
243
252
  project;
@@ -250,10 +259,15 @@ export class LibraryClaimsVerifier {
250
259
  * A check on an instance or a scope reads the maker or scope claims of the
251
260
  * same request first, whatever their place in it.
252
261
  */
253
- run(fromDir, checks, budgetMs) {
262
+ run(fromDir, checks, budgetMs, scanRoot) {
254
263
  const deadline = performance.now() + budgetMs;
255
264
  const stamp = (check, outcome) => outcome.verdict === 'verified'
256
- ? { claim_id: check.claim_id, receiver: check.receiver, verdict: 'verified' }
265
+ ? {
266
+ claim_id: check.claim_id,
267
+ receiver: check.receiver,
268
+ verdict: 'verified',
269
+ ...(outcome.declarations?.length ? { declarations: outcome.declarations } : {}),
270
+ }
257
271
  : {
258
272
  claim_id: check.claim_id,
259
273
  receiver: check.receiver,
@@ -330,7 +344,7 @@ export class LibraryClaimsVerifier {
330
344
  const file = program.getSourceFile(probe.getFilePath());
331
345
  if (!file)
332
346
  throw new Error(`probe file ${probePath} is not in the program`);
333
- const reader = new DeclarationReader(program, file, this.project.getModuleResolutionHost(), fromDir);
347
+ const reader = new DeclarationReader(program, file, this.project.getModuleResolutionHost(), fromDir, scanRoot);
334
348
  const localRanges = readLocalRanges(fromDir);
335
349
  const moduleReads = new Map();
336
350
  const exportReads = new Map();
@@ -501,13 +515,15 @@ export class LibraryClaimsVerifier {
501
515
  const built = builtReceiver(check);
502
516
  const builtAtDefaults = built === undefined ? undefined : atDefaults(check, built);
503
517
  const holding = [];
518
+ const declarations = [];
504
519
  for (const receiver of receivers.value) {
505
520
  const result = reader.judgeMessage(receiver, check.claim, rules, builtAtDefaults);
506
521
  if (result.outcome.verdict !== 'verified')
507
522
  return result;
508
523
  holding.push(...(result.holding ?? []));
524
+ declarations.push(...(result.outcome.declarations ?? []));
509
525
  }
510
- return { outcome: VERIFIED, holding };
526
+ return { outcome: declarations.length > 0 ? { verdict: 'verified', declarations: sortedDeclarations(declarations) } : VERIFIED, holding };
511
527
  };
512
528
  const semantics = [];
513
529
  for (let index = 0; index < checks.length; index++) {
@@ -695,6 +711,19 @@ function placementInvalid(claim) {
695
711
  const of = parseReceiver(claim.of);
696
712
  return of === undefined || (of.maker === undefined && of.scope === undefined);
697
713
  }
714
+ /**
715
+ * Where a handler's request holds its path and method (#2474 R0) is read on
716
+ * a receive op with a handler, and names `path` or `url`; every member path
717
+ * it gives is non-empty.
718
+ */
719
+ function requestAtValid(claim) {
720
+ const request = claim.request;
721
+ if (claim.op !== 'receive' || claim.handler === undefined)
722
+ return false;
723
+ if (request.path === undefined && request.url === undefined)
724
+ return false;
725
+ return [request.path, request.url, request.method].every(members => members === undefined || members.length > 0);
726
+ }
698
727
  /**
699
728
  * A message check names a receiver its claim can be read on (contract
700
729
  * amendment 2, B3). A maker is read on the export itself. An element with
@@ -841,6 +870,8 @@ class DeclarationReader {
841
870
  probe;
842
871
  host;
843
872
  checker;
873
+ /** Names declaring files as position rows do: from the scan root, else the service root. */
874
+ pathNames;
844
875
  /** The service root, as a realpath. */
845
876
  root;
846
877
  packageDirectories = new Map();
@@ -853,11 +884,12 @@ class DeclarationReader {
853
884
  declared = new Map();
854
885
  /** The service root as given and as a realpath, longest first: what a listing prints as `<root>`. */
855
886
  printedRoots;
856
- constructor(program, probe, host, serviceRoot) {
887
+ constructor(program, probe, host, serviceRoot, scanRoot) {
857
888
  this.program = program;
858
889
  this.probe = probe;
859
890
  this.host = host;
860
891
  this.checker = program.getTypeChecker();
892
+ this.pathNames = new PathNames(scanRoot ?? serviceRoot);
861
893
  this.root = this.realpath(serviceRoot);
862
894
  this.printedRoots = [...new Set([this.root, serviceRoot])]
863
895
  .filter(root => root.length > 1)
@@ -1472,9 +1504,26 @@ class DeclarationReader {
1472
1504
  * `member` (after `path`) is a home-declared callable member whose name
1473
1505
  * slot accepts a string, under the name-slot rules, and returns a type that
1474
1506
  * says something, read at its type-parameter defaults (`returnOf`).
1507
+ *
1508
+ * Route fields (#2474 R0, contract amendment of 2026-10-22 on carrick#2434):
1509
+ * - `runs` with `yields`: the member runs the function at `runs` (a
1510
+ * function with a declared signature) and hands it, at `yields.param`,
1511
+ * an instance of the package's export `yields.export` (`yieldsOutcome`);
1512
+ * - `mounts`: the receiver the claim is read on fits the parameter there
1513
+ * (`mountsOutcome`).
1514
+ * Either one binds its instance through a parameter or an argument, not
1515
+ * the member's return, so the return is not read and the claim holds no
1516
+ * overloads: a `>scope:` receiver read through it is not resolved.
1517
+ * `runs` without `yields` (or the reverse), or `mounts` beside either, is
1518
+ * `claim_invalid`.
1475
1519
  */
1476
1520
  judgeScope(receiver, claim, built) {
1477
- const layout = this.layout({ name: claim.name });
1521
+ if ((claim.runs === undefined) !== (claim.yields === undefined))
1522
+ return { outcome: unchecked('claim_invalid') };
1523
+ if (claim.mounts !== undefined && claim.runs !== undefined)
1524
+ return { outcome: unchecked('claim_invalid') };
1525
+ const binds = claim.runs !== undefined || claim.mounts !== undefined;
1526
+ const layout = this.layout({ name: claim.name, runs: claim.runs, mounts: claim.mounts });
1478
1527
  if ('failure' in layout)
1479
1528
  return { outcome: layout.failure };
1480
1529
  const callee = this.ownCallee(receiver, claim.path ?? [], claim.member, 'call');
@@ -1485,7 +1534,17 @@ class DeclarationReader {
1485
1534
  let best;
1486
1535
  for (const signature of callee.signatures) {
1487
1536
  let result = this.messageSignatureOutcome(signature, layout.value, labels);
1488
- if (result.rank === Infinity && this.returnSaysNothing(this.returnOf(signature, built))) {
1537
+ if (result.rank === Infinity && claim.yields !== undefined) {
1538
+ const given = this.yieldsOutcome(signature, claim.runs, claim.yields, receiver);
1539
+ if (given)
1540
+ result = { rank: 5, outcome: given };
1541
+ }
1542
+ if (result.rank === Infinity && claim.mounts !== undefined) {
1543
+ const mounted = this.mountsOutcome(signature, claim.mounts, receiver);
1544
+ if (mounted)
1545
+ result = { rank: 5, outcome: mounted };
1546
+ }
1547
+ if (result.rank === Infinity && !binds && this.returnSaysNothing(this.returnOf(signature, built))) {
1489
1548
  result = { rank: 6, outcome: unchecked('scope_unresolved') };
1490
1549
  }
1491
1550
  if (result.rank === Infinity)
@@ -1494,9 +1553,38 @@ class DeclarationReader {
1494
1553
  best = furthest(best, result.rank, result.outcome);
1495
1554
  }
1496
1555
  if (holding.length > 0)
1497
- return { outcome: VERIFIED, holding };
1556
+ return binds ? { outcome: VERIFIED } : { outcome: VERIFIED, holding };
1498
1557
  return { outcome: best.outcome };
1499
1558
  }
1559
+ /**
1560
+ * `mounts: { arg, key? }` on a scope (#2474 R0): the receiver the claim is
1561
+ * read on is assignable to the parameter there, so an instance like it can
1562
+ * be served under the scope's name. A parameter typed `any`, `unknown`,
1563
+ * `{}` or by an unconstrained type parameter says nothing
1564
+ * (`mounts_untyped`). There is no separate "not a function" rule: a router
1565
+ * type that is itself a handler is a common shape, and a parameter only a
1566
+ * function fits fails here because the instance does not fit it.
1567
+ * Undefined when it holds.
1568
+ */
1569
+ mountsOutcome(signature, mounts, receiver) {
1570
+ let declared = this.keyParameterAt(signature, mounts.arg);
1571
+ if (declared !== undefined && mounts.key !== undefined) {
1572
+ const property = this.declaredProperty(declared, mounts.key);
1573
+ declared = property ? this.checker.getTypeOfSymbol(property) : undefined;
1574
+ }
1575
+ if (declared === undefined)
1576
+ return failed('param_missing');
1577
+ const slot = this.throughConstraint(declared);
1578
+ if (slot === VARIADIC)
1579
+ return unchecked('mounts_untyped');
1580
+ const parts = this.parts(slot);
1581
+ if (parts.length === 0)
1582
+ return failed('mounts_not_instance');
1583
+ if (parts.some(part => this.isOpenTop(part) || this.isUnconstrained(part) || this.isEmptyObject(part))) {
1584
+ return unchecked('mounts_untyped');
1585
+ }
1586
+ return this.checker.isTypeAssignableTo(receiver.type, slot) ? undefined : failed('mounts_not_instance');
1587
+ }
1500
1588
  /**
1501
1589
  * A send, receive or request op: the home-declared callable member (after
1502
1590
  * `path`; or the receiver itself, when null) with some overload that takes
@@ -1513,6 +1601,9 @@ class DeclarationReader {
1513
1601
  if (claim.yields !== undefined && (claim.op !== 'receive' || claim.handler === undefined)) {
1514
1602
  return unchecked('claim_invalid');
1515
1603
  }
1604
+ // So is where the handler's request holds its path and method (#2474 R0).
1605
+ if (claim.request !== undefined && !requestAtValid(claim))
1606
+ return unchecked('claim_invalid');
1516
1607
  const name = claim.name;
1517
1608
  // A send carries a payload: a name alone on `send(data)` is the data. An
1518
1609
  // op that runs a function or client it is handed takes its name from
@@ -1562,6 +1653,11 @@ class DeclarationReader {
1562
1653
  if (given)
1563
1654
  result = { rank: 5, outcome: given };
1564
1655
  }
1656
+ if (result.rank === Infinity && claim.request !== undefined) {
1657
+ const read = this.requestOutcome(signature, claim.handler, claim.request);
1658
+ if (read)
1659
+ result = { rank: 5, outcome: read };
1660
+ }
1565
1661
  if (result.rank === Infinity) {
1566
1662
  held = true;
1567
1663
  break;
@@ -1572,7 +1668,28 @@ class DeclarationReader {
1572
1668
  return best.outcome;
1573
1669
  if (boundRuns)
1574
1670
  return this.boundRunsOutcome(receiver.makerHolding, boundRuns);
1575
- return VERIFIED;
1671
+ const declarations = this.declarationsOf(callee.signatures);
1672
+ return declarations.length > 0 ? { verdict: 'verified', declarations } : VERIFIED;
1673
+ }
1674
+ /**
1675
+ * Where the signatures a verified op resolved to are declared, as position
1676
+ * rows spell it (`decl_file`, `decl_line`): the member on this receiver, so
1677
+ * two members of one name on two types give two answers. A declaration no
1678
+ * row could name is left out.
1679
+ */
1680
+ declarationsOf(signatures) {
1681
+ const found = [];
1682
+ for (const signature of signatures) {
1683
+ const declaration = signature.getDeclaration();
1684
+ if (!declaration)
1685
+ continue;
1686
+ const file = declaration.getSourceFile();
1687
+ const decl_file = declaringFileName(this.program, this.pathNames, file);
1688
+ if (decl_file === null)
1689
+ continue;
1690
+ found.push({ decl_file, decl_line: file.getLineAndCharacterOfPosition(declaration.getStart(file)).line + 1 });
1691
+ }
1692
+ return sortedDeclarations(found);
1576
1693
  }
1577
1694
  /**
1578
1695
  * The method fields of an op (label structure v2, carrick#2434), under the
@@ -1680,6 +1797,48 @@ class DeclarationReader {
1680
1797
  }
1681
1798
  return failed('yields_not_export');
1682
1799
  }
1800
+ /**
1801
+ * `request: { param, path?, url?, method? }` on a receive op (#2474 R0):
1802
+ * some signature of the handler this overload takes declares a parameter
1803
+ * at `param` on which every member path the claim gives resolves, step by
1804
+ * step, through properties the library declares (`declaredProperty`).
1805
+ * Undefined when it holds.
1806
+ */
1807
+ requestOutcome(signature, handler, request) {
1808
+ let declared = this.keyParameterAt(signature, handler.arg);
1809
+ if (declared !== undefined && handler.key !== undefined) {
1810
+ const property = this.declaredProperty(declared, handler.key);
1811
+ declared = property ? this.checker.getTypeOfSymbol(property) : undefined;
1812
+ }
1813
+ if (declared === undefined)
1814
+ return failed('param_missing');
1815
+ const slot = this.throughConditional(this.throughConstraint(declared));
1816
+ if (slot === VARIADIC)
1817
+ return unchecked('handler_untyped');
1818
+ const paths = [request.path, request.url, request.method].filter((p) => p !== undefined);
1819
+ let best = failed('param_missing');
1820
+ for (const part of this.parts(slot)) {
1821
+ for (const handed of part.getCallSignatures()) {
1822
+ const given = this.parameterAt(handed, request.param);
1823
+ if (given === undefined || given === VARIADIC)
1824
+ continue;
1825
+ const resolves = paths.every(members => {
1826
+ let at = given;
1827
+ for (const member of members) {
1828
+ const property = this.declaredProperty(at, member);
1829
+ if (!property)
1830
+ return false;
1831
+ at = this.checker.getTypeOfSymbol(property);
1832
+ }
1833
+ return true;
1834
+ });
1835
+ if (resolves)
1836
+ return undefined;
1837
+ best = failed('request_member_missing');
1838
+ }
1839
+ }
1840
+ return best;
1841
+ }
1683
1842
  /**
1684
1843
  * The declarations spell `name` in one of `member`'s parameters: an
1685
1844
  * overload whose parameter there is that literal, or a key of the map a