ucn 5.5.0 → 5.5.2

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 (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  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 +41 -27
  6. package/cli/index.js +6 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +106 -31
  9. package/core/analysis.js +266 -127
  10. package/core/ast-analysis.js +12 -3
  11. package/core/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +144 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +5111 -1024
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +228 -114
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +1202 -157
  63. package/core/rust-modules.js +21 -3
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +157 -46
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +548 -89
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. package/package.json +2 -2
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;
@@ -918,9 +919,130 @@ function clearAllCaches() {
918
919
  // `passedAsArgument`.
919
920
  // v274 (fix #403): a Python reference inside a lambda whose enclosing def
920
921
  // binds the name records that def as its `localShadowScope`.
921
- const CACHE_FORMAT_VERSION = 274;
922
+ // v275 (fix #405): a Rust file whose macro-expansion calls are pending
923
+ // records every expansion's hygiene start and expanded length
924
+ // (`rustMacroExpansion.expansions`); its `words` leave out the text of
925
+ // literals and comments.
926
+ // v276 (fix #405b): a pending Rust file whose expansions the calls pass
927
+ // reads by groups records each group's lines, expanded length and the words
928
+ // its expansions output and its headers hold (`rustMacroExpansion.groups`).
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;
922
1002
  const USAGE_CACHE_FILE = 'usage-results.json';
923
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
+
924
1046
  /**
925
1047
  * Persist the small hot-name usage cache independently of index/call shards.
926
1048
  * This keeps one-shot CLI repeats fast without rewriting the whole project
@@ -980,7 +1102,7 @@ function loadUsageCache(index, cacheFile) {
980
1102
  if (!fileEntry || fileEntry.hash !== fileHash) continue;
981
1103
  const suffix = mode ? `\0${mode}` : '';
982
1104
  const key = `${filePath}\0${fileHash}\0${name}${suffix}`;
983
- const weight = 64 + value.length * 40;
1105
+ const weight = usageResultWeight(value, mode);
984
1106
  restored.set(key, { value, weight });
985
1107
  weightTotal += weight;
986
1108
  while (restored.size > 4096 || weightTotal > 16 * 1024 * 1024) {
@@ -1051,14 +1173,18 @@ function saveCache(index, cachePath) {
1051
1173
  const strippedSymbols = [];
1052
1174
  for (const [name, defs] of index.symbols) {
1053
1175
  const stripped = defs.map(s => {
1054
- const { file, bindingId, ...rest } = s;
1055
1176
  const defaultBindingId = s.relativePath && s.type && s.startLine
1056
1177
  ? `${s.relativePath}:${s.type}:${s.startLine}`
1057
1178
  : null;
1058
- if (bindingId && bindingId !== defaultBindingId) {
1059
- 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];
1060
1186
  }
1061
- return rest;
1187
+ return out;
1062
1188
  });
1063
1189
  strippedSymbols.push([name, stripped]);
1064
1190
  }
@@ -1121,6 +1247,7 @@ function saveCache(index, cachePath) {
1121
1247
  // argKinds for overload discipline (stale shapes would silently
1122
1248
  // disable arity pruning)
1123
1249
  version: CACHE_FORMAT_VERSION,
1250
+ callsGeneration: crypto.randomBytes(16).toString('hex'),
1124
1251
  ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
1125
1252
  configHash,
1126
1253
  discoveryHash,
@@ -1183,6 +1310,7 @@ function saveCache(index, cachePath) {
1183
1310
 
1184
1311
  // PERF-3: atomic write — tmp file + rename so concurrent readers/writers
1185
1312
  // never see a torn JSON. The calls/ shard write below already does this.
1313
+ index._callsGenerationKey = cacheData.callsGeneration;
1186
1314
  const tmpFile = cacheFile + '.tmp';
1187
1315
  fs.writeFileSync(tmpFile, JSON.stringify(cacheData));
1188
1316
  fs.renameSync(tmpFile, cacheFile);
@@ -1247,6 +1375,7 @@ function saveCache(index, cachePath) {
1247
1375
  fs.rmSync(callsDir, { recursive: true, force: true });
1248
1376
  }
1249
1377
  fs.renameSync(callsTmpDir, callsDir);
1378
+ require('./call-name-cache').save(index, callsDir);
1250
1379
 
1251
1380
  // Clean up legacy monolithic file
1252
1381
  const legacyFile = path.join(cacheDir, 'calls-cache.json');
@@ -1332,6 +1461,8 @@ function loadCache(index, cachePath) {
1332
1461
  index._rustMacroState = null;
1333
1462
  index._cppScope = null;
1334
1463
  index._contractIndex = null;
1464
+ index._implicitConversionOwners = null;
1465
+ index._constructionAliases = null;
1335
1466
  index._typeDenotation = null;
1336
1467
  index._languageFeatureMemo = null; // fix #380 protocol-type feature memo
1337
1468
 
@@ -1398,6 +1529,11 @@ function loadCache(index, cachePath) {
1398
1529
  // extendsGraph/extendedByGraph are derived below from the rehydrated
1399
1530
  // symbols (fix #342) — never read from the payload.
1400
1531
 
1532
+ index._callsGenerationKey = typeof cacheData.callsGeneration === 'string'
1533
+ ? cacheData.callsGeneration : null;
1534
+ index._callNameCache = null;
1535
+ index._callNameCacheReadGeneration = null;
1536
+
1401
1537
  // Prepare lazy calls cache loading — load manifest but defer shard parsing.
1402
1538
  // Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
1403
1539
  if (index.callsCache.size === 0) {
@@ -1686,6 +1822,7 @@ function _prepareCallsCache(index, cacheFile) {
1686
1822
  });
1687
1823
  }
1688
1824
  index._callsCachePrepared = true;
1825
+ index._callNameCacheDir = path.join(cacheDir, 'calls');
1689
1826
  return;
1690
1827
  } catch (e) {
1691
1828
  // Corrupted manifest — fall through
@@ -2107,6 +2244,7 @@ function buildWithLock(index, buildOpts = {}, options = {}) {
2107
2244
  module.exports = {
2108
2245
  buildWithLock,
2109
2246
  saveCache, saveUsageCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
2247
+ getUsageResult, storeUsageResult,
2110
2248
  ensureCallsShardForFile,
2111
2249
  getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
2112
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 };