ucn 3.8.26 → 4.0.1

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.
@@ -194,7 +194,7 @@ function formatStatsJson(stats) {
194
194
  * @param {string} [options.exportedHint] - Hint about exported symbols exclusion
195
195
  */
196
196
  function formatDeadcode(results, options = {}) {
197
- if (results.length === 0 && !results.excludedDecorated && !results.excludedExported) {
197
+ if (results.length === 0 && !results.excludedDecorated && !results.excludedExported && !results.excludedExternalContract) {
198
198
  return 'No dead code found.';
199
199
  }
200
200
 
@@ -227,7 +227,16 @@ function formatDeadcode(results, options = {}) {
227
227
  hints.push(...item.annotations.map(a => `@${a}`));
228
228
  }
229
229
  const hintStr = hints.length > 0 ? ` [has ${hints.join(', ')}]` : '';
230
- lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}`);
230
+ // Interface/trait member declarations: unreferenced is true, but
231
+ // deleting one changes the contract, not dead logic — say so.
232
+ const declStr = item.declaredOn
233
+ ? ` [declared on ${item.declaredOn.kind} ${item.declaredOn.name} — contract surface, not executable code]`
234
+ : '';
235
+ // Revealed under --include-exported: mark as external-reachable, not dead.
236
+ const extStr = item.externalContract
237
+ ? ' [reachable via out-of-tree base — external contract, not dead]'
238
+ : '';
239
+ lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}${declStr}${extStr}`);
231
240
  }
232
241
 
233
242
  if (hidden > 0) {
@@ -246,6 +255,10 @@ function formatDeadcode(results, options = {}) {
246
255
  const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.`;
247
256
  lines.push(`\n${exportedHint}`);
248
257
  }
258
+ if (results.excludedExternalContract > 0) {
259
+ const extHint = options.externalContractHint || `${results.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.`;
260
+ lines.push(`\n${extHint}`);
261
+ }
249
262
 
250
263
  if (lines.length === 0) {
251
264
  return 'No dead code found.';
@@ -265,6 +278,7 @@ function formatDeadcodeJson(results) {
265
278
  count: results.length,
266
279
  ...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
267
280
  ...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
281
+ ...(results.excludedExternalContract > 0 && { excludedExternalContract: results.excludedExternalContract }),
268
282
  symbols: results.map(item => {
269
283
  const handleSym = { ...item, relativePath: item.relativePath || item.file };
270
284
  const handle = formatSymbolHandle(handleSym);
@@ -277,7 +291,9 @@ function formatDeadcodeJson(results) {
277
291
  ...(handle && { handle }),
278
292
  ...(item.isExported && { isExported: true }),
279
293
  ...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
280
- ...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations })
294
+ ...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations }),
295
+ ...(item.declaredOn && { declaredOn: item.declaredOn }),
296
+ ...(item.externalContract && { externalContract: true })
281
297
  };
282
298
  }),
283
299
  },
@@ -103,8 +103,8 @@ function formatFunctionSignature(fn) {
103
103
  else paramText = '...';
104
104
  sig += `(${paramText})`;
105
105
 
106
- // Return type
107
- if (fn.returnType) sig += `: ${fn.returnType}`;
106
+ // Return type (collapse whitespace — multi-line annotations must not break the one-line signature)
107
+ if (fn.returnType) sig += `: ${String(fn.returnType).replace(/\s+/g, ' ').trim()}`;
108
108
 
109
109
  // Arrow indicator
110
110
  if (fn.isArrow) sig += ' =>';
@@ -306,9 +306,40 @@ function formatGitLine(git) {
306
306
  return line;
307
307
  }
308
308
 
309
+ /**
310
+ * Display label for an unverified-tier entry's reason. Dispatch-tiered
311
+ * entries (nominal languages) carry attribution metadata: the declared
312
+ * supertype the call dispatches through (dispatchVia) and how many
313
+ * same-name definitions the dispatch could land on (dispatchCandidates).
314
+ */
315
+ function unverifiedReasonLabel(entry) {
316
+ if (!entry || !entry.reason) return '';
317
+ if (entry.reason === 'possible-dispatch' && entry.externalContract) {
318
+ // External contract (fix #210): the candidate set is open — any
319
+ // external subtype of the contract — so no implementation count.
320
+ return entry.dispatchVia
321
+ ? `possible-dispatch via ${entry.dispatchVia} — external contract`
322
+ : 'possible-dispatch — external contract';
323
+ }
324
+ if (entry.reason === 'possible-dispatch' && entry.dispatchVia) {
325
+ const n = entry.dispatchCandidates;
326
+ return n > 1
327
+ ? `possible-dispatch via ${entry.dispatchVia} — 1 of ${n} implementations`
328
+ : `possible-dispatch via ${entry.dispatchVia}`;
329
+ }
330
+ if (entry.reason === 'method-ambiguous' && entry.dispatchCandidates > 1) {
331
+ return `method-ambiguous — ${entry.dispatchCandidates} same-name definitions`;
332
+ }
333
+ if (entry.reason === 'overload-ambiguous' && entry.dispatchCandidates > 1) {
334
+ return `overload-ambiguous — 1 of ${entry.dispatchCandidates} applicable overloads`;
335
+ }
336
+ return entry.reason;
337
+ }
338
+
309
339
  module.exports = {
310
340
  dynamicImportsNote,
311
341
  formatFileError,
342
+ unverifiedReasonLabel,
312
343
  normalizeParams,
313
344
  lineNum,
314
345
  lineRange,
@@ -1,7 +1,90 @@
1
1
  /**
2
2
  * core/output/tracing.js - Trace/blast/reverse tree formatters
3
+ *
4
+ * Tree contract rendering: the trunk is confirmed-tier; unverified caller
5
+ * edges render in an UNVERIFIED EDGES section with parent attribution and a
6
+ * reason; unresolved callee calls render as [unverified] leaves under their
7
+ * node; ACCOUNT (root text-ground) and TREE ACCOUNT (interior candidate
8
+ * conservation) lines close the arithmetic.
3
9
  */
4
10
 
11
+ const { unverifiedReasonLabel } = require('./shared');
12
+ const { formatAccountLines } = require('./analysis');
13
+
14
+ /**
15
+ * Render the caller-direction unverified frontier. Returns true if anything
16
+ * was rendered.
17
+ */
18
+ function renderFrontier(lines, frontier, options = {}, expanded = false) {
19
+ if (!frontier || frontier.length === 0) return false;
20
+ lines.push('');
21
+ const suffix = expanded
22
+ ? 'followed (--expand-unverified); downstream nodes are possible impact'
23
+ : 'not expanded; possible additional impact';
24
+ lines.push(`UNVERIFIED EDGES (${frontier.length}) — call syntax, no binding/receiver evidence; ${suffix}:`);
25
+ const cap = options.all ? Infinity : 20;
26
+ let shown = 0;
27
+ for (const f of frontier) {
28
+ if (shown >= cap) break;
29
+ const callerName = f.callerName ? ` [${f.callerName}]` : '';
30
+ const expr = f.content ? `: ${f.content.trim().replace(/\s+/g, ' ').slice(0, 100)}` : '';
31
+ const reason = f.reason ? ` (${unverifiedReasonLabel(f)})` : '';
32
+ lines.push(` at ${f.atNode.name} (hop ${f.hop}): ${f.relativePath}:${f.line}${callerName}${expr}${reason}`);
33
+ shown++;
34
+ }
35
+ if (frontier.length > shown) {
36
+ lines.push(` (+${frontier.length - shown} more unverified — use --all)`);
37
+ }
38
+ return true;
39
+ }
40
+
41
+ /** Render a node's unresolved callee calls as [unverified] leaves. */
42
+ function renderUnverifiedCallees(lines, node, prefix, isParentLast) {
43
+ if (!node.unverifiedCallees || node.unverifiedCallees.length === 0) return;
44
+ const extension = isParentLast ? ' ' : '│ ';
45
+ for (let i = 0; i < node.unverifiedCallees.length; i++) {
46
+ const u = node.unverifiedCallees[i];
47
+ const isLast = i === node.unverifiedCallees.length - 1;
48
+ const connector = isLast ? '└── ' : '├── ';
49
+ const owners = u.ownerCount > 1 ? ` (${u.ownerCount} owners)` : '';
50
+ const linesPart = u.sites && u.sites.length > 0 ? ` L${u.sites.join(',L')}` : '';
51
+ lines.push(`${prefix}${extension}${connector}[unverified] ${u.name} — ${u.reason}${owners}${linesPart}`);
52
+ }
53
+ }
54
+
55
+ /** TREE ACCOUNT line for caller-direction trees. */
56
+ function treeAccountLine(ta) {
57
+ if (!ta) return null;
58
+ const reasons = Object.entries(ta.unverifiedByReason || {})
59
+ .sort((a, b) => a[0].localeCompare(b[0]))
60
+ .map(([r, n]) => `${n} ${r}`).join(', ');
61
+ const excludedReasons = Object.entries(ta.excludedByReason || {})
62
+ .sort((a, b) => a[0].localeCompare(b[0]))
63
+ .map(([r, n]) => `${n} ${r}`).join(', ');
64
+ let line = `TREE ACCOUNT: ${ta.nodesExpanded} node${ta.nodesExpanded === 1 ? '' : 's'} expanded · ` +
65
+ `${ta.confirmedEdges} confirmed edge${ta.confirmedEdges === 1 ? '' : 's'} · ` +
66
+ `${ta.unverifiedEdges} unverified${reasons ? ` (${reasons})` : ''} · ` +
67
+ `${ta.excludedTotal} excluded${excludedReasons ? ` (${excludedReasons})` : ''}`;
68
+ if (ta.filteredEdges > 0) line += ` · ${ta.filteredEdges} hidden by --exclude`;
69
+ if (ta.depthLimitNodes > 0) line += ` · ${ta.depthLimitNodes} node${ta.depthLimitNodes === 1 ? '' : 's'} at depth limit (callers not searched)`;
70
+ return line;
71
+ }
72
+
73
+ /** CALLEE ACCOUNT rollup line for down-direction trees. */
74
+ function calleeAccountLine(ta) {
75
+ if (!ta || !ta.callSites) return null;
76
+ const cs = ta.callSites;
77
+ const reasons = Object.entries(ta.unverifiedByReason || {})
78
+ .sort((a, b) => a[0].localeCompare(b[0]))
79
+ .map(([r, n]) => `${n} ${r}`).join(', ');
80
+ let line = `CALLEE ACCOUNT: ${ta.nodesExpanded} node${ta.nodesExpanded === 1 ? '' : 's'} expanded · ` +
81
+ `${cs.total} call site${cs.total === 1 ? '' : 's'} = ${cs.confirmed} confirmed + ` +
82
+ `${cs.unverified} unverified${reasons ? ` (${reasons})` : ''} + ` +
83
+ `${cs.external} external/builtin + ${cs.excluded} excluded`;
84
+ if (cs.filtered > 0) line += ` + ${cs.filtered} filtered`;
85
+ return line;
86
+ }
87
+
5
88
  /**
6
89
  * Format trace command output - text
7
90
  * Shows call tree visualization
@@ -53,14 +136,16 @@ function formatTrace(trace, options = {}) {
53
136
 
54
137
  if (node.children && !node.alreadyShown) {
55
138
  const hasMore = node.truncatedChildren > 0;
139
+ const hasUnverified = node.unverifiedCallees && node.unverifiedCallees.length > 0;
56
140
  for (let i = 0; i < node.children.length; i++) {
57
- const isChildLast = !hasMore && i === node.children.length - 1;
141
+ const isChildLast = !hasMore && !hasUnverified && i === node.children.length - 1;
58
142
  renderNode(node.children[i], prefix + extension, isChildLast);
59
143
  }
60
144
  if (hasMore) {
61
145
  hasTruncation = true;
62
- lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callees`);
146
+ lines.push(prefix + extension + (hasUnverified ? '├── ' : '└── ') + `... and ${node.truncatedChildren} more callees`);
63
147
  }
148
+ renderUnverifiedCallees(lines, node, prefix, isLast);
64
149
  }
65
150
  };
66
151
 
@@ -68,13 +153,25 @@ function formatTrace(trace, options = {}) {
68
153
  lines.push(trace.root);
69
154
  if (trace.tree && trace.tree.children) {
70
155
  const rootHasMore = trace.tree.truncatedChildren > 0;
156
+ const rootHasUnverified = trace.tree.unverifiedCallees && trace.tree.unverifiedCallees.length > 0;
71
157
  for (let i = 0; i < trace.tree.children.length; i++) {
72
- const isLast = !rootHasMore && i === trace.tree.children.length - 1;
158
+ const isLast = !rootHasMore && !rootHasUnverified && i === trace.tree.children.length - 1;
73
159
  renderNode(trace.tree.children[i], '', isLast);
74
160
  }
75
161
  if (rootHasMore) {
76
162
  hasTruncation = true;
77
- lines.push(`└── ... and ${trace.tree.truncatedChildren} more callees`);
163
+ lines.push((rootHasUnverified ? '├── ' : '└── ') + `... and ${trace.tree.truncatedChildren} more callees`);
164
+ }
165
+ if (rootHasUnverified) {
166
+ // Root-level unverified callees: render with no prefix
167
+ for (let i = 0; i < trace.tree.unverifiedCallees.length; i++) {
168
+ const u = trace.tree.unverifiedCallees[i];
169
+ const isLast = i === trace.tree.unverifiedCallees.length - 1;
170
+ const connector = isLast ? '└── ' : '├── ';
171
+ const owners = u.ownerCount > 1 ? ` (${u.ownerCount} owners)` : '';
172
+ const linesPart = u.sites && u.sites.length > 0 ? ` L${u.sites.join(',L')}` : '';
173
+ lines.push(`${connector}[unverified] ${u.name} — ${u.reason}${owners}${linesPart}`);
174
+ }
78
175
  }
79
176
  }
80
177
 
@@ -92,6 +189,19 @@ function formatTrace(trace, options = {}) {
92
189
  }
93
190
  }
94
191
 
192
+ // Caller-direction unverified frontier (up/both)
193
+ renderFrontier(lines, trace.unverifiedFrontier, options);
194
+
195
+ // Conservation lines: callee rollup (down/both), root account (up/both)
196
+ const accountParts = [];
197
+ const downLine = calleeAccountLine(trace.treeAccount);
198
+ if (downLine) accountParts.push(downLine);
199
+ accountParts.push(...formatAccountLines(trace.account));
200
+ if (accountParts.length > 0) {
201
+ lines.push('');
202
+ lines.push(...accountParts);
203
+ }
204
+
95
205
  if (hasTruncation) {
96
206
  const allHint = options.allHint || 'Use --all to show all.';
97
207
  lines.push(`\nSome results truncated. ${allHint}`);
@@ -153,6 +263,11 @@ function formatBlast(blast, options = {}) {
153
263
  if (node.callSites && node.callSites > 1) {
154
264
  label += ` ${node.callSites}x`;
155
265
  }
266
+ if (node.viaUnverified) {
267
+ label += ` [⚠ via ${node.viaUnverified}]`;
268
+ } else if (node.chainUnverified) {
269
+ label += ' [⚠ unverified chain]';
270
+ }
156
271
  if (node.alreadyShown) {
157
272
  label += ' (see above)';
158
273
  }
@@ -186,17 +301,39 @@ function formatBlast(blast, options = {}) {
186
301
  }
187
302
  }
188
303
 
304
+ // Unverified frontier
305
+ renderFrontier(lines, blast.unverifiedFrontier, options, !!blast.expandUnverified);
306
+
189
307
  // Summary
190
308
  if (blast.summary) {
191
309
  lines.push('');
192
- const { totalAffected, totalFiles } = blast.summary;
310
+ const { totalAffected, totalFiles, unverifiedEdges, possiblyAffected } = blast.summary;
193
311
  if (totalAffected > 0) {
194
- lines.push(`Summary: 1 function changed → ${totalAffected} function${totalAffected !== 1 ? 's' : ''} affected across ${totalFiles} file${totalFiles !== 1 ? 's' : ''}`);
312
+ let s = `Summary: 1 function changed → ${totalAffected} function${totalAffected !== 1 ? 's' : ''} affected across ${totalFiles} file${totalFiles !== 1 ? 's' : ''}`;
313
+ if (unverifiedEdges > 0) {
314
+ s += blast.expandUnverified
315
+ ? ` · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} followed (${possiblyAffected || 0} possibly affected)`
316
+ : ` · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} (--expand-unverified to follow them)`;
317
+ }
318
+ lines.push(s);
319
+ } else if (unverifiedEdges > 0) {
320
+ lines.push(blast.expandUnverified
321
+ ? `Summary: no confirmed callers · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} followed (${possiblyAffected || 0} possibly affected)`
322
+ : `Summary: no confirmed callers · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} (--expand-unverified to follow them)`);
195
323
  } else {
196
324
  lines.push('Summary: No callers found — this function is a root/entry point.');
197
325
  }
198
326
  }
199
327
 
328
+ // Conservation lines
329
+ const taLine = treeAccountLine(blast.treeAccount);
330
+ const accountLines = formatAccountLines(blast.account);
331
+ if (taLine || accountLines.length > 0) {
332
+ lines.push('');
333
+ if (accountLines.length > 0) lines.push(...accountLines);
334
+ if (taLine) lines.push(taLine);
335
+ }
336
+
200
337
  if (hasTruncation) {
201
338
  const allHint = options.allHint || 'Use --all to show all.';
202
339
  lines.push(`\nSome results truncated. ${allHint}`);
@@ -257,8 +394,15 @@ function formatReverseTrace(result, options = {}) {
257
394
  if (node.callSites && node.callSites > 1) {
258
395
  label += ` ${node.callSites}x`;
259
396
  }
397
+ if (node.viaUnverified) {
398
+ label += ` [⚠ via ${node.viaUnverified}]`;
399
+ } else if (node.chainUnverified) {
400
+ label += ' [⚠ unverified chain]';
401
+ }
260
402
  if (node.entryPoint) {
261
403
  label += ' ★ entry point';
404
+ } else if (node.unverifiedCallerCount > 0 && (!node.children || node.children.length === 0)) {
405
+ label += ` ⚠ no confirmed callers — ${node.unverifiedCallerCount} unverified`;
262
406
  }
263
407
  if (node.alreadyShown) {
264
408
  label += ' (see above)';
@@ -283,6 +427,8 @@ function formatReverseTrace(result, options = {}) {
283
427
  let rootLabel = result.root;
284
428
  if (result.tree && result.tree.entryPoint) {
285
429
  rootLabel += ' ★ entry point (no callers)';
430
+ } else if (result.tree && result.tree.unverifiedCallerCount > 0 && result.tree.children.length === 0) {
431
+ rootLabel += ` ⚠ no confirmed callers — ${result.tree.unverifiedCallerCount} unverified`;
286
432
  }
287
433
  lines.push(rootLabel);
288
434
  if (result.tree && result.tree.children) {
@@ -297,6 +443,9 @@ function formatReverseTrace(result, options = {}) {
297
443
  }
298
444
  }
299
445
 
446
+ // Unverified frontier
447
+ renderFrontier(lines, result.unverifiedFrontier, options, !!result.expandUnverified);
448
+
300
449
  // Entry points summary
301
450
  if (result.entryPoints && result.entryPoints.length > 0) {
302
451
  lines.push('');
@@ -309,12 +458,28 @@ function formatReverseTrace(result, options = {}) {
309
458
  // Summary
310
459
  if (result.summary) {
311
460
  lines.push('');
312
- const { totalEntryPoints, totalFunctions } = result.summary;
461
+ const { totalEntryPoints, totalFunctions, unverifiedEdges } = result.summary;
462
+ let s;
313
463
  if (totalFunctions > 0) {
314
- lines.push(`Summary: ${totalEntryPoints} entry point${totalEntryPoints !== 1 ? 's' : ''} reach${totalEntryPoints === 1 ? 'es' : ''} ${result.root} through ${totalFunctions} intermediate function${totalFunctions !== 1 ? 's' : ''}`);
464
+ s = `Summary: ${totalEntryPoints} entry point${totalEntryPoints !== 1 ? 's' : ''} reach${totalEntryPoints === 1 ? 'es' : ''} ${result.root} through ${totalFunctions} intermediate function${totalFunctions !== 1 ? 's' : ''}`;
465
+ } else if (unverifiedEdges > 0) {
466
+ s = `Summary: no confirmed callers — ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} (not an entry-point claim)`;
315
467
  } else {
316
- lines.push('Summary: No callers found — this function is itself an entry point.');
468
+ s = 'Summary: No callers found — this function is itself an entry point.';
469
+ }
470
+ if (totalFunctions > 0 && unverifiedEdges > 0) {
471
+ s += ` · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''}`;
317
472
  }
473
+ lines.push(s);
474
+ }
475
+
476
+ // Conservation lines
477
+ const taLine = treeAccountLine(result.treeAccount);
478
+ const accountLines = formatAccountLines(result.account);
479
+ if (taLine || accountLines.length > 0) {
480
+ lines.push('');
481
+ if (accountLines.length > 0) lines.push(...accountLines);
482
+ if (taLine) lines.push(taLine);
318
483
  }
319
484
 
320
485
  if (hasTruncation) {
@@ -377,6 +542,28 @@ function formatAffectedTests(result, options = {}) {
377
542
  }
378
543
  }
379
544
 
545
+ // Possible band: functions reachable only through unverified chains.
546
+ if ((result.possiblyAffected && result.possiblyAffected.length > 0) ||
547
+ (result.possiblyAffectedTests && result.possiblyAffectedTests.length > 0)) {
548
+ lines.push('');
549
+ const pa = result.possiblyAffected || [];
550
+ lines.push(`POSSIBLY AFFECTED (${pa.length}) — reachable only through unverified call edges:`);
551
+ if (pa.length > 0) {
552
+ lines.push(` ${pa.join(', ')}`);
553
+ }
554
+ const pat = result.possiblyAffectedTests || [];
555
+ if (pat.length > 0) {
556
+ lines.push(` Additional test files (${pat.length}):`);
557
+ const MAX_POSSIBLE = options.all ? Infinity : 10;
558
+ for (const tf of pat.slice(0, MAX_POSSIBLE)) {
559
+ lines.push(` ${tf.file} (covers: ${tf.coveredFunctions.join(', ')})`);
560
+ }
561
+ if (pat.length > MAX_POSSIBLE) {
562
+ lines.push(` ... ${pat.length - MAX_POSSIBLE} more (use --all)`);
563
+ }
564
+ }
565
+ }
566
+
380
567
  if (result.uncovered.length > 0) {
381
568
  lines.push('');
382
569
  lines.push(`Uncovered (${result.uncovered.length}): ${result.uncovered.join(', ')}`);
@@ -387,7 +574,18 @@ function formatAffectedTests(result, options = {}) {
387
574
  const pct = summary.totalAffected > 0
388
575
  ? Math.round(summary.coveredFunctions / summary.totalAffected * 100)
389
576
  : 0;
390
- lines.push(`Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} test files, ${summary.coveredFunctions}/${summary.totalAffected} functions covered (${pct}%)`);
577
+ let summaryLine = `Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} test files, ${summary.coveredFunctions}/${summary.totalAffected} functions covered (${pct}%)`;
578
+ if (summary.possiblyAffected > 0) {
579
+ summaryLine += ` · ${summary.possiblyAffected} possibly affected (unverified chains)`;
580
+ }
581
+ lines.push(summaryLine);
582
+
583
+ // Conservation lines (root hop)
584
+ const accountLines = formatAccountLines(result.account);
585
+ if (accountLines.length > 0) {
586
+ lines.push('');
587
+ lines.push(...accountLines);
588
+ }
391
589
 
392
590
  if (result.warnings?.length > 0) {
393
591
  lines.push('');
package/core/project.js CHANGED
@@ -384,11 +384,23 @@ class ProjectIndex {
384
384
  size: stat.size,
385
385
  imports: imports.map(i => i.module),
386
386
  importNames: imports.flatMap(i => i.names || []),
387
+ // Paired name↔module bindings (fix #209): importNames flattens the
388
+ // pairing away, but name-level shadow detection needs to know WHICH
389
+ // module bound a name (`from urllib.parse import unquote` rebinds
390
+ // the bare name for the whole file).
391
+ importBindings: imports.flatMap(i => (i.names || [])
392
+ .filter(n => n && n !== '*' && n !== '_' && n !== '.')
393
+ .map(n => ({ name: n, module: i.module }))),
387
394
  exports: exports.map(e => e.name),
388
395
  exportDetails: exports,
389
396
  symbols: [],
390
397
  bindings: [],
391
398
  ...(importAliases && { importAliases }),
399
+ // Module-scope assignment targets (fix #217): names a module can
400
+ // expose WITHOUT a def/class/import binding (`render = impl`,
401
+ // `global name`). The import-binding name-chase treats these as
402
+ // undetermined — a dead-end verdict would be unsound.
403
+ ...(parsed.moduleAssignedNames && { moduleAssignedNames: parsed.moduleAssignedNames }),
392
404
  ...(isBundled && { isBundled: true }),
393
405
  ...(isGenerated && { isGenerated: true })
394
406
  };
@@ -422,6 +434,7 @@ class ProjectIndex {
422
434
  ...(item.className && { className: item.className }),
423
435
  ...(item.memberType && { memberType: item.memberType }),
424
436
  ...(item.fieldType && { fieldType: item.fieldType }),
437
+ ...(item.aliasOf && { aliasOf: item.aliasOf }),
425
438
  ...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
426
439
  // Decorator/annotation/attribute argument capture for endpoints command:
427
440
  // these fields hold the parsed first-string-arg of each route-style annotation.
@@ -432,6 +445,10 @@ class ProjectIndex {
432
445
  ...(item.attributesWithArgs && item.attributesWithArgs.length > 0 && { attributesWithArgs: item.attributesWithArgs }),
433
446
  ...(item.nameLine && { nameLine: item.nameLine }),
434
447
  ...(item.traitImpl && { traitImpl: true }),
448
+ // Trait the impl block implements (rust `impl Trait for X`
449
+ // members) — external-contract detection needs the NAME, not
450
+ // just the traitImpl flag (fix #210).
451
+ ...(item.traitName && { traitName: item.traitName }),
435
452
  ...(item.isSignature && { isSignature: true })
436
453
  };
437
454
  fileEntry.symbols.push(symbol);
@@ -463,7 +480,7 @@ class ProjectIndex {
463
480
  if (cls.members) {
464
481
  for (const m of cls.members) {
465
482
  const memberType = m.memberType || 'method';
466
- addSymbol({ ...m, className: cls.name, ...(cls.traitName && { traitImpl: true }) }, memberType);
483
+ addSymbol({ ...m, className: cls.name, ...(cls.traitName && { traitImpl: true, traitName: cls.traitName }) }, memberType);
467
484
  }
468
485
  }
469
486
  }
@@ -1397,7 +1414,7 @@ class ProjectIndex {
1397
1414
  parts.push(`(${typed != null ? typed : def.params})`);
1398
1415
  }
1399
1416
  if (def.returnType) {
1400
- parts.push(`: ${def.returnType}`);
1417
+ parts.push(`: ${String(def.returnType).replace(/\s+/g, ' ').trim()}`);
1401
1418
  }
1402
1419
  return parts.join(' ');
1403
1420
  }
package/core/registry.js CHANGED
@@ -95,6 +95,7 @@ const PARAM_MAP = {
95
95
  server_only: 'serverOnly',
96
96
  client_only: 'clientOnly',
97
97
  hide_uncertain: 'hideUncertain',
98
+ expand_unverified: 'expandUnverified',
98
99
  };
99
100
 
100
101
  // ============================================================================
@@ -107,10 +108,21 @@ const PARAM_MAP = {
107
108
  const FLAG_APPLICABILITY = {
108
109
  // Understanding code
109
110
  about: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git'],
110
- context: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact'],
111
+ // Note: includeMethods/includeUncertain are deprecated no-ops for
112
+ // about/context/impact since the tiered-output contract (unverified
113
+ // callers are always shown in their own section); kept in the matrix so
114
+ // legacy invocations don't warn as "inapplicable". `all` lifts the
115
+ // unverified display cap.
116
+ context: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'all'],
111
117
  impact: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'top', 'unreachableOnly', 'compact'],
112
- blast: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
113
- reverseTrace: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
118
+ // trace/blast/reverseTrace/affectedTests run the tiered tree contract:
119
+ // includeUncertain is an implied no-op (unverified edges are always
120
+ // visible — frontier/possible band); expandUnverified follows unverified
121
+ // CALLER edges, marking downstream nodes chainUnverified (blast/
122
+ // reverseTrace only — surface trace is down-direction, where unresolved
123
+ // callees have no definition to expand into).
124
+ blast: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence', 'expandUnverified'],
125
+ reverseTrace: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence', 'expandUnverified'],
114
126
  smart: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'withTypes', 'minConfidence'],
115
127
  trace: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
116
128
  example: ['name', 'file', 'className', 'diverse', 'top', 'includeTests'],
package/core/shared.js CHANGED
@@ -139,6 +139,26 @@ function looksLikeHandle(input) {
139
139
  return /^.+:\d+(?::.+)?$/.test(input);
140
140
  }
141
141
 
142
+ /**
143
+ * Explicit override marker on a method definition (fix #210). Marker fields are
144
+ * language-disjoint and compiler-checked syntax (never inferred): traitImpl is
145
+ * Rust's `impl Trait`, an 'override' modifier is Java's lowercased @Override, an
146
+ * override-bearing memberType is TS's `override` keyword, and an override
147
+ * decorator is Python's typing.@override. Shared by the external-contract
148
+ * reasoning in both the caller dispatch gate and deadcode (out-of-tree override
149
+ * suppression) — one source of truth so a new marker is added once, not in two
150
+ * drifting copies.
151
+ */
152
+ function isOverrideMarked(def) {
153
+ if (def.traitImpl) return true;
154
+ const mods = def.modifiers || [];
155
+ if (mods.includes('override')) return true;
156
+ if (def.memberType && /\boverride\b/.test(def.memberType)) return true;
157
+ if (def.decorators && def.decorators.some(d =>
158
+ String(d).replace(/\(.*$/, '').split('.').pop() === 'override')) return true;
159
+ return false;
160
+ }
161
+
142
162
  module.exports = {
143
163
  pickBestDefinition,
144
164
  addTestExclusions,
@@ -148,4 +168,5 @@ module.exports = {
148
168
  parseSymbolHandle,
149
169
  looksLikeHandle,
150
170
  isTestPath,
171
+ isOverrideMarked,
151
172
  };