carrick 0.3.134 → 0.3.135

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.134",
3
+ "version": "0.3.135",
4
4
  "carrickCache": {
5
5
  "cache_version": 22,
6
6
  "intent_cache_version": 2
@@ -114,11 +114,11 @@
114
114
  "zod": "^3.23.0"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@carrick-tools/cli-darwin-arm64": "0.3.134",
118
- "@carrick-tools/cli-darwin-x64": "0.3.134",
119
- "@carrick-tools/cli-linux-arm64": "0.3.134",
120
- "@carrick-tools/cli-linux-x64": "0.3.134",
121
- "@carrick-tools/cli-win32-x64": "0.3.134"
117
+ "@carrick-tools/cli-darwin-arm64": "0.3.135",
118
+ "@carrick-tools/cli-darwin-x64": "0.3.135",
119
+ "@carrick-tools/cli-linux-arm64": "0.3.135",
120
+ "@carrick-tools/cli-linux-x64": "0.3.135",
121
+ "@carrick-tools/cli-win32-x64": "0.3.135"
122
122
  },
123
123
  "devDependencies": {
124
124
  "@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.134",
4
+ "version": "0.3.135",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -35,6 +35,7 @@ import { findAugmentationFiles } from './augmentations.js';
35
35
  import { installedVersions, lockfileVersions } from './lockfile.js';
36
36
  import { installedResolutionEdges, RESOLUTION_FILE } from './resolution-edges.js';
37
37
  import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
38
+ import { substituteMachinePathImports } from './machine-paths.js';
38
39
  import { installedPackageSpecifier, typesPackageOf, withInstalledPackages } from './installed-package.js';
39
40
  import { selfCheckStub } from './self-check.js';
40
41
  import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
@@ -448,6 +449,23 @@ export function captureStub(opts) {
448
449
  entryDir,
449
450
  });
450
451
  const specifierRewrites = denoRewrites + placed.rewrites + rewritten.rewrites;
452
+ // What the rewrite could not map still names a machine path (carrick#2943).
453
+ const machinePaths = substituteMachinePathImports({
454
+ files: emittedFiles.map((rel) => path.join(typesDir, rel)),
455
+ surfaceAbsPath,
456
+ tree: stubGuard.narrow(typesDir),
457
+ labelRoot: opts.scanRoot ?? repoRoot,
458
+ });
459
+ if (machinePaths.size > 0) {
460
+ resolved = resolved.map((anchor) => {
461
+ const found = machinePaths.get(anchor.request.alias);
462
+ if (!found)
463
+ return anchor;
464
+ const paths = [...new Set([...(anchor.unresolved?.paths ?? []), ...found.paths])];
465
+ const specifiers = [...new Set([...found.modules, ...(anchor.unresolved?.specifiers ?? [])])];
466
+ return { ...anchor, unresolved: { paths, specifiers } };
467
+ });
468
+ }
451
469
  // ---- Pin external deps: installed node_modules first, lockfile fallback ----
452
470
  // Externals are collected AFTER the rewrite pass: a rewritten paths
453
471
  // specifier is internal, not a dependency.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * No stub names a module by a machine path (carrick#2943).
3
+ *
4
+ * The specifier rewrite maps an absolute specifier onto a file of the stub's
5
+ * tree or onto an installed package. What it cannot map stays as printed: a
6
+ * type that a printer could name only by where its module sits on this
7
+ * machine, such as a workspace sibling's source reached through a
8
+ * `node_modules` link, which the declaration emit does not carry. That path
9
+ * resolves on the machine that ran the capture, to a source file outside the
10
+ * stub, and nowhere else: the upload rewrites it to `<checkout>/...`, so the
11
+ * stored stub and a fresh one disagree about the same alias.
12
+ *
13
+ * Each such reference becomes `unknown` at its own position. In the surface
14
+ * the position is reported per alias, so the self-check labels the finding
15
+ * there as a reference that did not resolve rather than as declared; the rest
16
+ * of the alias's type survives. Any other tree file gets the dangling-import
17
+ * repair (carrick#1397), which writes `unknown` where the import's names were
18
+ * used. The stub then reads the same on every machine.
19
+ *
20
+ * Seam: node builtins + `typescript`, like the rest of this directory.
21
+ */
22
+ import type { WriteGuard } from './guarded-fs.js';
23
+ /** A specifier that names a module by where it sits on one machine. */
24
+ export declare function isMachinePath(specifier: string): boolean;
25
+ /** What the surface substitution replaced in one alias. */
26
+ export interface MachinePathPositions {
27
+ /** Member paths now `unknown`, in the deep walk's notation (`''` = root). */
28
+ paths: string[];
29
+ /** The modules they named, relative to `labelRoot` (never absolute). */
30
+ modules: string[];
31
+ }
32
+ /**
33
+ * Replace every machine-path import in the stub's tree with `unknown`.
34
+ * `files` are absolute tree paths; `surfaceAbsPath` is one of them. Returns
35
+ * the positions replaced in the surface, by alias.
36
+ */
37
+ export declare function substituteMachinePathImports(args: {
38
+ files: readonly string[];
39
+ surfaceAbsPath: string;
40
+ /** A guard over the stub's types tree. */
41
+ tree: WriteGuard;
42
+ /** The root a module is named from in a label (the scanned repo's). */
43
+ labelRoot: string;
44
+ }): Map<string, MachinePathPositions>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * No stub names a module by a machine path (carrick#2943).
3
+ *
4
+ * The specifier rewrite maps an absolute specifier onto a file of the stub's
5
+ * tree or onto an installed package. What it cannot map stays as printed: a
6
+ * type that a printer could name only by where its module sits on this
7
+ * machine, such as a workspace sibling's source reached through a
8
+ * `node_modules` link, which the declaration emit does not carry. That path
9
+ * resolves on the machine that ran the capture, to a source file outside the
10
+ * stub, and nowhere else: the upload rewrites it to `<checkout>/...`, so the
11
+ * stored stub and a fresh one disagree about the same alias.
12
+ *
13
+ * Each such reference becomes `unknown` at its own position. In the surface
14
+ * the position is reported per alias, so the self-check labels the finding
15
+ * there as a reference that did not resolve rather than as declared; the rest
16
+ * of the alias's type survives. Any other tree file gets the dangling-import
17
+ * repair (carrick#1397), which writes `unknown` where the import's names were
18
+ * used. The stub then reads the same on every machine.
19
+ *
20
+ * Seam: node builtins + `typescript`, like the rest of this directory.
21
+ */
22
+ import ts from 'typescript';
23
+ import * as fs from 'node:fs';
24
+ import * as path from 'node:path';
25
+ import { childPath } from './node-builder.js';
26
+ import { repairDanglingImports } from './repair-dangling.js';
27
+ import { collectSpecifiers } from './specifiers.js';
28
+ /** A specifier that names a module by where it sits on one machine. */
29
+ export function isMachinePath(specifier) {
30
+ return path.posix.isAbsolute(specifier) || /^[A-Za-z]:[\\/]/.test(specifier) || specifier.startsWith('\\\\');
31
+ }
32
+ /**
33
+ * Replace every machine-path import in the stub's tree with `unknown`.
34
+ * `files` are absolute tree paths; `surfaceAbsPath` is one of them. Returns
35
+ * the positions replaced in the surface, by alias.
36
+ */
37
+ export function substituteMachinePathImports(args) {
38
+ const failing = new Map();
39
+ for (const file of args.files) {
40
+ if (path.resolve(file) === path.resolve(args.surfaceAbsPath))
41
+ continue;
42
+ const machine = [...collectSpecifiers(fs.readFileSync(file, 'utf8'))].filter(isMachinePath);
43
+ if (machine.length > 0)
44
+ failing.set(file, new Set(machine));
45
+ }
46
+ if (failing.size > 0)
47
+ repairDanglingImports(failing, args.tree);
48
+ return substituteInSurface(args.surfaceAbsPath, args.tree, args.labelRoot);
49
+ }
50
+ function substituteInSurface(surfaceAbsPath, tree, labelRoot) {
51
+ const byAlias = new Map();
52
+ const text = fs.readFileSync(surfaceAbsPath, 'utf8');
53
+ if (!/import\s*\(/.test(text))
54
+ return byAlias;
55
+ const source = ts.createSourceFile(surfaceAbsPath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
56
+ const edits = [];
57
+ for (const statement of source.statements) {
58
+ if (!ts.isTypeAliasDeclaration(statement))
59
+ continue;
60
+ const found = { paths: [], modules: [] };
61
+ const visit = (node, at) => {
62
+ const specifier = importTypeSpecifier(node);
63
+ if (specifier !== undefined && isMachinePath(specifier)) {
64
+ edits.push({ start: node.getStart(source), end: node.getEnd() });
65
+ if (!found.paths.includes(at))
66
+ found.paths.push(at);
67
+ const module = moduleLabel(specifier, labelRoot);
68
+ if (!found.modules.includes(module))
69
+ found.modules.push(module);
70
+ return;
71
+ }
72
+ node.forEachChild((child) => visit(child, childPath(node, child, at)));
73
+ };
74
+ visit(statement.type, '');
75
+ if (found.paths.length > 0)
76
+ byAlias.set(statement.name.text, found);
77
+ }
78
+ if (edits.length === 0)
79
+ return byAlias;
80
+ edits.sort((a, b) => b.start - a.start);
81
+ let out = text;
82
+ for (const edit of edits)
83
+ out = `${out.slice(0, edit.start)}unknown${out.slice(edit.end)}`;
84
+ tree.writeFile(surfaceAbsPath, out);
85
+ return byAlias;
86
+ }
87
+ /** The string an `import("...")` type names, when `node` is one. */
88
+ function importTypeSpecifier(node) {
89
+ if (!ts.isImportTypeNode(node) || !ts.isLiteralTypeNode(node.argument))
90
+ return undefined;
91
+ const literal = node.argument.literal;
92
+ return ts.isStringLiteral(literal) ? literal.text : undefined;
93
+ }
94
+ /** `specifier` relative to `root`, with forward slashes. */
95
+ function moduleLabel(specifier, root) {
96
+ return path.relative(path.resolve(root), path.resolve(specifier)).split(path.sep).join('/');
97
+ }
@@ -67,6 +67,8 @@ export declare function substituteUndeclaredNames(node: ts.TypeNode, program: ts
67
67
  node: ts.TypeNode;
68
68
  substitutions: UndeclaredSubstitution[];
69
69
  };
70
+ /** The deep walk's path for `child` inside `parent`, extending `path`. */
71
+ export declare function childPath(parent: ts.Node, child: ts.Node, path: string): string;
70
72
  /**
71
73
  * Bare names in a printed type node that nothing in the producer's program
72
74
  * declares (carrick#1165).
@@ -174,7 +174,7 @@ export function substituteUndeclaredNames(node, program, destination) {
174
174
  return { node: rewritten, substitutions };
175
175
  }
176
176
  /** The deep walk's path for `child` inside `parent`, extending `path`. */
177
- function childPath(parent, child, path) {
177
+ export function childPath(parent, child, path) {
178
178
  if (ts.isPropertySignature(parent) && parent.type === child) {
179
179
  const name = ts.isIdentifier(parent.name) || ts.isStringLiteral(parent.name)
180
180
  ? parent.name.text
@@ -637,7 +637,7 @@ function handleListLibrarySurface(request) {
637
637
  const { claimsVerifier } = projectComponents();
638
638
  const fromDir = path.resolve(projectLoader.getRepoRoot(), request.from_dir);
639
639
  log(`Listing the declared surface of ${request.packages.length} specifier(s) from ${fromDir}`);
640
- const { surfaces, surface_sha256 } = claimsVerifier.listSurface(fromDir, request.packages, request.max_entries ?? SURFACE_MAX_ENTRIES, request.exports ?? {});
640
+ const { surfaces, surface_sha256 } = claimsVerifier.listSurface(fromDir, request.packages, request.max_entries ?? SURFACE_MAX_ENTRIES, request.exports ?? {}, { handlers: request.handlers, resultCandidates: request.result_candidates });
641
641
  return { request_id: request.request_id, status: 'success', surfaces, surface_sha256 };
642
642
  }
643
643
  catch (err) {
@@ -50,6 +50,11 @@ type Outcome = {
50
50
  */
51
51
  members?: readonly ts.Declaration[];
52
52
  bodyMembers?: readonly ts.Declaration[];
53
+ /**
54
+ * Stamped: why a receive op's `request` was dropped while the op
55
+ * itself verified (carrick#2968). Only `request_member_missing`.
56
+ */
57
+ requestDropped?: string;
53
58
  } | {
54
59
  verdict: 'failed' | 'unchecked';
55
60
  reason: string;
@@ -86,6 +91,15 @@ export interface ModuleGraph {
86
91
  /** How the program resolves `name` from `from`; `undefined` where there is no graph, and Node resolution answers. */
87
92
  resolveModule(name: string, from: string, options: ts.CompilerOptions): ts.ResolvedModuleFull | undefined;
88
93
  }
94
+ /**
95
+ * Test instrumentation, never on the wire: how many times the lister built a
96
+ * handler parameter's members and printed a type, since the caller last
97
+ * reset them (carrick#2977).
98
+ */
99
+ export declare const surfaceReadCounts: {
100
+ handlerMemberBuilds: number;
101
+ typePrints: number;
102
+ };
89
103
  export declare class LibraryClaimsVerifier {
90
104
  private readonly project;
91
105
  private readonly graph;
@@ -124,9 +138,20 @@ export declare class LibraryClaimsVerifier {
124
138
  * (`receiverExpression` on the export's `importLine`, read through
125
139
  * `DeclarationReader.returnOf`) and lists from it.
126
140
  *
141
+ * A member of an `instance:` receiver whose call returns another object
142
+ * that routes are registered on lists that object as its `scope_receiver`
143
+ * (carrick#2774), built as the verifier builds a scope step, under a cap of
144
+ * its own and left out of the hash.
145
+ *
146
+ * Each member's handler-parameter members (carrick#2674 S3) are listed
147
+ * only when `options.handlers` is set, and each signature's result
148
+ * candidates (carrick#2817) only when `options.resultCandidates` is
149
+ * (carrick#2966): on a large package either costs more than the rest of the
150
+ * listing.
151
+ *
127
152
  * `surface_sha256` is the full-surface hash (see `fullSurfaceSha256`).
128
153
  */
129
- listSurface(fromDir: string, packages: string[], maxEntries: number, only?: Readonly<Record<string, string[]>>): ListedSurface;
154
+ listSurface(fromDir: string, packages: string[], maxEntries: number, only?: Readonly<Record<string, string[]>>, options?: SurfaceOptions): ListedSurface;
130
155
  /** `read` over a probe file holding `lines` in `fromDir`, removed again afterwards. */
131
156
  private readProbe;
132
157
  }
@@ -140,8 +165,8 @@ export interface ListedSurface {
140
165
  * array of `[package, exports]` for every surface that listed at least one
141
166
  * export, sorted by `package` (code unit order). A specifier that lists
142
167
  * nothing (unresolved, local, no value exports) is left out, so asking for a
143
- * subpath a version does not have changes nothing. Handler members are not
144
- * hashed. Type text already names
168
+ * subpath a version does not have changes nothing. Handler members, result
169
+ * candidates and scope receivers are not hashed. Type text already names
145
170
  * the request's directory as `<root>`.
146
171
  */
147
172
  export declare function fullSurfaceSha256(surfaces: readonly LibrarySurface[]): string;
@@ -177,4 +202,11 @@ export interface LabelledMembers {
177
202
  /** How each label read came out (`verified`, or the reason it was not), for the log. */
178
203
  outcomes?: Readonly<Record<string, number>>;
179
204
  }
205
+ /** What a surface listing adds beyond the hashed surface, each only when asked (carrick#2966). */
206
+ export interface SurfaceOptions {
207
+ /** Each member's `handlers` (carrick#2674 S3). */
208
+ handlers?: boolean;
209
+ /** Each signature's `result_candidates` (carrick#2817). */
210
+ resultCandidates?: boolean;
211
+ }
180
212
  export {};
@@ -259,6 +259,12 @@ function sortedDeclarations(declarations) {
259
259
  }
260
260
  let probeSequence = 0;
261
261
  const NO_MODULE_GRAPH = { packageDirectoryOf: () => undefined, resolveModule: () => undefined };
262
+ /**
263
+ * Test instrumentation, never on the wire: how many times the lister built a
264
+ * handler parameter's members and printed a type, since the caller last
265
+ * reset them (carrick#2977).
266
+ */
267
+ export const surfaceReadCounts = { handlerMemberBuilds: 0, typePrints: 0 };
262
268
  export class LibraryClaimsVerifier {
263
269
  project;
264
270
  graph;
@@ -281,6 +287,7 @@ export class LibraryClaimsVerifier {
281
287
  receiver: check.receiver,
282
288
  verdict: 'verified',
283
289
  ...(outcome.declarations?.length ? { declarations: outcome.declarations } : {}),
290
+ ...(outcome.requestDropped === undefined ? {} : { request_dropped: outcome.requestDropped }),
284
291
  }
285
292
  : {
286
293
  claim_id: check.claim_id,
@@ -587,6 +594,8 @@ export class LibraryClaimsVerifier {
587
594
  const declarations = [];
588
595
  const members = [];
589
596
  const bodyMembers = [];
597
+ // A request one receiver does not hold is dropped for the op (carrick#2968).
598
+ let requestDropped;
590
599
  for (const receiver of receivers.value) {
591
600
  const result = reader.judgeMessage(receiver, check.claim, rules, builtAtDefaults, collectionsOf);
592
601
  if (result.outcome.verdict !== 'verified')
@@ -595,11 +604,14 @@ export class LibraryClaimsVerifier {
595
604
  declarations.push(...(result.outcome.declarations ?? []));
596
605
  members.push(...(result.outcome.members ?? []));
597
606
  bodyMembers.push(...(result.outcome.bodyMembers ?? []));
607
+ requestDropped ??= result.outcome.requestDropped;
598
608
  }
599
609
  const verified = declarations.length > 0 ? { verdict: 'verified', declarations: sortedDeclarations(declarations) } : { ...VERIFIED };
600
610
  if (verified.verdict === 'verified' && members.length > 0)
601
611
  verified.members = members;
602
- if (verified.verdict === 'verified' && bodyMembers.length > 0)
612
+ if (verified.verdict === 'verified' && requestDropped !== undefined)
613
+ verified.requestDropped = requestDropped;
614
+ else if (verified.verdict === 'verified' && bodyMembers.length > 0)
603
615
  verified.bodyMembers = bodyMembers;
604
616
  return { outcome: verified, holding };
605
617
  };
@@ -645,7 +657,11 @@ export class LibraryClaimsVerifier {
645
657
  const observe = (check, outcome) => {
646
658
  if (check.claim.kind !== 'op')
647
659
  return;
648
- const read = outcome.verdict === 'verified' ? 'verified' : `${outcome.verdict} ${outcome.reason}`;
660
+ const read = outcome.verdict !== 'verified'
661
+ ? `${outcome.verdict} ${outcome.reason}`
662
+ : outcome.requestDropped === undefined
663
+ ? 'verified'
664
+ : `verified request_dropped ${outcome.requestDropped}`;
649
665
  outcomes[read] = (outcomes[read] ?? 0) + 1;
650
666
  if (outcome.verdict !== 'verified')
651
667
  return;
@@ -678,9 +694,20 @@ export class LibraryClaimsVerifier {
678
694
  * (`receiverExpression` on the export's `importLine`, read through
679
695
  * `DeclarationReader.returnOf`) and lists from it.
680
696
  *
697
+ * A member of an `instance:` receiver whose call returns another object
698
+ * that routes are registered on lists that object as its `scope_receiver`
699
+ * (carrick#2774), built as the verifier builds a scope step, under a cap of
700
+ * its own and left out of the hash.
701
+ *
702
+ * Each member's handler-parameter members (carrick#2674 S3) are listed
703
+ * only when `options.handlers` is set, and each signature's result
704
+ * candidates (carrick#2817) only when `options.resultCandidates` is
705
+ * (carrick#2966): on a large package either costs more than the rest of the
706
+ * listing.
707
+ *
681
708
  * `surface_sha256` is the full-surface hash (see `fullSurfaceSha256`).
682
709
  */
683
- listSurface(fromDir, packages, maxEntries, only = {}) {
710
+ listSurface(fromDir, packages, maxEntries, only = {}, options = {}) {
684
711
  if (packages.length === 0)
685
712
  return { surfaces: [], surface_sha256: fullSurfaceSha256([]) };
686
713
  // Each specifier twice: as a namespace, which holds its named exports, and
@@ -709,10 +736,12 @@ export class LibraryClaimsVerifier {
709
736
  const buildLines = [];
710
737
  const builtIndex = new Map();
711
738
  packages.forEach((pkg, i) => {
712
- for (const { export: name, receivers } of makers[i]) {
739
+ for (const { export: name, receivers, scopes } of makers[i]) {
713
740
  const local = `__carrick_e${importLines.length}`;
714
741
  importLines.push(probeLine(pkg, name, local));
715
- for (const receiver of receivers) {
742
+ // The scope receivers a maker's members may return (carrick#2774) are
743
+ // built beside the makers, so `listPackage` reads them as it reads those.
744
+ for (const receiver of [...receivers, ...scopes]) {
716
745
  const expression = receiverExpression(local, receiver);
717
746
  if (expression === undefined)
718
747
  continue;
@@ -722,7 +751,7 @@ export class LibraryClaimsVerifier {
722
751
  }
723
752
  });
724
753
  const firstBuild = imports.length + importLines.length;
725
- const surfaces = this.readProbe(fromDir, [...imports, ...importLines, ...buildLines], (checker, reader, file) => packages.map((pkg, i) => reader.listPackage(pkg, probeOf(file, i), maxEntries, only[pkg], (name, receiver) => {
754
+ const surfaces = this.readProbe(fromDir, [...imports, ...importLines, ...buildLines], (checker, reader, file) => packages.map((pkg, i) => reader.listPackage(pkg, probeOf(file, i), maxEntries, only[pkg], options, (name, receiver) => {
726
755
  const index = builtIndex.get(builtKey({ package: pkg, export: name }, receiver));
727
756
  return index === undefined ? undefined : resolvedBuild(checker, file.statements[firstBuild + index]);
728
757
  })));
@@ -763,8 +792,8 @@ function resolvedBuild(checker, statement) {
763
792
  * array of `[package, exports]` for every surface that listed at least one
764
793
  * export, sorted by `package` (code unit order). A specifier that lists
765
794
  * nothing (unresolved, local, no value exports) is left out, so asking for a
766
- * subpath a version does not have changes nothing. Handler members are not
767
- * hashed. Type text already names
795
+ * subpath a version does not have changes nothing. Handler members, result
796
+ * candidates and scope receivers are not hashed. Type text already names
768
797
  * the request's directory as `<root>`.
769
798
  */
770
799
  export function fullSurfaceSha256(surfaces) {
@@ -772,10 +801,11 @@ export function fullSurfaceSha256(surfaces) {
772
801
  .filter(surface => surface.reason === undefined && surface.exports.length > 0)
773
802
  .map(surface => [surface.package, surface.exports])
774
803
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
775
- // A member's `handlers` (carrick#2674) and a signature's `result_candidates`
776
- // (carrick#2817) are out of the hash at every depth, so a package's key does
777
- // not change when the lister learns to list them.
778
- const text = JSON.stringify(listed, (key, value) => key === 'handlers' || key === 'result_candidates' ? undefined : value);
804
+ // A member's `handlers` (carrick#2674), a signature's `result_candidates`
805
+ // (carrick#2817) and a member's `scope_receiver` (carrick#2774) are out of
806
+ // the hash at every depth, so a package's key does not change when the
807
+ // lister learns to list them.
808
+ const text = JSON.stringify(listed, (key, value) => key === 'handlers' || key === 'result_candidates' || key === 'scope_receiver' ? undefined : value);
779
809
  return crypto.createHash('sha256').update(text).digest('hex');
780
810
  }
781
811
  function exportKey(check) {
@@ -928,6 +958,18 @@ function receiverExpression(local, receiver) {
928
958
  text = `${text}${parsed.scope.split('.').map(access).join('')}()`;
929
959
  return text;
930
960
  }
961
+ /**
962
+ * The id of the receiver `member` of the receiver `receiver` returns when
963
+ * called with a path (carrick#2774): `receiver>scope:member`. Undefined for a
964
+ * receiver that is not an instance, or a member name the verifier's grammar
965
+ * does not read back as one scope step.
966
+ */
967
+ function scopeReceiverId(receiver, member) {
968
+ if (!receiver.startsWith('instance:') || receiver.includes('>') || /[>.\s]/.test(member))
969
+ return undefined;
970
+ const id = `${receiver}>scope:${member}`;
971
+ return parseReceiver(id)?.scope === member ? id : undefined;
972
+ }
931
973
  /** The symbols that say who declared a type: its own, its alias's, its parts'. */
932
974
  function typeSymbols(type) {
933
975
  const symbols = [type.getSymbol(), type.aliasSymbol];
@@ -1020,8 +1062,24 @@ class DeclarationReader {
1020
1062
  currentPackage = '';
1021
1063
  /** `declaredProperties` per type, for `currentPackage` (its augmentations decide them). */
1022
1064
  declared = new Map();
1065
+ /**
1066
+ * A handler parameter's listed members per type (`handlerMembersOf`), kept
1067
+ * across every specifier of one package and cleared with `declared` when
1068
+ * the package changes: a member's listing reads that package's
1069
+ * augmentations (carrick#2977).
1070
+ */
1071
+ handlerMembers = new Map();
1072
+ /** `takesString` per signature, and `scopeMembers` per type and home: the same types recur across receivers. */
1073
+ takesStringRead = new Map();
1074
+ scopeMembersRead = new Map();
1075
+ /** Handler-parameter members `HANDLER_MEMBER_CAP` dropped in the package being listed. */
1076
+ handlersDropped = 0;
1077
+ /** Whether the package being listed lists each signature's `result_candidates`. */
1078
+ listResultCandidates = false;
1023
1079
  /** The service root as given and as a realpath, longest first: what a listing prints as `<root>`. */
1024
1080
  printedRoots;
1081
+ /** `printed` per type: its text reads nothing but the type and `printedRoots` (carrick#2977). */
1082
+ printedTexts = new Map();
1025
1083
  constructor(program, probe, host, serviceRoot, scanRoot, graph) {
1026
1084
  this.program = program;
1027
1085
  this.probe = probe;
@@ -2045,6 +2103,10 @@ class DeclarationReader {
2045
2103
  const labels = claim.key_labels ?? {};
2046
2104
  let best;
2047
2105
  let held = false;
2106
+ // An overload that holds but for a request member the declarations lack
2107
+ // (carrick#2968): the op stands without its request, unless another
2108
+ // overload holds the request too. Every other request fault fails the op.
2109
+ let requestDropped;
2048
2110
  const bodyMembers = [];
2049
2111
  for (const signature of callee.signatures) {
2050
2112
  let result = this.messageSignatureOutcome(signature, layout.value, labels);
@@ -2055,19 +2117,26 @@ class DeclarationReader {
2055
2117
  }
2056
2118
  if (result.rank === Infinity && claim.request !== undefined) {
2057
2119
  const read = this.requestOutcome(signature, claim.handler, claim.request, bodyMembers);
2120
+ if (read?.verdict === 'failed' && read.reason === 'request_member_missing') {
2121
+ requestDropped = read.reason;
2122
+ continue;
2123
+ }
2058
2124
  if (read)
2059
2125
  result = { rank: 5, outcome: read };
2060
2126
  }
2061
2127
  if (result.rank === Infinity) {
2062
2128
  held = true;
2129
+ requestDropped = undefined;
2063
2130
  break;
2064
2131
  }
2065
2132
  best = furthest(best, result.rank, result.outcome);
2066
2133
  }
2067
- if (!held)
2134
+ if (!held && requestDropped === undefined)
2068
2135
  return best.outcome;
2069
- if (boundRuns)
2070
- return this.boundRunsOutcome(receiver.makerHolding, boundRuns);
2136
+ if (boundRuns) {
2137
+ const ran = this.boundRunsOutcome(receiver.makerHolding, boundRuns);
2138
+ return ran.verdict === 'verified' && requestDropped !== undefined ? { ...ran, requestDropped } : ran;
2139
+ }
2071
2140
  const declarations = this.declarationsOf(callee);
2072
2141
  const members = callee.named
2073
2142
  ? (callee.named.property.declarations ?? [])
@@ -2077,6 +2146,7 @@ class DeclarationReader {
2077
2146
  ...(declarations.length > 0 ? { declarations } : {}),
2078
2147
  ...(members.length > 0 ? { members } : {}),
2079
2148
  ...(bodyMembers.length > 0 ? { bodyMembers } : {}),
2149
+ ...(requestDropped === undefined ? {} : { requestDropped }),
2080
2150
  };
2081
2151
  }
2082
2152
  /**
@@ -3011,7 +3081,7 @@ class DeclarationReader {
3011
3081
  * `built` answers the probe's no-argument build of a receiver an export
3012
3082
  * makes, resolved, when the probe holds one.
3013
3083
  */
3014
- listPackage(pkg, probe, maxEntries, only, built) {
3084
+ listPackage(pkg, probe, maxEntries, only, options, built) {
3015
3085
  this.setPackage(pkg);
3016
3086
  // Runtime modules on: a `node:` specifier is listed from the runtime's
3017
3087
  // types package, as the message roles read it.
@@ -3036,24 +3106,24 @@ class DeclarationReader {
3036
3106
  dropped += 1;
3037
3107
  return false;
3038
3108
  };
3039
- // Handler-parameter members (carrick#2674) have a cap of their own, so they
3040
- // take nothing from the main one: the listing, `truncated` and the hash are
3041
- // the ones the lister gave before handlers were listed.
3042
- let handlerBudget = maxEntries;
3043
- const takeHandler = () => {
3044
- if (handlerBudget <= 0)
3045
- return false;
3046
- handlerBudget -= 1;
3047
- return true;
3048
- };
3109
+ // Handler-parameter members (carrick#2674) take nothing from the main cap,
3110
+ // so the listing, `truncated` and the hash are the ones the lister gave
3111
+ // before handlers were listed; each parameter has a cap of its own
3112
+ // (`HANDLER_MEMBER_CAP`, carrick#2966).
3113
+ this.handlersDropped = 0;
3114
+ this.listResultCandidates = options.resultCandidates === true;
3115
+ const withHandlers = options.handlers === true;
3049
3116
  // Four passes: every export and receiver, then every member name, then the
3050
- // members of each handler's parameters (carrick#2674), then signatures.
3117
+ // members of each handler's parameters (carrick#2674; only when asked,
3118
+ // carrick#2966), then signatures.
3051
3119
  const names = [];
3052
3120
  const handlers = [];
3053
3121
  const fills = [];
3054
3122
  // What the package's default export names: a maker also exported by name
3055
3123
  // under this declaration is imported the same way, so it is read the same.
3056
3124
  const defaultTarget = exported.find(each => each.name === 'default')?.target;
3125
+ // The instance receivers listed, for the scope pass (carrick#2774).
3126
+ const instances = [];
3057
3127
  for (const { name, target, type } of exported) {
3058
3128
  if (!take())
3059
3129
  continue;
@@ -3065,7 +3135,11 @@ class DeclarationReader {
3065
3135
  if (!take())
3066
3136
  return;
3067
3137
  const home = this.homeOf(pkg, [target, ...typeSymbols(receiverType)]);
3068
- entry.receivers.push(this.outlineReceiver(pkg, receiverName, receiverType, home, take, takeHandler, names, handlers, fills));
3138
+ const receiver = this.outlineReceiver(pkg, receiverName, receiverType, home, take, names, handlers, fills);
3139
+ entry.receivers.push(receiver);
3140
+ if (receiverName.startsWith('instance:')) {
3141
+ instances.push({ export: name, target, type: receiverType, home, receiver });
3142
+ }
3069
3143
  };
3070
3144
  receiverOf('export', type);
3071
3145
  for (const maker of this.makers(pkg, target, type)) {
@@ -3082,13 +3156,57 @@ class DeclarationReader {
3082
3156
  }
3083
3157
  for (const run of names)
3084
3158
  run();
3085
- for (const run of handlers)
3086
- run();
3159
+ if (withHandlers)
3160
+ for (const run of handlers)
3161
+ run();
3087
3162
  for (const fill of fills)
3088
3163
  fill();
3164
+ this.listScopeReceivers(pkg, instances, maxEntries, withHandlers, built);
3089
3165
  surface.truncated = dropped;
3166
+ if (withHandlers)
3167
+ surface.handlers_truncated = this.handlersDropped;
3090
3168
  return surface;
3091
3169
  }
3170
+ /**
3171
+ * The fifth pass (carrick#2774): each own member of an instance receiver
3172
+ * whose call returns the object routes are registered on lists it as
3173
+ * `scope_receiver`, outlined as any receiver is. Everything it lists draws
3174
+ * on one budget of its own, spent after every other pass, so the listing
3175
+ * without it, `truncated` and the hash are the ones the lister gave before.
3176
+ * One level: a scope receiver lists no scope receiver of its own.
3177
+ */
3178
+ listScopeReceivers(pkg, sources, maxEntries, withHandlers, built) {
3179
+ let budget = maxEntries;
3180
+ const take = () => budget > 0 && (budget--, true);
3181
+ const names = [];
3182
+ const handlers = [];
3183
+ const fills = [];
3184
+ for (const source of sources) {
3185
+ const holder = this.surfaceHolder(pkg, source.type, source.home);
3186
+ for (const member of source.receiver.members) {
3187
+ const id = member.own ? scopeReceiverId(source.receiver.receiver, member.name) : undefined;
3188
+ if (id === undefined)
3189
+ continue;
3190
+ const callee = this.ownCallee(holder, [], member.name, 'call');
3191
+ if (!('signatures' in callee))
3192
+ continue;
3193
+ if (!callee.signatures.some(signature => this.takesString(signature)))
3194
+ continue;
3195
+ const type = this.scopeTypeOf(callee.signatures, built(source.export, id), source.type, source.home);
3196
+ if (type === undefined || !take())
3197
+ continue;
3198
+ const home = this.homeOf(pkg, [source.target, ...typeSymbols(type)]);
3199
+ member.scope_receiver = this.outlineReceiver(pkg, id, type, home, take, names, handlers, fills);
3200
+ }
3201
+ }
3202
+ for (const run of names)
3203
+ run();
3204
+ if (withHandlers)
3205
+ for (const run of handlers)
3206
+ run();
3207
+ for (const fill of fills)
3208
+ fill();
3209
+ }
3092
3210
  /**
3093
3211
  * The receivers each export of a specifier can make, by receiver id, for
3094
3212
  * the probe statements that build them (see `LibraryClaimsVerifier.listSurface`).
@@ -3100,12 +3218,157 @@ class DeclarationReader {
3100
3218
  return [];
3101
3219
  return (this.listedExports(probe, only, moduleRead?.globalFile) ?? [])
3102
3220
  .filter(({ type }) => !this.isOpenTop(type))
3103
- .map(({ name, target, type }) => ({
3104
- export: name,
3105
- receivers: this.makers(pkg, target, type).map(maker => maker.receiver),
3106
- }))
3221
+ .map(({ name, target, type }) => {
3222
+ const makers = this.makers(pkg, target, type);
3223
+ return {
3224
+ export: name,
3225
+ receivers: makers.map(maker => maker.receiver),
3226
+ scopes: this.scopeCandidates(pkg, target, makers),
3227
+ };
3228
+ })
3107
3229
  .filter(({ receivers }) => receivers.length > 0);
3108
3230
  }
3231
+ /**
3232
+ * The `R>scope:m` receivers an export's `instance:` makers may have
3233
+ * (carrick#2774): for each object type a maker's overloads declare as their
3234
+ * return, each own member that `scopeTypeOf` lists at its declared return.
3235
+ * A superset of the listing, which reads each at the type the probe's build
3236
+ * resolves; the second program builds these so `listPackage` can.
3237
+ */
3238
+ scopeCandidates(pkg, target, makers) {
3239
+ const found = new Set();
3240
+ for (const maker of makers) {
3241
+ if (!maker.receiver.startsWith('instance:'))
3242
+ continue;
3243
+ const types = new Set(maker.signatures.map(sig => this.checker.getReturnTypeOfSignature(sig)));
3244
+ for (const type of types) {
3245
+ if (this.returnSaysNothing(type) || !this.isObjectType(type))
3246
+ continue;
3247
+ const home = this.homeOf(pkg, [target, ...typeSymbols(type)]);
3248
+ for (const name of this.scopeMembers(pkg, type, home)) {
3249
+ const id = scopeReceiverId(maker.receiver, name);
3250
+ if (id !== undefined)
3251
+ found.add(id);
3252
+ }
3253
+ }
3254
+ }
3255
+ return [...found];
3256
+ }
3257
+ /** The members of `type` that list a scope receiver at their declared return, read once per type and home. */
3258
+ scopeMembers(pkg, type, home) {
3259
+ const key = [...home].sort().join('\u0000');
3260
+ let byHome = this.scopeMembersRead.get(type);
3261
+ if (byHome === undefined)
3262
+ this.scopeMembersRead.set(type, (byHome = new Map()));
3263
+ const read = byHome.get(key);
3264
+ if (read !== undefined)
3265
+ return read;
3266
+ const names = [];
3267
+ const holder = this.surfaceHolder(pkg, type, home);
3268
+ for (const property of sortedByName(this.namedProperties(type))) {
3269
+ if (!this.mayScope(property))
3270
+ continue;
3271
+ const callee = this.ownCallee(holder, [], property.getName(), 'call');
3272
+ if ('signatures' in callee && this.scopeTypeOf(callee.signatures, undefined, type, home) !== undefined) {
3273
+ names.push(property.getName());
3274
+ }
3275
+ }
3276
+ byHome.set(key, names);
3277
+ return names;
3278
+ }
3279
+ /** Some library signature of the member takes a string: the cheap superset of what `scopeTypeOf` asks first. */
3280
+ mayScope(property) {
3281
+ return this.librarySignatures(this.checker.getTypeOfSymbol(property), 'call').some(signature => this.takesString(signature));
3282
+ }
3283
+ /**
3284
+ * The object a scope member returns, as the verifier reads a scope step
3285
+ * (carrick#2774): what a route is registered on when the member is called
3286
+ * with a path. Undefined unless:
3287
+ * - some own signature takes a string, positionally or at a key of an
3288
+ * object parameter;
3289
+ * - the returns (at `built`'s defaults, less those that say nothing) are
3290
+ * all object types, none thenable, sharing one declared symbol that is
3291
+ * declared in the receiver's home and has a callable member of its own;
3292
+ * - that symbol and the receiver's share no declaration in the home
3293
+ * (`family`): the receiver itself (`basePath(): this`), a sibling or a
3294
+ * wrapper of one base is a builder's next link, not an object routes
3295
+ * are registered on.
3296
+ * The type is the return of the overload `built` resolves, when that is
3297
+ * one of the signatures that take a string, else the first of them.
3298
+ */
3299
+ scopeTypeOf(signatures, built, receiver, home) {
3300
+ const takers = signatures.filter(signature => this.takesString(signature));
3301
+ const returns = takers
3302
+ .map(signature => ({ signature, type: this.returnOf(signature, built) }))
3303
+ .filter(({ type }) => !this.returnSaysNothing(type));
3304
+ if (returns.length === 0)
3305
+ return undefined;
3306
+ const symbolOf = (type) => type.getSymbol() ?? type.aliasSymbol;
3307
+ const symbol = symbolOf(returns[0].type);
3308
+ if (symbol === undefined)
3309
+ return undefined;
3310
+ // The receiver itself, a sibling of it, or a wrapper built from the same
3311
+ // base (a fluent builder's `.default(x)`) is the same kind of thing, not
3312
+ // an object routes are registered on: abstain.
3313
+ const mine = this.family(receiver, home);
3314
+ if ([...this.family(returns[0].type, home)].some(each => mine.has(each)))
3315
+ return undefined;
3316
+ for (const { type } of returns) {
3317
+ if (!this.isObjectType(type) || symbolOf(type) !== symbol || this.isThenable(type))
3318
+ return undefined;
3319
+ }
3320
+ if (!(symbol.declarations ?? []).some(declaration => this.isOwnFile(declaration.getSourceFile(), home)))
3321
+ return undefined;
3322
+ const chosen = returns.find(({ signature }) => built !== undefined && this.instantiates(built, signature)) ?? returns[0];
3323
+ const callable = this.namedProperties(chosen.type).some(member => this.librarySignatures(this.checker.getTypeOfSymbol(member), 'call').length > 0);
3324
+ return callable ? chosen.type : undefined;
3325
+ }
3326
+ /** The type's own declaration and every base it extends or implements that its home declares. */
3327
+ family(type, home) {
3328
+ const found = new Set();
3329
+ const visit = (each, depth) => {
3330
+ const target = (each.objectFlags ?? 0) & ts.ObjectFlags.Reference ? each.target : each;
3331
+ const symbol = target.getSymbol();
3332
+ if (symbol === undefined || found.has(symbol) || depth > 12)
3333
+ return;
3334
+ if (!(symbol.declarations ?? []).some(declaration => this.isOwnFile(declaration.getSourceFile(), home)))
3335
+ return;
3336
+ found.add(symbol);
3337
+ if ((target.objectFlags ?? 0) & ts.ObjectFlags.ClassOrInterface) {
3338
+ for (const base of this.checker.getBaseTypes(target))
3339
+ visit(base, depth + 1);
3340
+ }
3341
+ };
3342
+ visit(type, 0);
3343
+ return found;
3344
+ }
3345
+ /** A parameter of the signature accepts a string, positionally or at a key of its object. */
3346
+ takesString(signature) {
3347
+ let read = this.takesStringRead.get(signature);
3348
+ if (read === undefined)
3349
+ this.takesStringRead.set(signature, (read = this.readTakesString(signature)));
3350
+ return read;
3351
+ }
3352
+ readTakesString(signature) {
3353
+ const count = signature.getParameters().length;
3354
+ for (let index = 0; index < count; index++) {
3355
+ const slot = this.keyParameterAt(signature, index);
3356
+ if (slot === undefined || slot === VARIADIC)
3357
+ continue;
3358
+ if (this.acceptsString(this.throughConditional(slot)))
3359
+ return true;
3360
+ if (this.isObjectType(slot) && this.handlerFailure(slot) !== undefined) {
3361
+ if (this.listKeys(slot, () => true).some(key => key.accepts_string))
3362
+ return true;
3363
+ }
3364
+ }
3365
+ return false;
3366
+ }
3367
+ /** The type has a callable `then`: awaiting it gives something else. */
3368
+ isThenable(type) {
3369
+ const then = this.checker.getPropertyOfType(type, 'then');
3370
+ return then !== undefined && this.checker.getNonNullableType(this.checker.getTypeOfSymbol(then)).getCallSignatures().length > 0;
3371
+ }
3109
3372
  /**
3110
3373
  * The value exports a specifier lists (every one, or only those `only`
3111
3374
  * names), sorted by name: what its namespace holds, and `default` for a
@@ -3232,7 +3495,7 @@ class DeclarationReader {
3232
3495
  * parameters next (`handlers`), once every name is; its call, construct and
3233
3496
  * member signatures last (`fills`), once every handler is.
3234
3497
  */
3235
- outlineReceiver(pkg, receiverName, type, home, take, takeHandler, names, handlers, fills) {
3498
+ outlineReceiver(pkg, receiverName, type, home, take, names, handlers, fills) {
3236
3499
  const receiver = { receiver: receiverName, members: [] };
3237
3500
  const holder = this.surfaceHolder(pkg, type, home);
3238
3501
  // The receiver's own call and construct signatures, as a claim with a
@@ -3260,7 +3523,7 @@ class DeclarationReader {
3260
3523
  const member = { name: property.getName(), own, signatures: [] };
3261
3524
  receiver.members.push(member);
3262
3525
  handlers.push(() => {
3263
- const listed = this.listHandlers(own ? callee.signatures : library, takeHandler);
3526
+ const listed = this.listHandlers(own ? callee.signatures : library);
3264
3527
  if (listed.length > 0)
3265
3528
  member.handlers = listed;
3266
3529
  });
@@ -3276,14 +3539,14 @@ class DeclarationReader {
3276
3539
  * A parameter that declares no member lists nothing, and so does a handler
3277
3540
  * none of whose parameters do.
3278
3541
  */
3279
- listHandlers(signatures, take) {
3542
+ listHandlers(signatures) {
3280
3543
  const listed = [];
3281
3544
  const seen = new Set();
3282
3545
  const add = (arg, key, slot) => {
3283
3546
  const place = JSON.stringify([arg, key ?? null]);
3284
3547
  if (seen.has(place))
3285
3548
  return;
3286
- const params = this.listHandlerParams(slot, take);
3549
+ const params = this.listHandlerParams(slot);
3287
3550
  if (params.length === 0)
3288
3551
  return;
3289
3552
  seen.add(place);
@@ -3315,8 +3578,8 @@ class DeclarationReader {
3315
3578
  }
3316
3579
  return listed;
3317
3580
  }
3318
- /** The parameters of a function type's first signature that declare members, each member counting against the cap. */
3319
- listHandlerParams(handler, take) {
3581
+ /** The parameters of a function type's first signature that declare members, each with its members (`handlerMembersOf`). */
3582
+ listHandlerParams(handler) {
3320
3583
  const slot = this.throughConditional(this.throughConstraint(handler));
3321
3584
  if (slot === VARIADIC)
3322
3585
  return [];
@@ -3328,32 +3591,73 @@ class DeclarationReader {
3328
3591
  const listed = [];
3329
3592
  signature.getParameters().forEach((parameter, index) => {
3330
3593
  const type = this.checker.getTypeOfSymbol(parameter);
3331
- if (this.namedProperties(type).length === 0 || !take())
3594
+ if (this.namedProperties(type).length === 0)
3332
3595
  return;
3333
- const members = [];
3334
- this.listMembers(type, [], members, take);
3596
+ const { members, dropped } = this.handlerMembersOf(type);
3597
+ this.handlersDropped += dropped;
3335
3598
  listed.push({ param: index, name: parameter.getName(), type: this.printed(type), members });
3336
3599
  });
3337
3600
  return listed;
3338
3601
  }
3339
- /** A type's declared members by path, parents before children, each level sorted by name, down to `HANDLER_MEMBER_DEPTH`. */
3340
- listMembers(type, prefix, into, take) {
3341
- for (const property of sortedByName(this.namedProperties(type))) {
3342
- if (!take())
3343
- continue;
3344
- const memberType = this.checker.getTypeOfSymbol(property);
3345
- const path = [...prefix, property.getName()];
3346
- const callable = this.librarySignatures(memberType, 'call').length > 0;
3347
- into.push({
3348
- path,
3349
- type: this.printed(memberType),
3350
- optional: (property.flags & ts.SymbolFlags.Optional) !== 0,
3351
- function: callable,
3352
- });
3353
- if (!callable && path.length < HANDLER_MEMBER_DEPTH && this.isObjectType(memberType)) {
3354
- this.listMembers(memberType, path, into, take);
3602
+ /**
3603
+ * A handler parameter's declared members by path, down to
3604
+ * `HANDLER_MEMBER_DEPTH`, at most `HANDLER_MEMBER_CAP` of them (carrick#2966).
3605
+ * The cap keeps every top-level member before any nested one, so a wide
3606
+ * parameter loses its deepest members, never a top-level one a later member
3607
+ * name sorts after. Kept members come back parents before children, each
3608
+ * level sorted by name. Read once per type in a package, across its
3609
+ * specifiers: a type many handlers share is listed once.
3610
+ */
3611
+ handlerMembersOf(type) {
3612
+ const known = this.handlerMembers.get(type);
3613
+ if (known)
3614
+ return known;
3615
+ surfaceReadCounts.handlerMemberBuilds += 1;
3616
+ const roots = [];
3617
+ let kept = 0;
3618
+ let dropped = 0;
3619
+ // Breadth first: a level is kept whole, in name order, before the next.
3620
+ let level = [{ type, prefix: [], into: roots }];
3621
+ while (level.length > 0) {
3622
+ const next = [];
3623
+ for (const { type: holder, prefix, into } of level) {
3624
+ for (const property of sortedByName(this.namedProperties(holder))) {
3625
+ if (kept >= HANDLER_MEMBER_CAP) {
3626
+ dropped += 1;
3627
+ continue;
3628
+ }
3629
+ kept += 1;
3630
+ const memberType = this.checker.getTypeOfSymbol(property);
3631
+ const path = [...prefix, property.getName()];
3632
+ const callable = this.librarySignatures(memberType, 'call').length > 0;
3633
+ const node = {
3634
+ member: {
3635
+ path,
3636
+ type: this.printed(memberType),
3637
+ optional: (property.flags & ts.SymbolFlags.Optional) !== 0,
3638
+ function: callable,
3639
+ },
3640
+ children: [],
3641
+ };
3642
+ into.push(node);
3643
+ if (!callable && path.length < HANDLER_MEMBER_DEPTH && this.isObjectType(memberType)) {
3644
+ next.push({ type: memberType, prefix: path, into: node.children });
3645
+ }
3646
+ }
3355
3647
  }
3648
+ level = next;
3356
3649
  }
3650
+ const members = [];
3651
+ const emit = (nodes) => {
3652
+ for (const node of nodes) {
3653
+ members.push(node.member);
3654
+ emit(node.children);
3655
+ }
3656
+ };
3657
+ emit(roots);
3658
+ const listed = { members, dropped };
3659
+ this.handlerMembers.set(type, listed);
3660
+ return listed;
3357
3661
  }
3358
3662
  listSignatures(signatures, holder, take) {
3359
3663
  const listed = [];
@@ -3391,7 +3695,7 @@ class DeclarationReader {
3391
3695
  params.push(param);
3392
3696
  }
3393
3697
  const entry = { params, returns: this.printed(this.checker.getReturnTypeOfSignature(signature)) };
3394
- const candidates = resultCandidatesOf(this.checker, signature);
3698
+ const candidates = this.listResultCandidates ? resultCandidatesOf(this.checker, signature) : undefined;
3395
3699
  if (candidates !== undefined)
3396
3700
  entry.result_candidates = candidates;
3397
3701
  listed.push(entry);
@@ -3441,9 +3745,15 @@ class DeclarationReader {
3441
3745
  * so where the cut falls does not depend on where the package is installed.
3442
3746
  */
3443
3747
  printed(type) {
3748
+ const known = this.printedTexts.get(type);
3749
+ if (known !== undefined)
3750
+ return known;
3751
+ surfaceReadCounts.typePrints += 1;
3444
3752
  const node = this.checker.typeToTypeNode(type, undefined, PRINT_FLAGS);
3445
3753
  const text = node === undefined ? this.checker.typeToString(type) : printTypeNode(this.orderUnions(node));
3446
- return truncate(this.scrubbed(text));
3754
+ const listed = truncate(this.scrubbed(text));
3755
+ this.printedTexts.set(type, listed);
3756
+ return listed;
3447
3757
  }
3448
3758
  /** `text` with the service root written as `<root>`. */
3449
3759
  scrubbed(text) {
@@ -3617,8 +3927,10 @@ class DeclarationReader {
3617
3927
  /** Judge the next check as a claim about `pkg`. */
3618
3928
  setPackage(pkg) {
3619
3929
  const named = packageNameOf(pkg);
3620
- if (named !== this.currentPackage)
3930
+ if (named !== this.currentPackage) {
3621
3931
  this.declared.clear();
3932
+ this.handlerMembers.clear();
3933
+ }
3622
3934
  this.currentPackage = named;
3623
3935
  }
3624
3936
  /**
@@ -3911,6 +4223,12 @@ class DeclarationReader {
3911
4223
  */
3912
4224
  /** The longest member path a handler parameter's members are listed to (`['raw', 'url']`). */
3913
4225
  const HANDLER_MEMBER_DEPTH = 2;
4226
+ /**
4227
+ * The most members one handler parameter lists (carrick#2966), top-level
4228
+ * first. The store's server question shows at most 300 of them in the same
4229
+ * order, so this must stay at least that.
4230
+ */
4231
+ const HANDLER_MEMBER_CAP = 500;
3914
4232
  const PRINT_FLAGS = ts.NodeBuilderFlags.AllowUniqueESSymbolType |
3915
4233
  ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope |
3916
4234
  ts.NodeBuilderFlags.IgnoreErrors |
@@ -679,6 +679,10 @@ export interface ListLibrarySurfaceRequest extends BaseRequest {
679
679
  max_entries?: number;
680
680
  /** Per package, the only exports to list (the ones the service imports); absent lists every value export. */
681
681
  exports?: Record<string, string[]>;
682
+ /** List each member's handler-parameter members (`handlers`, carrick#2674 S3); default false (carrick#2966). */
683
+ handlers?: boolean;
684
+ /** List each signature's `result_candidates` (carrick#2817); default false (carrick#2966). */
685
+ result_candidates?: boolean;
682
686
  }
683
687
  /**
684
688
  * Union type for all possible sidecar requests
@@ -982,6 +986,12 @@ export interface SemanticsResult {
982
986
  reason?: string;
983
987
  /** Verified op verdicts only: the declarations its member resolves to on the receiver. */
984
988
  declarations?: SemanticsDeclaration[];
989
+ /**
990
+ * Verified receive op verdicts only: the op verified but its `request`
991
+ * names a member the declarations lack, so the request is dropped and the
992
+ * route kept (carrick#2968). The reason, `request_member_missing`.
993
+ */
994
+ request_dropped?: string;
985
995
  }
986
996
  /** How one package of the request resolved, for logs. */
987
997
  export interface SemanticsModule {
@@ -1032,8 +1042,9 @@ export interface SurfaceSignature {
1032
1042
  returns: string;
1033
1043
  /**
1034
1044
  * Which parts of the awaited return could hold the response body
1035
- * (carrick#2817). Left out of the surface hash. Absent when the signature
1036
- * returns nothing.
1045
+ * (carrick#2817). Left out of the surface hash. Listed only on a
1046
+ * `result_candidates: true` request (carrick#2966); absent when the
1047
+ * signature returns nothing.
1037
1048
  */
1038
1049
  result_candidates?: SurfaceResultCandidates;
1039
1050
  }
@@ -1065,9 +1076,17 @@ export interface SurfaceMember {
1065
1076
  /**
1066
1077
  * The members the parameters of a function this member takes declare
1067
1078
  * (carrick#2674): what a receive op's `request` field names its member
1068
- * paths in. Under a cap of their own, apart from the main one.
1079
+ * paths in. Listed only on a `handlers: true` request, under a cap of
1080
+ * their own: 500 members per parameter, top-level first (carrick#2966).
1069
1081
  */
1070
1082
  handlers?: SurfaceHandler[];
1083
+ /**
1084
+ * The object this member returns when called with a path, listed as a
1085
+ * receiver (id `R>scope:m`) when routes are registered on it (carrick#2774).
1086
+ * Instance receivers only, one level, under a cap of its own, and left out
1087
+ * of the hash.
1088
+ */
1089
+ scope_receiver?: SurfaceReceiver;
1071
1090
  signatures: SurfaceSignature[];
1072
1091
  }
1073
1092
  /**
@@ -1116,6 +1135,11 @@ export interface LibrarySurface {
1116
1135
  reason?: string;
1117
1136
  /** Entries dropped by `max_entries`. */
1118
1137
  truncated: number;
1138
+ /**
1139
+ * With `handlers: true`, the handler-parameter members dropped by the cap of
1140
+ * 500 per parameter (carrick#2966), summed over every listed parameter.
1141
+ */
1142
+ handlers_truncated?: number;
1119
1143
  exports: SurfaceExport[];
1120
1144
  }
1121
1145
  export interface ListLibrarySurfaceResponse extends BaseResponse {
@@ -2810,11 +2810,15 @@ export declare const ListLibrarySurfaceRequestSchema: z.ZodObject<{
2810
2810
  packages: z.ZodArray<z.ZodString, "many">;
2811
2811
  max_entries: z.ZodOptional<z.ZodNumber>;
2812
2812
  exports: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
2813
+ handlers: z.ZodOptional<z.ZodBoolean>;
2814
+ result_candidates: z.ZodOptional<z.ZodBoolean>;
2813
2815
  }, "strip", z.ZodTypeAny, {
2814
2816
  packages: string[];
2815
2817
  request_id: string;
2816
2818
  action: "list_library_surface";
2817
2819
  from_dir: string;
2820
+ handlers?: boolean | undefined;
2821
+ result_candidates?: boolean | undefined;
2818
2822
  max_entries?: number | undefined;
2819
2823
  exports?: Record<string, string[]> | undefined;
2820
2824
  }, {
@@ -2822,6 +2826,8 @@ export declare const ListLibrarySurfaceRequestSchema: z.ZodObject<{
2822
2826
  request_id: string;
2823
2827
  action: "list_library_surface";
2824
2828
  from_dir: string;
2829
+ handlers?: boolean | undefined;
2830
+ result_candidates?: boolean | undefined;
2825
2831
  max_entries?: number | undefined;
2826
2832
  exports?: Record<string, string[]> | undefined;
2827
2833
  }>;
@@ -5388,11 +5394,15 @@ export declare const SidecarRequestSchema: z.ZodDiscriminatedUnion<"action", [z.
5388
5394
  packages: z.ZodArray<z.ZodString, "many">;
5389
5395
  max_entries: z.ZodOptional<z.ZodNumber>;
5390
5396
  exports: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
5397
+ handlers: z.ZodOptional<z.ZodBoolean>;
5398
+ result_candidates: z.ZodOptional<z.ZodBoolean>;
5391
5399
  }, "strip", z.ZodTypeAny, {
5392
5400
  packages: string[];
5393
5401
  request_id: string;
5394
5402
  action: "list_library_surface";
5395
5403
  from_dir: string;
5404
+ handlers?: boolean | undefined;
5405
+ result_candidates?: boolean | undefined;
5396
5406
  max_entries?: number | undefined;
5397
5407
  exports?: Record<string, string[]> | undefined;
5398
5408
  }, {
@@ -5400,6 +5410,8 @@ export declare const SidecarRequestSchema: z.ZodDiscriminatedUnion<"action", [z.
5400
5410
  request_id: string;
5401
5411
  action: "list_library_surface";
5402
5412
  from_dir: string;
5413
+ handlers?: boolean | undefined;
5414
+ result_candidates?: boolean | undefined;
5403
5415
  max_entries?: number | undefined;
5404
5416
  exports?: Record<string, string[]> | undefined;
5405
5417
  }>, z.ZodObject<{
@@ -588,6 +588,8 @@ export const ListLibrarySurfaceRequestSchema = BaseRequestSchema.extend({
588
588
  packages: z.array(z.string().min(1)),
589
589
  max_entries: z.number().int().positive().optional(),
590
590
  exports: z.record(z.string(), z.array(z.string().min(1))).optional(),
591
+ handlers: z.boolean().optional(),
592
+ result_candidates: z.boolean().optional(),
591
593
  });
592
594
  // ============================================================================
593
595
  // Discriminated Union Schema