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
@@ -1,66 +1,291 @@
1
1
  'use strict';
2
2
 
3
- const { declarationIdentity, sameDeclaration } = require('./provenance');
3
+ const { declarationIdentity, identityKey, sameDeclaration } = require('./provenance');
4
+ const { langTraits } = require('../languages');
5
+ const { UcnError, isInternalError } = require('./errors');
6
+
7
+ // Modules read lazily (load cycles), each resolved once: a require() per
8
+ // call in hot paths was measurable (fix #436).
9
+ let lazyCallersModuleCache = null;
10
+ const lazyCallersModule = () => lazyCallersModuleCache || (lazyCallersModuleCache = require('./callers'));
11
+
12
+ /**
13
+ * A witness that cannot prove its answer (fix #421): a fact its view did
14
+ * not record, or a filesystem read that failed when it was captured. A
15
+ * capture or a replay that meets one proves nothing; any other exception is
16
+ * a defect and propagates.
17
+ */
18
+ class ProvenanceRefusal extends UcnError {
19
+ constructor(message, code) {
20
+ super(message);
21
+ this.name = 'UcnError';
22
+ this.provenanceRefusal = true;
23
+ if (code !== undefined) this.code = code;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Null for an expected refusal (a missing witness fact, an unreadable
29
+ * source); rethrows an internal error so it reaches the surface as one
30
+ * instead of becoming a review row (fix #421).
31
+ */
32
+ function refusedProof(error, refusal = null) {
33
+ if (error?.provenanceRefusal || !isInternalError(error)) return refusal;
34
+ throw error;
35
+ }
36
+
37
+ const OVERLOAD_FILE_FIELDS = ['language', 'relativePath', 'importBindings',
38
+ 'importDetails', 'symbols', 'moduleResolved', 'cppUsings', 'macroScopeMarkers'];
39
+
40
+ // Work counters of witness construction (fix #418): a scaling test asserts
41
+ // that encoded and decoded facts grow with the sites a query visits, never
42
+ // with the project each witness could copy.
43
+ const WORK = { encodedNodes: 0, decodedNodes: 0, captures: 0, replays: 0 };
44
+ function provenanceWork() { return { ...WORK }; }
4
45
 
5
46
  // Materialize the declaration/ancestry reads used by overload selection.
6
47
  // Rechecking a witness runs the same selector over these data, with no live
7
48
  // index, filesystem, or name-ranking fallback. An unrecorded read abstains.
8
- function encode(value) {
9
- if (value === undefined) return { $undefined: true };
10
- if (value instanceof Set) return { $set: [...value].map(encode) };
11
- if (value instanceof Map) return { $map: [...value].map(([k, v]) => [encode(k), encode(v)]) };
12
- if (Array.isArray(value)) return value.map(encode);
13
- if (value && typeof value === 'object') return Object.fromEntries(
14
- Object.entries(value).filter(([, v]) => typeof v !== 'function').map(([k, v]) => [k, encode(v)]));
49
+ // `frozen` (inside an operation) records immutable facts; `frozen.plain`
50
+ // turns false when the value holds a Map or Set, which cannot be frozen.
51
+ function encode(value, frozen = null) {
52
+ WORK.encodedNodes++;
53
+ const seal = out => frozen ? Object.freeze(out) : out;
54
+ if (value === undefined) return seal({ $undefined: true });
55
+ if (value instanceof Set) {
56
+ if (frozen) frozen.plain = false;
57
+ return seal({ $set: seal([...value].map(item => encode(item, frozen))) });
58
+ }
59
+ if (value instanceof Map) {
60
+ if (frozen) frozen.plain = false;
61
+ return seal({ $map: seal([...value].map(([k, v]) => seal([encode(k, frozen), encode(v, frozen)]))) });
62
+ }
63
+ if (Array.isArray(value)) return seal(value.map(item => encode(item, frozen)));
64
+ if (value && typeof value === 'object') {
65
+ const out = {};
66
+ for (const key of Object.keys(value)) {
67
+ const item = value[key];
68
+ if (item === undefined || typeof item === 'function') continue;
69
+ const encoded = encode(item, frozen);
70
+ if (key === '__proto__') Object.defineProperty(out, key, {
71
+ value: encoded, enumerable: true, writable: true, configurable: true,
72
+ });
73
+ else out[key] = encoded;
74
+ }
75
+ return seal(out);
76
+ }
15
77
  return value;
16
78
  }
17
- function decode(value) {
79
+ function decode(value, freeze = false) {
80
+ WORK.decodedNodes++;
81
+ const seal = out => freeze ? Object.freeze(out) : out;
18
82
  if (value?.$undefined) return undefined;
19
- if (value?.$set) return new Set(value.$set.map(decode));
83
+ if (value?.$set) return new Set(value.$set.map(item => decode(item)));
20
84
  if (value?.$map) return new Map(value.$map.map(([k, v]) => [decode(k), decode(v)]));
21
- if (Array.isArray(value)) return value.map(decode);
22
- if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, decode(v)]));
85
+ if (Array.isArray(value)) return seal(value.map(item => decode(item, freeze)));
86
+ if (value && typeof value === 'object') {
87
+ const out = {};
88
+ for (const key of Object.keys(value)) {
89
+ const decoded = decode(value[key], freeze);
90
+ if (key === '__proto__') Object.defineProperty(out, key, {
91
+ value: decoded, enumerable: true, writable: true, configurable: true,
92
+ });
93
+ else out[key] = decoded;
94
+ }
95
+ return seal(out);
96
+ }
23
97
  return value;
24
98
  }
25
99
 
26
- function factIndex(reads, live) {
27
- const local = new Map();
28
- const read = (key, compute) => {
29
- if (live && !Object.hasOwn(reads, key)) reads[key] = encode(compute());
30
- if (!Object.hasOwn(reads, key)) throw new Error(`Missing overload fact: ${key}`);
31
- return decode(reads[key]);
100
+ // Recorded facts are immutable. Within one operation each live index value
101
+ // is encoded once for every witness that reads it, and a plain recorded
102
+ // value has one frozen decoded twin that every view reads (fix #418: a copy
103
+ // of a whole symbol table per site and per replay made constructor and
104
+ // overload queries quadratic). Writing to a shared fact throws rather than
105
+ // leaking into another view; a Map or Set fact is decoded per view.
106
+ const PLAIN_FACTS = new WeakSet();
107
+ const DECODED_FACTS = new WeakMap();
108
+
109
+ function encodeFact(value, memo) {
110
+ if (!memo || !value || typeof value !== 'object') return encode(value, memo && {});
111
+ let encoded = memo.get(value);
112
+ if (encoded === undefined) {
113
+ const frozen = { plain: true };
114
+ encoded = encode(value, frozen);
115
+ if (frozen.plain) PLAIN_FACTS.add(encoded);
116
+ memo.set(value, encoded);
117
+ }
118
+ return encoded;
119
+ }
120
+
121
+ function decodeFact(value) {
122
+ if (!value || typeof value !== 'object' || !PLAIN_FACTS.has(value)) return decode(value);
123
+ let decoded = DECODED_FACTS.get(value);
124
+ if (decoded === undefined) {
125
+ decoded = decode(value, true);
126
+ DECODED_FACTS.set(value, decoded);
127
+ }
128
+ return decoded;
129
+ }
130
+
131
+ // Shared computations of one operation by key and view kind (fix #418).
132
+ const FIELD_SET_TAGS = new WeakMap();
133
+ let fieldSetTags = 0;
134
+ const fieldSetTag = fields => {
135
+ if (!fields) return 'all';
136
+ if (!FIELD_SET_TAGS.has(fields)) FIELD_SET_TAGS.set(fields, `fields${++fieldSetTags}`);
137
+ return FIELD_SET_TAGS.get(fields);
138
+ };
139
+
140
+ function factIndex(reads, live, fileFields) {
141
+ if (live) WORK.captures++;
142
+ else WORK.replays++;
143
+ // Derived identities must be rebuilt from captured declarations. A live
144
+ // WeakMap is neither a raw input nor a replayable serialization.
145
+ const local = new Map([['_classIdentity', null], ['_typeDenotation', null], ['_cppScope', null]]);
146
+ // Inside an operation of the live index, facts are immutable and an
147
+ // index value is encoded once for every witness of the operation.
148
+ const encoded = live && !live.isProvenanceFactIndex && typeof live._opMemo === 'function'
149
+ ? live._opMemo('provenanceEncodedFacts', () => new WeakMap()) : null;
150
+ let missingFacts = false;
151
+ // `indexed`: the value is an index structure (a file field, a symbol
152
+ // list, a graph entry), the same object for every reader.
153
+ const read = (key, compute, indexed = false) => {
154
+ if (live && !Object.hasOwn(reads, key)) {
155
+ reads[key] = indexed ? encodeFact(compute(), encoded) : encode(compute(), encoded && {});
156
+ }
157
+ if (!Object.hasOwn(reads, key)) {
158
+ missingFacts = true;
159
+ throw new ProvenanceRefusal(`Missing provenance fact: ${key}`);
160
+ }
161
+ return decodeFact(reads[key]);
32
162
  };
33
163
  // A map-like read answers with one object per key, as the live index's
34
164
  // Map.get does (fix #388: decoding the same file entry again for every
35
165
  // read dominated warm `find` on Java projects).
36
166
  const mapReads = new Map();
37
- const readMapValue = (key, compute) => {
167
+ const readMapValue = (key, compute, indexed = false) => {
38
168
  if (mapReads.has(key)) return mapReads.get(key);
39
- const value = read(key, compute);
169
+ const value = read(key, compute, indexed);
40
170
  mapReads.set(key, value);
41
171
  return value;
42
172
  };
43
- return new Proxy({}, {
173
+ const readFile = key => {
174
+ const base = `files.get:${JSON.stringify(key)}`;
175
+ if (mapReads.has(base)) return mapReads.get(base);
176
+ if (!read(`${base}.$present`, () => !!live.files.get(key))) {
177
+ mapReads.set(base, undefined);
178
+ return undefined;
179
+ }
180
+ // A language or package read must not copy every declaration in the
181
+ // file. Each observed field is an independent raw input; replaying
182
+ // an unobserved field still refuses for missing facts.
183
+ const value = {};
184
+ for (const field of fileFields) Object.defineProperty(value, field, {
185
+ enumerable: true, configurable: true,
186
+ get: () => readMapValue(`${base}.${JSON.stringify(field)}`,
187
+ () => {
188
+ const entry = live.files.get(key);
189
+ if (fileFields === OVERLOAD_FILE_FIELDS && field === 'symbols' &&
190
+ !langTraits(entry.language)?.overloadFileSymbols) return undefined;
191
+ return entry[field];
192
+ }, true),
193
+ set: next => {
194
+ Object.defineProperty(value, field, {
195
+ value: next, writable: true, enumerable: true, configurable: true,
196
+ });
197
+ },
198
+ });
199
+ mapReads.set(base, value);
200
+ return value;
201
+ };
202
+ const view = new Proxy({}, {
44
203
  get(_, property) {
45
204
  // Callers skip their per-operation memos on this facade: every
46
205
  // read must be recorded for the replay (fix #396).
47
206
  if (property === 'isProvenanceFactIndex') return true;
207
+ if (property === 'hasMissingProvenanceFacts') return missingFacts;
208
+ // A hint never changes an answer, only how it is reached: a
209
+ // capture may consult the live index for it, a replay goes without.
210
+ if (property === 'unrecordedHint') {
211
+ return (compute) => live && !live.isProvenanceFactIndex ? compute(live) : undefined;
212
+ }
213
+ // A pure computation over the view, shared by every witness of
214
+ // the operation: a capture runs it once on a view of its own and
215
+ // records that view's reads; a replay recomputes it.
216
+ if (property === 'sharedRead') {
217
+ return (key, compute) => {
218
+ const memo = live && !live.isProvenanceFactIndex && typeof live._opMemo === 'function'
219
+ ? live._opMemo('provenanceSharedReads', () => new Map()) : null;
220
+ if (!memo) return compute(view);
221
+ const memoKey = `${fieldSetTag(fileFields)}\0${key}`;
222
+ let saved = memo.get(memoKey);
223
+ if (!saved) {
224
+ const childReads = {};
225
+ const value = compute(factIndex(childReads, live, fileFields));
226
+ saved = { value, reads: childReads };
227
+ memo.set(memoKey, saved);
228
+ }
229
+ for (const name in saved.reads) {
230
+ if (!Object.hasOwn(reads, name)) reads[name] = saved.reads[name];
231
+ }
232
+ return saved.value;
233
+ };
234
+ }
235
+ if (property === 'projectFact') {
236
+ return (key, compute) => readMapValue(`fact:${key}`, () => projectFact(live, key, compute), true);
237
+ }
238
+ if (property === 'projectFileSymbols') {
239
+ return (file, key, compute) => readMapValue(`fact:${key}`, () => {
240
+ const entry = live.files.get(file);
241
+ // Keep exactly the file fields this view exposes. In
242
+ // particular, overload views omit declarations for
243
+ // languages whose selector does not consume them.
244
+ const exposed = fileFields ? fileFields.includes('symbols') &&
245
+ (fileFields !== OVERLOAD_FILE_FIELDS || langTraits(entry?.language)?.overloadFileSymbols)
246
+ : langTraits(entry?.language)?.overloadFileSymbols;
247
+ return compute(exposed ? entry?.symbols : undefined, live);
248
+ }, true);
249
+ }
250
+ if (property === 'provenanceFs') return Object.fromEntries(
251
+ ['existsSync', 'readdirSync', 'readFileSync'].map(method => [method, (...args) => {
252
+ const result = read(`fs.${method}:${JSON.stringify(args)}`, () => {
253
+ try { return { ok: true, value: require('fs')[method](...args) }; }
254
+ catch (error) { return { ok: false, code: error.code || null }; }
255
+ });
256
+ if (!result.ok) throw new ProvenanceRefusal('Captured filesystem read failed', result.code);
257
+ return result.value;
258
+ }]));
48
259
  if (local.has(property)) return local.get(property);
49
260
  if (String(property).includes('Cache')) {
50
261
  const cache = new Map(); local.set(property, cache); return cache;
51
262
  }
52
263
  if (['symbols', 'files', 'importGraph', 'extendsGraph', 'extendedByGraph'].includes(property)) {
53
- return {
264
+ // One map facade per view, as on the live index. Reusing
265
+ // the facade does not skip its methods' recorded reads.
266
+ const map = {
54
267
  get(key) {
268
+ if (property === 'files' && fileFields) return readFile(key);
55
269
  return readMapValue(`${property}.get:${JSON.stringify(key)}`, () => {
56
270
  const value = live[property]?.get(key);
57
271
  if (property !== 'files' || !value) return value;
58
272
  return { language: value.language, relativePath: value.relativePath,
59
- importBindings: value.importBindings, moduleResolved: value.moduleResolved };
60
- });
273
+ importBindings: value.importBindings,
274
+ ...(value.importDetails !== undefined && { importDetails: value.importDetails }),
275
+ ...(langTraits(value.language)?.overloadFileSymbols && value.symbols !== undefined && { symbols: value.symbols }),
276
+ moduleResolved: value.moduleResolved };
277
+ }, property !== 'files');
61
278
  },
62
279
  has(key) { return !!this.get(key); },
280
+ keys() {
281
+ return read(`${property}.keys`, () => keyList(live, live[property]), true)[Symbol.iterator]();
282
+ },
283
+ *values() { for (const key of this.keys()) yield this.get(key); },
284
+ *entries() { for (const key of this.keys()) yield [key, this.get(key)]; },
285
+ [Symbol.iterator]() { return this.entries(); },
63
286
  };
287
+ local.set(property, map);
288
+ return map;
64
289
  }
65
290
  const method = live ? typeof live[property] === 'function'
66
291
  : reads[`method:${String(property)}`] === true;
@@ -73,65 +298,243 @@ function factIndex(reads, live) {
73
298
  },
74
299
  set(_, property, value) { local.set(property, value); return true; },
75
300
  });
301
+ return view;
302
+ }
303
+
304
+ // One key list per index map and operation: the shared encoding of a key
305
+ // read needs the same object for every witness.
306
+ function keyList(live, map) {
307
+ const lists = !live.isProvenanceFactIndex && typeof live._opMemo === 'function'
308
+ ? live._opMemo('provenanceKeyLists', () => new WeakMap()) : null;
309
+ let keys = lists?.get(map);
310
+ if (!keys) {
311
+ keys = [...map.keys()];
312
+ lists?.set(map, keys);
313
+ }
314
+ return keys;
315
+ }
316
+
317
+ /**
318
+ * A whole-program projection (every namespace, every alias spelling) that a
319
+ * witness consults. The witness records the projection's answer as one
320
+ * fact, never every file it scans: a per-site copy of the project made each
321
+ * constructor and overload query quadratic in project size (fix #418).
322
+ * `compute(index)` runs on the live index, under its own memo.
323
+ */
324
+ function projectFact(index, key, compute) {
325
+ return index?.isProvenanceFactIndex ? index.projectFact(key, compute) : compute(index);
326
+ }
327
+
328
+ function fileSymbolFact(index, file, key, compute) {
329
+ return index.isProvenanceFactIndex ? index.projectFileSymbols(file, key, compute)
330
+ : compute(index.files.get(file)?.symbols, index);
331
+ }
332
+
333
+ /** Raw declarations whose line ranges contain the queried declaration.
334
+ * Scope proofs need these candidates, not every unrelated member in the
335
+ * file. The innermost choice is still derived from the recorded ranges.
336
+ */
337
+ function containingDeclarations(index, definition) {
338
+ const { file, startLine, endLine } = definition;
339
+ if (!index.isProvenanceFactIndex || !Number.isInteger(startLine) || !Number.isInteger(endLine)) {
340
+ return index.files.get(file)?.symbols;
341
+ }
342
+ const key = JSON.stringify(['containing-declarations', file, startLine, endLine]);
343
+ return fileSymbolFact(index, file, key, (symbols, live) => {
344
+ if (!Array.isArray(symbols)) return symbols;
345
+ const memo = live._opMemo?.('containingDeclarations', () => new Map());
346
+ if (memo?.has(key)) return memo.get(key);
347
+ const candidates = require('./shared').symbolsSpanning(symbols, startLine)
348
+ .filter(symbol => !(symbol.endLine < endLine));
349
+ memo?.set(key, candidates);
350
+ return candidates;
351
+ });
352
+ }
353
+
354
+ /**
355
+ * `compute(index)` for a pure answer (plain data) of the index, once per
356
+ * operation and key; on a witness view, its reads join the witness.
357
+ */
358
+ function sharedRead(index, key, compute) {
359
+ if (index?.isProvenanceFactIndex) return index.sharedRead(key, compute);
360
+ const memo = index?._opMemo?.('provenanceSharedReads:live', () => new Map());
361
+ if (!memo) return compute(index);
362
+ if (!memo.has(key)) memo.set(key, compute(index));
363
+ return memo.get(key);
364
+ }
365
+
366
+ /**
367
+ * Is `member` in a whole-program set (`computeSet(liveIndex)`, memoized by
368
+ * the caller)? A witness records the answer for that member only.
369
+ */
370
+ function projectHas(index, setKey, member, computeSet) {
371
+ return projectFact(index, `${setKey}\0${member}`, live => computeSet(live).has(member));
76
372
  }
77
373
 
78
- // Index definitions are immutable records: one encoding each (fix #396:
79
- // every excluded receiver site of a many-overload name encoded the whole
80
- // candidate set again). The encoding is read-only witness data.
374
+ // A witness captured inside an operation holds only frozen facts. Its
375
+ // replays are pure functions of those facts and of the replay's other
376
+ // inputs, so each distinct replay runs once (fix #418).
377
+ const SEALED_FACTS = new WeakSet();
378
+ const REPLAYS = new WeakMap();
379
+
380
+ function sealFacts(reads) {
381
+ for (const key in reads) {
382
+ const value = reads[key];
383
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) return reads;
384
+ }
385
+ SEALED_FACTS.add(Object.freeze(reads));
386
+ return reads;
387
+ }
388
+
389
+ /** `compute()` once per sealed fact set and key; it returns plain data. */
390
+ function replayed(reads, key, compute) {
391
+ if (!SEALED_FACTS.has(reads)) return compute();
392
+ let memo = REPLAYS.get(reads);
393
+ if (!memo) REPLAYS.set(reads, memo = new Map());
394
+ if (!memo.has(key)) memo.set(key, compute());
395
+ return memo.get(key);
396
+ }
397
+
398
+ /**
399
+ * A capture runs its selector over exactly the facts it records: every read
400
+ * answers with the recorded value, and the facts it records are the reads
401
+ * it made. Its answer is therefore the first replay of the sealed witness
402
+ * (fix #418); a deserialized or changed copy is replayed in full.
403
+ */
404
+ function seedReplay(reads, key, value) {
405
+ if (!SEALED_FACTS.has(reads)) return;
406
+ let memo = REPLAYS.get(reads);
407
+ if (!memo) REPLAYS.set(reads, memo = new Map());
408
+ if (!memo.has(key)) memo.set(key, value);
409
+ }
410
+
411
+ // Reuse the encoding while its signature fields are unchanged. A check
412
+ // against a base revision temporarily restores those fields on the indexed
413
+ // declaration; that view needs its own witness, even for the same object.
414
+ // The encoding is immutable: every witness of the candidate shares it.
81
415
  const ENCODED_DEFINITIONS = new WeakMap();
82
416
  function encodeDefinition(definition) {
83
417
  if (!definition || typeof definition !== 'object') return encode(definition);
84
- let encoded = ENCODED_DEFINITIONS.get(definition);
85
- if (!encoded) {
86
- encoded = encode(definition);
87
- ENCODED_DEFINITIONS.set(definition, encoded);
418
+ let saved = ENCODED_DEFINITIONS.get(definition);
419
+ if (!saved || saved.params !== definition.params || saved.paramsStructured !== definition.paramsStructured ||
420
+ saved.generics !== definition.generics) {
421
+ const frozen = { plain: true };
422
+ saved = { value: encode(definition, frozen), params: definition.params,
423
+ paramsStructured: definition.paramsStructured, generics: definition.generics };
424
+ // A plain recorded declaration has one frozen decoded twin every
425
+ // capture selects over (fix #428).
426
+ if (frozen.plain) PLAIN_FACTS.add(saved.value);
427
+ ENCODED_DEFINITIONS.set(definition, saved);
88
428
  }
89
- return encoded;
429
+ return saved.value;
90
430
  }
91
431
 
92
432
  function captureOverload(index, call, candidates, language, select) {
93
433
  const reads = {};
94
434
  try {
95
- const result = select(factIndex(reads, index), call, candidates, language);
96
- return { call: encode(call), candidates: candidates.map(encodeDefinition), language, reads,
435
+ // The selector reads the recorded call and candidates, as a replay
436
+ // does, so the capture's answer is the witness's first replay.
437
+ const recordedCall = encode(call, {});
438
+ const recordedCandidates = Object.freeze(candidates.map(encodeDefinition));
439
+ const view = factIndex(reads, index, OVERLOAD_FILE_FIELDS);
440
+ const selectable = recordedCandidates.map(decodeFact);
441
+ const result = select(view, decode(recordedCall), selectable, language);
442
+ const witness = { call: recordedCall, candidates: recordedCandidates, language,
443
+ reads: sealFacts(reads), fileFields: true,
97
444
  selected: declarationIdentity(result.match),
98
445
  outcome: result.match ? 'selected' : result.ambiguous ? 'ambiguous' : 'no-fit' };
99
- } catch { return null; }
446
+ if (SEALED_FACTS.has(witness.reads)) {
447
+ OVERLOAD_REPLAYS.set(witness, { reads: witness.reads, call: recordedCall,
448
+ candidates: recordedCandidates, language, fileFields: true, replay: {
449
+ candidates: selectable.map(declarationIdentity), call: decode(recordedCall),
450
+ result: { missing: view.hasMissingProvenanceFacts, ambiguous: result.ambiguous === true,
451
+ match: result.match ? declarationIdentity(result.match) : null } } });
452
+ }
453
+ return witness;
454
+ } catch (error) { return refusedProof(error); }
100
455
  }
101
456
 
102
- function validateOverload(witness, members, selected, invalidCall = false, ambiguous = false) {
103
- if (!witness || !Array.isArray(witness.candidates)) return false;
457
+ // The selector's answer over a witness's recorded facts. A frozen witness
458
+ // is replayed once; a changed or deserialized copy is replayed again.
459
+ const OVERLOAD_REPLAYS = new WeakMap();
460
+ function overloadReplay(witness) {
461
+ const reusable = SEALED_FACTS.has(witness.reads) && Object.isFrozen(witness.call) &&
462
+ Object.isFrozen(witness.candidates);
463
+ const saved = reusable ? OVERLOAD_REPLAYS.get(witness) : null;
464
+ if (saved && saved.reads === witness.reads && saved.call === witness.call &&
465
+ saved.candidates === witness.candidates && saved.language === witness.language &&
466
+ saved.fileFields === witness.fileFields) return saved.replay;
104
467
  const candidates = decode(witness.candidates);
105
- if (candidates.length !== members.length || candidates.some(candidate =>
106
- !members.some(member => sameDeclaration(member, declarationIdentity(candidate))))) return false;
468
+ const replay = { candidates: candidates.map(declarationIdentity), call: decode(witness.call), result: null };
107
469
  try {
108
470
  // Lazy access avoids an initialization cycle with confidence.js.
109
471
  // The selector only sees the data-only replay facade above.
110
- const select = require('./callers').selectProvenanceOverload;
111
- const result = select(factIndex(witness.reads), decode(witness.call), candidates, witness.language);
112
- if (ambiguous) return !result.match && result.ambiguous === true;
113
- return invalidCall ? !result.match && !result.ambiguous :
114
- !!result.match && sameDeclaration(declarationIdentity(result.match), selected);
115
- } catch { return false; }
472
+ const select = lazyCallersModule().selectProvenanceOverload;
473
+ const index = factIndex(witness.reads, undefined, witness.fileFields ? OVERLOAD_FILE_FIELDS : undefined);
474
+ const result = select(index, decode(witness.call), candidates, witness.language);
475
+ replay.result = { missing: index.hasMissingProvenanceFacts, ambiguous: result.ambiguous === true,
476
+ match: result.match ? declarationIdentity(result.match) : null };
477
+ } catch (error) { refusedProof(error); /* An unreplayable witness proves nothing. */ }
478
+ if (reusable) OVERLOAD_REPLAYS.set(witness, { reads: witness.reads, call: witness.call,
479
+ candidates: witness.candidates, language: witness.language, fileFields: witness.fileFields, replay });
480
+ return replay;
481
+ }
482
+
483
+ function validateOverload(witness, members, selected, invalidCall = false, ambiguous = false, site = null) {
484
+ if (!witness || !Array.isArray(witness.candidates)) return false;
485
+ let replay;
486
+ try { replay = overloadReplay(witness); } catch (error) { return refusedProof(error, false); }
487
+ // Members usually arrive in the replay's own order: an aligned pass
488
+ // decides those in one comparison each (fix #428).
489
+ if (replay.candidates.length !== members.length || (!replay.candidates.every((candidate, i) =>
490
+ sameDeclaration(members[i], candidate)) && replay.candidates.some(candidate =>
491
+ !members.some(member => sameDeclaration(member, candidate))))) return false;
492
+ const { call, result } = replay;
493
+ if (site && call?.argumentFile !== undefined && call.argumentFile !== site.file) return false;
494
+ if (site && call?.argumentFile !== undefined && call.callSite &&
495
+ (call.line !== site.line || call.callSite.line !== site.line ||
496
+ call.callSite.start !== site.start || call.callSite.column !== site.column)) return false;
497
+ if (!result || result.missing) return false;
498
+ if (ambiguous) return !result.match && result.ambiguous;
499
+ return invalidCall ? !result.match && !result.ambiguous :
500
+ !!result.match && sameDeclaration(result.match, selected);
116
501
  }
117
502
 
118
503
  // Reconstruct inherited overload slots from the actual declarations, with
119
504
  // the nearest declaration of an identical signature hiding the ancestor.
505
+ const SLOT_SIGNATURES = new WeakMap();
506
+ const STEP_MEMBER_CHECKS = new WeakMap();
120
507
  function overloadMemberGroup(steps) {
121
- const normalize = require('./callers').provenanceParameterIdentity;
508
+ const normalize = lazyCallersModule().provenanceParameterIdentity;
122
509
  const seen = new Set();
123
510
  const group = [];
124
511
  for (const step of steps) {
125
512
  if (!Array.isArray(step.memberDefinitions) ||
126
- step.memberDefinitions.length !== step.members.length ||
127
- step.memberDefinitions.some(d => !step.members.some(m =>
128
- sameDeclaration(m, declarationIdentity(d))))) return null;
513
+ step.memberDefinitions.length !== step.members.length) return null;
514
+ // Every definition is one of the step's members: one key set per
515
+ // pair of lists, never a pairwise comparison per witness (fix #428).
516
+ let byDefinitions = STEP_MEMBER_CHECKS.get(step.members);
517
+ if (!byDefinitions) STEP_MEMBER_CHECKS.set(step.members, byDefinitions = new WeakMap());
518
+ let consistent = byDefinitions.get(step.memberDefinitions);
519
+ if (consistent === undefined) {
520
+ const memberKeys = new Set(step.members.map(member => identityKey(member)));
521
+ consistent = !memberKeys.has(null) &&
522
+ step.memberDefinitions.every(d => memberKeys.has(identityKey(declarationIdentity(d))));
523
+ byDefinitions.set(step.memberDefinitions, consistent);
524
+ }
525
+ if (!consistent) return null;
129
526
  const sameDepth = [];
130
527
  for (const definition of step.memberDefinitions) {
131
- const signature = Array.isArray(definition.paramsStructured)
132
- ? definition.paramsStructured.filter(p => !p?.extensionReceiver)
133
- .map(p => `${normalize(p?.type || '')}:${p?.rest ? 'rest' : 'fixed'}`).join(',')
134
- : `${definition.file}:${definition.startLine}`;
528
+ // One signature per declaration and parameter list (fix #428).
529
+ let saved = SLOT_SIGNATURES.get(definition);
530
+ if (!saved || saved.params !== definition.paramsStructured) {
531
+ saved = { params: definition.paramsStructured, signature: Array.isArray(definition.paramsStructured)
532
+ ? definition.paramsStructured.filter(p => !p?.extensionReceiver)
533
+ .map(p => `${normalize(p?.type || '')}:${p?.rest ? 'rest' : 'fixed'}`).join(',')
534
+ : `${definition.file}:${definition.startLine}` };
535
+ SLOT_SIGNATURES.set(definition, saved);
536
+ }
537
+ const signature = saved.signature;
135
538
  if (!seen.has(signature)) {
136
539
  group.push(definition);
137
540
  sameDepth.push(signature);
@@ -142,4 +545,5 @@ function overloadMemberGroup(steps) {
142
545
  return group;
143
546
  }
144
547
 
145
- module.exports = { captureOverload, validateOverload, overloadMemberGroup, factIndex };
548
+ module.exports = { captureOverload, validateOverload, overloadMemberGroup, factIndex, projectFact, projectHas, sharedRead, containingDeclarations, fileSymbolFact,
549
+ sealFacts, replayed, seedReplay, provenanceWork, ProvenanceRefusal, refusedProof };