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.
- package/.claude/skills/ucn/SKILL.md +31 -17
- package/README.md +95 -28
- package/cli/index.js +32 -7
- package/core/account.js +354 -0
- package/core/analysis.js +335 -15
- package/core/build-worker.js +21 -1
- package/core/cache.js +52 -3
- package/core/callers.js +3421 -159
- package/core/confidence.js +82 -19
- package/core/deadcode.js +211 -21
- package/core/execute.js +6 -1
- package/core/graph-build.js +45 -3
- package/core/imports.js +118 -1
- package/core/output/analysis.js +345 -83
- package/core/output/reporting.js +19 -3
- 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/shared.js +21 -0
- 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 +5 -4
- package/package.json +9 -3
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -79
package/core/output/reporting.js
CHANGED
|
@@ -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
|
-
|
|
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
|
},
|
package/core/output/shared.js
CHANGED
|
@@ -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,
|
package/core/output/tracing.js
CHANGED
|
@@ -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 +
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
113
|
-
|
|
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
|
};
|