ucn 3.8.25 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +44 -18
- package/README.md +95 -28
- package/cli/index.js +28 -5
- package/core/account.js +354 -0
- package/core/analysis.js +335 -15
- package/core/bridge.js +0 -16
- package/core/build-worker.js +21 -1
- package/core/cache.js +52 -3
- package/core/callers.js +3434 -158
- package/core/confidence.js +82 -19
- package/core/deadcode.js +114 -21
- package/core/execute.js +4 -0
- package/core/graph-build.js +44 -2
- package/core/imports.js +118 -1
- package/core/output/analysis.js +345 -83
- package/core/output/reporting.js +8 -2
- package/core/output/shared.js +33 -2
- package/core/output/tracing.js +208 -10
- package/core/project.js +19 -2
- package/core/registry.js +15 -3
- package/core/search.js +0 -42
- package/core/tracing.js +534 -190
- package/languages/go.js +317 -6
- package/languages/index.js +79 -0
- package/languages/java.js +243 -16
- package/languages/javascript.js +357 -24
- package/languages/python.js +423 -28
- package/languages/rust.js +377 -8
- package/languages/utils.js +72 -18
- package/mcp/server.js +3 -3
- package/package.json +9 -3
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -79
package/core/output/analysis.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
259
|
-
if (
|
|
260
|
-
const
|
|
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
|
-
//
|
|
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
|
|
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 = (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
474
|
-
|
|
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
|
|
480
|
-
if (
|
|
481
|
-
const
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
505
|
-
if (
|
|
506
|
-
const
|
|
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
|
-
|
|
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
|
};
|