ucn 4.0.2 → 4.1.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 +31 -7
- package/README.md +89 -50
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +4 -1
package/core/output/graph.js
CHANGED
|
@@ -11,9 +11,12 @@ function formatImports(imports, filePath) {
|
|
|
11
11
|
if (imports?.error) return formatFileError(imports, filePath);
|
|
12
12
|
const lines = [`Imports in ${filePath}:\n`];
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// A string-literal dynamic import that resolved is a project-internal
|
|
15
|
+
// dependency (isDynamic marks the MECHANISM); only unresolved dynamic
|
|
16
|
+
// imports belong in the DYNAMIC (unresolved) section.
|
|
17
|
+
const internal = imports.filter(i => i.resolved);
|
|
18
|
+
const external = imports.filter(i => !i.resolved && !i.isDynamic);
|
|
19
|
+
const dynamic = imports.filter(i => !i.resolved && i.isDynamic);
|
|
17
20
|
|
|
18
21
|
if (internal.length > 0) {
|
|
19
22
|
lines.push('INTERNAL:');
|
|
@@ -50,6 +53,10 @@ function formatImports(imports, filePath) {
|
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
if (internal.length === 0 && external.length === 0 && dynamic.length === 0) {
|
|
57
|
+
lines.push(' (none)');
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
return lines.join('\n');
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -66,7 +73,8 @@ function formatImportsJson(imports, filePath) {
|
|
|
66
73
|
names: i.names,
|
|
67
74
|
type: i.type,
|
|
68
75
|
resolved: i.resolved || null,
|
|
69
|
-
isDynamic: !!i.isDynamic
|
|
76
|
+
isDynamic: !!i.isDynamic,
|
|
77
|
+
line: i.line ?? null
|
|
70
78
|
}))
|
|
71
79
|
}, null, 2);
|
|
72
80
|
}
|
|
@@ -196,8 +204,15 @@ function formatApi(symbols, filePath) {
|
|
|
196
204
|
function formatApiJson(symbols, filePath) {
|
|
197
205
|
const { formatSymbolHandle } = require('../shared');
|
|
198
206
|
const deduped = dedupExportSymbols(symbols);
|
|
207
|
+
// Under --limit the handler slices and attaches the full-set size
|
|
208
|
+
// (fix #251, the #242/#247 shape) — meta.total describes the FULL set.
|
|
209
|
+
const li = symbols.limitInfo;
|
|
199
210
|
return JSON.stringify({
|
|
200
|
-
meta: {
|
|
211
|
+
meta: {
|
|
212
|
+
command: 'api',
|
|
213
|
+
count: deduped.length,
|
|
214
|
+
...(li && { total: li.total, truncated: true }),
|
|
215
|
+
},
|
|
201
216
|
...(filePath && { file: filePath }),
|
|
202
217
|
data: {
|
|
203
218
|
exportCount: deduped.length,
|
|
@@ -247,7 +262,11 @@ function formatGraph(graph, options = {}) {
|
|
|
247
262
|
|
|
248
263
|
const showAll = options.showAll || false;
|
|
249
264
|
const maxChildren = showAll ? Infinity : 8;
|
|
250
|
-
const maxDepth = options.maxDepth !== undefined ? options.maxDepth
|
|
265
|
+
const maxDepth = options.maxDepth !== undefined ? options.maxDepth
|
|
266
|
+
: (graph.maxDepth !== undefined ? graph.maxDepth : Infinity);
|
|
267
|
+
// The engine stops traversal at ITS maxDepth, so the data never reaches
|
|
268
|
+
// the formatter's own depth check — the engine reports the cut instead.
|
|
269
|
+
const engineTruncated = !!graph.depthTruncated;
|
|
251
270
|
|
|
252
271
|
function printTree(nodes, edges, rootFile) {
|
|
253
272
|
const visited = new Set(); // all nodes ever printed (for diamond dep detection)
|
|
@@ -310,7 +329,7 @@ function formatGraph(graph, options = {}) {
|
|
|
310
329
|
let totalTruncated = 0;
|
|
311
330
|
let anyDepthLimited = false;
|
|
312
331
|
|
|
313
|
-
lines.push(`\nIMPORTS (what this file depends on): ${importCount}
|
|
332
|
+
lines.push(`\nIMPORTS (what this file depends on): ${importCount} file${importCount === 1 ? '' : 's'}`);
|
|
314
333
|
if (importCount > 0) {
|
|
315
334
|
const r = printTree(graph.imports.nodes, graph.imports.edges, graph.root);
|
|
316
335
|
totalTruncated += r.truncatedNodes;
|
|
@@ -319,7 +338,7 @@ function formatGraph(graph, options = {}) {
|
|
|
319
338
|
lines.push(' (none)');
|
|
320
339
|
}
|
|
321
340
|
|
|
322
|
-
lines.push(`\nIMPORTERS (what depends on this file): ${importerCount}
|
|
341
|
+
lines.push(`\nIMPORTERS (what depends on this file): ${importerCount} file${importerCount === 1 ? '' : 's'}`);
|
|
323
342
|
if (importerCount > 0) {
|
|
324
343
|
const r = printTree(graph.importers.nodes, graph.importers.edges, graph.root);
|
|
325
344
|
totalTruncated += r.truncatedNodes;
|
|
@@ -328,6 +347,7 @@ function formatGraph(graph, options = {}) {
|
|
|
328
347
|
lines.push(' (none)');
|
|
329
348
|
}
|
|
330
349
|
|
|
350
|
+
anyDepthLimited = anyDepthLimited || engineTruncated;
|
|
331
351
|
if (anyDepthLimited || totalTruncated > 0) {
|
|
332
352
|
lines.push('\n' + '─'.repeat(60));
|
|
333
353
|
if (anyDepthLimited) {
|
|
@@ -343,7 +363,8 @@ function formatGraph(graph, options = {}) {
|
|
|
343
363
|
lines.push(`Dependency graph for ${rootRelPath}`);
|
|
344
364
|
lines.push('═'.repeat(60));
|
|
345
365
|
|
|
346
|
-
const { truncatedNodes, depthLimited } = printTree(graph.nodes, graph.edges, graph.root);
|
|
366
|
+
const { truncatedNodes, depthLimited: fmtDepthLimited } = printTree(graph.nodes, graph.edges, graph.root);
|
|
367
|
+
const depthLimited = fmtDepthLimited || engineTruncated;
|
|
347
368
|
|
|
348
369
|
if (depthLimited || truncatedNodes > 0) {
|
|
349
370
|
lines.push('\n' + '─'.repeat(60));
|
|
@@ -372,6 +393,8 @@ function formatGraphJson(graph) {
|
|
|
372
393
|
nodes: graph.nodes,
|
|
373
394
|
edges: graph.edges
|
|
374
395
|
};
|
|
396
|
+
if (graph.maxDepth !== undefined) result.maxDepth = graph.maxDepth;
|
|
397
|
+
if (graph.depthTruncated) result.depthTruncated = true;
|
|
375
398
|
if (graph.imports) result.imports = graph.imports;
|
|
376
399
|
if (graph.importers) result.importers = graph.importers;
|
|
377
400
|
return JSON.stringify(result, null, 2);
|
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
* core/output/refactoring.js - Verify/plan/stacktrace formatters
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
const { unverifiedReasonLabel, advisoryLine } = require('./shared');
|
|
6
|
+
const { formatAccountLines } = require('./analysis');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Render the v4 unverified band shared by verify and plan: capped one-liners
|
|
10
|
+
* with reasons. Returns [] when the band is empty.
|
|
11
|
+
*/
|
|
12
|
+
function _unverifiedBandLines(sites, cap = 10) {
|
|
13
|
+
if (!sites || sites.length === 0) return [];
|
|
14
|
+
const lines = [];
|
|
15
|
+
lines.push('');
|
|
16
|
+
lines.push(`UNVERIFIED CALL SITES (${sites.length}) — call syntax, no binding/receiver evidence; review manually:`);
|
|
17
|
+
for (const u of sites.slice(0, cap)) {
|
|
18
|
+
const caller = u.callerName ? ` [${u.callerName}]` : '';
|
|
19
|
+
const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
|
|
20
|
+
const expr = u.expression ? `: ${u.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
21
|
+
lines.push(` ${u.file}:${u.line}${caller}${expr}${reason}`);
|
|
22
|
+
}
|
|
23
|
+
if (sites.length > cap) {
|
|
24
|
+
lines.push(` (+${sites.length - cap} more unverified)`);
|
|
25
|
+
}
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
/**
|
|
6
30
|
* Format plan command output - text
|
|
7
31
|
* Shows before/after signatures and all changes needed
|
|
@@ -14,7 +38,12 @@ function formatPlan(plan, options = {}) {
|
|
|
14
38
|
return `Function "${plan.function}" not found.`;
|
|
15
39
|
}
|
|
16
40
|
if (plan.error) {
|
|
17
|
-
|
|
41
|
+
// Only show the parameter list when the error result carries one —
|
|
42
|
+
// unrelated errors (multi-op rejection) don't, and "none" would be
|
|
43
|
+
// wrong for functions that have parameters.
|
|
44
|
+
return plan.currentParams
|
|
45
|
+
? `Error: ${plan.error}\nCurrent parameters: ${plan.currentParams.join(', ') || 'none'}`
|
|
46
|
+
: `Error: ${plan.error}`;
|
|
18
47
|
}
|
|
19
48
|
|
|
20
49
|
const lines = [];
|
|
@@ -58,6 +87,15 @@ function formatPlan(plan, options = {}) {
|
|
|
58
87
|
}
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
// v4 tiered contract: candidates without evidence are not planned but
|
|
91
|
+
// stay visible — a rename that misses one of these breaks at runtime.
|
|
92
|
+
lines.push(..._unverifiedBandLines(plan.unverifiedSites));
|
|
93
|
+
const planAccountLines = formatAccountLines(plan.account);
|
|
94
|
+
if (planAccountLines.length > 0) {
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push(...planAccountLines);
|
|
97
|
+
}
|
|
98
|
+
|
|
61
99
|
return lines.join('\n');
|
|
62
100
|
}
|
|
63
101
|
|
|
@@ -83,22 +121,34 @@ function formatPlanJson(plan) {
|
|
|
83
121
|
}, null, 2);
|
|
84
122
|
}
|
|
85
123
|
|
|
124
|
+
// Standard {meta, data} envelope (fix #230 — plan was one of three
|
|
125
|
+
// commands still emitting a bare result object).
|
|
86
126
|
return JSON.stringify({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
meta: {
|
|
128
|
+
complete: (plan.unverifiedCount || 0) === 0,
|
|
129
|
+
unverified: plan.unverifiedCount || 0,
|
|
130
|
+
...(plan.account && { account: plan.account }),
|
|
131
|
+
},
|
|
132
|
+
data: {
|
|
133
|
+
found: true,
|
|
134
|
+
function: plan.function,
|
|
135
|
+
file: plan.file,
|
|
136
|
+
startLine: plan.startLine,
|
|
137
|
+
operation: plan.operation,
|
|
138
|
+
before: { signature: plan.before.signature },
|
|
139
|
+
after: { signature: plan.after.signature },
|
|
140
|
+
totalChanges: plan.totalChanges,
|
|
141
|
+
filesAffected: plan.filesAffected,
|
|
142
|
+
changes: plan.changes.map(c => ({
|
|
143
|
+
file: c.file,
|
|
144
|
+
line: c.line,
|
|
145
|
+
expression: c.expression,
|
|
146
|
+
suggestion: c.suggestion
|
|
147
|
+
})),
|
|
148
|
+
// v4 tiered contract passthrough
|
|
149
|
+
unverifiedCount: plan.unverifiedCount,
|
|
150
|
+
unverifiedSites: plan.unverifiedSites,
|
|
151
|
+
},
|
|
102
152
|
}, null, 2);
|
|
103
153
|
}
|
|
104
154
|
|
|
@@ -139,9 +189,9 @@ function formatVerify(result, options = {}) {
|
|
|
139
189
|
lines.push(result.signature);
|
|
140
190
|
lines.push('');
|
|
141
191
|
|
|
142
|
-
// Expected args
|
|
192
|
+
// Expected args (max null = unbounded rest param)
|
|
143
193
|
const { min, max } = result.expectedArgs;
|
|
144
|
-
const expectedStr = min === max ? `${min}` : `${min}-${max}
|
|
194
|
+
const expectedStr = max == null ? `${min}+` : (min === max ? `${min}` : `${min}-${max}`);
|
|
145
195
|
lines.push(`Expected arguments: ${expectedStr}`);
|
|
146
196
|
lines.push('');
|
|
147
197
|
|
|
@@ -164,6 +214,9 @@ function formatVerify(result, options = {}) {
|
|
|
164
214
|
lines.push(` Valid: ${result.valid}`);
|
|
165
215
|
lines.push(` Mismatches: ${result.mismatches}`);
|
|
166
216
|
lines.push(` Uncertain: ${result.uncertain}`);
|
|
217
|
+
if (result.unverifiedCount > 0) {
|
|
218
|
+
lines.push(` Unverified (not arg-checked): ${result.unverifiedCount}`);
|
|
219
|
+
}
|
|
167
220
|
if (result.scopeWarning) {
|
|
168
221
|
lines.push(` Note: ${result.scopeWarning.hint}`);
|
|
169
222
|
}
|
|
@@ -206,6 +259,15 @@ function formatVerify(result, options = {}) {
|
|
|
206
259
|
}
|
|
207
260
|
}
|
|
208
261
|
|
|
262
|
+
// v4 tiered contract: unverified band (distinct from UNCERTAIN above,
|
|
263
|
+
// which is arg-parse uncertainty among CONFIRMED sites).
|
|
264
|
+
lines.push(..._unverifiedBandLines(result.unverifiedSites));
|
|
265
|
+
const accountLines = formatAccountLines(result.account);
|
|
266
|
+
if (accountLines.length > 0) {
|
|
267
|
+
lines.push('');
|
|
268
|
+
lines.push(...accountLines);
|
|
269
|
+
}
|
|
270
|
+
|
|
209
271
|
return lines.join('\n');
|
|
210
272
|
}
|
|
211
273
|
|
|
@@ -220,35 +282,49 @@ function formatVerifyJson(result) {
|
|
|
220
282
|
return JSON.stringify({ found: false, error: `Function "${result.function}" not found.` }, null, 2);
|
|
221
283
|
}
|
|
222
284
|
|
|
285
|
+
// Standard {meta, data} envelope (fix #230 — verify was one of three
|
|
286
|
+
// commands still emitting a bare result object). Completeness signals
|
|
287
|
+
// and the account live in meta, like context/impact/about.
|
|
223
288
|
return JSON.stringify({
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
289
|
+
meta: {
|
|
290
|
+
complete: result.uncertain === 0 && (result.unverifiedCount || 0) === 0,
|
|
291
|
+
uncertain: result.uncertain,
|
|
292
|
+
unverified: result.unverifiedCount || 0,
|
|
293
|
+
...(result.account && { account: result.account }),
|
|
294
|
+
},
|
|
295
|
+
data: {
|
|
296
|
+
found: true,
|
|
297
|
+
function: result.function,
|
|
298
|
+
file: result.file,
|
|
299
|
+
startLine: result.startLine,
|
|
300
|
+
signature: result.signature,
|
|
301
|
+
expectedArgs: result.expectedArgs,
|
|
302
|
+
totalCalls: result.totalCalls,
|
|
303
|
+
valid: result.valid,
|
|
304
|
+
mismatches: result.mismatches,
|
|
305
|
+
uncertain: result.uncertain,
|
|
306
|
+
// Feature A/B: surface aggregate patterns and per-site flags.
|
|
307
|
+
patterns: result.patterns,
|
|
308
|
+
mismatchDetails: result.mismatchDetails.map(m => ({
|
|
309
|
+
file: m.file,
|
|
310
|
+
line: m.line,
|
|
311
|
+
expression: m.expression,
|
|
312
|
+
expected: m.expected,
|
|
313
|
+
actual: m.actual,
|
|
314
|
+
args: m.args || [],
|
|
315
|
+
patterns: m.patterns,
|
|
316
|
+
})),
|
|
317
|
+
uncertainDetails: result.uncertainDetails.map(u => ({
|
|
318
|
+
file: u.file,
|
|
319
|
+
line: u.line,
|
|
320
|
+
expression: u.expression,
|
|
321
|
+
reason: u.reason,
|
|
322
|
+
patterns: u.patterns,
|
|
323
|
+
})),
|
|
324
|
+
// v4 tiered contract passthrough
|
|
325
|
+
unverifiedCount: result.unverifiedCount,
|
|
326
|
+
unverifiedSites: result.unverifiedSites,
|
|
327
|
+
},
|
|
252
328
|
}, null, 2);
|
|
253
329
|
}
|
|
254
330
|
|
|
@@ -262,8 +338,13 @@ function formatStackTrace(result) {
|
|
|
262
338
|
}
|
|
263
339
|
|
|
264
340
|
const lines = [];
|
|
265
|
-
lines.push(`Stack trace: ${result.frameCount}
|
|
341
|
+
lines.push(`Stack trace: ${result.frameCount} frame${result.frameCount === 1 ? '' : 's'}`);
|
|
266
342
|
lines.push('═'.repeat(60));
|
|
343
|
+
const stAdvisory = advisoryLine(result.advisory);
|
|
344
|
+
if (stAdvisory) lines.push(stAdvisory);
|
|
345
|
+
if (result.skippedFrames > 0) {
|
|
346
|
+
lines.push(`(${result.skippedFrames} frame(s) without file:line — Unknown Source, native — skipped)`);
|
|
347
|
+
}
|
|
267
348
|
|
|
268
349
|
for (let i = 0; i < result.frames.length; i++) {
|
|
269
350
|
const frame = result.frames[i];
|
|
@@ -289,6 +370,12 @@ function formatStackTrace(result) {
|
|
|
289
370
|
lines.push('');
|
|
290
371
|
lines.push(` In: ${frame.functionInfo.name}(${frame.functionInfo.params || ''})`);
|
|
291
372
|
lines.push(` Range: ${frame.functionInfo.startLine}-${frame.functionInfo.endLine}`);
|
|
373
|
+
// The engine computed this and both surfaces dropped it —
|
|
374
|
+
// an out-of-range line got an unqualified attribution
|
|
375
|
+
// (fix #251).
|
|
376
|
+
if (frame.functionInfo.lineMismatch) {
|
|
377
|
+
lines.push(` ⚠ line ${frame.line} is outside this range — nearest same-name definition shown`);
|
|
378
|
+
}
|
|
292
379
|
}
|
|
293
380
|
} else {
|
|
294
381
|
lines.push(` ${frame.file}:${frame.line} (file not found in project)`);
|
|
@@ -304,16 +391,26 @@ function formatStackTrace(result) {
|
|
|
304
391
|
*/
|
|
305
392
|
function formatStackTraceJson(result) {
|
|
306
393
|
if (!result || result.frameCount === 0) {
|
|
307
|
-
return JSON.stringify({
|
|
394
|
+
return JSON.stringify({
|
|
395
|
+
frameCount: 0,
|
|
396
|
+
frames: [],
|
|
397
|
+
...(result && result.advisory && { advisory: result.advisory }),
|
|
398
|
+
}, null, 2);
|
|
308
399
|
}
|
|
309
400
|
|
|
310
401
|
return JSON.stringify({
|
|
311
402
|
frameCount: result.frameCount,
|
|
403
|
+
// The v4 two-tier surface: advisory commands self-label in JSON too
|
|
404
|
+
// (fix #251 — the result carried this and the text rendered it, but
|
|
405
|
+
// the JSON rebuild dropped it in every language cell).
|
|
406
|
+
...(result.advisory && { advisory: result.advisory }),
|
|
407
|
+
...(result.skippedFrames > 0 && { skippedFrames: result.skippedFrames }),
|
|
312
408
|
frames: result.frames.map(f => ({
|
|
313
409
|
function: f.function || null,
|
|
314
410
|
file: f.file,
|
|
315
411
|
line: f.line,
|
|
316
412
|
found: !!f.found,
|
|
413
|
+
...(f.confidence !== undefined && { confidence: f.confidence }),
|
|
317
414
|
...(f.resolvedFile && { resolvedFile: f.resolvedFile }),
|
|
318
415
|
...(f.context && { context: f.context.map(c => ({
|
|
319
416
|
line: c.line,
|
|
@@ -324,7 +421,8 @@ function formatStackTraceJson(result) {
|
|
|
324
421
|
name: f.functionInfo.name,
|
|
325
422
|
params: f.functionInfo.params || null,
|
|
326
423
|
startLine: f.functionInfo.startLine,
|
|
327
|
-
endLine: f.functionInfo.endLine
|
|
424
|
+
endLine: f.functionInfo.endLine,
|
|
425
|
+
...(f.functionInfo.lineMismatch && { lineMismatch: true }),
|
|
328
426
|
} }),
|
|
329
427
|
...(f.raw && { raw: f.raw })
|
|
330
428
|
}))
|
package/core/output/reporting.js
CHANGED
|
@@ -236,7 +236,10 @@ function formatDeadcode(results, options = {}) {
|
|
|
236
236
|
const extStr = item.externalContract
|
|
237
237
|
? ' [reachable via out-of-tree base — external contract, not dead]'
|
|
238
238
|
: '';
|
|
239
|
-
|
|
239
|
+
// The only references are the symbol's own recursion (fix #253c).
|
|
240
|
+
const recStr = item.selfRecursive ? ' [only self-references — recursive]' : '';
|
|
241
|
+
const displayName = item.className ? `${item.className}.${item.name}` : item.name;
|
|
242
|
+
lines.push(` ${lineRange(item.startLine, item.endLine)} ${displayName} (${item.type})${exported}${hintStr}${declStr}${extStr}${recStr}`);
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
if (hidden > 0) {
|
|
@@ -252,7 +255,7 @@ function formatDeadcode(results, options = {}) {
|
|
|
252
255
|
lines.push(`\n${decoratedHint}`);
|
|
253
256
|
}
|
|
254
257
|
if (results.excludedExported > 0) {
|
|
255
|
-
const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded (
|
|
258
|
+
const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded from the audit (public API may have external callers). Use --include-exported to audit them.`;
|
|
256
259
|
lines.push(`\n${exportedHint}`);
|
|
257
260
|
}
|
|
258
261
|
if (results.excludedExternalContract > 0) {
|
|
@@ -272,10 +275,18 @@ function formatDeadcode(results, options = {}) {
|
|
|
272
275
|
*/
|
|
273
276
|
function formatDeadcodeJson(results) {
|
|
274
277
|
const { formatSymbolHandle } = require('../shared');
|
|
278
|
+
// Under --limit the handler slices the array and attaches the full-set
|
|
279
|
+
// size (fix #242) — the payload itself must say it is truncated.
|
|
280
|
+
const li = results.limitInfo;
|
|
275
281
|
return JSON.stringify({
|
|
276
|
-
meta: {
|
|
282
|
+
meta: {
|
|
283
|
+
command: 'deadcode',
|
|
284
|
+
count: results.length,
|
|
285
|
+
...(li && { total: li.total, truncated: true }),
|
|
286
|
+
},
|
|
277
287
|
data: {
|
|
278
288
|
count: results.length,
|
|
289
|
+
...(li && { total: li.total, truncated: true }),
|
|
279
290
|
...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
|
|
280
291
|
...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
|
|
281
292
|
...(results.excludedExternalContract > 0 && { excludedExternalContract: results.excludedExternalContract }),
|
|
@@ -288,12 +299,14 @@ function formatDeadcodeJson(results) {
|
|
|
288
299
|
file: item.file,
|
|
289
300
|
startLine: item.startLine,
|
|
290
301
|
endLine: item.endLine,
|
|
302
|
+
...(item.className && { className: item.className }),
|
|
291
303
|
...(handle && { handle }),
|
|
292
304
|
...(item.isExported && { isExported: true }),
|
|
293
305
|
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
294
306
|
...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations }),
|
|
295
307
|
...(item.declaredOn && { declaredOn: item.declaredOn }),
|
|
296
|
-
...(item.externalContract && { externalContract: true })
|
|
308
|
+
...(item.externalContract && { externalContract: true }),
|
|
309
|
+
...(item.selfRecursive && { selfRecursive: true })
|
|
297
310
|
};
|
|
298
311
|
}),
|
|
299
312
|
},
|
|
@@ -354,8 +367,16 @@ function formatEntrypoints(results, options = {}) {
|
|
|
354
367
|
* Format entrypoints command output (JSON)
|
|
355
368
|
*/
|
|
356
369
|
function formatEntrypointsJson(results) {
|
|
370
|
+
// Under --limit the handler slices the array and attaches the full-set
|
|
371
|
+
// size (fix #247, the deadcode #242 shape) — meta.total must describe
|
|
372
|
+
// the FULL set, with `truncated` saying the list below is a prefix.
|
|
373
|
+
const li = results.limitInfo;
|
|
357
374
|
return JSON.stringify({
|
|
358
|
-
meta: {
|
|
375
|
+
meta: {
|
|
376
|
+
count: results.length,
|
|
377
|
+
total: li ? li.total : results.length,
|
|
378
|
+
...(li && { truncated: true }),
|
|
379
|
+
},
|
|
359
380
|
data: {
|
|
360
381
|
entrypoints: results.map(ep => {
|
|
361
382
|
const handle = ep.line && ep.name ? `${ep.file}:${ep.line}:${ep.name}` : null;
|
|
@@ -368,6 +389,7 @@ function formatEntrypointsJson(results) {
|
|
|
368
389
|
framework: ep.framework,
|
|
369
390
|
patternId: ep.patternId,
|
|
370
391
|
evidence: ep.evidence,
|
|
392
|
+
...(ep.registeredAt && { registeredAt: ep.registeredAt }),
|
|
371
393
|
confidence: ep.confidence,
|
|
372
394
|
};
|
|
373
395
|
}),
|
|
@@ -375,9 +397,86 @@ function formatEntrypointsJson(results) {
|
|
|
375
397
|
}, null, 2);
|
|
376
398
|
}
|
|
377
399
|
|
|
400
|
+
/**
|
|
401
|
+
* formatOrient — one-screen cold-repo orientation.
|
|
402
|
+
*/
|
|
403
|
+
function formatOrient(result) {
|
|
404
|
+
const lines = [];
|
|
405
|
+
lines.push(`PROJECT ORIENTATION — ${result.root}`);
|
|
406
|
+
lines.push('═'.repeat(60));
|
|
407
|
+
|
|
408
|
+
// Size + language mix (percent by symbols, largest first)
|
|
409
|
+
const langs = Object.entries(result.byLanguage || {})
|
|
410
|
+
.map(([lang, v]) => ({ lang, symbols: v.symbols || 0 }))
|
|
411
|
+
.sort((a, b) => b.symbols - a.symbols || (a.lang < b.lang ? -1 : a.lang > b.lang ? 1 : 0));
|
|
412
|
+
const totalSym = result.symbols || 1;
|
|
413
|
+
const langStr = langs
|
|
414
|
+
.map(l => `${l.lang} ${Math.round((l.symbols / totalSym) * 100)}%`)
|
|
415
|
+
.join(', ');
|
|
416
|
+
lines.push(`${result.files} files · ${result.symbols} symbols · ${langStr}`);
|
|
417
|
+
lines.push('');
|
|
418
|
+
|
|
419
|
+
if (result.dirs?.length) {
|
|
420
|
+
lines.push('TOP DIRS (by symbols):');
|
|
421
|
+
const width = Math.max(...result.dirs.map(d => d.dir.length));
|
|
422
|
+
for (const d of result.dirs) {
|
|
423
|
+
lines.push(` ${d.dir.padEnd(width)} ${d.symbols} symbols · ${d.files} file(s)`);
|
|
424
|
+
}
|
|
425
|
+
lines.push('');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (result.hot?.items?.length) {
|
|
429
|
+
const scope = result.hot.production ? 'production functions' : 'functions';
|
|
430
|
+
lines.push(`HOT (most-called ${scope}, top ${result.hot.items.length} of ${result.hot.total}):`);
|
|
431
|
+
for (const h of result.hot.items) {
|
|
432
|
+
const label = h.className ? `${h.className}.${h.name}` : h.name;
|
|
433
|
+
lines.push(` ${label} — ${h.callCount} call(s) · ${h.file}:${h.line}`);
|
|
434
|
+
}
|
|
435
|
+
lines.push('');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (result.entrypoints) {
|
|
439
|
+
const byType = result.entrypoints.byType
|
|
440
|
+
.map(t => `${t.type} ${t.count}`).join(', ');
|
|
441
|
+
lines.push(`ENTRY POINTS: ${result.entrypoints.total} — ${byType}`);
|
|
442
|
+
} else {
|
|
443
|
+
lines.push('ENTRY POINTS: (detection unavailable)');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const bs = result.trust?.blindSpots || {};
|
|
447
|
+
const bsParts = [];
|
|
448
|
+
if (bs.dynamicImports) bsParts.push(`${bs.dynamicImports} dynamic import(s)`);
|
|
449
|
+
if (bs.evalCalls) bsParts.push(`${bs.evalCalls} eval`);
|
|
450
|
+
if (bs.reflection) bsParts.push(`${bs.reflection} reflection`);
|
|
451
|
+
if (bs.parseFailures) bsParts.push(`${bs.parseFailures} parse failure(s)`);
|
|
452
|
+
lines.push(`TRUST: ${result.trust?.level || 'UNKNOWN'}${bsParts.length ? ' — ' + bsParts.join(', ') : ''} (ucn doctor for detail)`);
|
|
453
|
+
lines.push('');
|
|
454
|
+
|
|
455
|
+
const next = [];
|
|
456
|
+
if (result.suggest) next.push(`ucn about ${result.suggest}`);
|
|
457
|
+
next.push('ucn toc --detailed', 'ucn stats --hot --top=20', 'ucn doctor --deep');
|
|
458
|
+
lines.push(`Next: ${next.join(' · ')}`);
|
|
459
|
+
|
|
460
|
+
return lines.join('\n');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function formatOrientJson(result) {
|
|
464
|
+
return JSON.stringify({
|
|
465
|
+
meta: {
|
|
466
|
+
command: 'orient',
|
|
467
|
+
files: result.files,
|
|
468
|
+
symbols: result.symbols,
|
|
469
|
+
trust: result.trust?.level ?? null,
|
|
470
|
+
},
|
|
471
|
+
data: result,
|
|
472
|
+
}, null, 2);
|
|
473
|
+
}
|
|
474
|
+
|
|
378
475
|
module.exports = {
|
|
379
476
|
formatToc,
|
|
380
477
|
formatTocJson,
|
|
478
|
+
formatOrient,
|
|
479
|
+
formatOrientJson,
|
|
381
480
|
formatStats,
|
|
382
481
|
formatStatsJson,
|
|
383
482
|
formatDeadcode,
|
package/core/output/search.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* core/output/search.js - Text search, structural search, example, typedef, tests formatters
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const { detectDoubleEscaping } = require('./shared');
|
|
5
|
+
const { detectDoubleEscaping, advisoryLine } = require('./shared');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Format search command output
|
|
@@ -170,6 +170,8 @@ function formatExample(result, name) {
|
|
|
170
170
|
lines.push(`Best example of "${name}":`);
|
|
171
171
|
lines.push('═'.repeat(60));
|
|
172
172
|
lines.push(`${best.relativePath}:${best.line}`);
|
|
173
|
+
const exAdvisory = advisoryLine(result.advisory);
|
|
174
|
+
if (exAdvisory) lines.push(exAdvisory);
|
|
173
175
|
lines.push('');
|
|
174
176
|
|
|
175
177
|
if (best.before) {
|
|
@@ -206,6 +208,8 @@ function formatExampleDiverse(result, name) {
|
|
|
206
208
|
const total = result.totalClusters || result.clusters.length;
|
|
207
209
|
lines.push(`Diverse examples of "${name}" — ${result.clusters.length} of ${total} cluster(s), ${result.totalCalls} total calls:`);
|
|
208
210
|
lines.push('═'.repeat(60));
|
|
211
|
+
const divAdvisory = advisoryLine(result.advisory);
|
|
212
|
+
if (divAdvisory) lines.push(divAdvisory);
|
|
209
213
|
|
|
210
214
|
for (let i = 0; i < result.clusters.length; i++) {
|
|
211
215
|
const c = result.clusters[i];
|
|
@@ -249,13 +253,23 @@ function formatExampleDiverse(result, name) {
|
|
|
249
253
|
*/
|
|
250
254
|
function formatExampleJson(result, name) {
|
|
251
255
|
if (!result || !result.best) {
|
|
252
|
-
|
|
256
|
+
// Surface the excluded-test-usages count in JSON too (fix #237 — the
|
|
257
|
+
// handler note that used to carry it duplicated the text body).
|
|
258
|
+
const excluded = result?.excludedTestCalls || 0;
|
|
259
|
+
return JSON.stringify({
|
|
260
|
+
found: false, query: name,
|
|
261
|
+
...(excluded > 0 && { excludedTestCalls: excluded }),
|
|
262
|
+
error: excluded > 0
|
|
263
|
+
? `No call examples found for "${name}" (excluded ${excluded} test-file usage${excluded === 1 ? '' : 's'} — pass --include-tests to include them)`
|
|
264
|
+
: `No call examples found for "${name}"`,
|
|
265
|
+
}, null, 2);
|
|
253
266
|
}
|
|
254
267
|
|
|
255
268
|
const best = result.best;
|
|
256
269
|
const env = {
|
|
257
270
|
found: true,
|
|
258
271
|
query: name,
|
|
272
|
+
...(result.advisory && { advisory: result.advisory }),
|
|
259
273
|
totalCalls: result.totalCalls,
|
|
260
274
|
best: {
|
|
261
275
|
file: best.relativePath || best.file,
|
|
@@ -302,7 +316,20 @@ function formatTypedef(types, name) {
|
|
|
302
316
|
} else {
|
|
303
317
|
for (const t of types) {
|
|
304
318
|
lines.push(`${t.relativePath}:${t.startLine} ${t.type} ${t.name}`);
|
|
305
|
-
|
|
319
|
+
// Honest breakdown (fix #251): the fast-path count sees calls/
|
|
320
|
+
// defs/imports only — for TYPE symbols, reference-kind usage
|
|
321
|
+
// dominates and is NOT counted here; say what the number is.
|
|
322
|
+
if (t.usageCounts !== undefined) {
|
|
323
|
+
const c = t.usageCounts;
|
|
324
|
+
const parts = [];
|
|
325
|
+
if (c.calls > 0) parts.push(`${c.calls} calls`);
|
|
326
|
+
if (c.definitions > 0) parts.push(`${c.definitions} def`);
|
|
327
|
+
if (c.imports > 0) parts.push(`${c.imports} imports`);
|
|
328
|
+
if (c.references > 0) parts.push(`${c.references} refs`);
|
|
329
|
+
lines.push(parts.length > 0
|
|
330
|
+
? ` (${c.total} usages: ${parts.join(', ')} — type references not counted; use \`usages ${t.name}\`)`
|
|
331
|
+
: ` (${c.total} usages)`);
|
|
332
|
+
} else if (t.usageCount !== undefined) {
|
|
306
333
|
lines.push(` (${t.usageCount} usages)`);
|
|
307
334
|
}
|
|
308
335
|
if (t.code) {
|