ucn 3.8.25 → 4.0.0
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/.claude/skills/ucn/SKILL.md +44 -18
- package/README.md +95 -28
- package/cli/index.js +28 -5
- package/core/account.js +354 -0
- package/core/analysis.js +335 -15
- package/core/bridge.js +0 -16
- package/core/build-worker.js +21 -1
- package/core/cache.js +52 -3
- package/core/callers.js +3434 -158
- package/core/confidence.js +82 -19
- package/core/deadcode.js +114 -21
- package/core/execute.js +4 -0
- package/core/graph-build.js +44 -2
- package/core/imports.js +118 -1
- package/core/output/analysis.js +345 -83
- package/core/output/reporting.js +8 -2
- package/core/output/shared.js +33 -2
- package/core/output/tracing.js +208 -10
- package/core/project.js +19 -2
- package/core/registry.js +15 -3
- package/core/search.js +0 -42
- package/core/tracing.js +534 -190
- package/languages/go.js +317 -6
- package/languages/index.js +79 -0
- package/languages/java.js +243 -16
- package/languages/javascript.js +357 -24
- package/languages/python.js +423 -28
- package/languages/rust.js +377 -8
- package/languages/utils.js +72 -18
- package/mcp/server.js +3 -3
- package/package.json +9 -3
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -79
package/core/analysis.js
CHANGED
|
@@ -198,6 +198,77 @@ function tagCalleesSideEffects(index, callees) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Compose the conservation account for a caller query (the tiered
|
|
203
|
+
* caller contract). Claims come from the PRE-display-filter findCallers result —
|
|
204
|
+
* the account reconciles pre-display truth; display filters are reported
|
|
205
|
+
* separately in account.filtered.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} index - ProjectIndex instance
|
|
208
|
+
* @param {string} name - Symbol name
|
|
209
|
+
* @param {Array} rawCallers - findCallers result BEFORE display filters
|
|
210
|
+
* (carries non-enumerable accountRaw/shadowEntries from collectAccount)
|
|
211
|
+
* @param {object} [filtered] - display-level hide counts { total, byFlag }
|
|
212
|
+
* @returns {object} account (see core/account.js)
|
|
213
|
+
*/
|
|
214
|
+
function composeAccount(index, name, rawCallers, filtered) {
|
|
215
|
+
const { computeGroundSet, buildAccount } = require('./account');
|
|
216
|
+
const groundSet = computeGroundSet(index, name);
|
|
217
|
+
const accountRaw = rawCallers.accountRaw || { unverifiedLines: [], excludedEntries: [] };
|
|
218
|
+
|
|
219
|
+
const confirmedEntries = [];
|
|
220
|
+
const unverifiedEntries = [...accountRaw.unverifiedLines];
|
|
221
|
+
const claimByTier = (entry) => {
|
|
222
|
+
if (entry.tier === 'unverified') unverifiedEntries.push({ file: entry.file, line: entry.line });
|
|
223
|
+
else confirmedEntries.push({ file: entry.file, line: entry.line });
|
|
224
|
+
};
|
|
225
|
+
for (const c of rawCallers) claimByTier(c);
|
|
226
|
+
for (const s of (rawCallers.shadowEntries || [])) claimByTier(s);
|
|
227
|
+
// Retained unverified-tier entries (routed drops — Phase 3 engine retention)
|
|
228
|
+
for (const u of (rawCallers.unverifiedEntries || [])) {
|
|
229
|
+
unverifiedEntries.push({ file: u.file, line: u.line });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return buildAccount(index, name, {
|
|
233
|
+
groundSet,
|
|
234
|
+
confirmedEntries,
|
|
235
|
+
unverifiedEntries,
|
|
236
|
+
excludedEntries: accountRaw.excludedEntries,
|
|
237
|
+
filtered,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Visible entries for ground call-lines no engine candidate claimed
|
|
243
|
+
* (reason `call-not-resolved`). The account already COUNTS them in its
|
|
244
|
+
* unverified total (core/account.js buildAccount); listing them closes the
|
|
245
|
+
* last visible-bucket gap — a real call edge at such a line was conserved
|
|
246
|
+
* in the arithmetic but rendered nowhere (cursive-measured: 24 oracle call
|
|
247
|
+
* edges sat invisible in reportedNonCall). Bare one-liners: file:line +
|
|
248
|
+
* source text, no caller enrichment (the engine had no candidate to enrich).
|
|
249
|
+
*/
|
|
250
|
+
function callNotResolvedEntries(index, account, options = {}) {
|
|
251
|
+
let entries = (account && account.callNotResolved) || [];
|
|
252
|
+
if (options.exclude && options.exclude.length > 0) {
|
|
253
|
+
entries = entries.filter(e => index.matchesFilters(e.relativePath, { exclude: options.exclude }));
|
|
254
|
+
}
|
|
255
|
+
return entries.map(e => {
|
|
256
|
+
let content = '';
|
|
257
|
+
try {
|
|
258
|
+
content = (index._readFile(e.file).split('\n')[e.line - 1] || '').trim();
|
|
259
|
+
} catch { /* unreadable since scan — keep bare */ }
|
|
260
|
+
return {
|
|
261
|
+
file: e.file,
|
|
262
|
+
relativePath: e.relativePath,
|
|
263
|
+
line: e.line,
|
|
264
|
+
content,
|
|
265
|
+
callerName: null,
|
|
266
|
+
tier: 'unverified',
|
|
267
|
+
reason: 'call-not-resolved',
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
201
272
|
/**
|
|
202
273
|
* Context: quick caller/callee view for a symbol.
|
|
203
274
|
*
|
|
@@ -219,11 +290,38 @@ function context(index, name, options = {}) {
|
|
|
219
290
|
if (['class', 'struct', 'interface', 'type'].includes(def.type)) {
|
|
220
291
|
const methods = index.findMethodsForType(name);
|
|
221
292
|
|
|
222
|
-
|
|
293
|
+
// Pin caller resolution to the resolved class definition — same as the
|
|
294
|
+
// function path below. Without this, same-name classes in other files
|
|
295
|
+
// conflate (their usages attribute to whichever def displays).
|
|
296
|
+
let typeCallers = index.findCallers(name, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, collectAccount: true, targetDefinitions: [def] });
|
|
297
|
+
const rawTypeCallers = typeCallers;
|
|
298
|
+
const typeFilteredByFlag = { exclude: 0, minConfidence: 0, unreachableOnly: 0 };
|
|
299
|
+
// Tier partition — same contract as the function path: constructor/usage
|
|
300
|
+
// sites without evidence are visible as unverified, never hidden.
|
|
301
|
+
let typeUnverified = [
|
|
302
|
+
...typeCallers.filter(c => c.tier === 'unverified'),
|
|
303
|
+
...(rawTypeCallers.unverifiedEntries || []),
|
|
304
|
+
];
|
|
305
|
+
typeCallers = typeCallers.filter(c => c.tier !== 'unverified');
|
|
223
306
|
// Apply exclude filter
|
|
224
307
|
if (options.exclude && options.exclude.length > 0) {
|
|
308
|
+
const before = typeCallers.length + typeUnverified.length;
|
|
225
309
|
typeCallers = typeCallers.filter(c => index.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
310
|
+
typeUnverified = typeUnverified.filter(c => index.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
311
|
+
typeFilteredByFlag.exclude = before - typeCallers.length - typeUnverified.length;
|
|
226
312
|
}
|
|
313
|
+
const byFileLine = (a, b) => {
|
|
314
|
+
const fa = a.relativePath || a.file || '';
|
|
315
|
+
const fb = b.relativePath || b.file || '';
|
|
316
|
+
if (fa !== fb) return fa.localeCompare(fb);
|
|
317
|
+
return (a.line || 0) - (b.line || 0);
|
|
318
|
+
};
|
|
319
|
+
const typeAccount = composeAccount(index, name, rawTypeCallers,
|
|
320
|
+
typeFilteredByFlag.exclude > 0 ? { total: typeFilteredByFlag.exclude, byFlag: typeFilteredByFlag } : undefined);
|
|
321
|
+
typeUnverified.push(...callNotResolvedEntries(index, typeAccount, options));
|
|
322
|
+
|
|
323
|
+
typeCallers = [...typeCallers].sort(byFileLine);
|
|
324
|
+
typeUnverified = [...typeUnverified].sort(byFileLine);
|
|
227
325
|
|
|
228
326
|
const result = {
|
|
229
327
|
type: def.type,
|
|
@@ -240,7 +338,9 @@ function context(index, name, options = {}) {
|
|
|
240
338
|
receiver: m.receiver
|
|
241
339
|
})),
|
|
242
340
|
// Also include places where the type is used in function parameters/returns
|
|
243
|
-
callers: typeCallers
|
|
341
|
+
callers: typeCallers,
|
|
342
|
+
unverifiedCallers: typeUnverified,
|
|
343
|
+
meta: { account: typeAccount }
|
|
244
344
|
};
|
|
245
345
|
|
|
246
346
|
if (warnings.length > 0) {
|
|
@@ -251,12 +351,38 @@ function context(index, name, options = {}) {
|
|
|
251
351
|
}
|
|
252
352
|
|
|
253
353
|
const stats = { uncertain: 0 };
|
|
254
|
-
let callers = index.findCallers(name, {
|
|
354
|
+
let callers = index.findCallers(name, {
|
|
355
|
+
includeMethods: options.includeMethods,
|
|
356
|
+
includeUncertain: options.includeUncertain,
|
|
357
|
+
stats,
|
|
358
|
+
targetDefinitions: [def],
|
|
359
|
+
collectAccount: true,
|
|
360
|
+
// --all lifts the unverified enrichment cap (content + caller lookup)
|
|
361
|
+
unverifiedEnrichLimit: options.all ? Infinity : undefined,
|
|
362
|
+
});
|
|
255
363
|
let callees = index.findCallees(def, { includeMethods: options.includeMethods, includeUncertain: options.includeUncertain, stats });
|
|
364
|
+
// Pre-display-filter result for the conservation account (filters below
|
|
365
|
+
// build new arrays and would lose the non-enumerable accountRaw).
|
|
366
|
+
const rawCallers = callers;
|
|
367
|
+
const filteredByFlag = { exclude: 0, minConfidence: 0, unreachableOnly: 0 };
|
|
368
|
+
|
|
369
|
+
// Tier partition (tiered caller contract): `callers` = confirmed tier
|
|
370
|
+
// only; unverified-tier entries (name match without binding/receiver
|
|
371
|
+
// evidence) render in their own section — visible, never silently hidden.
|
|
372
|
+
let unverifiedCallers = [
|
|
373
|
+
...callers.filter(c => c.tier === 'unverified'),
|
|
374
|
+
...(rawCallers.unverifiedEntries || []),
|
|
375
|
+
];
|
|
376
|
+
callers = callers.filter(c => c.tier !== 'unverified');
|
|
256
377
|
|
|
257
378
|
// Apply exclude filter
|
|
258
379
|
if (options.exclude && options.exclude.length > 0) {
|
|
380
|
+
const before = callers.length;
|
|
259
381
|
callers = callers.filter(c => index.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
382
|
+
filteredByFlag.exclude = before - callers.length;
|
|
383
|
+
const beforeUnverified = unverifiedCallers.length;
|
|
384
|
+
unverifiedCallers = unverifiedCallers.filter(c => index.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
385
|
+
filteredByFlag.exclude += beforeUnverified - unverifiedCallers.length;
|
|
260
386
|
callees = callees.filter(c => index.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
261
387
|
}
|
|
262
388
|
|
|
@@ -269,18 +395,21 @@ function context(index, name, options = {}) {
|
|
|
269
395
|
callers = callerResult.kept;
|
|
270
396
|
callees = calleeResult.kept;
|
|
271
397
|
confidenceFiltered = callerResult.filtered + calleeResult.filtered;
|
|
398
|
+
filteredByFlag.minConfidence = callerResult.filtered;
|
|
272
399
|
}
|
|
273
400
|
|
|
274
401
|
// Stable output ordering: callers by (file, line). Callees retain their
|
|
275
402
|
// call-count order from findCallees (most-called first) — that's a value
|
|
276
403
|
// the user expects, not a stability concern, since the secondary sort
|
|
277
404
|
// by line keeps ties deterministic.
|
|
278
|
-
|
|
405
|
+
const byFileLine = (a, b) => {
|
|
279
406
|
const fa = a.relativePath || a.file || '';
|
|
280
407
|
const fb = b.relativePath || b.file || '';
|
|
281
408
|
if (fa !== fb) return fa.localeCompare(fb);
|
|
282
409
|
return (a.line || 0) - (b.line || 0);
|
|
283
|
-
}
|
|
410
|
+
};
|
|
411
|
+
callers = [...callers].sort(byFileLine);
|
|
412
|
+
unverifiedCallers = [...unverifiedCallers].sort(byFileLine);
|
|
284
413
|
callees = [...callees].sort((a, b) => {
|
|
285
414
|
// Primary: callCount desc (preserves "most-called first" UX)
|
|
286
415
|
const ca = a.callCount || 0, cb = b.callCount || 0;
|
|
@@ -303,10 +432,22 @@ function context(index, name, options = {}) {
|
|
|
303
432
|
|
|
304
433
|
// Optional: filter to unreachable-only (helps surface dead-path callers/callees)
|
|
305
434
|
if (options.unreachableOnly) {
|
|
435
|
+
const before = callers.length;
|
|
306
436
|
callers = callers.filter(c => !c.reachable);
|
|
437
|
+
filteredByFlag.unreachableOnly = before - callers.length;
|
|
307
438
|
callees = callees.filter(c => !c.reachable);
|
|
308
439
|
}
|
|
309
440
|
|
|
441
|
+
// Conservation account: reconciles the caller answer against the text
|
|
442
|
+
// ground set (pre-display truth; display-filter hides reported separately).
|
|
443
|
+
const filteredTotal = filteredByFlag.exclude + filteredByFlag.minConfidence + filteredByFlag.unreachableOnly;
|
|
444
|
+
const account = composeAccount(index, name, rawCallers,
|
|
445
|
+
filteredTotal > 0 ? { total: filteredTotal, byFlag: filteredByFlag } : undefined);
|
|
446
|
+
const ctxNotResolved = callNotResolvedEntries(index, account, options);
|
|
447
|
+
if (ctxNotResolved.length > 0) {
|
|
448
|
+
unverifiedCallers = [...unverifiedCallers, ...ctxNotResolved].sort(byFileLine);
|
|
449
|
+
}
|
|
450
|
+
|
|
310
451
|
const callerHistogram = buildHistogram(callers);
|
|
311
452
|
const calleeHistogram = buildHistogram(callees);
|
|
312
453
|
|
|
@@ -327,6 +468,7 @@ function context(index, name, options = {}) {
|
|
|
327
468
|
params: def.params,
|
|
328
469
|
returnType: def.returnType,
|
|
329
470
|
callers,
|
|
471
|
+
unverifiedCallers,
|
|
330
472
|
callees,
|
|
331
473
|
callerHistogram,
|
|
332
474
|
calleeHistogram,
|
|
@@ -338,6 +480,11 @@ function context(index, name, options = {}) {
|
|
|
338
480
|
confidenceFiltered,
|
|
339
481
|
includeMethods: !!options.includeMethods,
|
|
340
482
|
projectLanguage: index._getPredominantLanguage(),
|
|
483
|
+
account,
|
|
484
|
+
// No detected entry points (e.g. library code) — reachability
|
|
485
|
+
// markers are meaningless and suppressed by formatters.
|
|
486
|
+
hasEntrypoints: reachableSet.size > 0,
|
|
487
|
+
...(options.all && { all: true }),
|
|
341
488
|
// Structural facts for reliability hints
|
|
342
489
|
...(def.isMethod && { isMethod: true }),
|
|
343
490
|
...(def.className && { className: def.className }),
|
|
@@ -692,22 +839,34 @@ function impact(index, name, options = {}) {
|
|
|
692
839
|
'enum', 'trait', 'impl', 'record', 'namespace']);
|
|
693
840
|
const defIsTypeDef = TYPE_DEF_KINDS.has(def.type);
|
|
694
841
|
|
|
695
|
-
// BUG-H3
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
|
|
842
|
+
// BUG-H3 + tiered contract: impact always analyzes every callable site —
|
|
843
|
+
// method calls included unconditionally, tiered by receiver evidence.
|
|
844
|
+
// --no-include-methods is a deprecated no-op (evidence-less method sites
|
|
845
|
+
// land in the unverified tier instead of disappearing).
|
|
846
|
+
const impactIncludeMethods = true;
|
|
699
847
|
const impactIncludeUncertain = options.includeUncertain ?? false;
|
|
700
848
|
|
|
701
849
|
// Use findCallers for className-scoped or method queries (sophisticated binding resolution)
|
|
702
850
|
// Fall back to usages-based approach for simple function queries (backward compatible)
|
|
703
851
|
let callSites;
|
|
852
|
+
// Conservation accounting: engine-recorded drops + post-engine drops in
|
|
853
|
+
// this function (className filter, binding cross-check, method skips).
|
|
854
|
+
// Claims use ABSOLUTE paths (ground set is keyed by absolute file path).
|
|
855
|
+
let impactAccountRaw = null;
|
|
856
|
+
let impactRoutedUnverified = []; // engine-routed retained drops (unverifiedEntries)
|
|
857
|
+
const impactClaims = [];
|
|
858
|
+
const impactPostHocExcluded = [];
|
|
859
|
+
const impactPostHocUnverified = [];
|
|
704
860
|
if (options.className || defIsMethod || defIsTypeDef) {
|
|
705
861
|
// findCallers has proper method call resolution (self/this, binding IDs, receiver checks)
|
|
706
862
|
let callerResults = index.findCallers(name, {
|
|
707
863
|
includeMethods: impactIncludeMethods,
|
|
708
864
|
includeUncertain: impactIncludeUncertain,
|
|
709
865
|
targetDefinitions: [def],
|
|
866
|
+
collectAccount: true,
|
|
710
867
|
});
|
|
868
|
+
impactAccountRaw = callerResults.accountRaw;
|
|
869
|
+
impactRoutedUnverified = callerResults.unverifiedEntries || [];
|
|
711
870
|
|
|
712
871
|
// When the target definition has a className (including Go/Rust methods which
|
|
713
872
|
// now get className from receiver), filter out method calls whose receiver
|
|
@@ -724,14 +883,14 @@ function impact(index, name, options = {}) {
|
|
|
724
883
|
else if (d.receiver) _impClassNames.add(d.receiver.replace(/^\*/, ''));
|
|
725
884
|
}
|
|
726
885
|
}
|
|
727
|
-
|
|
886
|
+
const keepForTargetClass = (c) => {
|
|
728
887
|
// Keep non-method calls and self/this/cls calls (already resolved by findCallers)
|
|
729
888
|
if (!c.isMethod) return true;
|
|
730
889
|
const r = c.receiver;
|
|
731
890
|
if (r && ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
732
891
|
// Use receiverType from findCallers when available (Go/Java/Rust type inference)
|
|
733
892
|
if (c.receiverType) {
|
|
734
|
-
return c.receiverType === targetClassName;
|
|
893
|
+
return c.receiverType === targetClassName ? 'strong' : false;
|
|
735
894
|
}
|
|
736
895
|
// No receiver (chained/complex expression): only include if method is
|
|
737
896
|
// unique or rare across types — otherwise too many false positives
|
|
@@ -770,7 +929,7 @@ function impact(index, name, options = {}) {
|
|
|
770
929
|
}
|
|
771
930
|
const receiverType = localTypes.get(r);
|
|
772
931
|
if (receiverType) {
|
|
773
|
-
return receiverType === targetClassName;
|
|
932
|
+
return receiverType === targetClassName ? 'strong' : false;
|
|
774
933
|
}
|
|
775
934
|
}
|
|
776
935
|
}
|
|
@@ -792,7 +951,7 @@ function impact(index, name, options = {}) {
|
|
|
792
951
|
const fieldMatch = line.match(new RegExp(`\\b(\\w+)(?:<[^>]*>)?\\s+${r.replace(/[.*+?^${}()|[\]\\]/g, '\\\\$&')}\\s*[;=]`));
|
|
793
952
|
if (fieldMatch) {
|
|
794
953
|
const fieldType = fieldMatch[1];
|
|
795
|
-
if (fieldType === targetClassName) return
|
|
954
|
+
if (fieldType === targetClassName) return 'strong';
|
|
796
955
|
break;
|
|
797
956
|
}
|
|
798
957
|
}
|
|
@@ -809,7 +968,7 @@ function impact(index, name, options = {}) {
|
|
|
809
968
|
// Check if the type annotation contains the target class name
|
|
810
969
|
const typeMatches = param.type.match(/\b([A-Za-z_]\w*)\b/g);
|
|
811
970
|
if (typeMatches && typeMatches.some(t => t === targetClassName)) {
|
|
812
|
-
return
|
|
971
|
+
return 'strong';
|
|
813
972
|
}
|
|
814
973
|
// Type annotation exists but doesn't match target class — filter out
|
|
815
974
|
return false;
|
|
@@ -825,6 +984,24 @@ function impact(index, name, options = {}) {
|
|
|
825
984
|
// Type-scoped query but receiver type unknown — filter it out.
|
|
826
985
|
// Unknown receivers are likely unrelated.
|
|
827
986
|
return false;
|
|
987
|
+
};
|
|
988
|
+
// Conservation: post-hoc rejects are positive type-mismatch evidence —
|
|
989
|
+
// they MOVE into the excluded bucket instead of vanishing. Survivors
|
|
990
|
+
// verified by STRONG evidence (receiverType / local constructor /
|
|
991
|
+
// field declaration / param annotation match) are upgraded to the
|
|
992
|
+
// confirmed tier — the filter just proved the receiver's type.
|
|
993
|
+
callerResults = callerResults.filter(c => {
|
|
994
|
+
const keep = keepForTargetClass(c);
|
|
995
|
+
if (!keep) {
|
|
996
|
+
impactPostHocExcluded.push({ file: c.file, line: c.line, reason: 'receiver-type-mismatch' });
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
if (keep === 'strong' && c.tier === 'unverified') {
|
|
1000
|
+
c.tier = 'confirmed';
|
|
1001
|
+
c.resolution = 'receiver-hint';
|
|
1002
|
+
c.confidence = 0.80;
|
|
1003
|
+
}
|
|
1004
|
+
return true;
|
|
828
1005
|
});
|
|
829
1006
|
}
|
|
830
1007
|
|
|
@@ -834,6 +1011,7 @@ function impact(index, name, options = {}) {
|
|
|
834
1011
|
{ file: c.file, relativePath: c.relativePath, line: c.line, content: c.content },
|
|
835
1012
|
name
|
|
836
1013
|
);
|
|
1014
|
+
impactClaims.push({ file: c.file, line: c.line, tier: c.tier });
|
|
837
1015
|
callSites.push({
|
|
838
1016
|
file: c.relativePath,
|
|
839
1017
|
line: c.line,
|
|
@@ -843,6 +1021,7 @@ function impact(index, name, options = {}) {
|
|
|
843
1021
|
callerStartLine: c.callerStartLine,
|
|
844
1022
|
confidence: c.confidence,
|
|
845
1023
|
resolution: c.resolution,
|
|
1024
|
+
...(c.tier && { tier: c.tier }),
|
|
846
1025
|
...analysis
|
|
847
1026
|
});
|
|
848
1027
|
}
|
|
@@ -856,7 +1035,10 @@ function impact(index, name, options = {}) {
|
|
|
856
1035
|
includeMethods: impactIncludeMethods,
|
|
857
1036
|
includeUncertain: impactIncludeUncertain,
|
|
858
1037
|
targetDefinitions: [def],
|
|
1038
|
+
collectAccount: true,
|
|
859
1039
|
});
|
|
1040
|
+
impactAccountRaw = callerResults.accountRaw;
|
|
1041
|
+
impactRoutedUnverified = callerResults.unverifiedEntries || [];
|
|
860
1042
|
const targetBindingId = def.bindingId;
|
|
861
1043
|
// Convert findCallers results to the format expected by analyzeCallSite
|
|
862
1044
|
const calls = callerResults.map(c => ({
|
|
@@ -870,6 +1052,7 @@ function impact(index, name, options = {}) {
|
|
|
870
1052
|
callerStartLine: c.callerStartLine,
|
|
871
1053
|
confidence: c.confidence,
|
|
872
1054
|
resolution: c.resolution,
|
|
1055
|
+
tier: c.tier,
|
|
873
1056
|
}));
|
|
874
1057
|
// Keep the same binding filter for backward compat (findCallers already handles this,
|
|
875
1058
|
// but cross-check with usages-based binding filter for safety)
|
|
@@ -887,6 +1070,7 @@ function impact(index, name, options = {}) {
|
|
|
887
1070
|
}
|
|
888
1071
|
}
|
|
889
1072
|
if (localBindings.length > 0 && !localBindings.some(b => b.id === targetBindingId)) {
|
|
1073
|
+
impactPostHocExcluded.push({ file: u.file, line: u.line, reason: 'other-definition' });
|
|
890
1074
|
return false;
|
|
891
1075
|
}
|
|
892
1076
|
}
|
|
@@ -915,12 +1099,15 @@ function impact(index, name, options = {}) {
|
|
|
915
1099
|
if (matchedCall?.receiver === targetDir) {
|
|
916
1100
|
// Receiver matches package directory — keep it
|
|
917
1101
|
} else {
|
|
1102
|
+
impactPostHocUnverified.push({ file: call.file, line: call.line, reason: 'method-no-evidence' });
|
|
918
1103
|
continue;
|
|
919
1104
|
}
|
|
920
1105
|
} else {
|
|
1106
|
+
impactPostHocUnverified.push({ file: call.file, line: call.line, reason: 'method-no-evidence' });
|
|
921
1107
|
continue;
|
|
922
1108
|
}
|
|
923
1109
|
}
|
|
1110
|
+
impactClaims.push({ file: call.file, line: call.line, tier: call.tier });
|
|
924
1111
|
callSites.push({
|
|
925
1112
|
file: call.relativePath,
|
|
926
1113
|
line: call.line,
|
|
@@ -930,16 +1117,45 @@ function impact(index, name, options = {}) {
|
|
|
930
1117
|
callerStartLine: call.callerStartLine,
|
|
931
1118
|
confidence: call.confidence,
|
|
932
1119
|
resolution: call.resolution,
|
|
1120
|
+
...(call.tier && { tier: call.tier }),
|
|
933
1121
|
...analysis
|
|
934
1122
|
});
|
|
935
1123
|
}
|
|
936
1124
|
index._clearTreeCache();
|
|
937
1125
|
}
|
|
938
1126
|
|
|
1127
|
+
// Tier partition: confirmed sites stay in callSites; unverified-tier sites
|
|
1128
|
+
// (incl. engine-routed retained drops) render in their own section.
|
|
1129
|
+
let unverifiedSites = callSites.filter(s => s.tier === 'unverified');
|
|
1130
|
+
callSites = callSites.filter(s => s.tier !== 'unverified');
|
|
1131
|
+
for (const u of impactRoutedUnverified) {
|
|
1132
|
+
unverifiedSites.push({
|
|
1133
|
+
file: u.relativePath,
|
|
1134
|
+
line: u.line,
|
|
1135
|
+
expression: (u.content || '').trim(),
|
|
1136
|
+
callerName: u.callerName ?? null,
|
|
1137
|
+
confidence: u.confidence,
|
|
1138
|
+
resolution: u.resolution,
|
|
1139
|
+
tier: 'unverified',
|
|
1140
|
+
...(u.reason && { reason: u.reason }),
|
|
1141
|
+
...(u.dispatchVia && { dispatchVia: u.dispatchVia }),
|
|
1142
|
+
...(u.dispatchCandidates != null && { dispatchCandidates: u.dispatchCandidates }),
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
unverifiedSites.sort((a, b) => {
|
|
1146
|
+
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
1147
|
+
return (a.line || 0) - (b.line || 0);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
939
1150
|
// Apply exclude filter
|
|
1151
|
+
const impactFilteredByFlag = { exclude: 0, unreachableOnly: 0 };
|
|
940
1152
|
let filteredSites = callSites;
|
|
941
1153
|
if (options.exclude && options.exclude.length > 0) {
|
|
942
1154
|
filteredSites = callSites.filter(s => index.matchesFilters(s.file, { exclude: options.exclude }));
|
|
1155
|
+
impactFilteredByFlag.exclude = callSites.length - filteredSites.length;
|
|
1156
|
+
const beforeUnverified = unverifiedSites.length;
|
|
1157
|
+
unverifiedSites = unverifiedSites.filter(s => index.matchesFilters(s.file, { exclude: options.exclude }));
|
|
1158
|
+
impactFilteredByFlag.exclude += beforeUnverified - unverifiedSites.length;
|
|
943
1159
|
}
|
|
944
1160
|
|
|
945
1161
|
// Trust signals: tag each call site with reachability, build a confidence histogram.
|
|
@@ -953,10 +1169,47 @@ function impact(index, name, options = {}) {
|
|
|
953
1169
|
}
|
|
954
1170
|
}
|
|
955
1171
|
if (options.unreachableOnly) {
|
|
1172
|
+
const before = filteredSites.length;
|
|
956
1173
|
filteredSites = filteredSites.filter(s => !s.reachable);
|
|
1174
|
+
impactFilteredByFlag.unreachableOnly = before - filteredSites.length;
|
|
957
1175
|
}
|
|
958
1176
|
const callerHistogram = buildHistogram(filteredSites);
|
|
959
1177
|
|
|
1178
|
+
// Conservation account: claims from ALL call sites (pre-display filters,
|
|
1179
|
+
// pre-top truncation) plus engine-recorded and post-hoc drops.
|
|
1180
|
+
const impactAccount = (() => {
|
|
1181
|
+
const { computeGroundSet, buildAccount } = require('./account');
|
|
1182
|
+
const groundSet = computeGroundSet(index, name);
|
|
1183
|
+
const confirmedEntries = [];
|
|
1184
|
+
const unverifiedEntries = [...(impactAccountRaw?.unverifiedLines || []), ...impactPostHocUnverified];
|
|
1185
|
+
for (const u of impactRoutedUnverified) unverifiedEntries.push({ file: u.file, line: u.line });
|
|
1186
|
+
for (const cl of impactClaims) {
|
|
1187
|
+
if (cl.tier === 'unverified') unverifiedEntries.push(cl);
|
|
1188
|
+
else confirmedEntries.push(cl);
|
|
1189
|
+
}
|
|
1190
|
+
const excludedEntries = [...(impactAccountRaw?.excludedEntries || []), ...impactPostHocExcluded];
|
|
1191
|
+
const filteredTotal = impactFilteredByFlag.exclude + impactFilteredByFlag.unreachableOnly;
|
|
1192
|
+
return buildAccount(index, name, {
|
|
1193
|
+
groundSet,
|
|
1194
|
+
confirmedEntries,
|
|
1195
|
+
unverifiedEntries,
|
|
1196
|
+
excludedEntries,
|
|
1197
|
+
filtered: filteredTotal > 0 ? { total: filteredTotal, byFlag: impactFilteredByFlag } : undefined,
|
|
1198
|
+
});
|
|
1199
|
+
})();
|
|
1200
|
+
// Impact's unverified sites carry relative paths in `file`
|
|
1201
|
+
const impactNotResolved = callNotResolvedEntries(index, impactAccount, options)
|
|
1202
|
+
.map(e => ({
|
|
1203
|
+
file: e.relativePath, line: e.line, expression: e.content,
|
|
1204
|
+
callerName: null, tier: 'unverified', reason: e.reason,
|
|
1205
|
+
}));
|
|
1206
|
+
if (impactNotResolved.length > 0) {
|
|
1207
|
+
unverifiedSites = [...unverifiedSites, ...impactNotResolved].sort((a, b) => {
|
|
1208
|
+
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
1209
|
+
return (a.line || 0) - (b.line || 0);
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
960
1213
|
// Apply top limit if specified (limits total call sites shown)
|
|
961
1214
|
const totalBeforeLimit = filteredSites.length;
|
|
962
1215
|
if (options.top && options.top > 0 && filteredSites.length > options.top) {
|
|
@@ -1007,6 +1260,9 @@ function impact(index, name, options = {}) {
|
|
|
1007
1260
|
paramsStructured: def.paramsStructured,
|
|
1008
1261
|
totalCallSites: totalBeforeLimit,
|
|
1009
1262
|
shownCallSites: filteredSites.length,
|
|
1263
|
+
unverifiedSites,
|
|
1264
|
+
account: impactAccount,
|
|
1265
|
+
hasEntrypoints: impactReachable.size > 0,
|
|
1010
1266
|
callerHistogram,
|
|
1011
1267
|
// Stable ordering: files alphabetical, sites by line ascending. Documented contract.
|
|
1012
1268
|
byFile: Array.from(byFile.entries())
|
|
@@ -1130,6 +1386,8 @@ function about(index, name, options = {}) {
|
|
|
1130
1386
|
let allCallers = null;
|
|
1131
1387
|
let allCallees = null;
|
|
1132
1388
|
let aboutConfFiltered = 0;
|
|
1389
|
+
let aboutAccount = null;
|
|
1390
|
+
let aboutUnverified = { total: 0, top: [] };
|
|
1133
1391
|
// BUG-M3: include classes/structs/interfaces — `new Foo()` invocations are
|
|
1134
1392
|
// tracked as calls in the parser (isConstructor:true) and findCallers resolves
|
|
1135
1393
|
// them. Without this, `about ClassName` produced "USAGES: 5 calls" but no
|
|
@@ -1145,12 +1403,25 @@ function about(index, name, options = {}) {
|
|
|
1145
1403
|
// BUG-H1: pass needsTotal:true so the returned array's `totalCount` reflects the
|
|
1146
1404
|
// true pre-truncation candidate count. Without this, `about` would report the
|
|
1147
1405
|
// capped count as the total (e.g. "showing 10 of 30" when there are actually 153).
|
|
1148
|
-
const rawCallers = index.findCallers(symbolName, { includeMethods, includeUncertain: options.includeUncertain, targetDefinitions: [primary], maxResults: callerCap, needsTotal: true });
|
|
1406
|
+
const rawCallers = index.findCallers(symbolName, { includeMethods, includeUncertain: options.includeUncertain, targetDefinitions: [primary], maxResults: callerCap, needsTotal: true, collectAccount: true });
|
|
1149
1407
|
const shadowCallers = rawCallers.shadowEntries || [];
|
|
1150
1408
|
allCallers = rawCallers;
|
|
1409
|
+
const aboutFilteredByFlag = { exclude: 0, minConfidence: 0, unreachableOnly: 0 };
|
|
1410
|
+
// Tier partition: confirmed callers stay in allCallers; unverified-tier
|
|
1411
|
+
// entries (incl. engine-routed retained drops) get their own section.
|
|
1412
|
+
let unverifiedPool = [
|
|
1413
|
+
...allCallers.filter(c => c.tier === 'unverified'),
|
|
1414
|
+
...(rawCallers.unverifiedEntries || []),
|
|
1415
|
+
];
|
|
1416
|
+
allCallers = allCallers.filter(c => c.tier !== 'unverified');
|
|
1151
1417
|
// Apply exclude filter before slicing
|
|
1152
1418
|
if (options.exclude && options.exclude.length > 0) {
|
|
1419
|
+
const before = allCallers.length;
|
|
1153
1420
|
allCallers = allCallers.filter(c => index.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
1421
|
+
aboutFilteredByFlag.exclude = before - allCallers.length;
|
|
1422
|
+
const beforeUnverified = unverifiedPool.length;
|
|
1423
|
+
unverifiedPool = unverifiedPool.filter(c => index.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
1424
|
+
aboutFilteredByFlag.exclude += beforeUnverified - unverifiedPool.length;
|
|
1154
1425
|
}
|
|
1155
1426
|
// Apply confidence filtering before slicing
|
|
1156
1427
|
if (options.minConfidence > 0) {
|
|
@@ -1158,10 +1429,13 @@ function about(index, name, options = {}) {
|
|
|
1158
1429
|
const callerResult = filterByConfidence(allCallers, options.minConfidence);
|
|
1159
1430
|
allCallers = callerResult.kept;
|
|
1160
1431
|
aboutConfFiltered += callerResult.filtered;
|
|
1432
|
+
aboutFilteredByFlag.minConfidence = callerResult.filtered;
|
|
1161
1433
|
}
|
|
1162
1434
|
// BUG-H1: post-filter total — count the un-enriched shadow candidates that
|
|
1163
1435
|
// also pass the same filters, so the displayed "showing N of <total>"
|
|
1164
1436
|
// matches what `context` (which runs unbounded) would have shown.
|
|
1437
|
+
// Per-tier: unverified-tier shadows count toward the unverified total,
|
|
1438
|
+
// never toward the confirmed total.
|
|
1165
1439
|
let shadowSurvivors = shadowCallers;
|
|
1166
1440
|
if (options.exclude && options.exclude.length > 0) {
|
|
1167
1441
|
shadowSurvivors = shadowSurvivors.filter(c => index.matchesFilters(c.relativePath, { exclude: options.exclude }));
|
|
@@ -1169,6 +1443,8 @@ function about(index, name, options = {}) {
|
|
|
1169
1443
|
if (options.minConfidence > 0) {
|
|
1170
1444
|
shadowSurvivors = shadowSurvivors.filter(c => (c.confidence || 0) >= options.minConfidence);
|
|
1171
1445
|
}
|
|
1446
|
+
const unverifiedShadowCount = shadowSurvivors.filter(s => s.tier === 'unverified').length;
|
|
1447
|
+
shadowSurvivors = shadowSurvivors.filter(s => s.tier !== 'unverified');
|
|
1172
1448
|
// Tag reachability on raw caller objects so we can preserve the field on the projection.
|
|
1173
1449
|
// Reachability is computed once per index and cached.
|
|
1174
1450
|
const aboutReachable = computeReachability(index);
|
|
@@ -1176,7 +1452,9 @@ function about(index, name, options = {}) {
|
|
|
1176
1452
|
|
|
1177
1453
|
// Optional: filter to unreachable-only callers
|
|
1178
1454
|
if (options.unreachableOnly) {
|
|
1455
|
+
const before = allCallers.length;
|
|
1179
1456
|
allCallers = allCallers.filter(c => !c.reachable);
|
|
1457
|
+
aboutFilteredByFlag.unreachableOnly = before - allCallers.length;
|
|
1180
1458
|
// Apply same filter to shadows using their callerStartLine/file when available.
|
|
1181
1459
|
// Shadows lack callerStartLine, so they're treated as reachable=false (conservative,
|
|
1182
1460
|
// matches the historical behavior where un-enriched callers had no reachability info).
|
|
@@ -1184,6 +1462,12 @@ function about(index, name, options = {}) {
|
|
|
1184
1462
|
// building a perfect estimate isn't justified.
|
|
1185
1463
|
shadowSurvivors = []; // conservative — drop shadows for unreachableOnly mode
|
|
1186
1464
|
}
|
|
1465
|
+
// Conservation account: claims from the PRE-filter rawCallers (+shadows);
|
|
1466
|
+
// display-filter hides are explanatory metadata, outside the invariant.
|
|
1467
|
+
const aboutFilteredTotal = aboutFilteredByFlag.exclude + aboutFilteredByFlag.minConfidence + aboutFilteredByFlag.unreachableOnly;
|
|
1468
|
+
aboutAccount = composeAccount(index, symbolName, rawCallers,
|
|
1469
|
+
aboutFilteredTotal > 0 ? { total: aboutFilteredTotal, byFlag: aboutFilteredByFlag } : undefined);
|
|
1470
|
+
unverifiedPool.push(...callNotResolvedEntries(index, aboutAccount, options));
|
|
1187
1471
|
// Stash the post-filter total on allCallers so the result builder can use it.
|
|
1188
1472
|
Object.defineProperty(allCallers, '__postFilterTotal', {
|
|
1189
1473
|
value: allCallers.length + shadowSurvivors.length,
|
|
@@ -1214,6 +1498,31 @@ function about(index, name, options = {}) {
|
|
|
1214
1498
|
reachable: c.reachable,
|
|
1215
1499
|
}));
|
|
1216
1500
|
|
|
1501
|
+
// Unverified tier projection: visible, capped, with the drop reason.
|
|
1502
|
+
unverifiedPool.sort((a, b) => {
|
|
1503
|
+
const fa = a.relativePath || '';
|
|
1504
|
+
const fb = b.relativePath || '';
|
|
1505
|
+
if (fa !== fb) return fa.localeCompare(fb);
|
|
1506
|
+
return (a.line || 0) - (b.line || 0);
|
|
1507
|
+
});
|
|
1508
|
+
aboutUnverified = {
|
|
1509
|
+
total: unverifiedPool.length + unverifiedShadowCount,
|
|
1510
|
+
top: unverifiedPool.slice(0, 10).map(c => ({
|
|
1511
|
+
file: c.relativePath,
|
|
1512
|
+
line: c.line,
|
|
1513
|
+
...(c.callerStartLine && c.callerName && {
|
|
1514
|
+
handle: `${c.relativePath}:${c.callerStartLine}:${c.callerName}`
|
|
1515
|
+
}),
|
|
1516
|
+
expression: (c.content || '').trim(),
|
|
1517
|
+
callerName: c.callerName ?? null,
|
|
1518
|
+
confidence: c.confidence,
|
|
1519
|
+
resolution: c.resolution,
|
|
1520
|
+
...(c.reason && { reason: c.reason }),
|
|
1521
|
+
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
1522
|
+
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
1523
|
+
})),
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1217
1526
|
// BUG-M3: classes/structs/interfaces don't have meaningful callees
|
|
1218
1527
|
// (their body is methods, not a sequence of calls). Skip findCallees
|
|
1219
1528
|
// for type definitions — callers (constructor/instantiation sites)
|
|
@@ -1357,8 +1666,10 @@ function about(index, name, options = {}) {
|
|
|
1357
1666
|
// BUG-H1: prefer post-filter total (computed from enriched + shadow candidates).
|
|
1358
1667
|
// Falls back to allCallers.length when the post-filter total wasn't computed
|
|
1359
1668
|
// (e.g., when primary is not a function and findCallers wasn't called).
|
|
1669
|
+
// Since the tier partition, this total counts CONFIRMED callers only.
|
|
1360
1670
|
total: allCallers?.__postFilterTotal ?? allCallers?.length ?? 0,
|
|
1361
1671
|
top: callers,
|
|
1672
|
+
unverified: aboutUnverified,
|
|
1362
1673
|
// R3-NEW-1: include shadow callers (un-enriched candidates that passed the
|
|
1363
1674
|
// same filters) so the histogram counts sum to `total`, not maxResults*3.
|
|
1364
1675
|
histogram: buildHistogram(
|
|
@@ -1381,6 +1692,8 @@ function about(index, name, options = {}) {
|
|
|
1381
1692
|
types,
|
|
1382
1693
|
code,
|
|
1383
1694
|
includeMethods,
|
|
1695
|
+
...(aboutAccount && { account: aboutAccount }),
|
|
1696
|
+
...(allCallers && { hasEntrypoints: computeReachability(index).size > 0 }),
|
|
1384
1697
|
...(aboutConfFiltered > 0 && { confidenceFiltered: aboutConfFiltered }),
|
|
1385
1698
|
// BUG-M4: surface ambiguous-resolution warnings so formatters can render
|
|
1386
1699
|
// a "auto-selected ... pass --file to choose" note.
|
|
@@ -2324,4 +2637,11 @@ module.exports = {
|
|
|
2324
2637
|
unquoteDiffPath,
|
|
2325
2638
|
auditAsync,
|
|
2326
2639
|
tagInTestCase,
|
|
2640
|
+
// Exported for tests: with the callback-gate fix every natural producer
|
|
2641
|
+
// of call-not-resolved is closed — the listing contract (counted ⇒
|
|
2642
|
+
// listed) is pinned directly since no fixture can reach it end-to-end.
|
|
2643
|
+
callNotResolvedEntries,
|
|
2644
|
+
// Shared with tracing.js (trace/blast/reverseTrace root accounts) — the
|
|
2645
|
+
// tree commands compose the same text-ground account at their root hop.
|
|
2646
|
+
composeAccount,
|
|
2327
2647
|
};
|