ucn 4.2.2 → 5.0.1

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.
Files changed (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +445 -300
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -131
  13. package/core/cache.js +533 -11
  14. package/core/callers.js +5533 -494
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +421 -20
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +204 -42
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +216 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -177
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +371 -116
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +428 -16
  65. package/languages/javascript.js +452 -49
  66. package/languages/python.js +1041 -32
  67. package/languages/rust.js +1415 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +41 -24
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
package/core/project.js CHANGED
@@ -8,10 +8,14 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const crypto = require('crypto');
11
- const { expandGlob, findProjectRoot, detectProjectPattern, isTestFile, parseGitignore, DEFAULT_IGNORES, compareNames } = require('./discovery');
12
- const { extractImports, extractExports } = require('./imports');
13
- const { parse, cleanHtmlScriptTags } = require('./parser');
14
- const { detectLanguage, getParser, getLanguageModule, safeParse, langTraits, PARSE_OPTIONS } = require('../languages');
11
+ const {
12
+ expandGlob, findProjectRoot, detectProjectPattern, isTestFile,
13
+ parseGitignore, gitTrackedPaths, DEFAULT_IGNORES, compareNames, classifyUnsupportedSourceFile,
14
+ } = require('./discovery');
15
+ const { cleanHtmlScriptTags } = require('./parser');
16
+ const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits, PARSE_OPTIONS } = require('../languages');
17
+ const { validateFileIR } = require('./ir');
18
+ const { createFileEntryFromIR, populateFileEntryFromIR } = require('./index-ir');
15
19
  const { getTokenTypeAtPosition } = require('../languages/utils');
16
20
  const { escapeRegExp, NON_CALLABLE_TYPES, codeUnitCompare } = require('./shared');
17
21
  const stacktrace = require('./stacktrace');
@@ -29,6 +33,15 @@ const reportingModule = require('./reporting');
29
33
  // Lazy-initialized per-language keyword sets (populated on first isKeyword call)
30
34
  let LANGUAGE_KEYWORDS = null;
31
35
 
36
+ // Query-time ASTs are immutable for the lifetime of an indexed file. Keep a
37
+ // small cross-operation LRU so a sequence of agent queries does not reparse the
38
+ // same large headers/modules for every conservation-account classification.
39
+ // The source-byte budget is deliberately conservative because tree-sitter's
40
+ // native tree is larger than its source. Evictions explicitly release native
41
+ // memory instead of waiting for N-API finalizers.
42
+ const PARSED_TREE_CACHE_MAX_ENTRIES = 128;
43
+ const PARSED_TREE_CACHE_MAX_SOURCE_BYTES = 32 * 1024 * 1024;
44
+
32
45
  /**
33
46
  * ProjectIndex - Manages symbol table for a project
34
47
  */
@@ -47,15 +60,27 @@ class ProjectIndex {
47
60
  this.extendedByGraph = new Map(); // parentName -> [childInfo]
48
61
  this.config = this.loadConfig();
49
62
  this.buildTime = null;
63
+ // Build telemetry is intentionally transient (never cached). Release
64
+ // performance gates use it to prove the requested worker shape was
65
+ // actually exercised instead of inferring parallelism from host CPUs.
66
+ this.lastBuildWorkerCount = 1;
67
+ this.lastBuildParallelEligible = false;
68
+ this.lastBuildRequestedWorkers = null;
50
69
  this.callsCache = new Map(); // filePath -> { mtime, hash, calls, content }
51
70
  this.callsCacheDirty = false; // set by getCachedCalls when entries are added or mutated
71
+ this.computedDispatchDirty = false; // persisted project-wide AST blind-spot inventory
52
72
  this.failedFiles = new Set(); // files that failed to index (e.g. large minified bundles)
73
+ this.unsupportedFiles = []; // common source files skipped by the parser registry
74
+ this.discoveryIssues = []; // explicit reasons the index may be partial
53
75
  this._opContentCache = null; // per-operation file content cache (Map<filePath, string>)
54
76
  this._opUsagesCache = null; // per-operation findUsagesInCode cache (Map<"file:name", usages[]>)
55
77
  this._opTreeCache = null; // per-operation parsed-tree cache (Map<filePath, tree|null>, bounded FIFO)
78
+ this._opTransientTrees = null; // evicted/errored trees kept alive until the active operation ends
56
79
  this._opLinesCache = null; // per-operation split-lines cache (Map<filePath, string[]>, bounded FIFO)
57
80
  this._opInnerSymbolRangesCache = null; // per-operation sorted class-method ranges by file
58
81
  this._opFlowTypeOriginCache = null; // per-operation annotation type identity results
82
+ this._parsedTreeCache = new Map(); // cross-operation LRU: filePath -> immutable tree entry
83
+ this._parsedTreeCacheSourceBytes = 0;
59
84
  this.calleeIndex = null; // name -> Set<filePath> — inverted call index (built lazily)
60
85
  }
61
86
 
@@ -84,6 +109,7 @@ class ProjectIndex {
84
109
  this._opUsageTotalsCache = new Map();
85
110
  this._opEnclosingFnCache = new Map();
86
111
  this._opTreeCache = new Map();
112
+ this._opTransientTrees = new Set();
87
113
  this._opLinesCache = new Map();
88
114
  // Query-time semantic derivations that depend only on one file's
89
115
  // immutable call records. Reachability may ask findCallees() for
@@ -109,6 +135,10 @@ class ProjectIndex {
109
135
  this._opUsageTotalsCache = null;
110
136
  this._opEnclosingFnCache = null;
111
137
  this._opTreeCache = null;
138
+ if (this._opTransientTrees) {
139
+ for (const tree of this._opTransientTrees) tree?.delete?.();
140
+ }
141
+ this._opTransientTrees = null;
112
142
  this._opLinesCache = null;
113
143
  this._opReturnTypeFlowCache = null;
114
144
  this._opCallsByLineCache = null;
@@ -124,11 +154,46 @@ class ProjectIndex {
124
154
  }
125
155
  }
126
156
 
157
+ /** Remove one persistent parsed tree, deferring disposal if an operation still uses it. */
158
+ _evictParsedTree(filePath) {
159
+ const entry = this._parsedTreeCache.get(filePath);
160
+ if (!entry) return;
161
+ this._parsedTreeCache.delete(filePath);
162
+ this._parsedTreeCacheSourceBytes -= entry.sourceBytes;
163
+ if (this._opTreeCache?.get(filePath) === entry.tree) {
164
+ this._opTransientTrees?.add(entry.tree);
165
+ } else {
166
+ entry.tree?.delete?.();
167
+ }
168
+ }
169
+
170
+ /** Dispose every cross-operation tree. Used when an index is replaced from cache. */
171
+ _clearParsedTreeCache() {
172
+ for (const filePath of [...this._parsedTreeCache.keys()]) {
173
+ this._evictParsedTree(filePath);
174
+ }
175
+ this._parsedTreeCacheSourceBytes = 0;
176
+ }
177
+
178
+ _cacheParsedTree(filePath, language, tree, sourceBytes) {
179
+ const fileHash = this.files.get(filePath)?.hash || null;
180
+ this._evictParsedTree(filePath);
181
+ this._parsedTreeCache.set(filePath, {
182
+ tree, language, fileHash, sourceBytes,
183
+ });
184
+ this._parsedTreeCacheSourceBytes += sourceBytes;
185
+ while (this._parsedTreeCache.size > PARSED_TREE_CACHE_MAX_ENTRIES ||
186
+ this._parsedTreeCacheSourceBytes > PARSED_TREE_CACHE_MAX_SOURCE_BYTES) {
187
+ this._evictParsedTree(this._parsedTreeCache.keys().next().value);
188
+ }
189
+ }
190
+
127
191
  /**
128
- * Parse a file once per operation and reuse the tree across symbol names.
192
+ * Parse a file once and reuse the immutable tree across operations.
129
193
  * Multi-symbol commands (diff-impact, check) classify ground lines for MANY
130
194
  * names against the SAME files; without this, each (file, name) pair costs a
131
- * full tree-sitter parse. Bounded FIFO (trees hold native memory).
195
+ * full tree-sitter parse. Both operation and persistent caches are bounded
196
+ * because trees hold native memory.
132
197
  * Returns null when parsing fails or no operation cache is active — callers
133
198
  * fall back to parsing themselves.
134
199
  */
@@ -136,6 +201,19 @@ class ProjectIndex {
136
201
  if (!this._opTreeCache) return null;
137
202
  const cached = this._opTreeCache.get(filePath);
138
203
  if (cached !== undefined) return cached;
204
+
205
+ const persistent = this._parsedTreeCache.get(filePath);
206
+ const currentHash = this.files.get(filePath)?.hash || null;
207
+ if (persistent && persistent.language === language &&
208
+ persistent.fileHash === currentHash) {
209
+ // LRU touch.
210
+ this._parsedTreeCache.delete(filePath);
211
+ this._parsedTreeCache.set(filePath, persistent);
212
+ this._opTreeCache.set(filePath, persistent.tree);
213
+ return persistent.tree;
214
+ }
215
+ if (persistent) this._evictParsedTree(filePath);
216
+
139
217
  let tree;
140
218
  try {
141
219
  const parser = getParser(language);
@@ -147,6 +225,9 @@ class ProjectIndex {
147
225
  this._opTreeCache.delete(this._opTreeCache.keys().next().value);
148
226
  }
149
227
  this._opTreeCache.set(filePath, tree);
228
+ if (tree) {
229
+ this._cacheParsedTree(filePath, language, tree, Buffer.byteLength(content));
230
+ }
150
231
  return tree;
151
232
  }
152
233
 
@@ -183,8 +264,13 @@ class ProjectIndex {
183
264
  if (cached !== undefined) return cached;
184
265
  }
185
266
 
186
- const lang = detectLanguage(filePath);
187
- const langModule = getLanguageModule(lang);
267
+ // Header language is resolved during indexing from compilation
268
+ // databases/include context. Re-detecting `.h` here can choose C for
269
+ // a C++ header and silently drop member/template usages from the raw
270
+ // inventory. The indexed language is the authoritative parse mode.
271
+ const lang = this.files.get(filePath)?.language ||
272
+ detectLanguage(filePath, this.root);
273
+ const langModule = getLanguageAdapter(lang);
188
274
  if (!langModule || typeof langModule.findUsagesInCode !== 'function') return null;
189
275
 
190
276
  try {
@@ -202,7 +288,8 @@ class ProjectIndex {
202
288
  if (!parser) return null;
203
289
  // Language modules that accept a pre-parsed tree (4th param) reuse
204
290
  // the per-operation tree cache; others (html) parse internally.
205
- const tree = langModule.findUsagesInCode.length >= 4
291
+ const tree = langModule.findUsagesInCode.length >= 4 &&
292
+ !langModule.managesOwnParseTree
206
293
  ? this._getParsedTree(filePath, content, lang)
207
294
  : null;
208
295
  const usages = langModule.findUsagesInCode(content, name, parser, tree);
@@ -224,7 +311,7 @@ class ProjectIndex {
224
311
  try {
225
312
  return JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
226
313
  } catch (e) {
227
- // Config load failed, use defaults
314
+ this.configError = `.ucn.json could not be parsed: ${e.message}`;
228
315
  }
229
316
  }
230
317
  return {};
@@ -233,10 +320,10 @@ class ProjectIndex {
233
320
  /**
234
321
  * Build index for files matching pattern
235
322
  *
236
- * @param {string} pattern - Glob pattern (e.g., "**\/*.js")
323
+ * @param {string|null} [pattern=null] - Glob pattern (e.g., "**\/*.js")
237
324
  * @param {object} options - { forceRebuild, maxFiles, quiet }
238
325
  */
239
- build(pattern, options = {}) {
326
+ build(pattern = null, options = {}) {
240
327
  const startTime = Date.now();
241
328
  const quiet = options.quiet !== false;
242
329
 
@@ -252,6 +339,23 @@ class ProjectIndex {
252
339
 
253
340
  // Accept pre-expanded file array (glob mode) or a pattern string
254
341
  let files;
342
+ const implicitProjectDiscovery = !Array.isArray(pattern) && !pattern;
343
+ this.unsupportedFiles = [];
344
+ this.discoveryIssues = [];
345
+ const discoveryIssueKeys = new Set();
346
+ const recordDiscoveryIssue = (issue) => {
347
+ const rel = path.relative(this.root, issue.path || this.root) || '.';
348
+ const key = `${issue.reason}\0${rel}`;
349
+ if (discoveryIssueKeys.has(key)) return;
350
+ discoveryIssueKeys.add(key);
351
+ this.discoveryIssues.push({ ...issue, relativePath: rel, path: undefined });
352
+ };
353
+ if (this.configError) {
354
+ recordDiscoveryIssue({
355
+ path: path.join(this.root, '.ucn.json'),
356
+ kind: 'file', reason: 'invalid-config', detail: this.configError,
357
+ });
358
+ }
255
359
  if (Array.isArray(pattern)) {
256
360
  files = pattern;
257
361
  } else {
@@ -262,23 +366,44 @@ class ProjectIndex {
262
366
  const globOpts = {
263
367
  root: this.root,
264
368
  maxFiles: options.maxFiles || this.config.maxFiles || 50000,
369
+ maxDepth: options.maxDepth ?? this.config.maxDepth,
370
+ maxFileSize: options.maxFileSize ?? this.config.maxFileSize,
265
371
  followSymlinks: options.followSymlinks
266
372
  };
267
373
 
268
374
  // Merge .gitignore and .ucn.json exclude into file discovery
269
375
  const gitignorePatterns = parseGitignore(this.root);
270
- const configExclude = this.config.exclude || [];
271
- if (gitignorePatterns.length > 0 || configExclude.length > 0) {
272
- globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
376
+ globOpts.gitignorePatterns = gitignorePatterns;
377
+ globOpts.trackedPaths = gitTrackedPaths(this.root);
378
+ const configExclude = Array.isArray(this.config.exclude)
379
+ ? this.config.exclude
380
+ : (this.config.exclude ? [String(this.config.exclude)] : []);
381
+ if (configExclude.length > 0) {
382
+ globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
383
+ }
384
+ globOpts.disclosureIgnores = configExclude;
385
+ globOpts.onDiscoveryIssue = recordDiscoveryIssue;
386
+
387
+ if (implicitProjectDiscovery) {
388
+ globOpts.onSkippedFile = (filePath) => {
389
+ const kind = classifyUnsupportedSourceFile(filePath);
390
+ if (!kind) return;
391
+ this.unsupportedFiles.push({
392
+ relativePath: path.relative(this.root, filePath),
393
+ ...kind,
394
+ });
395
+ };
273
396
  }
274
-
275
397
  files = expandGlob(pattern, globOpts);
398
+ this.unsupportedFiles.sort((a, b) => compareNames(a.relativePath, b.relativePath));
399
+ this.discoveryIssues.sort((a, b) => compareNames(a.relativePath, b.relativePath));
276
400
  }
277
401
 
278
402
  // Track if files were truncated by maxFiles limit
279
403
  const maxFiles = options.maxFiles || this.config.maxFiles || 50000;
280
- if (!Array.isArray(pattern) && files.length >= maxFiles) {
281
- this.truncated = { indexed: files.length, maxFiles };
404
+ const maxFileIssues = this.discoveryIssues.filter(issue => issue.reason === 'max-files');
405
+ if (!Array.isArray(pattern) && maxFileIssues.length > 0) {
406
+ this.truncated = { indexed: files.length, maxFiles, skipped: maxFileIssues.length };
282
407
  } else {
283
408
  this.truncated = null;
284
409
  }
@@ -318,6 +443,7 @@ class ProjectIndex {
318
443
  // Always invalidate caches on rebuild
319
444
  this._completenessCache = null;
320
445
  this._attrTypeCache = null;
446
+ this._computedDispatchBlindspots = null;
321
447
  // Endpoints cache (server routes / client requests / bridges) becomes
322
448
  // stale when files change; clear on every rebuild.
323
449
  this._endpointsCache = null;
@@ -330,13 +456,43 @@ class ProjectIndex {
330
456
  const workersSetting = options.workers;
331
457
  const envWorkers = parseInt(process.env.UCN_WORKERS, 10);
332
458
  const disableParallel = workersSetting === 0 || envWorkers === 0;
459
+ const explicitWorkerCount = workersSetting > 0 || envWorkers > 0;
460
+ const requestedWorkerCount = workersSetting > 0
461
+ ? workersSetting : (envWorkers > 0 ? envWorkers : null);
462
+ this.lastBuildWorkerCount = 1;
463
+ this.lastBuildRequestedWorkers = requestedWorkerCount;
333
464
  let usedParallel = false;
334
465
 
335
- if (!disableParallel && files.length > 150) {
466
+ // C/C++ recovery performs materially heavier parser work per file
467
+ // (attribute normalization plus bounded preprocessor views). Medium
468
+ // native projects therefore benefit from workers well before the
469
+ // generic 150-file crossover; measured release repos cross over at
470
+ // roughly 25 C-family files.
471
+ const cFamilyFileCount = files.reduce((count, filePath) => {
472
+ const language = detectLanguage(filePath, this.root);
473
+ return count + (language === 'c' || language === 'cpp' ? 1 : 0);
474
+ }, 0);
475
+ // An explicit worker count is a request for a reproducible execution
476
+ // shape (not merely an upper bound). This is useful to users tuning a
477
+ // constrained host and lets the release gate compare like with like.
478
+ const parallelWorthwhile = explicitWorkerCount
479
+ ? files.length >= 2
480
+ : files.length > 150 || cFamilyFileCount >= 25;
481
+ this.lastBuildParallelEligible = !disableParallel && parallelWorthwhile;
482
+ if (!disableParallel && parallelWorthwhile) {
336
483
  try {
337
484
  const { parallelBuild } = require('./parallel-build');
338
485
  const result = parallelBuild(this, files, {
339
486
  workerCount: workersSetting > 0 ? workersSetting : (envWorkers > 0 ? envWorkers : undefined),
487
+ // After the recovery scorer stopped repeatedly walking
488
+ // every candidate tree, real-repo sweeps put the native
489
+ // throughput knee at 5-6 workers. Default to the lower
490
+ // knee (five) for RSS headroom; explicit user settings
491
+ // retain the ordinary eight-worker safety cap.
492
+ maxWorkers: cFamilyFileCount >= 25 && !explicitWorkerCount
493
+ ? 5 : 8,
494
+ minFilesPerWorker: explicitWorkerCount
495
+ ? 1 : (cFamilyFileCount >= 25 ? 10 : 100),
340
496
  quiet,
341
497
  });
342
498
  if (result !== false) {
@@ -345,6 +501,7 @@ class ProjectIndex {
345
501
  usedParallel = true;
346
502
  }
347
503
  } catch (e) {
504
+ this.lastBuildWorkerCount = 1;
348
505
  if (!quiet) {
349
506
  console.error(`Parallel build failed, falling back to sequential: ${e.message}`);
350
507
  }
@@ -384,9 +541,10 @@ class ProjectIndex {
384
541
  // avoiding the 2+ minute deferred cost when the first analysis command runs later.
385
542
  this.buildCalleeIndex();
386
543
 
387
- // buildCalleeIndex re-parses changed files via getCachedCalls, which
388
- // appends their entries at the callsCache TAIL — restore canonical
389
- // key order so iteration-order consumers match a fresh build.
544
+ // Keep persisted iteration deterministic. Fresh and changed files
545
+ // already populated callsCache through the shared IR ingestion path;
546
+ // unchanged entries may have arrived from a sharded cache in a
547
+ // different order.
390
548
  this.callsCache = new Map([...this.callsCache.entries()].sort((a, b) => compareNames(a[0], b[0])));
391
549
 
392
550
  this.buildTime = Date.now() - startTime;
@@ -435,16 +593,16 @@ class ProjectIndex {
435
593
  this.removeFileSymbols(filePath);
436
594
  }
437
595
 
438
- const language = detectLanguage(filePath);
596
+ const language = detectLanguage(filePath, this.root);
439
597
  if (!language) return;
440
598
 
441
- // Parse content once — the tree-sitter cache in safeParse ensures the tree
442
- // is shared across parse()/extractImports()/extractExports() (5→1 parse per file)
443
- const parsed = parse(content, language);
444
- parsed.filePath = filePath;
445
- parsed.relativePath = filePath;
446
- const { imports, dynamicCount, importAliases } = extractImports(content, language);
447
- const { exports } = extractExports(content, language);
599
+ const adapter = getLanguageAdapter(language);
600
+ const parser = getParser(language);
601
+ const ir = adapter.analyze(content, parser, filePath);
602
+ const irFailures = validateFileIR(ir);
603
+ if (irFailures.length > 0) {
604
+ throw new Error(`Invalid ${language} IR: ${irFailures.join('; ')}`);
605
+ }
448
606
 
449
607
  // Detect bundled/minified files (webpack bundles, minified code)
450
608
  // These are build artifacts, not user-written source code
@@ -470,8 +628,12 @@ class ProjectIndex {
470
628
  const isBundled = (() => {
471
629
  // Webpack bundles contain __webpack_require__ or __webpack_modules__
472
630
  if (content.includes('__webpack_require__') || content.includes('__webpack_modules__')) return true;
473
- // Minified files: very few lines but large content (avg > 500 chars/line)
474
- if (lineCount > 0 && lineCount < 50 && content.length / lineCount > 500) return true;
631
+ // Minified files: large content with extreme bytes/line. Bundlers
632
+ // often insert 50-200 licence/chunk-separator lines, so an
633
+ // arbitrary <50 cap lets the same bytes flip classification when
634
+ // a banner is added. The size floor protects authored small files.
635
+ if (lineCount > 0 && content.length >= 100 * 1024 &&
636
+ content.length / lineCount > 500) return true;
475
637
  // Very long single lines (> 1000 chars) in most of the file suggest minification
476
638
  if (lineCount > 0 && longLineCount > 0 && longLineCount / lineCount > 0.3) return true;
477
639
  return false;
@@ -484,140 +646,26 @@ class ProjectIndex {
484
646
  content.slice(0, 500)
485
647
  );
486
648
 
487
- const fileEntry = {
488
- path: filePath,
649
+ const fileEntry = createFileEntryFromIR({
650
+ ir,
651
+ filePath,
489
652
  relativePath: path.relative(this.root, filePath),
490
- language,
491
- lines: lineCount,
492
653
  hash,
493
654
  mtime: stat.mtimeMs,
494
655
  size: stat.size,
495
- imports: imports.map(i => i.module),
496
- importNames: imports.flatMap(i => i.names || []),
497
- // Paired name↔module bindings (fix #209): importNames flattens the
498
- // pairing away, but name-level shadow detection needs to know WHICH
499
- // module bound a name (`from urllib.parse import unquote` rebinds
500
- // the bare name for the whole file).
501
- importBindings: imports.flatMap(i => (i.names || [])
502
- .filter(n => n && n !== '*' && n !== '_' && n !== '.')
503
- .map(n => {
504
- // Rename pairing (fix #269): `{ validate: validateSchema }
505
- // = require('./validation')` — the record's local alias
506
- // pins to ITS module, not any module exporting the name.
507
- const rn = (i.renames || []).find(r => r.original === n);
508
- return { name: n, module: i.module, ...(rn && { alias: rn.local }) };
509
- })),
510
- exports: exports.map(e => e.name),
511
- exportDetails: exports,
512
- symbols: [],
513
- bindings: [],
514
- ...(parsed.parseRecovery && { parseRecovery: true }),
515
- ...(importAliases && { importAliases }),
516
- // Module-scope assignment targets (fix #217): names a module can
517
- // expose WITHOUT a def/class/import binding (`render = impl`,
518
- // `global name`). The import-binding name-chase treats these as
519
- // undetermined — a dead-end verdict would be unsound.
520
- ...(parsed.moduleAssignedNames && { moduleAssignedNames: parsed.moduleAssignedNames }),
521
- ...(isBundled && { isBundled: true }),
522
- ...(isGenerated && { isGenerated: true })
523
- };
524
- fileEntry.dynamicImports = dynamicCount || 0;
525
-
526
- // Add symbols
527
- const addSymbol = (item, type) => {
528
- const symbol = {
529
- name: item.name,
530
- type,
531
- file: filePath,
532
- relativePath: fileEntry.relativePath,
533
- startLine: item.startLine,
534
- endLine: item.endLine,
535
- params: item.params,
536
- paramsStructured: item.paramsStructured,
537
- returnType: item.returnType,
538
- ...(item.returnedFunctionResult && { returnedFunctionResult: item.returnedFunctionResult }),
539
- ...(item.isFunctionVariable && { isFunctionVariable: true }),
540
- ...(item.paramTypes && { paramTypes: item.paramTypes }),
541
- ...(item.isAsync && { isAsync: true }),
542
- ...(item.isGenerator && { isGenerator: true }),
543
- modifiers: item.modifiers,
544
- docstring: item.docstring,
545
- bindingId: `${fileEntry.relativePath}:${type}:${item.startLine}`,
546
- ...(item.generics && { generics: item.generics }),
547
- ...(item.extends && { extends: item.extends }),
548
- ...(item.implements && { implements: item.implements }),
549
- ...(item.indent !== undefined && { indent: item.indent }),
550
- ...(item.isNested && { isNested: item.isNested }),
551
- ...(item.isMethod && { isMethod: item.isMethod }),
552
- ...(item.receiver && { receiver: item.receiver }),
553
- ...(item.className && { className: item.className }),
554
- ...(item.memberType && { memberType: item.memberType }),
555
- ...(item.fieldType && { fieldType: item.fieldType }),
556
- ...(item.aliasOf && { aliasOf: item.aliasOf }),
557
- ...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
558
- // Decorator/annotation/attribute argument capture for endpoints command:
559
- // these fields hold the parsed first-string-arg of each route-style annotation.
560
- // Only populated when at least one entry has a string-literal arg, keeping memory
561
- // overhead minimal for non-route code.
562
- ...(item.decoratorsWithArgs && item.decoratorsWithArgs.length > 0 && { decoratorsWithArgs: item.decoratorsWithArgs }),
563
- ...(item.annotationsWithArgs && item.annotationsWithArgs.length > 0 && { annotationsWithArgs: item.annotationsWithArgs }),
564
- ...(item.attributesWithArgs && item.attributesWithArgs.length > 0 && { attributesWithArgs: item.attributesWithArgs }),
565
- ...(item.nameLine && { nameLine: item.nameLine }),
566
- ...(item.traitImpl && { traitImpl: true }),
567
- // Trait the impl block implements (rust `impl Trait for X`
568
- // members) — external-contract detection needs the NAME, not
569
- // just the traitImpl flag (fix #210).
570
- ...(item.traitName && { traitName: item.traitName }),
571
- ...(item.isSignature && { isSignature: true }),
572
- ...(item.memberAssigned && { memberAssigned: true }),
573
- ...(item.registryMember && { registryMember: true }),
574
- ...(item.registryContainer && { registryContainer: item.registryContainer })
575
- };
576
- fileEntry.symbols.push(symbol);
577
- // Property-assignment defs (fix #269: Reply.prototype.serialize
578
- // = function) declare no lexical name — a bare reference in the
579
- // file can never bind them, so they never enter the bindings
580
- // table (the symbol stays indexed and method-reachable).
581
- if (!item.memberAssigned) {
582
- fileEntry.bindings.push({
583
- id: symbol.bindingId,
584
- name: symbol.name,
585
- type: symbol.type,
586
- startLine: symbol.startLine
587
- });
588
- }
589
-
590
- if (!this.symbols.has(item.name)) {
591
- this.symbols.set(item.name, []);
592
- }
593
- this.symbols.get(item.name).push(symbol);
594
- };
595
-
596
- for (const fn of parsed.functions) {
597
- // Go/Rust methods: set className from receiver for consistent method resolution.
598
- // Go/Rust methods are standalone functions with receiver, not class members,
599
- // so className is never set by the class member loop below.
600
- if (fn.receiver && !fn.className) {
601
- fn.className = fn.receiver.replace(/^\*/, '');
602
- }
603
- addSymbol(fn, fn.isConstructor ? 'constructor' : 'function');
604
- }
605
-
606
- for (const cls of parsed.classes) {
607
- addSymbol(cls, cls.type || 'class');
608
- if (cls.members) {
609
- for (const m of cls.members) {
610
- const memberType = m.memberType || 'method';
611
- addSymbol({ ...m, className: cls.name, ...(cls.traitName && { traitImpl: true, traitName: cls.traitName }) }, memberType);
612
- }
613
- }
614
- }
615
-
616
- for (const state of parsed.stateObjects) {
617
- addSymbol(state, 'state');
618
- }
656
+ lineCount,
657
+ isBundled,
658
+ isGenerated,
659
+ });
660
+ populateFileEntryFromIR(fileEntry, ir, this.symbols);
619
661
 
620
662
  this.files.set(filePath, fileEntry);
663
+ this.callsCache.set(filePath, {
664
+ mtime: stat.mtimeMs,
665
+ hash,
666
+ calls: ir.calls,
667
+ });
668
+ this.callsCacheDirty = true;
621
669
  return true;
622
670
  }
623
671
 
@@ -628,6 +676,8 @@ class ProjectIndex {
628
676
  const existing = this.files.get(filePath);
629
677
  if (!existing) return;
630
678
 
679
+ this._evictParsedTree(filePath);
680
+
631
681
  for (const symbol of existing.symbols) {
632
682
  const entries = this.symbols.get(symbol.name);
633
683
  if (entries) {
@@ -652,6 +702,8 @@ class ProjectIndex {
652
702
 
653
703
  // Invalidate lazy Java file index (will be rebuilt on next use)
654
704
  this._javaFileIndex = null;
705
+ // Computed-dispatch diagnostics are project-wide and read source text.
706
+ this._computedDispatchBlindspots = null;
655
707
  // Endpoints cache is project-wide; safest to clear on any file removal.
656
708
  this._endpointsCache = null;
657
709
  }
@@ -748,6 +800,17 @@ class ProjectIndex {
748
800
  }
749
801
  }
750
802
  }
803
+ // A compiler-recognized type qualifier is also a semantic use of
804
+ // the receiver type: `JValue.Compare(...)` references both
805
+ // JValue and Compare. Index the receiver so class/type queries do
806
+ // not fall back to a full project scan or leave the occurrence as
807
+ // an unexplained call-not-resolved line.
808
+ if (call.receiverIsTypeQualified && call.receiver) {
809
+ if (!this.calleeIndex.has(call.receiver)) {
810
+ this.calleeIndex.set(call.receiver, new Set());
811
+ }
812
+ this.calleeIndex.get(call.receiver).add(filePath);
813
+ }
751
814
  }
752
815
  }
753
816
 
@@ -772,6 +835,9 @@ class ProjectIndex {
772
835
  if (rn !== call.name) removeName(rn);
773
836
  }
774
837
  }
838
+ if (call.receiverIsTypeQualified && call.receiver) {
839
+ removeName(call.receiver);
840
+ }
775
841
  }
776
842
  }
777
843
 
@@ -828,6 +894,10 @@ class ProjectIndex {
828
894
  return graphBuildModule._resolveJavaPackageImport(this, importModule, javaFileIndex);
829
895
  }
830
896
 
897
+ _resolveCSharpUsing(importModule) {
898
+ return graphBuildModule._resolveCSharpUsing(this, importModule);
899
+ }
900
+
831
901
  /**
832
902
  * Build import/export relationship graphs
833
903
  */
@@ -1038,9 +1108,11 @@ class ProjectIndex {
1038
1108
 
1039
1109
  // Filter by file if specified
1040
1110
  if (options.file) {
1041
- const filtered = definitions.filter(d =>
1042
- d.relativePath && d.relativePath.includes(options.file)
1043
- );
1111
+ const resolvedFile = this.resolveFilePathForQuery(options.file);
1112
+ const filtered = typeof resolvedFile === 'string'
1113
+ ? definitions.filter(d => d.file === resolvedFile)
1114
+ : definitions.filter(d =>
1115
+ d.relativePath && d.relativePath.includes(options.file));
1044
1116
  if (filtered.length > 0) {
1045
1117
  definitions = filtered;
1046
1118
  }
@@ -1048,15 +1120,23 @@ class ProjectIndex {
1048
1120
 
1049
1121
  // Filter by exact startLine when a handle was supplied. This pins
1050
1122
  // the resolution to one specific definition — no ambiguity allowed.
1051
- if (options.line && Number.isFinite(options.line)) {
1052
- const filtered = definitions.filter(d => d.startLine === options.line);
1053
- if (filtered.length > 0) {
1054
- definitions = filtered;
1123
+ if (options.line != null && options.line !== '') {
1124
+ const exactLine = Number(options.line);
1125
+ if (Number.isFinite(exactLine)) {
1126
+ // A line is an exact identity pin, never a ranking hint.
1127
+ // An unsatisfied pin must not fall back to another definition.
1128
+ definitions = definitions.filter(d =>
1129
+ d.startLine === exactLine || d.nameLine === exactLine);
1055
1130
  }
1056
1131
  }
1132
+ if (definitions.length === 0) {
1133
+ return { def: null, definitions: [], warnings: [] };
1134
+ }
1057
1135
 
1058
1136
  // Score each definition for selection
1059
- const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
1137
+ const typeOrder = new Set([
1138
+ 'class', 'struct', 'interface', 'type', 'impl', 'enum', 'record',
1139
+ ]);
1060
1140
  const { isTestPath } = require('./shared');
1061
1141
  const scored = definitions.map(d => {
1062
1142
  let score = 0;
@@ -1303,7 +1383,15 @@ class ProjectIndex {
1303
1383
  }
1304
1384
 
1305
1385
  const total = calls + definitions + imports;
1306
- const result = { total, calls, definitions, imports, references: 0 };
1386
+ const result = {
1387
+ total,
1388
+ calls,
1389
+ definitions,
1390
+ imports,
1391
+ references: 0,
1392
+ complete: false,
1393
+ countKind: 'fast-disambiguation-excludes-references',
1394
+ };
1307
1395
  if (memoKey) this._opUsageTotalsCache.set(memoKey, result);
1308
1396
  return result;
1309
1397
  }
@@ -1412,7 +1500,9 @@ class ProjectIndex {
1412
1500
  calls,
1413
1501
  definitions,
1414
1502
  imports,
1415
- references
1503
+ references,
1504
+ complete: true,
1505
+ countKind: 'detailed-ast-usages',
1416
1506
  };
1417
1507
  }
1418
1508
 
@@ -1434,11 +1524,53 @@ class ProjectIndex {
1434
1524
  * @param {string} typeName - The class/struct/interface name
1435
1525
  * @returns {Array} Methods belonging to this type
1436
1526
  */
1437
- findMethodsForType(typeName) {
1527
+ findMethodsForType(typeName, definition = null) {
1438
1528
  const methods = [];
1439
1529
  // Match both "TypeName" and "*TypeName" receivers (for Go/Rust pointer receivers)
1440
1530
  const baseTypeName = typeName.replace(/^\*/, '');
1441
1531
 
1532
+ const targetLanguage = definition?.file
1533
+ ? this.files.get(definition.file)?.language : null;
1534
+ const targetNamespace = definition?.namespace || null;
1535
+ const targetDir = definition?.file ? path.dirname(definition.file) : null;
1536
+ const sameCompilerType = symbol => {
1537
+ if (!definition || !targetLanguage) return true;
1538
+ const language = this.files.get(symbol.file)?.language;
1539
+ if (language !== targetLanguage) return false;
1540
+ if (['javascript', 'typescript', 'tsx', 'python', 'html']
1541
+ .includes(targetLanguage)) return symbol.file === definition.file;
1542
+ if (targetLanguage === 'go') return path.dirname(symbol.file) === targetDir;
1543
+ if (targetLanguage === 'java' || targetLanguage === 'csharp') {
1544
+ return (symbol.namespace || null) === targetNamespace;
1545
+ }
1546
+ if (targetLanguage === 'c' || targetLanguage === 'cpp') {
1547
+ if (symbol.file === definition.file) return true;
1548
+ const reachable = (from, to) => {
1549
+ const queue = [from];
1550
+ const seen = new Set(queue);
1551
+ for (let depth = 0; queue.length > 0 && depth < 16; depth++) {
1552
+ const current = queue.shift();
1553
+ for (const imported of this.importGraph.get(current) || []) {
1554
+ if (imported === to) return true;
1555
+ if (!seen.has(imported)) {
1556
+ seen.add(imported);
1557
+ queue.push(imported);
1558
+ }
1559
+ }
1560
+ }
1561
+ return false;
1562
+ };
1563
+ return reachable(symbol.file, definition.file) ||
1564
+ reachable(definition.file, symbol.file);
1565
+ }
1566
+ if (targetLanguage === 'rust') {
1567
+ const typeDefs = (this.symbols.get(baseTypeName) || []).filter(candidate =>
1568
+ ['struct', 'enum', 'trait', 'type'].includes(candidate.type));
1569
+ return typeDefs.length === 1 || symbol.file === definition.file;
1570
+ }
1571
+ return symbol.file === definition.file;
1572
+ };
1573
+
1442
1574
  for (const [, symbols] of this.symbols) {
1443
1575
  for (const symbol of symbols) {
1444
1576
  // Skip non-method types (fields, properties, etc.)
@@ -1450,7 +1582,7 @@ class ProjectIndex {
1450
1582
  // Also matches Rust associated functions (have receiver but isMethod=false)
1451
1583
  if (symbol.receiver) {
1452
1584
  const receiverBase = symbol.receiver.replace(/^\*/, '');
1453
- if (receiverBase === baseTypeName) {
1585
+ if (receiverBase === baseTypeName && sameCompilerType(symbol)) {
1454
1586
  methods.push(symbol);
1455
1587
  continue;
1456
1588
  }
@@ -1459,7 +1591,8 @@ class ProjectIndex {
1459
1591
  // Check Python/Java/JS-style className (class members)
1460
1592
  // Must be a method type, not just any symbol with className
1461
1593
  if (symbol.className === baseTypeName &&
1462
- (symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor')) {
1594
+ (symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor') &&
1595
+ sameCompilerType(symbol)) {
1463
1596
  methods.push(symbol);
1464
1597
  continue;
1465
1598
  }
@@ -1496,7 +1629,7 @@ class ProjectIndex {
1496
1629
  * @returns {boolean} true if ALL occurrences of name are inside strings
1497
1630
  */
1498
1631
  isInsideStringAST(content, lineNum, line, name, filePath) {
1499
- const language = detectLanguage(filePath);
1632
+ const language = detectLanguage(filePath, this.root);
1500
1633
  if (!language) {
1501
1634
  return false; // Unsupported language - assume not inside string
1502
1635
  }
@@ -1617,6 +1750,18 @@ class ProjectIndex {
1617
1750
  parts.push(def.modifiers.join(' '));
1618
1751
  }
1619
1752
  parts.push(def.name);
1753
+ const dataMember = def.type === 'field' || def.type === 'state' ||
1754
+ def.memberType === 'field' || def.memberType === 'property';
1755
+ if (dataMember) {
1756
+ const declaredType = def.fieldType || def.returnType;
1757
+ if (declaredType) {
1758
+ parts.push(`: ${String(declaredType).replace(/\s+/g, ' ').trim()}`);
1759
+ }
1760
+ return parts.join(' ');
1761
+ }
1762
+ if (def.type === 'macro' && def.functionLike === false) {
1763
+ return parts.join(' ');
1764
+ }
1620
1765
  if (def.params !== undefined) {
1621
1766
  // Prefer typed rendering when paramTypes/paramsStructured carry annotations
1622
1767
  const { renderTypedParams } = require('./output/shared');
@@ -1641,12 +1786,12 @@ class ProjectIndex {
1641
1786
  * @returns {string} 'call', 'import', 'definition', or 'reference'
1642
1787
  */
1643
1788
  classifyUsageAST(content, lineNum, name, filePath) {
1644
- const language = detectLanguage(filePath);
1789
+ const language = detectLanguage(filePath, this.root);
1645
1790
  if (!language) {
1646
1791
  return null; // Signal to use fallback
1647
1792
  }
1648
1793
 
1649
- const langModule = getLanguageModule(language);
1794
+ const langModule = getLanguageAdapter(language);
1650
1795
  if (!langModule || typeof langModule.findUsagesInCode !== 'function') {
1651
1796
  return null;
1652
1797
  }
@@ -1680,7 +1825,7 @@ class ProjectIndex {
1680
1825
  * @returns {boolean}
1681
1826
  */
1682
1827
  isCommentOrStringAtPosition(content, lineNum, column, filePath) {
1683
- const language = detectLanguage(filePath);
1828
+ const language = detectLanguage(filePath, this.root);
1684
1829
  if (!language) {
1685
1830
  return false; // Can't determine, assume code
1686
1831
  }
@@ -1844,6 +1989,62 @@ class ProjectIndex {
1844
1989
  'FunctionalInterface', 'SafeVarargs',
1845
1990
  'Iterable', 'Comparable', 'AutoCloseable', 'Cloneable',
1846
1991
  'Enum', 'Record', 'Void'
1992
+ ]),
1993
+ c: new Set([
1994
+ 'auto', 'break', 'case', 'char', 'const', 'continue',
1995
+ 'default', 'do', 'double', 'else', 'enum', 'extern',
1996
+ 'float', 'for', 'goto', 'if', 'inline', 'int', 'long',
1997
+ 'register', 'restrict', 'return', 'short', 'signed',
1998
+ 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union',
1999
+ 'unsigned', 'void', 'volatile', 'while', '_Bool',
2000
+ '_Complex', '_Atomic', '_Generic', '_Noreturn',
2001
+ 'NULL', 'true', 'false', 'size_t', 'ptrdiff_t',
2002
+ 'printf', 'fprintf', 'sprintf', 'snprintf', 'scanf',
2003
+ 'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove',
2004
+ 'memset', 'strlen', 'strcmp', 'strcpy', 'strncpy',
2005
+ 'fopen', 'fclose', 'fread', 'fwrite', 'exit', 'abort',
2006
+ ]),
2007
+ cpp: new Set([
2008
+ 'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto',
2009
+ 'bitand', 'bitor', 'bool', 'break', 'case', 'catch',
2010
+ 'char', 'char8_t', 'char16_t', 'char32_t', 'class',
2011
+ 'compl', 'concept', 'const', 'consteval', 'constexpr',
2012
+ 'constinit', 'const_cast', 'continue', 'co_await',
2013
+ 'co_return', 'co_yield', 'decltype', 'default', 'delete',
2014
+ 'do', 'double', 'dynamic_cast', 'else', 'enum', 'explicit',
2015
+ 'export', 'extern', 'false', 'float', 'for', 'friend',
2016
+ 'goto', 'if', 'inline', 'int', 'long', 'mutable',
2017
+ 'namespace', 'new', 'noexcept', 'not', 'nullptr',
2018
+ 'operator', 'or', 'private', 'protected', 'public',
2019
+ 'register', 'reinterpret_cast', 'requires', 'return',
2020
+ 'short', 'signed', 'sizeof', 'static', 'static_assert',
2021
+ 'static_cast', 'struct', 'switch', 'template', 'this',
2022
+ 'thread_local', 'throw', 'true', 'try', 'typedef',
2023
+ 'typeid', 'typename', 'union', 'unsigned', 'using',
2024
+ 'virtual', 'void', 'volatile', 'wchar_t', 'while',
2025
+ 'std', 'string', 'vector', 'map', 'unordered_map', 'set',
2026
+ 'unique_ptr', 'shared_ptr', 'weak_ptr', 'optional',
2027
+ 'variant', 'tuple', 'move', 'forward', 'make_shared',
2028
+ 'make_unique', 'cout', 'cerr', 'cin', 'endl',
2029
+ ]),
2030
+ csharp: new Set([
2031
+ 'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case',
2032
+ 'catch', 'char', 'checked', 'class', 'const', 'continue',
2033
+ 'decimal', 'default', 'delegate', 'do', 'double', 'else',
2034
+ 'enum', 'event', 'explicit', 'extern', 'false', 'finally',
2035
+ 'fixed', 'float', 'for', 'foreach', 'goto', 'if',
2036
+ 'implicit', 'in', 'int', 'interface', 'internal', 'is',
2037
+ 'lock', 'long', 'namespace', 'new', 'null', 'object',
2038
+ 'operator', 'out', 'override', 'params', 'private',
2039
+ 'protected', 'public', 'readonly', 'record', 'ref',
2040
+ 'return', 'sbyte', 'sealed', 'short', 'sizeof',
2041
+ 'stackalloc', 'static', 'string', 'struct', 'switch',
2042
+ 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong',
2043
+ 'unchecked', 'unsafe', 'ushort', 'using', 'virtual',
2044
+ 'void', 'volatile', 'while', 'async', 'await', 'var',
2045
+ 'dynamic', 'yield', 'Console', 'Math', 'String', 'Object',
2046
+ 'Task', 'ValueTask', 'List', 'Dictionary', 'HashSet',
2047
+ 'IEnumerable', 'IDisposable', 'Exception',
1847
2048
  ])
1848
2049
  };
1849
2050
  // TypeScript/TSX share JavaScript keywords
@@ -2181,6 +2382,12 @@ class ProjectIndex {
2181
2382
  /** Load index from cache file */
2182
2383
  loadCache(cachePath) { return indexCache.loadCache(this, cachePath); }
2183
2384
 
2385
+ /** Return this project's default per-user cache file path. */
2386
+ getCachePath() { return indexCache.getProjectCachePath(this.root); }
2387
+
2388
+ /** Remove this project's per-user cache and any legacy project-local cache. */
2389
+ clearCache() { return indexCache.clearProjectCache(this.root); }
2390
+
2184
2391
  /** Load callsCache from separate file on demand (called by findCallers/findCallees) */
2185
2392
  loadCallsCache() { return indexCache.loadCallsCache(this); }
2186
2393