ucn 5.5.1 → 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 +30 -21
  6. package/cli/index.js +1 -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/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +137 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +4726 -940
  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 +217 -112
  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 +151 -19
  63. package/core/rust-modules.js +1 -1
  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 +124 -37
  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 +510 -82
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. package/package.json +2 -2
@@ -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 };