ucn 5.5.1 → 5.5.3

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 (97) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +40 -18
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +32 -21
  6. package/cli/index.js +2 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +74 -20
  9. package/core/analysis.js +257 -125
  10. package/core/ast-analysis.js +12 -3
  11. package/core/bridge.js +15 -65
  12. package/core/brief.js +8 -4
  13. package/core/build-worker.js +2 -1
  14. package/core/cache.js +137 -6
  15. package/core/call-name-cache.js +134 -0
  16. package/core/callable-scope.js +21 -0
  17. package/core/callers.js +4726 -940
  18. package/core/check.js +22 -5
  19. package/core/class-identity.js +399 -12
  20. package/core/client-receivers.js +189 -0
  21. package/core/constructor-calls.js +831 -0
  22. package/core/constructor-hierarchy.js +147 -0
  23. package/core/constructor-protocol.js +196 -0
  24. package/core/contract-membership.js +346 -140
  25. package/core/cpp-conversion-constraints.js +123 -0
  26. package/core/cpp-conversion-syntax.js +216 -0
  27. package/core/cpp-conversions.js +414 -0
  28. package/core/cpp-scope.js +245 -44
  29. package/core/deadcode.js +61 -16
  30. package/core/decorator-binding.js +137 -3
  31. package/core/enum-reference-syntax.js +134 -0
  32. package/core/execute.js +17 -1
  33. package/core/external-macros.js +42 -10
  34. package/core/go-packages.js +171 -0
  35. package/core/graph-build.js +2 -0
  36. package/core/graph.js +75 -59
  37. package/core/ignore-rules.js +2 -1
  38. package/core/imports.js +145 -19
  39. package/core/index-ir.js +41 -4
  40. package/core/inprocess-clients.js +41 -15
  41. package/core/ir.js +27 -6
  42. package/core/macro-expansion.js +38 -2
  43. package/core/out-of-class-members.js +182 -0
  44. package/core/output/analysis-ext.js +4 -2
  45. package/core/output/analysis.js +89 -20
  46. package/core/output/graph.js +12 -3
  47. package/core/output/lines.js +4 -3
  48. package/core/output/public.js +2 -0
  49. package/core/output/reporting.js +12 -1
  50. package/core/output/shared.js +2 -0
  51. package/core/output/tracing.js +28 -22
  52. package/core/output-budget.js +2 -2
  53. package/core/parallel-build.js +52 -1
  54. package/core/project.js +217 -112
  55. package/core/provenance-constructor-base.js +161 -0
  56. package/core/provenance-facts.js +257 -27
  57. package/core/provenance-overload.js +458 -54
  58. package/core/provenance-type.js +487 -0
  59. package/core/provenance.js +234 -15
  60. package/core/public-command.js +17 -2
  61. package/core/query-bodies.js +159 -0
  62. package/core/reflection.js +270 -2
  63. package/core/rename-token-syntax.js +111 -0
  64. package/core/reporting.js +13 -7
  65. package/core/request-helpers.js +63 -7
  66. package/core/rust-macro-expansion.js +151 -19
  67. package/core/rust-modules.js +1 -1
  68. package/core/rust-names.js +607 -0
  69. package/core/search.js +70 -17
  70. package/core/shared.js +98 -1
  71. package/core/source-memo.js +201 -0
  72. package/core/text-usage-syntax.js +38 -0
  73. package/core/tracing.js +7 -1
  74. package/core/type-denotation.js +124 -37
  75. package/core/type-references.js +615 -246
  76. package/core/value-references.js +2777 -0
  77. package/core/verify.js +1097 -281
  78. package/languages/adapter.js +28 -4
  79. package/languages/c-family.js +1118 -164
  80. package/languages/constructor-bases.js +105 -0
  81. package/languages/cpp-platform.js +26 -0
  82. package/languages/cpp-site-syntax.js +83 -0
  83. package/languages/csharp-scope.js +439 -0
  84. package/languages/csharp.js +428 -68
  85. package/languages/go.js +385 -207
  86. package/languages/html.js +4 -2
  87. package/languages/index.js +397 -2
  88. package/languages/java-platform.js +226 -0
  89. package/languages/java-unicode.js +269 -0
  90. package/languages/java.js +461 -117
  91. package/languages/javascript.js +418 -66
  92. package/languages/lexical-scope.js +190 -23
  93. package/languages/python.js +404 -54
  94. package/languages/rust.js +510 -82
  95. package/languages/utils.js +67 -8
  96. package/mcp/server.js +8 -2
  97. package/package.json +3 -3
package/core/bridge.js CHANGED
@@ -2279,53 +2279,6 @@ function extractNextjsRoutes(index) {
2279
2279
  * Detect HTTP client requests across the project.
2280
2280
  * Cached on `index._endpointsCache.clientRequests`.
2281
2281
  */
2282
- // Python HTTP client types whose instances are receivers of request calls.
2283
- // A receiver typed to one of these (with-binding, constructor assignment) or
2284
- // bound to a pytest fixture that constructs one is a client by evidence, not
2285
- // by name (fix #349: 145 of 625 real `tc.get("/api/...")` sites on one repo
2286
- // were invisible because the receiver was not literally named `client`).
2287
- const PY_CLIENT_TYPES = new Set([
2288
- 'TestClient', 'Client', 'AsyncClient', 'Session', 'FlaskClient',
2289
- 'ClientSession', 'AsyncSession', 'HTTPConnection', 'HTTPSConnection',
2290
- ]);
2291
- const PY_CLIENT_FACTORIES = new Set(['test_client', 'Session', 'Client', 'AsyncClient']);
2292
- const PY_REQUEST_METHODS = /^(get|post|put|delete|patch|options|head|request)$/;
2293
-
2294
- function _pythonClientFixtures(index) {
2295
- if (index._endpointsCache?.pyClientFixtures) return index._endpointsCache.pyClientFixtures;
2296
- const fixtures = new Set();
2297
- for (const [filePath, fileEntry] of index.files) {
2298
- if (fileEntry.language !== 'python') continue;
2299
- const fixtureDefs = (fileEntry.symbols || []).filter(s =>
2300
- (s.decorators || []).some(d => /(^|\.)fixture$/.test(String(d))));
2301
- if (fixtureDefs.length === 0) continue;
2302
- const calls = getCachedCalls(index, filePath) || [];
2303
- for (const def of fixtureDefs) {
2304
- const constructsClient = calls.some(c =>
2305
- c.enclosingFunction?.name === def.name &&
2306
- c.enclosingFunction?.startLine === def.startLine &&
2307
- (PY_CLIENT_TYPES.has(c.name) || (c.isMethod && PY_CLIENT_FACTORIES.has(c.name))));
2308
- if (constructsClient) fixtures.add(def.name);
2309
- }
2310
- }
2311
- if (!index._endpointsCache) index._endpointsCache = {};
2312
- index._endpointsCache.pyClientFixtures = fixtures;
2313
- return fixtures;
2314
- }
2315
-
2316
- function _pythonClientReceiver(index, fileEntry, call, fixtures) {
2317
- if (!call.isMethod || !call.receiver || !PY_REQUEST_METHODS.test(call.name)) return null;
2318
- if (call.receiverType && PY_CLIENT_TYPES.has(String(call.receiverType).split('.').pop())) {
2319
- return 'python-client';
2320
- }
2321
- if (fixtures.size === 0 || !fixtures.has(call.receiver)) return null;
2322
- const fn = call.enclosingFunction;
2323
- if (!fn) return null;
2324
- const sym = (fileEntry.symbols || []).find(s => s.name === fn.name && s.startLine === fn.startLine);
2325
- const params = String(sym?.params || '').split(',').map(p => p.trim().split(/[:=]/)[0].trim());
2326
- return params.includes(call.receiver) ? 'pytest-client-fixture' : null;
2327
- }
2328
-
2329
2282
  // `fastify().route({...})` - a route declared on a freshly built server.
2330
2283
  const JS_SERVER_FACTORY_CALL = /^(fastify|Fastify|express|Hono|Koa)$/;
2331
2284
 
@@ -2347,7 +2300,7 @@ function extractClientRequests(index) {
2347
2300
  }
2348
2301
  const requests = [];
2349
2302
  const uncertain = [];
2350
- const pyFixtures = _pythonClientFixtures(index);
2303
+ const clientProof = new (require('./client-receivers').ClientReceiverProof)(index, endpointsSession(index));
2351
2304
  let helperProof = null;
2352
2305
 
2353
2306
  for (const [filePath, fileEntry] of index.files) {
@@ -2431,7 +2384,9 @@ function extractClientRequests(index) {
2431
2384
  continue;
2432
2385
  }
2433
2386
  if (!helperProof) helperProof = new RequestHelperProof(index, endpointsSession(index));
2434
- const proof = helperProof.classify(filePath, call);
2387
+ const proof = call.isMethod
2388
+ ? { proven: !!clientProof.classify(filePath, call), reason: 'receiver-unproven' }
2389
+ : helperProof.classify(filePath, call);
2435
2390
  const verb = call.isMethod && /^(get|post|put|delete|patch|options|head)$/i.test(call.name)
2436
2391
  ? call.name.toUpperCase() : null;
2437
2392
  const method = cfg.method || verb || 'ALL';
@@ -2462,7 +2417,7 @@ function extractClientRequests(index) {
2462
2417
  // fix #383: Go request constructors read their method and URL
2463
2418
  // from their own argument positions.
2464
2419
  if (call.requestTarget && lang === 'go' && call.receiver && !call.isPathCall &&
2465
- matchClientRequest({ ...call, firstStringArg: call.requestTarget.url }, lang, calls)) {
2420
+ matchClientRequest({ ...call, firstStringArg: call.requestTarget.url }, lang, clientProof.classify(filePath, call))) {
2466
2421
  const target = call.requestTarget;
2467
2422
  if (isPathShaped(target.url)) {
2468
2423
  // A request with a path but no scheme and host cannot be
@@ -2496,26 +2451,22 @@ function extractClientRequests(index) {
2496
2451
  continue;
2497
2452
  }
2498
2453
  if (!call.firstStringArg) continue;
2499
- let r = matchClientRequest(call, lang, calls);
2500
- if (!r && lang === 'python') {
2501
- const framework = _pythonClientReceiver(index, fileEntry, call, pyFixtures);
2502
- if (framework) {
2503
- r = { method: call.name.toUpperCase() === 'REQUEST' ? 'ALL' : call.name.toUpperCase(),
2504
- framework, methodInferred: call.name === 'request' };
2505
- }
2506
- }
2454
+ const conf = CLIENT_PATTERNS[lang];
2455
+ const candidate = conf && (conf.bareCalls.has(call.name) ||
2456
+ conf.receivers.some(p => p.methodPattern.test(call.name)));
2457
+ const r = matchClientRequest(call, lang, candidate ? clientProof.classify(filePath, call) : null);
2507
2458
  if (!r) {
2508
2459
  // Visible uncertainty: request-shaped call on an unrecognized
2509
2460
  // receiver with a path-shaped literal. Listed, never counted.
2510
- const conf = CLIENT_PATTERNS[lang];
2511
2461
  const pathShaped = call.firstStringArg.startsWith('/') || call.firstStringArg.includes('://');
2512
- if (conf && call.isMethod && call.receiver && pathShaped &&
2462
+ if (conf && call.receiver && pathShaped &&
2513
2463
  conf.receivers.some(p => p.methodPattern.test(call.name))) {
2514
2464
  uncertain.push({
2515
2465
  receiver: call.receiver, method: call.name, path: call.firstStringArg,
2516
2466
  file: fileEntry.relativePath || filePath, absoluteFile: filePath, line: call.line,
2517
2467
  callerName: call.enclosingFunction?.name || '<top-level>',
2518
- reason: 'receiver-unrecognized',
2468
+ reason: conf.receivers.some(p => p.receiverPattern.test(call.receiver))
2469
+ ? 'receiver-name-only' : 'receiver-unrecognized',
2519
2470
  });
2520
2471
  }
2521
2472
  continue;
@@ -2640,9 +2591,9 @@ function supertestProducerOf(call, calls, producers, locals) {
2640
2591
  * Match a call against client request patterns.
2641
2592
  * Returns { method, framework, methodInferred } or null.
2642
2593
  */
2643
- function matchClientRequest(call, lang, allCallsInFile) {
2594
+ function matchClientRequest(call, lang, origin) {
2644
2595
  const conf = CLIENT_PATTERNS[lang];
2645
- if (!conf) return null;
2596
+ if (!conf || !origin) return null;
2646
2597
 
2647
2598
  // 1) Bare-call patterns: fetch('/path') or fetch('/path', { method: 'POST' })
2648
2599
  if (!call.isMethod && conf.bareCalls.has(call.name)) {
@@ -2662,7 +2613,6 @@ function matchClientRequest(call, lang, allCallsInFile) {
2662
2613
  const isMethodLike = call.isMethod || (lang === 'go' && !!call.receiver && !call.isPathCall);
2663
2614
  if (isMethodLike && call.receiver) {
2664
2615
  for (const p of conf.receivers) {
2665
- if (!p.receiverPattern.test(call.receiver)) continue;
2666
2616
  if (!p.methodPattern.test(call.name)) continue;
2667
2617
 
2668
2618
  // Determine method
@@ -2697,7 +2647,7 @@ function matchClientRequest(call, lang, allCallsInFile) {
2697
2647
  } else {
2698
2648
  method = methodName.toUpperCase();
2699
2649
  }
2700
- return { method, framework: p.framework, methodInferred: inferred };
2650
+ return { method, framework: origin, methodInferred: inferred };
2701
2651
  }
2702
2652
  }
2703
2653
 
package/core/brief.js CHANGED
@@ -102,7 +102,7 @@ function brief(index, name, options = {}) {
102
102
  const { def } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
103
103
  if (!def) return null;
104
104
 
105
- const language = detectLanguage(def.relativePath || def.file);
105
+ const language = index.languageOf(def.file || def.relativePath);
106
106
  const symbol = {
107
107
  name: def.name,
108
108
  type: def.type,
@@ -229,7 +229,10 @@ function firstSentence(text) {
229
229
  function countMembers(index, def) {
230
230
  if (!def || !def.file) return 0;
231
231
  let count = 0;
232
- for (const arr of index.symbols.values()) {
232
+ // Build and cache loading keep each file's declaration list alongside
233
+ // the name index. This count only reads declarations of the pinned file.
234
+ const fileSymbols = index.files?.get(def.file)?.symbols;
235
+ for (const arr of fileSymbols ? [fileSymbols] : index.symbols.values()) {
233
236
  for (const s of arr) {
234
237
  if (s.file === def.file && s.className === def.name && s.isMethod) count++;
235
238
  }
@@ -362,8 +365,9 @@ function sideEffectsFor(index, symbol) {
362
365
  index._sideEffectCache.set(key, null);
363
366
  return null;
364
367
  }
365
- const language = detectLanguage(symbol.relativePath || symbol.file);
366
- const fileEntry = index.files.get(symbol.file);
368
+ const language = typeof index.languageOf === 'function'
369
+ ? index.languageOf(filePath) : detectLanguage(filePath, index.root);
370
+ const fileEntry = index.files.get(filePath);
367
371
  const fileImports = collectImportNames(fileEntry);
368
372
  const tags = classifySideEffects(bodyText, language, fileImports);
369
373
  index._sideEffectCache.set(key, tags);
@@ -55,6 +55,7 @@ function externalMacrosFor(language) {
55
55
  consulted: new Set(),
56
56
  get objectBodies() { return resolve()?.objectBodies || EMPTY_MACROS; },
57
57
  get functionMacros() { return resolve()?.functionMacros || EMPTY_MACROS; },
58
+ get functionHeads() { return resolve()?.functionHeads || EMPTY_MACROS; },
58
59
  };
59
60
  }
60
61
  const signalArray = new Int32Array(signal);
@@ -180,7 +181,7 @@ if (control) {
180
181
  Atomics.wait(controlArray, workerIndex, 0);
181
182
  if (Atomics.load(controlArray, workerIndex) === 2) {
182
183
  const job = receiveMessageOnPort(port)?.message;
183
- if (job) require('./rust-macro-expansion').runExpansionQueue(job, port);
184
+ if (job) await require('./rust-macro-expansion').runExpansionQueue(job, port);
184
185
  }
185
186
  }
186
187
  try { port.close(); } catch (_) { /* closed */ }
package/core/cache.js CHANGED
@@ -14,6 +14,7 @@ const {
14
14
  classifyUnsupportedSourceFile,
15
15
  } = require('./discovery');
16
16
  const { codeUnitCompare } = require('./shared');
17
+ const { syntaxWeightKind } = require('./source-memo');
17
18
 
18
19
  // Read UCN version for cache invalidation
19
20
  const UCN_VERSION = require('../package.json').version;
@@ -925,9 +926,123 @@ function clearAllCaches() {
925
926
  // v276 (fix #405b): a pending Rust file whose expansions the calls pass
926
927
  // reads by groups records each group's lines, expanded length and the words
927
928
  // its expansions output and its headers hold (`rustMacroExpansion.groups`).
928
- const CACHE_FORMAT_VERSION = 276;
929
+ // v277: refactoring usages distinguish declarator-name tokens from value
930
+ // references; C# local-function name tokens are definitions.
931
+ // v278: generic invocation usages preserve the terminal name and receiver.
932
+ // v279: C++ using facts retain their enclosing preprocessor branch groups.
933
+ // v291: Value declarations retain lexical scope and forward-declaration facts;
934
+ // value queries retain source-local shadow/text proofs and lexical block
935
+ // ranges with each parse view's error positions.
936
+ // v292: Python class syntax facts retain members, writes and dynamic stores.
937
+ // v300: Java constructor declarations, construction arguments and delegation
938
+ // records; qualified construction types, declaration source extents and
939
+ // JS/TS assigned value names.
940
+ // v303: Java producer chains retain argument counts for collection contracts.
941
+ // v304: Java receiver producer paths retain their written type and source scope.
942
+ // v305: Java variadic parameters retain their declared array type.
943
+ // v306: Overload witnesses omit absent optional object fields consistently.
944
+ // v307: Overload witnesses capture observed file fields separately.
945
+ // v308 (fix #419): Java records without a declared canonical constructor
946
+ // carry its parameters (`primaryParams`); record constructors delegate.
947
+ // v309 (fix #421): C++ out-of-class constructor mem-initializers and
948
+ // default initializations are call records.
949
+ // v310 (fix #422): Rust bare and path call records carry the binding the
950
+ // enclosing blocks give their name (`scopeBinding`, `receiverScopeBinding`),
951
+ // qualified-path receivers and `macroScopeUnknown`; block `use` imports carry
952
+ // their block lines and `use` items their visibility; files list their
953
+ // item-position macro invocations (`itemMacros`).
954
+ // v311 (fix #423): JS/TS module resolution follows a directory's package.json
955
+ // `main` and maps workspace subpath exports naming absent build output to
956
+ // their `src/` sources (persisted `moduleResolved` and import edges).
957
+ // v312 (fix #424): Python usage records mark module-path segments
958
+ // (`modulePath`) and the source-side name of an aliased from-import
959
+ // (`importSource`).
960
+ // v313 (fix #425): Go call records read method expressions `(*T).M` like
961
+ // `T.M`, mark receivers no local binds (`receiverPackageLevel`), record
962
+ // conversion operands, instantiated values and positional literal-element
963
+ // selectors as values, and type type-switch bindings per clause.
964
+ // v314 (fix #426): C# `out var` / implicitly typed pattern locals record no
965
+ // receiver type (was the written `var`).
966
+ // v315 (fix #427): C/C++ call records mark `field->m()` receivers
967
+ // (`receiverFieldArrow`), the implicit object's constness
968
+ // (`receiverConst`) and `(N)(x)` cast-or-call shapes
969
+ // (`parenthesizedCallee`); member functions record `memberConst` /
970
+ // `refQualifier`; a definition after a `static` declaration is `static`;
971
+ // out-of-class conversion operators, pointer-to-member parameters,
972
+ // `decltype(x)` locals and macro-wrapped C bodies are read; deduction
973
+ // guides are no functions.
974
+ // v316 (fix #428): C# call records mark implicitly typed lambda arguments
975
+ // (`lambda:N:i`); Java files whose only parse errors are type annotations
976
+ // before a variable arity `...` are read from the recovered parse (the
977
+ // parameter is kept); reflection sites keyed by a parameter of the enclosing
978
+ // callable record it (`keyParameter`).
979
+ // v317 (fix #430): Python instance attributes (`self.x = v`), every name of
980
+ // a Go multi-name field, Java enum fields, C/C++ members of unnamed nested
981
+ // structs and unions (`memberPaths`) and C++ variable templates are indexed;
982
+ // C# receivers drop the null-forgiving `!`; Java resources take their
983
+ // declared type; C/C++ macro bodies read `#param` as a string literal.
984
+ // v318 (fix #431): JS/TS properties a class's methods assign on `this` are
985
+ // indexed as fields (`instanceAttribute`); value-reference records read
986
+ // destructuring assignment targets as writes, parameter defaults as reads
987
+ // and Python dict keys as values.
988
+ // v319 (fix #433): a saved symbol keeps `file` (and a default `bindingId`)
989
+ // as a 0 placeholder at its built position, so a loaded symbol has the key
990
+ // order of a fresh build.
991
+ // v320 (fix #434): call records of constructions through a method's own
992
+ // receiver (`cls(..)`, `type(self)(..)`, `self.__class__(..)`, `new
993
+ // this(..)`, `new this.constructor(..)`) carry `ownClass`.
994
+ // v321 (fix #436): file entries carry `computedDispatchSites`, extracted
995
+ // from the literal parse at index time (the doctor read them by parsing every
996
+ // file on the first `repo` of a process).
997
+ // v322: C# constructions retain terminal names and arity; nested generic receivers retain their qualifier.
998
+ // v326: C++ files retain raw base-clause positions and their syntactic scopes.
999
+ // v327: Python explicit-argument super calls retain a producer, not a lexical parent receiver.
1000
+ // v328: ambiguous C++ direct initializers retain empty template-call records.
1001
+ const CACHE_FORMAT_VERSION = 330;
929
1002
  const USAGE_CACHE_FILE = 'usage-results.json';
930
1003
 
1004
+ function usageResultKey(index, filePath, name, mode) {
1005
+ const hash = index.files.get(filePath)?.hash || '';
1006
+ return `${filePath}\0${hash}\0${name}${mode ? `\0${mode}` : ''}`;
1007
+ }
1008
+
1009
+ // A syntax family's stored value weighs what its producer declares
1010
+ // (core/source-memo.js SYNTAX_FAMILIES); a usage list weighs its sites.
1011
+ function usageResultWeight(value, mode) {
1012
+ const kind = syntaxWeightKind(mode);
1013
+ if (kind === 'rows') return 128 + value.length * 128;
1014
+ return kind === 'json' ? 128 + 2 * Buffer.byteLength(JSON.stringify(value)) : 64 + value.length * 40;
1015
+ }
1016
+
1017
+ /** Read an immutable, source-local analysis result from the shared LRU. */
1018
+ function getUsageResult(index, filePath, name, mode = '') {
1019
+ const key = usageResultKey(index, filePath, name, mode);
1020
+ const cached = index._usageResultCache?.get(key);
1021
+ if (!cached) return undefined;
1022
+ index._usageResultCache.delete(key);
1023
+ index._usageResultCache.set(key, cached);
1024
+ return cached.value;
1025
+ }
1026
+
1027
+ /** Store file-local facts only; project-dependent resolutions do not belong here. */
1028
+ function storeUsageResult(index, filePath, name, mode, value) {
1029
+ if (!index._usageResultCache || !Array.isArray(value)) return;
1030
+ const key = usageResultKey(index, filePath, name, mode);
1031
+ const prior = index._usageResultCache.get(key);
1032
+ if (prior) index._usageResultCacheWeight -= prior.weight;
1033
+ const weight = usageResultWeight(value, mode);
1034
+ index._usageResultCache.delete(key);
1035
+ index._usageResultCache.set(key, { value, weight });
1036
+ index._usageResultCacheWeight += weight;
1037
+ index.usageCacheDirty = true;
1038
+ while (index._usageResultCache.size > 4096 || index._usageResultCacheWeight > 16 * 1024 * 1024) {
1039
+ const oldest = index._usageResultCache.entries().next().value;
1040
+ if (!oldest) break;
1041
+ index._usageResultCache.delete(oldest[0]);
1042
+ index._usageResultCacheWeight -= oldest[1].weight;
1043
+ }
1044
+ }
1045
+
931
1046
  /**
932
1047
  * Persist the small hot-name usage cache independently of index/call shards.
933
1048
  * This keeps one-shot CLI repeats fast without rewriting the whole project
@@ -987,7 +1102,7 @@ function loadUsageCache(index, cacheFile) {
987
1102
  if (!fileEntry || fileEntry.hash !== fileHash) continue;
988
1103
  const suffix = mode ? `\0${mode}` : '';
989
1104
  const key = `${filePath}\0${fileHash}\0${name}${suffix}`;
990
- const weight = 64 + value.length * 40;
1105
+ const weight = usageResultWeight(value, mode);
991
1106
  restored.set(key, { value, weight });
992
1107
  weightTotal += weight;
993
1108
  while (restored.size > 4096 || weightTotal > 16 * 1024 * 1024) {
@@ -1058,14 +1173,18 @@ function saveCache(index, cachePath) {
1058
1173
  const strippedSymbols = [];
1059
1174
  for (const [name, defs] of index.symbols) {
1060
1175
  const stripped = defs.map(s => {
1061
- const { file, bindingId, ...rest } = s;
1062
1176
  const defaultBindingId = s.relativePath && s.type && s.startLine
1063
1177
  ? `${s.relativePath}:${s.type}:${s.startLine}`
1064
1178
  : null;
1065
- if (bindingId && bindingId !== defaultBindingId) {
1066
- rest.bindingId = bindingId;
1179
+ // `file` and a default `bindingId` are rebuilt on load; their 0
1180
+ // placeholders keep the key order of the built symbol (fix #433).
1181
+ const out = {};
1182
+ for (const key in s) {
1183
+ if (key === 'file') out.file = 0;
1184
+ else if (key === 'bindingId') out.bindingId = s.bindingId && s.bindingId !== defaultBindingId ? s.bindingId : 0;
1185
+ else out[key] = s[key];
1067
1186
  }
1068
- return rest;
1187
+ return out;
1069
1188
  });
1070
1189
  strippedSymbols.push([name, stripped]);
1071
1190
  }
@@ -1128,6 +1247,7 @@ function saveCache(index, cachePath) {
1128
1247
  // argKinds for overload discipline (stale shapes would silently
1129
1248
  // disable arity pruning)
1130
1249
  version: CACHE_FORMAT_VERSION,
1250
+ callsGeneration: crypto.randomBytes(16).toString('hex'),
1131
1251
  ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
1132
1252
  configHash,
1133
1253
  discoveryHash,
@@ -1190,6 +1310,7 @@ function saveCache(index, cachePath) {
1190
1310
 
1191
1311
  // PERF-3: atomic write — tmp file + rename so concurrent readers/writers
1192
1312
  // never see a torn JSON. The calls/ shard write below already does this.
1313
+ index._callsGenerationKey = cacheData.callsGeneration;
1193
1314
  const tmpFile = cacheFile + '.tmp';
1194
1315
  fs.writeFileSync(tmpFile, JSON.stringify(cacheData));
1195
1316
  fs.renameSync(tmpFile, cacheFile);
@@ -1254,6 +1375,7 @@ function saveCache(index, cachePath) {
1254
1375
  fs.rmSync(callsDir, { recursive: true, force: true });
1255
1376
  }
1256
1377
  fs.renameSync(callsTmpDir, callsDir);
1378
+ require('./call-name-cache').save(index, callsDir);
1257
1379
 
1258
1380
  // Clean up legacy monolithic file
1259
1381
  const legacyFile = path.join(cacheDir, 'calls-cache.json');
@@ -1339,6 +1461,8 @@ function loadCache(index, cachePath) {
1339
1461
  index._rustMacroState = null;
1340
1462
  index._cppScope = null;
1341
1463
  index._contractIndex = null;
1464
+ index._implicitConversionOwners = null;
1465
+ index._constructionAliases = null;
1342
1466
  index._typeDenotation = null;
1343
1467
  index._languageFeatureMemo = null; // fix #380 protocol-type feature memo
1344
1468
 
@@ -1405,6 +1529,11 @@ function loadCache(index, cachePath) {
1405
1529
  // extendsGraph/extendedByGraph are derived below from the rehydrated
1406
1530
  // symbols (fix #342) — never read from the payload.
1407
1531
 
1532
+ index._callsGenerationKey = typeof cacheData.callsGeneration === 'string'
1533
+ ? cacheData.callsGeneration : null;
1534
+ index._callNameCache = null;
1535
+ index._callNameCacheReadGeneration = null;
1536
+
1408
1537
  // Prepare lazy calls cache loading — load manifest but defer shard parsing.
1409
1538
  // Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
1410
1539
  if (index.callsCache.size === 0) {
@@ -1693,6 +1822,7 @@ function _prepareCallsCache(index, cacheFile) {
1693
1822
  });
1694
1823
  }
1695
1824
  index._callsCachePrepared = true;
1825
+ index._callNameCacheDir = path.join(cacheDir, 'calls');
1696
1826
  return;
1697
1827
  } catch (e) {
1698
1828
  // Corrupted manifest — fall through
@@ -2114,6 +2244,7 @@ function buildWithLock(index, buildOpts = {}, options = {}) {
2114
2244
  module.exports = {
2115
2245
  buildWithLock,
2116
2246
  saveCache, saveUsageCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
2247
+ getUsageResult, storeUsageResult,
2117
2248
  ensureCallsShardForFile,
2118
2249
  getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
2119
2250
  getLegacyProjectCacheDir, migrateLegacyProjectCache, clearProjectCache,
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { calleeNames, codeUnitCompare } = require('./shared');
6
+ const { fileMtime } = require('./source-memo');
7
+ const { version } = require('../package.json');
8
+
9
+ // Call-record names only. Binding, dispatch, aliases and type queries still
10
+ // run through their ordinary record consumers.
11
+ function snapshot(index, files, captured) {
12
+ return { files, captured, filesMap: index.files, graph: index.importGraph,
13
+ config: JSON.stringify(index.config), generation: index._callsGenerationKey };
14
+ }
15
+
16
+ function current(index, cache) {
17
+ return !!cache && cache.filesMap === index.files && cache.graph === index.importGraph &&
18
+ cache.config === JSON.stringify(index.config) && cache.files.size === index.files.size &&
19
+ [...cache.files].every(([file, item]) => index.files.get(file) === item.entry &&
20
+ item.entry.hash === item.hash);
21
+ }
22
+
23
+ function ensure(index) {
24
+ if (index.isProvenanceFactIndex || !index._callsGenerationKey ||
25
+ index._callNameCacheReadGeneration === index._callsGenerationKey) return;
26
+ index._callNameCacheReadGeneration = index._callsGenerationKey;
27
+ if (index._callNameCacheDir) load(index, index._callNameCacheDir);
28
+ }
29
+
30
+ function capture(index, collected, file, calls) {
31
+ if (index.isProvenanceFactIndex) return;
32
+ const entry = index.files.get(file), raw = index.callsCache.get(file);
33
+ if (!entry?.hash || raw?.hash !== entry.hash || !Array.isArray(calls) ||
34
+ !Number.isFinite(raw.mtime)) return;
35
+ const names = calleeNames(calls);
36
+ if ([...names].some(name => typeof name !== 'string')) return;
37
+ collected.set(file, { entry, hash: entry.hash, raw, rawCalls: raw.calls, names, mtime: raw.mtime });
38
+ }
39
+
40
+ function remember(index, files) {
41
+ if (index.isProvenanceFactIndex || files.size !== index.files.size) return;
42
+ index._callNameCache = snapshot(index, files, true);
43
+ if (index._callsGenerationKey && !index.callsCacheDirty && !index.macroExpansionDirty && index._callsCacheDir) save(index, path.join(index._callsCacheDir, 'calls'));
44
+ }
45
+
46
+ function save(index, directory) {
47
+ if (index.isProvenanceFactIndex) return;
48
+ const cache = index._callNameCache;
49
+ if (!cache?.captured || !index._callsGenerationKey ||
50
+ !current(index, cache) || !fs.existsSync(directory)) return;
51
+ for (const [file, item] of cache.files) {
52
+ if (index.files.get(file) !== item.entry ||
53
+ index.callsCache.get(file) !== item.raw || item.raw.calls !== item.rawCalls ||
54
+ item.raw.mtime !== item.mtime) return;
55
+ }
56
+ const dictionary = [...new Set([...cache.files.values()].flatMap(item =>
57
+ [...item.names]))].sort(codeUnitCompare);
58
+ const ids = new Map(dictionary.map((name, id) => [name, id]));
59
+ const files = [...cache.files].map(([file, item]) => [
60
+ path.relative(index.root, file), item.mtime,
61
+ [...item.names].map(name => ids.get(name)),
62
+ ]).sort((a, b) => codeUnitCompare(a[0], b[0]));
63
+ const target = path.join(directory, 'names.json');
64
+ try {
65
+ fs.writeFileSync(target + '.tmp', JSON.stringify({
66
+ version, format: 2, generation: index._callsGenerationKey, dictionary, files,
67
+ }));
68
+ fs.renameSync(target + '.tmp', target);
69
+ } catch (_) { /* A missing optimization falls back to the record shards. */ }
70
+ }
71
+
72
+ function load(index, directory) {
73
+ if (index.isProvenanceFactIndex) return;
74
+ index._callNameCache = null;
75
+ try {
76
+ const data = JSON.parse(fs.readFileSync(path.join(directory, 'names.json'), 'utf8'));
77
+ if (data.version !== version || data.format !== 2 ||
78
+ data.generation !== index._callsGenerationKey ||
79
+ !Array.isArray(data.dictionary) || !Array.isArray(data.files) ||
80
+ data.files.length !== index.files.size ||
81
+ data.dictionary.some(name => typeof name !== 'string') ||
82
+ new Set(data.dictionary).size !== data.dictionary.length) return;
83
+ const files = new Map();
84
+ for (const row of data.files) {
85
+ if (!Array.isArray(row) || row.length !== 3 || typeof row[0] !== 'string' ||
86
+ !Number.isFinite(row[1]) || !Array.isArray(row[2])) return;
87
+ const file = path.resolve(index.root, row[0]), entry = index.files.get(file);
88
+ if (!entry || files.has(file) || row[2].some(id =>
89
+ !Number.isInteger(id) || id < 0 || id >= data.dictionary.length) ||
90
+ new Set(row[2]).size !== row[2].length) return;
91
+ files.set(file, {
92
+ entry, hash: entry.hash, mtime: row[1], names: new Set(row[2].map(id => data.dictionary[id])),
93
+ });
94
+ }
95
+ index._callNameCache = snapshot(index, files, false);
96
+ } catch (_) { /* Missing or invalid names: use the record shards. */ }
97
+ }
98
+
99
+ function restore(index) {
100
+ if (index.isProvenanceFactIndex) return false;
101
+ ensure(index);
102
+ const cache = index._callNameCache;
103
+ // A fresh in-memory parse wins over persisted names, as it does over
104
+ // persisted call records. A rebuild discards this cache explicitly.
105
+ if (index.callsCache.size || !current(index, cache) ||
106
+ cache.generation !== index._callsGenerationKey) return false;
107
+ for (const [file, item] of cache.files) {
108
+ if (index.files.get(file) !== item.entry) return false;
109
+ }
110
+ const byName = new Map();
111
+ for (const [file] of index.files) {
112
+ for (const name of cache.files.get(file).names) {
113
+ let files = byName.get(name);
114
+ if (!files) byName.set(name, files = new Set());
115
+ files.add(file);
116
+ }
117
+ }
118
+ index.calleeIndex = byName;
119
+ return true;
120
+ }
121
+
122
+ function absent(index, file, name) {
123
+ if (index.isProvenanceFactIndex || index.callsCache.has(file)) return false;
124
+ ensure(index);
125
+ const item = index._callNameCache?.files.get(file);
126
+ if (!item || index.files.get(file) !== item.entry || item.entry.hash !== item.hash ||
127
+ item.entry.rustMacroExpansion || item.names.has(name)) return false;
128
+ // Match the raw record cache's mtime rule. Changed source must reach
129
+ // its normal read/hash/reparse path even if its previous names lacked
130
+ // this query. No negative binding or ownership verdict is cached.
131
+ try { return fileMtime(index, file) === item.mtime; } catch (_) { return false; }
132
+ }
133
+
134
+ module.exports = { capture, remember, save, load, restore, absent };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ /** Whether the written call lies inside the declaration's AST extent. */
4
+ function containsCall(definition, call) {
5
+ const span = definition?.sourceSpan;
6
+ if (!span || call?.macroExpansion || definition.macroExpansion || definition.generatedByMacro) return null;
7
+ const line = call.line, column = call.column ?? call.callSite?.column;
8
+ if (!Number.isInteger(line) || !Number.isInteger(span.startLine) || !Number.isInteger(span.endLine)) return null;
9
+ if (line < span.startLine || line > span.endLine) return false;
10
+ if (Number.isInteger(column)) {
11
+ return (line > span.startLine || column >= span.startColumn) &&
12
+ (line < span.endLine || column < span.endColumn);
13
+ }
14
+ const start = call.callStart ?? call.callSite?.start;
15
+ if (Number.isInteger(start) && Number.isInteger(span.start) && Number.isInteger(span.end)) {
16
+ return start >= span.start && start < span.end;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ module.exports = { containsCall };