ucn 3.8.26 → 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.
@@ -4,7 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { langTraits } = require('../../languages');
7
- const { dynamicImportsNote, formatGitLine } = require('./shared');
7
+ const { dynamicImportsNote, formatGitLine, unverifiedReasonLabel } = require('./shared');
8
8
 
9
9
  /**
10
10
  * One short sentence (~80 chars) of a docstring, suitable for inline display
@@ -43,6 +43,108 @@ function shouldShowReachability(items) {
43
43
  return items.some(c => c.reachable === false);
44
44
  }
45
45
 
46
+ /**
47
+ * Reachability display policy for a section: per-line [unreachable] markers
48
+ * ONLY when reachability is mixed (they distinguish which); when ALL items are
49
+ * unreachable a single aggregate note carries the information without
50
+ * repeating a marker on every line. Suppressed entirely when the project has
51
+ * no detected entry points (hasEntrypoints === false) — "unreachable" would
52
+ * be meaningless for library code.
53
+ *
54
+ * @returns {{ perLine: boolean, note: string|null }}
55
+ */
56
+ function reachabilityDisplay(items, hasEntrypoints, label) {
57
+ if (hasEntrypoints === false || !items || items.length === 0) return { perLine: false, note: null };
58
+ const unreachable = items.filter(c => c.reachable === false).length;
59
+ if (unreachable === 0) return { perLine: false, note: null };
60
+ if (unreachable === items.length) {
61
+ return { perLine: false, note: ` Note: all ${unreachable} ${label}${unreachable === 1 ? '' : 's'} unreachable from any entry point` };
62
+ }
63
+ return { perLine: true, note: ` Note: ${unreachable} of ${items.length} ${label}s unreachable from any entry point` };
64
+ }
65
+
66
+ // Display order for resolution labels in evidence aggregates (most → least confident)
67
+ const RESOLUTION_ORDER = ['exact-binding', 'same-class', 'receiver-hint', 'scope-match', 'name-only', 'uncertain'];
68
+
69
+ /**
70
+ * One aggregate evidence line per tier section, replacing per-edge confidence
71
+ * decimals: uniform → "evidence: scope-match (all)"; mixed → counts in
72
+ * RESOLUTION_ORDER. Returns null when no items carry a resolution.
73
+ */
74
+ function formatEvidenceLine(items) {
75
+ if (!items || items.length === 0) return null;
76
+ const counts = new Map();
77
+ for (const it of items) {
78
+ if (!it.resolution) continue;
79
+ counts.set(it.resolution, (counts.get(it.resolution) || 0) + 1);
80
+ }
81
+ if (counts.size === 0) return null;
82
+ if (counts.size === 1) {
83
+ return ` evidence: ${counts.keys().next().value} (all)`;
84
+ }
85
+ const parts = [];
86
+ for (const r of RESOLUTION_ORDER) {
87
+ if (counts.has(r)) parts.push(`${counts.get(r)} ${r}`);
88
+ }
89
+ for (const [r, n] of counts) {
90
+ if (!RESOLUTION_ORDER.includes(r)) parts.push(`${n} ${r}`);
91
+ }
92
+ return ` evidence: ${parts.join(', ')}`;
93
+ }
94
+
95
+ /** Classify a caller entry as test or prod by its path. */
96
+ function isTestEntry(entry) {
97
+ const { isTestPath } = require('../shared');
98
+ return isTestPath(entry.relativePath || entry.file || '');
99
+ }
100
+
101
+ /**
102
+ * Render the conservation contract lines: ACCOUNT (always), WARNING (unparsed
103
+ * files containing the symbol), FILTERED (display-filter hides). Returns [].
104
+ * when no account is present (e.g. class-type context).
105
+ */
106
+ function formatAccountLines(account) {
107
+ if (!account) return [];
108
+ const lines = [];
109
+ const nc = account.nonCall || { imports: 0, definitions: 0, references: 0, unclassifiedText: 0, total: 0 };
110
+ let line = `ACCOUNT: "${account.symbol}" occurs on ${account.groundTotal} line${account.groundTotal === 1 ? '' : 's'}` +
111
+ ` in ${account.fileCount} file${account.fileCount === 1 ? '' : 's'}: ` +
112
+ `${account.confirmed} confirmed, ${account.unverified} unverified, ` +
113
+ `${nc.total} non-call (${nc.imports} import, ${nc.definitions} definition, ${nc.references} reference, ${nc.unclassifiedText} other-text), ` +
114
+ `${account.excluded ? account.excluded.total : 0} other-target, ` +
115
+ `${account.unaccounted} unaccounted`;
116
+ if (account.beyondText && account.beyondText.count > 0) {
117
+ line += ` (+${account.beyondText.count} beyond-text caller${account.beyondText.count === 1 ? '' : 's'})`;
118
+ }
119
+ lines.push(line);
120
+ if (account.unparsed && account.unparsed.fileCount > 0) {
121
+ lines.push(`WARNING: ${account.unparsed.fileCount} unparsed file${account.unparsed.fileCount === 1 ? '' : 's'} ` +
122
+ `contain${account.unparsed.fileCount === 1 ? 's' : ''} "${account.symbol}" ` +
123
+ `(${account.unparsed.lines} line${account.unparsed.lines === 1 ? '' : 's'}, NOT analyzed): ` +
124
+ account.unparsed.files.join(', '));
125
+ }
126
+ if (account.unreadableFiles && account.unreadableFiles.length > 0) {
127
+ lines.push(`WARNING: ${account.unreadableFiles.length} indexed-but-unreadable file(s) skipped: ${account.unreadableFiles.join(', ')}`);
128
+ }
129
+ if (account.filtered && account.filtered.total > 0) {
130
+ const parts = [];
131
+ const f = account.filtered.byFlag || {};
132
+ if (f.exclude) parts.push(`${f.exclude} --exclude`);
133
+ if (f.minConfidence) parts.push(`${f.minConfidence} --min-confidence`);
134
+ if (f.unreachableOnly) parts.push(`${f.unreachableOnly} --unreachable-only`);
135
+ lines.push(`FILTERED: ${account.filtered.total} hidden by flags (${parts.join(', ')})`);
136
+ }
137
+ return lines;
138
+ }
139
+
140
+ /** "NON-CALL OCCURRENCES" summary line from the account. */
141
+ function formatNonCallLine(account, hintName) {
142
+ if (!account || !account.nonCall || account.nonCall.total === 0) return null;
143
+ const nc = account.nonCall;
144
+ return `NON-CALL OCCURRENCES: ${nc.total} (${nc.imports} imports, ${nc.definitions} definitions, ` +
145
+ `${nc.references} references, ${nc.unclassifiedText} other-text) — counts only; see: ucn usages ${hintName}`;
146
+ }
147
+
46
148
  /** Format context (callers + callees) as JSON */
47
149
  function formatContextJson(context) {
48
150
  const meta = context.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 };
@@ -72,7 +174,23 @@ function formatContextJson(context) {
72
174
  file: c.relativePath || c.file,
73
175
  line: c.line,
74
176
  expression: c.content,
75
- callerName: c.callerName
177
+ callerName: c.callerName,
178
+ // Tier parity with the function-path callers list: class
179
+ // usages are the confirmed-tier answer for type symbols.
180
+ ...(c.confidence !== undefined && { confidence: c.confidence }),
181
+ ...(c.resolution && { resolution: c.resolution }),
182
+ ...(c.tier && { tier: c.tier })
183
+ })),
184
+ unverifiedCallers: (context.unverifiedCallers || []).map(c => ({
185
+ file: c.relativePath || c.file,
186
+ line: c.line,
187
+ expression: c.content,
188
+ callerName: c.callerName ?? null,
189
+ tier: 'unverified',
190
+ ...(c.reason && { reason: c.reason }),
191
+ ...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
192
+ ...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
193
+ ...(c.externalContract && { externalContract: true }),
76
194
  })),
77
195
  ...(context.warnings && { warnings: context.warnings })
78
196
  }
@@ -81,6 +199,7 @@ function formatContextJson(context) {
81
199
 
82
200
  // Standard function/method context
83
201
  const callers = context.callers || [];
202
+ const unverifiedCallers = context.unverifiedCallers || [];
84
203
  const callees = context.callees || [];
85
204
  return JSON.stringify({
86
205
  meta,
@@ -88,6 +207,7 @@ function formatContextJson(context) {
88
207
  function: context.function,
89
208
  file: context.file,
90
209
  callerCount: callers.length,
210
+ unverifiedCount: unverifiedCallers.length,
91
211
  calleeCount: callees.length,
92
212
  callerHistogram: context.callerHistogram || null,
93
213
  calleeHistogram: context.calleeHistogram || null,
@@ -96,9 +216,26 @@ function formatContextJson(context) {
96
216
  line: c.line,
97
217
  expression: c.content, // FULL expression
98
218
  callerName: c.callerName,
219
+ ...(c.calledAs && { calledAs: c.calledAs }),
220
+ ...(c.isFunctionReference && { functionReference: true }),
99
221
  ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
222
+ ...(c.tier && { tier: c.tier }),
100
223
  ...(c.reachable !== undefined && { reachable: c.reachable }),
101
224
  })),
225
+ unverifiedCallers: unverifiedCallers.map(c => ({
226
+ file: c.relativePath || c.file,
227
+ line: c.line,
228
+ expression: c.content, // FULL expression
229
+ callerName: c.callerName ?? null,
230
+ ...(c.calledAs && { calledAs: c.calledAs }),
231
+ ...(c.isFunctionReference && { functionReference: true }),
232
+ ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
233
+ tier: 'unverified',
234
+ ...(c.reason && { reason: c.reason }),
235
+ ...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
236
+ ...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
237
+ ...(c.externalContract && { externalContract: true }),
238
+ })),
102
239
  callees: callees.map(c => ({
103
240
  name: c.name,
104
241
  type: c.type,
@@ -122,7 +259,6 @@ function formatContext(ctx, options = {}) {
122
259
  if (!ctx) return { text: 'Symbol not found.', expandable: [] };
123
260
 
124
261
  const expandHint = options.expandHint != null ? options.expandHint : 'Use ucn_expand with item number to see code for any item.';
125
- const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded. Use include_methods=true to include them.';
126
262
 
127
263
  const lines = [];
128
264
  const expandable = [];
@@ -159,7 +295,7 @@ function formatContext(ctx, options = {}) {
159
295
  }
160
296
 
161
297
  const callers = ctx.callers || [];
162
- lines.push(`\nCALLERS (${callers.length}):`);
298
+ lines.push(`\nCALLERS — CONFIRMED (${callers.length}):`);
163
299
  for (const c of callers) {
164
300
  const callerName = c.callerName ? ` [${c.callerName}]` : '';
165
301
  lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
@@ -176,6 +312,40 @@ function formatContext(ctx, options = {}) {
176
312
  });
177
313
  }
178
314
 
315
+ const typeUnverified = ctx.unverifiedCallers || [];
316
+ if (typeUnverified.length > 0) {
317
+ lines.push(`\nCALLERS — UNVERIFIED (${typeUnverified.length}) — call syntax, no binding/receiver evidence:`);
318
+ const cap = 10;
319
+ let shown = 0;
320
+ for (const u of typeUnverified) {
321
+ if (shown >= cap) break;
322
+ const callerName = u.callerName ? ` [${u.callerName}]` : '';
323
+ const reason = u.reason ? ` (${u.reason})` : '';
324
+ const expr = u.content ? `: ${u.content.trim().replace(/\s+/g, ' ').slice(0, 100)}` : '';
325
+ lines.push(` [${itemNum}] ${u.relativePath}:${u.line}${callerName}${expr}${reason}`);
326
+ expandable.push({
327
+ num: itemNum++,
328
+ type: 'caller',
329
+ name: u.callerName || '(module level)',
330
+ file: u.callerFile || u.file,
331
+ relativePath: u.relativePath,
332
+ line: u.line,
333
+ startLine: u.callerStartLine || u.line,
334
+ endLine: u.callerEndLine || u.line
335
+ });
336
+ shown++;
337
+ }
338
+ if (typeUnverified.length > shown) {
339
+ lines.push(` (+${typeUnverified.length - shown} more unverified — use --all)`);
340
+ }
341
+ }
342
+
343
+ const typeAccountLines = formatAccountLines(ctx.meta && ctx.meta.account);
344
+ if (typeAccountLines.length > 0) {
345
+ lines.push('');
346
+ lines.push(...typeAccountLines);
347
+ }
348
+
179
349
  if (expandable.length > 0) {
180
350
  lines.push(`\n${expandHint}`);
181
351
  }
@@ -195,46 +365,43 @@ function formatContext(ctx, options = {}) {
195
365
  if (ctx.meta) {
196
366
  const notes = [];
197
367
  if (ctx.meta.dynamicImports) { const dn = dynamicImportsNote(ctx.meta.dynamicImports, ctx.meta); if (dn) notes.push(dn); }
198
- if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
199
368
  if (ctx.meta.confidenceFiltered) notes.push(`${ctx.meta.confidenceFiltered} edge(s) below confidence threshold hidden`);
200
369
  if (notes.length) {
201
- const uncertainSuffix = ctx.meta.uncertain && options.uncertainHint ? ` ${options.uncertainHint}` : '';
202
- lines.push(` Note: ${notes.join(', ')}${uncertainSuffix}`);
370
+ lines.push(` Note: ${notes.join(', ')}`);
203
371
  }
204
372
  }
205
373
 
206
- if (ctx.meta && ctx.meta.includeMethods === false) {
207
- lines.push(` ${methodsHint}`);
208
- }
209
-
210
374
  if (ctx.warnings && ctx.warnings.length > 0) {
211
375
  for (const w of ctx.warnings) {
212
376
  lines.push(` Note: ${w.message}`);
213
377
  }
214
378
  }
215
379
 
216
- const showConf = options.showConfidence || false;
380
+ // Reachability markers are suppressed when the project has no detected
381
+ // entry points (library code) — "unreachable" would be meaningless noise.
382
+ const hasEntrypoints = !ctx.meta || ctx.meta.hasEntrypoints !== false;
383
+
217
384
  const callers = ctx.callers || [];
218
- lines.push(`${compact ? '' : '\n'}CALLERS (${callers.length}):`);
219
- const callerHistLine = showConf ? formatHistogramLine(ctx.callerHistogram) : null;
220
- if (callerHistLine) lines.push(callerHistLine);
221
- const showCallerReach = shouldShowReachability(callers);
222
- for (const c of callers) {
385
+ const prodCallers = callers.filter(c => !isTestEntry(c));
386
+ const testCallers = callers.filter(c => isTestEntry(c));
387
+ const tierHeader = testCallers.length > 0
388
+ ? `CALLERS CONFIRMED (${callers.length}, ${prodCallers.length} prod + ${testCallers.length} test):`
389
+ : `CALLERS CONFIRMED (${callers.length}):`;
390
+ lines.push(`${compact ? '' : '\n'}${tierHeader}`);
391
+ const callerEvidence = formatEvidenceLine(callers);
392
+ if (callerEvidence && !compact) lines.push(callerEvidence);
393
+ const callerReach = reachabilityDisplay(callers, hasEntrypoints, 'caller');
394
+ const renderCaller = (c) => {
223
395
  const callerName = c.callerName ? ` [${c.callerName}]` : '';
396
+ const unreachableMark = (callerReach.perLine && c.reachable === false) ? ' [unreachable]' : '';
224
397
  if (compact) {
225
398
  // One line per caller: "[N] file:line [callerName]: expression"
226
399
  const expr = c.content ? c.content.trim().replace(/\s+/g, ' ').slice(0, 100) : '';
227
- lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}: ${expr}`);
400
+ lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}${unreachableMark}: ${expr}`);
228
401
  } else {
229
- lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
402
+ lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}${unreachableMark}`);
230
403
  lines.push(` ${c.content.trim()}`);
231
404
  }
232
- if (showConf && c.confidence != null && !compact) {
233
- lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
234
- }
235
- if (showCallerReach && c.reachable === false && !compact) {
236
- lines.push(' (unreachable from any entry point)');
237
- }
238
405
  expandable.push({
239
406
  num: itemNum++,
240
407
  type: 'caller',
@@ -245,7 +412,13 @@ function formatContext(ctx, options = {}) {
245
412
  startLine: c.callerStartLine || c.line,
246
413
  endLine: c.callerEndLine || c.line
247
414
  });
415
+ };
416
+ for (const c of prodCallers) renderCaller(c);
417
+ if (testCallers.length > 0) {
418
+ if (!compact) lines.push(' test callers:');
419
+ for (const c of testCallers) renderCaller(c);
248
420
  }
421
+ if (callerReach.note && !compact) lines.push(callerReach.note);
249
422
 
250
423
  // Structural hint: class methods may have callers through constructed/injected instances
251
424
  // that static analysis can't track. Only show when caller count is low (≤3) to avoid noise.
@@ -253,32 +426,58 @@ function formatContext(ctx, options = {}) {
253
426
  lines.push(` Note: ${ctx.function} is a class/struct method — additional callers through constructed or injected instances are not tracked by static analysis.`);
254
427
  }
255
428
 
429
+ // UNVERIFIED tier: call-syntax matches without binding/receiver evidence.
430
+ // Always visible (the contract: never silently hide an occurrence), capped
431
+ // at 10 one-liners unless --all.
432
+ const unverified = ctx.unverifiedCallers || [];
433
+ if (unverified.length > 0) {
434
+ lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${unverified.length}) — call syntax, no binding/receiver evidence:`);
435
+ const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
436
+ let shown = 0;
437
+ for (const u of unverified) {
438
+ if (shown >= cap) break;
439
+ const callerName = u.callerName ? ` [${u.callerName}]` : '';
440
+ const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
441
+ const expr = u.content ? `: ${u.content.trim().replace(/\s+/g, ' ').slice(0, 100)}` : '';
442
+ lines.push(` [${itemNum}] ${u.relativePath}:${u.line}${callerName}${expr}${reason}`);
443
+ expandable.push({
444
+ num: itemNum++,
445
+ type: 'caller',
446
+ name: u.callerName || '(module level)',
447
+ file: u.callerFile || u.file,
448
+ relativePath: u.relativePath,
449
+ line: u.line,
450
+ startLine: u.callerStartLine || u.line,
451
+ endLine: u.callerEndLine || u.line
452
+ });
453
+ shown++;
454
+ }
455
+ if (unverified.length > shown) {
456
+ lines.push(` (+${unverified.length - shown} more unverified — use --all)`);
457
+ }
458
+ }
459
+
256
460
  const callees = ctx.callees || [];
257
461
  lines.push(`${compact ? '' : '\n'}CALLEES (${callees.length}):`);
258
- const calleeHistLine = showConf ? formatHistogramLine(ctx.calleeHistogram) : null;
259
- if (calleeHistLine && !compact) lines.push(calleeHistLine);
260
- const showCalleeReach = shouldShowReachability(callees);
462
+ const calleeEvidence = formatEvidenceLine(callees);
463
+ if (calleeEvidence && !compact) lines.push(calleeEvidence);
464
+ const calleeReach = reachabilityDisplay(callees, hasEntrypoints, 'callee');
261
465
  for (const c of callees) {
262
466
  const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
263
467
  const returnSuffix = c.returnType ? ` → ${c.returnType}` : '';
264
468
  const sideEffects = (c.sideEffects && c.sideEffects.length) ? ` {${c.sideEffects.join(',')}}` : '';
469
+ const unreachableMark = (calleeReach.perLine && c.reachable === false) ? ' [unreachable]' : '';
265
470
  if (compact) {
266
471
  const snip = c.docstring ? calleeDocstringSnippet(c.docstring) : '';
267
472
  const docPart = snip ? `: ${snip}` : '';
268
- lines.push(` [${itemNum}] ${c.name}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}${docPart}`);
473
+ lines.push(` [${itemNum}] ${c.name}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}${docPart}${unreachableMark}`);
269
474
  } else {
270
- lines.push(` [${itemNum}] ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}`);
475
+ lines.push(` [${itemNum}] ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}${unreachableMark}`);
271
476
  if (c.docstring) {
272
477
  const snip = calleeDocstringSnippet(c.docstring);
273
478
  if (snip) lines.push(` "${snip}"`);
274
479
  }
275
480
  }
276
- if (showConf && c.confidence != null && !compact) {
277
- lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
278
- }
279
- if (showCalleeReach && c.reachable === false && !compact) {
280
- lines.push(' (unreachable from any entry point)');
281
- }
282
481
  expandable.push({
283
482
  num: itemNum++,
284
483
  type: 'callee',
@@ -289,6 +488,19 @@ function formatContext(ctx, options = {}) {
289
488
  endLine: c.endLine
290
489
  });
291
490
  }
491
+ if (calleeReach.note && !compact) lines.push(calleeReach.note);
492
+
493
+ // Conservation contract lines: non-call summary + ACCOUNT/WARNING/FILTERED
494
+ const account = ctx.meta && ctx.meta.account;
495
+ if (account) {
496
+ const nonCallLine = formatNonCallLine(account, ctx.function);
497
+ if (nonCallLine) lines.push(`${compact ? '' : '\n'}${nonCallLine}`);
498
+ const accountLines = formatAccountLines(account);
499
+ if (accountLines.length > 0) {
500
+ if (!compact && !nonCallLine) lines.push('');
501
+ lines.push(...accountLines);
502
+ }
503
+ }
292
504
 
293
505
  if (expandable.length > 0) {
294
506
  lines.push(`\n${expandHint}`);
@@ -313,11 +525,13 @@ function formatImpact(impact, options = {}) {
313
525
  if (!compact) lines.push(impact.signature);
314
526
  if (!compact) lines.push('');
315
527
 
316
- // Summary
528
+ // Summary (confirmed + unverified tiers reported separately)
529
+ const impactUnverified = impact.unverifiedSites || [];
530
+ const unverifiedSuffix = impactUnverified.length > 0 ? ` confirmed + ${impactUnverified.length} unverified` : '';
317
531
  if (impact.shownCallSites !== undefined && impact.shownCallSites < impact.totalCallSites) {
318
- lines.push(`CALL SITES: ${impact.shownCallSites} shown of ${impact.totalCallSites} total`);
532
+ lines.push(`CALL SITES: ${impact.shownCallSites} shown of ${impact.totalCallSites}${unverifiedSuffix ? ` total${unverifiedSuffix}` : ' total'}`);
319
533
  } else {
320
- lines.push(`CALL SITES: ${impact.totalCallSites}`);
534
+ lines.push(`CALL SITES: ${impact.totalCallSites}${unverifiedSuffix}`);
321
535
  }
322
536
  lines.push(` Files affected: ${impact.byFile.length}`);
323
537
 
@@ -347,17 +561,18 @@ function formatImpact(impact, options = {}) {
347
561
  lines.push(` Note: ${impact.scopeWarning.hint}`);
348
562
  }
349
563
 
350
- // By file
564
+ // By file (confirmed tier)
351
565
  if (!compact) lines.push('');
352
566
  lines.push('BY FILE:');
353
567
 
354
- // Histogram (over the trust signals collected before truncation)
355
- const impactHistLine = formatHistogramLine(impact.callerHistogram);
356
- if (impactHistLine) lines.push(impactHistLine);
357
-
358
- // Compute reachability marker visibility across ALL sites (not per-file)
568
+ // Evidence aggregate over ALL sites (replaces per-edge confidence lines)
359
569
  const allSites = impact.byFile.flatMap(g => g.sites);
360
- const showImpactReach = shouldShowReachability(allSites);
570
+ const impactEvidence = formatEvidenceLine(allSites);
571
+ if (impactEvidence && !compact) lines.push(impactEvidence);
572
+
573
+ // Reachability policy across ALL sites (not per-file); suppressed entirely
574
+ // when the project has no detected entry points.
575
+ const impactReach = reachabilityDisplay(allSites, impact.hasEntrypoints, 'call site');
361
576
 
362
577
  for (const fileGroup of impact.byFile) {
363
578
  if (compact) {
@@ -365,24 +580,45 @@ function formatImpact(impact, options = {}) {
365
580
  for (const site of fileGroup.sites) {
366
581
  const caller = site.callerName ? ` [${site.callerName}]` : '';
367
582
  const expr = site.expression ? site.expression.replace(/\s+/g, ' ').slice(0, 100) : '';
368
- const reach = (showImpactReach && site.reachable === false) ? ' (unreachable)' : '';
583
+ const reach = (impactReach.perLine && site.reachable === false) ? ' [unreachable]' : '';
369
584
  lines.push(` ${fileGroup.file}:${site.line}${caller}${reach}: ${expr}`);
370
585
  }
371
586
  } else {
372
587
  lines.push(`\n${fileGroup.file} (${fileGroup.count} calls)`);
373
588
  for (const site of fileGroup.sites) {
374
589
  const caller = site.callerName ? `[${site.callerName}]` : '';
375
- lines.push(` :${site.line} ${caller}`);
590
+ const reach = (impactReach.perLine && site.reachable === false) ? ' [unreachable]' : '';
591
+ lines.push(` :${site.line} ${caller}${reach}`);
376
592
  lines.push(` ${site.expression}`);
377
593
  if (site.args && site.args.length > 0) {
378
594
  lines.push(` args: ${site.args.join(', ')}`);
379
595
  }
380
- if (showImpactReach && site.reachable === false) {
381
- lines.push(' (unreachable from any entry point)');
382
- }
383
596
  }
384
597
  }
385
598
  }
599
+ if (impactReach.note && !compact) lines.push(impactReach.note);
600
+
601
+ // Unverified tier: visible, capped at 10 one-liners
602
+ if (impactUnverified.length > 0) {
603
+ lines.push(`${compact ? '' : '\n'}UNVERIFIED CALL SITES (${impactUnverified.length}) — call syntax, no binding/receiver evidence:`);
604
+ const cap = 10;
605
+ for (const site of impactUnverified.slice(0, cap)) {
606
+ const caller = site.callerName ? ` [${site.callerName}]` : '';
607
+ const reason = site.reason ? ` (${unverifiedReasonLabel(site)})` : '';
608
+ const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
609
+ lines.push(` ${site.file}:${site.line}${caller}${expr}${reason}`);
610
+ }
611
+ if (impactUnverified.length > cap) {
612
+ lines.push(` (+${impactUnverified.length - cap} more unverified)`);
613
+ }
614
+ }
615
+
616
+ // Conservation contract lines
617
+ const impactAccountLines = formatAccountLines(impact.account);
618
+ if (impactAccountLines.length > 0) {
619
+ if (!compact) lines.push('');
620
+ lines.push(...impactAccountLines);
621
+ }
386
622
 
387
623
  return lines.join('\n');
388
624
  }
@@ -460,35 +696,57 @@ function formatAbout(about, options = {}) {
460
696
  lines.push(` Note: ${about.confidenceFiltered} edge(s) below confidence threshold hidden`);
461
697
  }
462
698
 
463
- // Usage summary
699
+ // Usage summary (fast-path approximation; ACCOUNT below is the exact
700
+ // text-ground truth — both are labeled to avoid confusion)
464
701
  lines.push('');
465
702
  lines.push(`USAGES: ${about.totalUsages} total`);
466
703
  lines.push(` ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} references`);
467
704
 
468
- // Callers
469
- const showConf = options.showConfidence || false;
705
+ // Callers — CONFIRMED tier, prod before test
706
+ const hasEntrypoints = about.hasEntrypoints !== false;
470
707
  let aboutTruncated = false;
471
708
  if (about.callers.total > 0) {
472
709
  lines.push('');
473
- if (about.callers.total > about.callers.top.length) {
474
- lines.push(`CALLERS (showing ${about.callers.top.length} of ${about.callers.total}):`);
710
+ const top = about.callers.top;
711
+ const prodTop = top.filter(c => !isTestEntry(c));
712
+ const testTop = top.filter(c => isTestEntry(c));
713
+ const split = testTop.length > 0 ? `, ${prodTop.length} prod + ${testTop.length} test shown` : '';
714
+ if (about.callers.total > top.length) {
715
+ lines.push(`CALLERS — CONFIRMED (showing ${top.length} of ${about.callers.total}${split}):`);
475
716
  aboutTruncated = true;
476
717
  } else {
477
- lines.push(`CALLERS (${about.callers.total}):`);
718
+ lines.push(`CALLERS — CONFIRMED (${about.callers.total}${testTop.length > 0 ? `, ${prodTop.length} prod + ${testTop.length} test` : ''}):`);
478
719
  }
479
- const aboutCallerHist = showConf ? formatHistogramLine(about.callers.histogram) : null;
480
- if (aboutCallerHist) lines.push(aboutCallerHist);
481
- const showAboutCallerReach = shouldShowReachability(about.callers.top);
482
- for (const c of about.callers.top) {
720
+ const callerEvidence = formatEvidenceLine(top);
721
+ if (callerEvidence) lines.push(callerEvidence);
722
+ const aboutCallerReach = reachabilityDisplay(top, hasEntrypoints, 'caller');
723
+ const renderAboutCaller = (c) => {
483
724
  const caller = c.callerName ? `[${c.callerName}]` : '';
484
- lines.push(` ${c.file}:${c.line} ${caller}`);
725
+ const unreachableMark = (aboutCallerReach.perLine && c.reachable === false) ? ' [unreachable]' : '';
726
+ lines.push(` ${c.file}:${c.line} ${caller}${unreachableMark}`);
485
727
  lines.push(` ${c.expression}`);
486
- if (showConf && c.confidence != null) {
487
- lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
488
- }
489
- if (showAboutCallerReach && c.reachable === false) {
490
- lines.push(' (unreachable from any entry point)');
491
- }
728
+ };
729
+ for (const c of prodTop) renderAboutCaller(c);
730
+ if (testTop.length > 0) {
731
+ lines.push(' test callers:');
732
+ for (const c of testTop) renderAboutCaller(c);
733
+ }
734
+ if (aboutCallerReach.note) lines.push(aboutCallerReach.note);
735
+ }
736
+
737
+ // Callers — UNVERIFIED tier (always visible; the contract forbids hiding)
738
+ const aboutUnverified = about.callers.unverified;
739
+ if (aboutUnverified && aboutUnverified.total > 0) {
740
+ lines.push('');
741
+ lines.push(`CALLERS — UNVERIFIED (${aboutUnverified.total}) — call syntax, no binding/receiver evidence:`);
742
+ for (const u of aboutUnverified.top) {
743
+ const caller = u.callerName ? ` [${u.callerName}]` : '';
744
+ const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
745
+ const expr = u.expression ? `: ${u.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
746
+ lines.push(` ${u.file}:${u.line}${caller}${expr}${reason}`);
747
+ }
748
+ if (aboutUnverified.total > aboutUnverified.top.length) {
749
+ lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified — use --all)`);
492
750
  }
493
751
  }
494
752
 
@@ -501,24 +759,19 @@ function formatAbout(about, options = {}) {
501
759
  } else {
502
760
  lines.push(`CALLEES (${about.callees.total}):`);
503
761
  }
504
- const aboutCalleeHist = showConf ? formatHistogramLine(about.callees.histogram) : null;
505
- if (aboutCalleeHist) lines.push(aboutCalleeHist);
506
- const showAboutCalleeReach = shouldShowReachability(about.callees.top);
762
+ const calleeEvidence = formatEvidenceLine(about.callees.top);
763
+ if (calleeEvidence) lines.push(calleeEvidence);
764
+ const aboutCalleeReach = reachabilityDisplay(about.callees.top, hasEntrypoints, 'callee');
507
765
  for (const c of about.callees.top) {
508
766
  const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
509
767
  const returnSuffix = c.returnType ? ` → ${c.returnType}` : '';
510
768
  const sideEffects = (c.sideEffects && c.sideEffects.length) ? ` {${c.sideEffects.join(',')}}` : '';
511
- lines.push(` ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.file}:${c.line} (${c.callCount}x)`);
769
+ const unreachableMark = (aboutCalleeReach.perLine && c.reachable === false) ? ' [unreachable]' : '';
770
+ lines.push(` ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.file}:${c.line} (${c.callCount}x)${unreachableMark}`);
512
771
  if (c.docstring) {
513
772
  const snip = calleeDocstringSnippet(c.docstring);
514
773
  if (snip) lines.push(` "${snip}"`);
515
774
  }
516
- if (showConf && c.confidence != null) {
517
- lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
518
- }
519
- if (showAboutCalleeReach && c.reachable === false) {
520
- lines.push(' (unreachable from any entry point)');
521
- }
522
775
 
523
776
  // Inline expansion: show first 3 lines of callee code
524
777
  if (expand && root && c.file && c.startLine) {
@@ -540,6 +793,17 @@ function formatAbout(about, options = {}) {
540
793
  }
541
794
  }
542
795
  }
796
+ if (aboutCalleeReach.note) lines.push(aboutCalleeReach.note);
797
+ }
798
+
799
+ // Conservation contract: exact text-ground reconciliation (ACCOUNT) plus
800
+ // unparsed-file warnings and display-filter notes.
801
+ if (about.account) {
802
+ const accountLines = formatAccountLines(about.account);
803
+ if (accountLines.length > 0) {
804
+ lines.push('');
805
+ lines.push(...accountLines);
806
+ }
543
807
  }
544
808
 
545
809
  // Tests
@@ -597,11 +861,6 @@ function formatAbout(about, options = {}) {
597
861
  lines.push(`\nSome sections truncated. ${allHint}`);
598
862
  }
599
863
 
600
- if (about.includeMethods === false) {
601
- const methodsHint = options.methodsHint || 'Note: obj.method() callers/callees excluded — use --include-methods to include them';
602
- lines.push(`\n${methodsHint}`);
603
- }
604
-
605
864
  return lines.join('\n');
606
865
  }
607
866
 
@@ -620,4 +879,7 @@ module.exports = {
620
879
  formatImpactJson,
621
880
  formatAbout,
622
881
  formatAboutJson,
882
+ // Shared with output/tracing.js: the tree commands render the same
883
+ // root-hop ACCOUNT/WARNING/FILTERED lines as context/impact.
884
+ formatAccountLines,
623
885
  };