ucn 5.2.1 → 5.2.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.
package/core/index-ir.js CHANGED
@@ -76,9 +76,9 @@ function createFileEntryFromIR({
76
76
 
77
77
  const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
78
78
  'returnedFunctionResult', 'isFunctionVariable', 'paramTypes', 'isAsync',
79
- 'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
79
+ 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
80
80
  'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
81
- 'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
81
+ 'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
82
82
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
83
83
  'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
84
84
  'registryMember', 'registryContainer', 'namespace',
@@ -86,9 +86,11 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
86
86
  'lexicalScopeStartLine', 'lexicalScopeEndLine',
87
87
  'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
88
88
  'returnedConcreteType', 'returnedConstructors', 'templateDependent',
89
+ 'returnedCallStart', 'returnedCallEnd',
90
+ 'returnedReceiverPath',
89
91
  'isSpecialization',
90
92
  'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
91
- 'aliasOwner', 'aliasMember', 'macroParamEffects',
93
+ 'aliasOwner', 'aliasMember', 'callableTarget', 'macroParamEffects',
92
94
  ]);
93
95
 
94
96
  function materializeSymbol(fileEntry, item) {
package/core/ir.js CHANGED
@@ -11,6 +11,24 @@
11
11
  const IR_SCHEMA_VERSION = 1;
12
12
  const EVIDENCE_TIERS = Object.freeze(['confirmed', 'unverified', 'excluded']);
13
13
 
14
+ // Runtime receiver identity for overload-heavy JS/TS member aliases. Generic
15
+ // arguments may differ while the produced value still has one concrete owner
16
+ // (`Factory.create(): Schema<A>` / `Schema<B>`). Transparent/async wrappers
17
+ // are deliberately rejected here: downstream assignment flow unwraps them,
18
+ // so agreement on Promise/Optional alone would not prove the inner receiver.
19
+ function callableAliasReturnHead(returnType) {
20
+ if (!returnType || typeof returnType !== 'string') return null;
21
+ const text = returnType.trim();
22
+ const match = text.match(
23
+ /^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*(?:<[\s\S]*>|\[[\s\S]*\])?$/);
24
+ if (!match) return null;
25
+ const head = match[1].split('.').pop();
26
+ if (['Promise', 'Awaitable', 'Optional', 'Annotated', 'Final'].includes(head)) {
27
+ return null;
28
+ }
29
+ return head;
30
+ }
31
+
14
32
  function normalizeSymbol(symbol, family, language, kind, owner = null) {
15
33
  let normalizedOwner = owner || symbol.className || null;
16
34
  if (!normalizedOwner && symbol.receiver && family === 'callable') {
@@ -43,9 +61,9 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
43
61
  };
44
62
  const passthrough = [
45
63
  'docstring', 'returnedFunctionResult', 'isFunctionVariable', 'paramTypes',
46
- 'isAsync', 'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent',
64
+ 'isAsync', 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent',
47
65
  'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType',
48
- 'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
66
+ 'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
49
67
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
50
68
  'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
51
69
  'registryMember', 'registryContainer', 'isConstructor',
@@ -53,9 +71,11 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
53
71
  'namespace', 'lexicalScopeStartLine', 'lexicalScopeEndLine',
54
72
  'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
55
73
  'returnedConcreteType', 'returnedConstructors', 'templateDependent',
74
+ 'returnedCallStart', 'returnedCallEnd',
75
+ 'returnedReceiverPath',
56
76
  'isSpecialization',
57
77
  'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
58
- 'aliasOwner', 'aliasMember', 'macroParamEffects',
78
+ 'aliasOwner', 'aliasMember', 'callableTarget', 'macroParamEffects',
59
79
  ];
60
80
  for (const field of passthrough) {
61
81
  if (symbol[field] !== undefined && symbol[field] !== null) {
@@ -106,6 +126,9 @@ function createFileIR({
106
126
  traitImpl: true,
107
127
  traitName: type.traitName,
108
128
  }),
129
+ ...(type.generics && {
130
+ ownerGenerics: type.generics,
131
+ }),
109
132
  };
110
133
  append(inherited,
111
134
  ['field', 'property'].includes(inherited.memberType)
@@ -120,19 +143,44 @@ function createFileIR({
120
143
  // An immutable module-scope member alias has the callable signature of
121
144
  // the class member it captures: `const make = Widget.create`. Materialize
122
145
  // the local value and each explicit export alias as real function symbols
123
- // only when its member is static and every declared return type agrees.
124
- // This is compiler-visible identity; mutable aliases and ambiguous
125
- // overload returns were rejected by the parser/agreement gate above.
146
+ // only when its member is static and every declared return type has the
147
+ // same concrete runtime head. Generic arguments may differ across legal
148
+ // overloads; a different head remains ambiguous. This is compiler-visible
149
+ // identity; mutable aliases and ambiguous overload returns stay rejected.
126
150
  for (const alias of (parsed.callableAliases || [])) {
127
- const sources = normalizedSymbols.filter(symbol =>
151
+ let sources = normalizedSymbols.filter(symbol =>
128
152
  symbol.name === alias.member && symbol.owner === alias.owner &&
129
153
  (symbol.params !== undefined || symbol.paramsStructured) &&
130
154
  (symbol.modifiers?.includes('static') ||
131
155
  String(symbol.memberType || symbol.kind).startsWith('static')) &&
132
156
  symbol.returnType);
157
+ if (sources.length === 0) {
158
+ // Static callable forwarding (zod-measured):
159
+ // `static create = createSchema; const schema = Type.create`.
160
+ // The parser records only a direct identifier initializer. Pin it
161
+ // to top-level callables in this file, and require every matching
162
+ // field declaration to agree before borrowing its signatures.
163
+ const forwarded = normalizedSymbols.filter(symbol =>
164
+ symbol.name === alias.member && symbol.owner === alias.owner &&
165
+ symbol.family === 'state' && symbol.callableTarget &&
166
+ symbol.modifiers?.includes('static'));
167
+ const targets = new Set(forwarded.map(symbol => symbol.callableTarget));
168
+ if (forwarded.length > 0 && targets.size === 1) {
169
+ const [target] = targets;
170
+ sources = normalizedSymbols.filter(symbol =>
171
+ symbol.name === target && symbol.family === 'callable' &&
172
+ !symbol.owner && !symbol.isNested &&
173
+ (symbol.params !== undefined || symbol.paramsStructured) &&
174
+ symbol.returnType);
175
+ }
176
+ }
133
177
  if (sources.length === 0) continue;
134
178
  const sourceReturns = new Set(sources.map(source => source.returnType));
135
- if (sourceReturns.size !== 1) continue;
179
+ if (sourceReturns.size !== 1) {
180
+ const heads = new Set(sources.map(source =>
181
+ callableAliasReturnHead(source.returnType)));
182
+ if (heads.has(null) || heads.size !== 1) continue;
183
+ }
136
184
  const source = sources[0];
137
185
  const exported = (parsed.exports || []).filter(item =>
138
186
  !item.source && item.name === alias.name);
package/core/project.js CHANGED
@@ -79,8 +79,33 @@ class ProjectIndex {
79
79
  this._opLinesCache = null; // per-operation split-lines cache (Map<filePath, string[]>, bounded FIFO)
80
80
  this._opInnerSymbolRangesCache = null; // per-operation sorted class-method ranges by file
81
81
  this._opFlowTypeOriginCache = null; // per-operation annotation type identity results
82
+ this._opCppTypeCategoryCache = null; // per-operation normalized C++ parameter categories
83
+ this._opCppPathReceiverTypeCache = null; // per-operation C++ qualified receiver identity
84
+ this._opDerefPairs = null; // per-operation Rust Deref identity pairs
85
+ this._opAliasPairs = null; // per-operation language type-alias identity pairs
82
86
  this._parsedTreeCache = new Map(); // cross-operation LRU: filePath -> immutable tree entry
83
87
  this._parsedTreeCacheSourceBytes = 0;
88
+ // Cross-operation, content-hash-keyed usage classifications. Account
89
+ // queries often ask several projections for the same hot symbol; the
90
+ // AST answer is immutable until the file hash changes. Bounded by
91
+ // both entries and approximate payload size to avoid turning a warm
92
+ // MCP process into an unbounded repository mirror.
93
+ this._usageResultCache = new Map();
94
+ this._usageResultCacheWeight = 0;
95
+ this.usageCacheDirty = false;
96
+ // Exact text-ground sets are likewise immutable for one built index.
97
+ // The cache is cleared at every build and bounded in account.js.
98
+ this._groundSetCache = new Map();
99
+ this._groundSetCacheLines = 0;
100
+ // Bounded cross-operation memo for immutable name-level export
101
+ // ownership. Agent workflows ask show/impact/tests about related
102
+ // symbols in sequence; retaining these tri-state barrel verdicts
103
+ // avoids repeating the same bounded graph walks after every command.
104
+ this._nameBindingReachCache = new Map();
105
+ // Query-derived return flow depends on cross-file annotations and is
106
+ // deliberately never persisted. It is safe across commands only
107
+ // until the next build, which resets it below.
108
+ this._returnTypeFlowCache = new Map();
84
109
  this.calleeIndex = null; // name -> Set<filePath> — inverted call index (built lazily)
85
110
  }
86
111
 
@@ -120,6 +145,10 @@ class ProjectIndex {
120
145
  this._opInnerSymbolRangesCache = new Map();
121
146
  this._opFlowTypeOriginCache = new Map();
122
147
  this._opImportReachCache = new Map();
148
+ this._opCppTypeCategoryCache = new Map();
149
+ this._opCppPathReceiverTypeCache = new Map();
150
+ this._opDerefPairs = undefined;
151
+ this._opAliasPairs = undefined;
123
152
  this._opDepth = 0;
124
153
  }
125
154
  this._opDepth++;
@@ -145,6 +174,10 @@ class ProjectIndex {
145
174
  this._opInnerSymbolRangesCache = null;
146
175
  this._opFlowTypeOriginCache = null;
147
176
  this._opImportReachCache = null;
177
+ this._opCppTypeCategoryCache = null;
178
+ this._opCppPathReceiverTypeCache = null;
179
+ this._opDerefPairs = null;
180
+ this._opAliasPairs = null;
148
181
  // Free cached file content from callsCache entries (retained during
149
182
  // operation for _readFile caching, not needed between operations)
150
183
  for (const entry of this.callsCache.values()) {
@@ -255,15 +288,38 @@ class ProjectIndex {
255
288
  * multiple times within one operation (e.g., about() calls both countSymbolUsages and usages).
256
289
  * @param {string} filePath - File to scan
257
290
  * @param {string} name - Symbol name to find
291
+ * @param {object} [options]
292
+ * @param {boolean} [options.skipCallRecovery] - omit usage-only call
293
+ * recovery when the caller has already classified lines from the call index
258
294
  * @returns {Array|null} Array of usage objects or null if parsing failed
259
295
  */
260
- _getCachedUsages(filePath, name) {
261
- const cacheKey = `${filePath}\0${name}`;
296
+ _getCachedUsages(filePath, name, options = {}) {
297
+ // Account construction checks the complete calls cache before it asks
298
+ // the language adapter to classify the remaining name occurrences.
299
+ // C/C++ can therefore skip its expensive macro replacement-list call
300
+ // recovery in that mode. Partition both cache layers so a partial
301
+ // account classification can never poison the full `usages` result.
302
+ const mode = [
303
+ options.skipCallRecovery ? 'skip-call-recovery' : '',
304
+ ].filter(Boolean).join('+');
305
+ const modeSuffix = mode ? `\0${mode}` : '';
306
+ const cacheKey = `${filePath}\0${name}${modeSuffix}`;
262
307
  if (this._opUsagesCache) {
263
308
  const cached = this._opUsagesCache.get(cacheKey);
264
309
  if (cached !== undefined) return cached;
265
310
  }
266
311
 
312
+ const fileHash = this.files.get(filePath)?.hash || '';
313
+ const persistentKey = `${filePath}\0${fileHash}\0${name}${modeSuffix}`;
314
+ if (this._usageResultCache?.has(persistentKey)) {
315
+ const cached = this._usageResultCache.get(persistentKey);
316
+ // Map insertion order is the LRU order.
317
+ this._usageResultCache.delete(persistentKey);
318
+ this._usageResultCache.set(persistentKey, cached);
319
+ if (this._opUsagesCache) this._opUsagesCache.set(cacheKey, cached.value);
320
+ return cached.value;
321
+ }
322
+
267
323
  // Header language is resolved during indexing from compilation
268
324
  // databases/include context. Re-detecting `.h` here can choose C for
269
325
  // a C++ header and silently drop member/template usages from the raw
@@ -292,10 +348,26 @@ class ProjectIndex {
292
348
  !langModule.managesOwnParseTree
293
349
  ? this._getParsedTree(filePath, content, lang)
294
350
  : null;
295
- const usages = langModule.findUsagesInCode(content, name, parser, tree);
351
+ const usages = langModule.findUsagesInCode(
352
+ content, name, parser, tree, options);
296
353
  if (this._opUsagesCache) {
297
354
  this._opUsagesCache.set(cacheKey, usages);
298
355
  }
356
+ if (Array.isArray(usages) && this._usageResultCache) {
357
+ const weight = 64 + usages.length * 40;
358
+ this._usageResultCache.set(persistentKey, { value: usages, weight });
359
+ this._usageResultCacheWeight += weight;
360
+ this.usageCacheDirty = true;
361
+ const maxEntries = 4096;
362
+ const maxWeight = 16 * 1024 * 1024;
363
+ while (this._usageResultCache.size > maxEntries ||
364
+ this._usageResultCacheWeight > maxWeight) {
365
+ const oldest = this._usageResultCache.entries().next().value;
366
+ if (!oldest) break;
367
+ this._usageResultCache.delete(oldest[0]);
368
+ this._usageResultCacheWeight -= oldest[1].weight;
369
+ }
370
+ }
299
371
  return usages;
300
372
  } catch (e) {
301
373
  return null;
@@ -327,6 +399,14 @@ class ProjectIndex {
327
399
  const startTime = Date.now();
328
400
  const quiet = options.quiet !== false;
329
401
 
402
+ // Build/discovery can add, remove, or reclassify files. Do not retain
403
+ // a text-universe answer across that boundary. Hash-keyed usage
404
+ // results remain safe and useful for unchanged files.
405
+ this._groundSetCache = new Map();
406
+ this._groundSetCacheLines = 0;
407
+ this._nameBindingReachCache = new Map();
408
+ this._returnTypeFlowCache = new Map();
409
+
330
410
  // A (re)build invalidates any cache-loaded reachability set — the
331
411
  // fingerprint guard in computeReachability is content-shaped and
332
412
  // cannot see every rebuild (fix #249: a stale loaded set survived
@@ -2414,6 +2494,11 @@ class ProjectIndex {
2414
2494
  /** Load index from cache file */
2415
2495
  loadCache(cachePath) { return indexCache.loadCache(this, cachePath); }
2416
2496
 
2497
+ /** Persist the bounded, content-hash-keyed usage-query cache. */
2498
+ saveUsageCache(cachePath = undefined) {
2499
+ return indexCache.saveUsageCache(this, cachePath);
2500
+ }
2501
+
2417
2502
  /** Return this project's default per-user cache file path. */
2418
2503
  getCachePath() { return indexCache.getProjectCachePath(this.root); }
2419
2504
 
@@ -2776,7 +2776,7 @@ function findImportsInCode(code, parser) {
2776
2776
  } finally { /* cached with the selected tree */ }
2777
2777
  }
2778
2778
 
2779
- function findUsagesInCode(code, name, parser, existingTree) {
2779
+ function findUsagesInCode(code, name, parser, existingTree, options = {}) {
2780
2780
  // Usage is the raw literal-name inventory. The literal C/C++ tree retains
2781
2781
  // identifiers from every preprocessor branch and is sufficient for
2782
2782
  // occurrence kind/line classification; symbol ownership still comes from
@@ -2860,22 +2860,24 @@ function findUsagesInCode(code, name, parser, existingTree) {
2860
2860
  // call extractor reparses those AST-proven regions; surface the resulting
2861
2861
  // call usages here as well so callers/callees, usages, and tests share one
2862
2862
  // semantic fact set.
2863
- const seenCalls = new Set(usages
2864
- .filter(usage => usage.usageType === 'call')
2865
- .map(usage => `${usage.line}:${usage.column ?? ''}`));
2866
- const macroCalls = findMacroBodyCalls(tree, code, parser, name);
2867
- for (const call of macroCalls) {
2868
- if (call.name !== name) continue;
2869
- const key = `${call.line}:${call.column ?? ''}`;
2870
- if (seenCalls.has(key)) continue;
2871
- seenCalls.add(key);
2872
- addUsage({
2873
- line: call.line,
2874
- column: call.column,
2875
- usageType: 'call',
2876
- ...(call.receiver && { receiver: call.receiver }),
2877
- ...(call.macroParameter && { macroParameter: true }),
2878
- });
2863
+ if (!options.skipCallRecovery) {
2864
+ const seenCalls = new Set(usages
2865
+ .filter(usage => usage.usageType === 'call')
2866
+ .map(usage => `${usage.line}:${usage.column ?? ''}`));
2867
+ const macroCalls = findMacroBodyCalls(tree, code, parser, name);
2868
+ for (const call of macroCalls) {
2869
+ if (call.name !== name) continue;
2870
+ const key = `${call.line}:${call.column ?? ''}`;
2871
+ if (seenCalls.has(key)) continue;
2872
+ seenCalls.add(key);
2873
+ addUsage({
2874
+ line: call.line,
2875
+ column: call.column,
2876
+ usageType: 'call',
2877
+ ...(call.receiver && { receiver: call.receiver }),
2878
+ ...(call.macroParameter && { macroParameter: true }),
2879
+ });
2880
+ }
2879
2881
  }
2880
2882
  return usages;
2881
2883
  }