ucn 3.8.13 → 3.8.14
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 +3 -1
- package/.github/workflows/ci.yml +13 -1
- package/README.md +1 -0
- package/cli/index.js +165 -246
- package/core/analysis.js +1400 -0
- package/core/build-worker.js +194 -0
- package/core/cache.js +105 -7
- package/core/callers.js +194 -64
- package/core/deadcode.js +22 -66
- package/core/discovery.js +9 -54
- package/core/execute.js +139 -54
- package/core/graph.js +615 -0
- package/core/output/analysis-ext.js +271 -0
- package/core/output/analysis.js +491 -0
- package/core/output/extraction.js +188 -0
- package/core/output/find.js +355 -0
- package/core/output/graph.js +399 -0
- package/core/output/refactoring.js +293 -0
- package/core/output/reporting.js +331 -0
- package/core/output/search.js +307 -0
- package/core/output/shared.js +271 -0
- package/core/output/tracing.js +416 -0
- package/core/output.js +15 -3293
- package/core/parallel-build.js +165 -0
- package/core/project.js +299 -3633
- package/core/registry.js +59 -0
- package/core/reporting.js +258 -0
- package/core/search.js +890 -0
- package/core/stacktrace.js +1 -1
- package/core/tracing.js +631 -0
- package/core/verify.js +10 -13
- package/eslint.config.js +43 -0
- package/jsconfig.json +10 -0
- package/languages/go.js +21 -2
- package/languages/html.js +8 -0
- package/languages/index.js +102 -40
- package/languages/java.js +13 -0
- package/languages/javascript.js +17 -1
- package/languages/python.js +14 -0
- package/languages/rust.js +13 -0
- package/languages/utils.js +1 -1
- package/mcp/server.js +45 -28
- package/package.json +8 -3
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/shared.js - Shared utility functions used by other formatters
|
|
3
|
+
*
|
|
4
|
+
* KEY PRINCIPLE: Never truncate critical information.
|
|
5
|
+
* Full expressions, full signatures, full context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { langTraits } = require('../../languages');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format dynamic imports note with language-appropriate terminology.
|
|
13
|
+
* Go doesn't have "dynamic imports" — uses "blank/dot imports" instead.
|
|
14
|
+
*/
|
|
15
|
+
function dynamicImportsNote(count, meta) {
|
|
16
|
+
if (!count) return null;
|
|
17
|
+
if (meta?.projectLanguage && !langTraits(meta.projectLanguage)?.hasDynamicImports) {
|
|
18
|
+
return `${count} blank/dot import(s)`;
|
|
19
|
+
}
|
|
20
|
+
return `${count} dynamic import(s)`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FILE_ERROR_MESSAGES = {
|
|
24
|
+
'file-not-found': 'File not found in project',
|
|
25
|
+
'file-ambiguous': 'Ambiguous file match'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function formatFileError(errorObj, fallbackPath) {
|
|
29
|
+
const msg = FILE_ERROR_MESSAGES[errorObj.error] || errorObj.error;
|
|
30
|
+
const file = errorObj.filePath || fallbackPath || '';
|
|
31
|
+
return `Error: ${msg}: ${file}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalize parameters for display
|
|
36
|
+
* Collapses multiline params to single line
|
|
37
|
+
* @param {string} params - Raw params string
|
|
38
|
+
* @returns {string} - Normalized params (NO truncation)
|
|
39
|
+
*/
|
|
40
|
+
function normalizeParams(params) {
|
|
41
|
+
if (!params || params === '...') return params || '...';
|
|
42
|
+
// Collapse whitespace (newlines, tabs, multiple spaces) to single space
|
|
43
|
+
return params.replace(/\s+/g, ' ').trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format a line number for display
|
|
48
|
+
* @param {number} line - 1-indexed line number
|
|
49
|
+
* @param {number} width - Padding width
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function lineNum(line, width = 4) {
|
|
53
|
+
return String(line).padStart(width);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format a line range
|
|
58
|
+
* @param {number} start - 1-indexed start line
|
|
59
|
+
* @param {number} end - 1-indexed end line
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function lineRange(start, end) {
|
|
63
|
+
return `[${lineNum(start)}-${lineNum(end)}]`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format a single line location
|
|
68
|
+
* @param {number} line - 1-indexed line number
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function lineLoc(line) {
|
|
72
|
+
return `[${lineNum(line)}]`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Format function signature for TOC display
|
|
77
|
+
* @param {object} fn - Function definition
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function formatFunctionSignature(fn) {
|
|
81
|
+
const prefix = [];
|
|
82
|
+
|
|
83
|
+
// Modifiers
|
|
84
|
+
if (fn.modifiers && fn.modifiers.length > 0) {
|
|
85
|
+
prefix.push(fn.modifiers.join(' '));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Generator marker
|
|
89
|
+
if (fn.isGenerator) prefix.push('*');
|
|
90
|
+
|
|
91
|
+
// Name + generics + params (concatenated without spaces)
|
|
92
|
+
let sig = fn.name;
|
|
93
|
+
if (fn.generics) sig += fn.generics;
|
|
94
|
+
const params = normalizeParams(fn.params);
|
|
95
|
+
sig += `(${params})`;
|
|
96
|
+
|
|
97
|
+
// Return type
|
|
98
|
+
if (fn.returnType) sig += `: ${fn.returnType}`;
|
|
99
|
+
|
|
100
|
+
// Arrow indicator
|
|
101
|
+
if (fn.isArrow) sig += ' =>';
|
|
102
|
+
|
|
103
|
+
if (prefix.length > 0) {
|
|
104
|
+
return prefix.join(' ') + ' ' + sig;
|
|
105
|
+
}
|
|
106
|
+
return sig;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format class/type signature for TOC display
|
|
111
|
+
*/
|
|
112
|
+
function formatClassSignature(cls) {
|
|
113
|
+
const parts = [cls.type, cls.name];
|
|
114
|
+
|
|
115
|
+
if (cls.generics) parts.push(cls.generics);
|
|
116
|
+
if (cls.extends) parts.push(`extends ${cls.extends}`);
|
|
117
|
+
if (cls.implements && cls.implements.length > 0) {
|
|
118
|
+
parts.push(`implements ${cls.implements.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return parts.join(' ');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Format class member for TOC display
|
|
126
|
+
*/
|
|
127
|
+
function formatMemberSignature(member) {
|
|
128
|
+
const parts = [];
|
|
129
|
+
|
|
130
|
+
// Member type (static, get, set, private, etc.)
|
|
131
|
+
if (member.memberType && member.memberType !== 'method') {
|
|
132
|
+
parts.push(member.memberType);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Async
|
|
136
|
+
if (member.isAsync) parts.push('async');
|
|
137
|
+
|
|
138
|
+
// Generator
|
|
139
|
+
if (member.isGenerator) parts.push('*');
|
|
140
|
+
|
|
141
|
+
// Name + Parameters (no space between name and parens)
|
|
142
|
+
if (member.params !== undefined) {
|
|
143
|
+
const params = normalizeParams(member.params);
|
|
144
|
+
parts.push(`${member.name}(${params})`);
|
|
145
|
+
} else {
|
|
146
|
+
parts.push(member.name);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Return type
|
|
150
|
+
if (member.returnType) parts.push(`: ${member.returnType}`);
|
|
151
|
+
|
|
152
|
+
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Compact display of line numbers, collapsing consecutive ranges
|
|
157
|
+
*/
|
|
158
|
+
function formatLineRanges(lineNums) {
|
|
159
|
+
if (lineNums.length === 0) return '';
|
|
160
|
+
const sorted = [...lineNums].sort((a, b) => a - b);
|
|
161
|
+
const ranges = [];
|
|
162
|
+
let start = sorted[0], end = sorted[0];
|
|
163
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
164
|
+
if (sorted[i] === end + 1) {
|
|
165
|
+
end = sorted[i];
|
|
166
|
+
} else {
|
|
167
|
+
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
168
|
+
start = end = sorted[i];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
172
|
+
return ranges.join(', ');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Detect common double-escaping patterns in regex search terms.
|
|
177
|
+
* When MCP/JSON transport is involved, agents often write \\. when they mean \. (literal dot).
|
|
178
|
+
* Returns a hint string if double-escaping is suspected, empty string otherwise.
|
|
179
|
+
*/
|
|
180
|
+
function detectDoubleEscaping(term) {
|
|
181
|
+
// Look for \\. \\d \\w \\s \\b \\D \\W \\S \\B \\( \\) \\[ \\] — common double-escaped sequences
|
|
182
|
+
const doubleEscaped = term.match(/\\\\[.dDwWsSbB()\[\]*+?^${}|]/g); // eslint-disable-line no-useless-escape
|
|
183
|
+
if (!doubleEscaped) return '';
|
|
184
|
+
const examples = [...new Set(doubleEscaped)].slice(0, 3);
|
|
185
|
+
const fixed = examples.map(e => e.slice(1)); // remove one backslash
|
|
186
|
+
return `\nHint: Pattern contains ${examples.join(', ')} which matches literal backslash(es). If you meant ${fixed.join(', ')}, use a single backslash (MCP/JSON parameters are already raw strings).`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Count depth of nested generic brackets.
|
|
191
|
+
*/
|
|
192
|
+
function countNestedGenerics(str) {
|
|
193
|
+
let maxDepth = 0;
|
|
194
|
+
let depth = 0;
|
|
195
|
+
for (const char of str) {
|
|
196
|
+
if (char === '<') {
|
|
197
|
+
depth++;
|
|
198
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
199
|
+
} else if (char === '>') {
|
|
200
|
+
depth--;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return maxDepth;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Compute confidence level for a symbol match.
|
|
208
|
+
* @returns {{ level: 'high'|'medium'|'low', reasons: string[] }}
|
|
209
|
+
*/
|
|
210
|
+
function computeConfidence(symbol) {
|
|
211
|
+
const reasons = [];
|
|
212
|
+
let score = 100;
|
|
213
|
+
|
|
214
|
+
const span = (symbol.endLine || symbol.startLine) - symbol.startLine;
|
|
215
|
+
if (span > 500) {
|
|
216
|
+
score -= 30;
|
|
217
|
+
reasons.push('very long function (>500 lines)');
|
|
218
|
+
} else if (span > 200) {
|
|
219
|
+
score -= 15;
|
|
220
|
+
reasons.push('long function (>200 lines)');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const params = Array.isArray(symbol.params) ? symbol.params : [];
|
|
224
|
+
const signature = params.map(p => p.type || '').join(' ') + (symbol.returnType || '');
|
|
225
|
+
const genericDepth = countNestedGenerics(signature);
|
|
226
|
+
if (genericDepth > 3) {
|
|
227
|
+
score -= 20;
|
|
228
|
+
reasons.push('complex nested generics');
|
|
229
|
+
} else if (genericDepth > 2) {
|
|
230
|
+
score -= 10;
|
|
231
|
+
reasons.push('nested generics');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (symbol.file) {
|
|
235
|
+
try {
|
|
236
|
+
const stats = fs.statSync(symbol.file);
|
|
237
|
+
const sizeKB = stats.size / 1024;
|
|
238
|
+
if (sizeKB > 500) {
|
|
239
|
+
score -= 20;
|
|
240
|
+
reasons.push('very large file (>500KB)');
|
|
241
|
+
} else if (sizeKB > 200) {
|
|
242
|
+
score -= 10;
|
|
243
|
+
reasons.push('large file (>200KB)');
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
// Skip file size check on error
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let level = 'high';
|
|
251
|
+
if (score < 50) level = 'low';
|
|
252
|
+
else if (score < 80) level = 'medium';
|
|
253
|
+
|
|
254
|
+
return { level, reasons };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
dynamicImportsNote,
|
|
259
|
+
formatFileError,
|
|
260
|
+
normalizeParams,
|
|
261
|
+
lineNum,
|
|
262
|
+
lineRange,
|
|
263
|
+
lineLoc,
|
|
264
|
+
formatFunctionSignature,
|
|
265
|
+
formatClassSignature,
|
|
266
|
+
formatMemberSignature,
|
|
267
|
+
formatLineRanges,
|
|
268
|
+
detectDoubleEscaping,
|
|
269
|
+
countNestedGenerics,
|
|
270
|
+
computeConfidence,
|
|
271
|
+
};
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/tracing.js - Trace/blast/reverse tree formatters
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format trace command output - text
|
|
7
|
+
* Shows call tree visualization
|
|
8
|
+
*/
|
|
9
|
+
function formatTrace(trace, options = {}) {
|
|
10
|
+
if (!trace) {
|
|
11
|
+
return 'Function not found.';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const lines = [];
|
|
15
|
+
|
|
16
|
+
// Header
|
|
17
|
+
lines.push(`Call tree for ${trace.root}`);
|
|
18
|
+
lines.push('═'.repeat(60));
|
|
19
|
+
lines.push(`${trace.file}:${trace.line}`);
|
|
20
|
+
lines.push(`Direction: ${trace.direction}, Max depth: ${trace.maxDepth}`);
|
|
21
|
+
|
|
22
|
+
if (trace.warnings && trace.warnings.length > 0) {
|
|
23
|
+
for (const w of trace.warnings) {
|
|
24
|
+
lines.push(`Note: ${w.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
lines.push('');
|
|
29
|
+
|
|
30
|
+
// Render tree
|
|
31
|
+
let hasTruncation = false;
|
|
32
|
+
const renderNode = (node, prefix = '', isLast = true) => {
|
|
33
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
34
|
+
const extension = isLast ? ' ' : '│ ';
|
|
35
|
+
|
|
36
|
+
let label = node.name;
|
|
37
|
+
if (node.external) {
|
|
38
|
+
label += ' [external]';
|
|
39
|
+
} else if (node.file) {
|
|
40
|
+
label += ` (${node.file}:${node.line})`;
|
|
41
|
+
}
|
|
42
|
+
if (node.weight && node.weight !== 'normal') {
|
|
43
|
+
label += ` [${node.weight}]`;
|
|
44
|
+
}
|
|
45
|
+
if (node.callCount) {
|
|
46
|
+
label += ` ${node.callCount}x`;
|
|
47
|
+
}
|
|
48
|
+
if (node.alreadyShown) {
|
|
49
|
+
label += ' (see above)';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lines.push(prefix + connector + label);
|
|
53
|
+
|
|
54
|
+
if (node.children && !node.alreadyShown) {
|
|
55
|
+
const hasMore = node.truncatedChildren > 0;
|
|
56
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
57
|
+
const isChildLast = !hasMore && i === node.children.length - 1;
|
|
58
|
+
renderNode(node.children[i], prefix + extension, isChildLast);
|
|
59
|
+
}
|
|
60
|
+
if (hasMore) {
|
|
61
|
+
hasTruncation = true;
|
|
62
|
+
lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callees`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Root node
|
|
68
|
+
lines.push(trace.root);
|
|
69
|
+
if (trace.tree && trace.tree.children) {
|
|
70
|
+
const rootHasMore = trace.tree.truncatedChildren > 0;
|
|
71
|
+
for (let i = 0; i < trace.tree.children.length; i++) {
|
|
72
|
+
const isLast = !rootHasMore && i === trace.tree.children.length - 1;
|
|
73
|
+
renderNode(trace.tree.children[i], '', isLast);
|
|
74
|
+
}
|
|
75
|
+
if (rootHasMore) {
|
|
76
|
+
hasTruncation = true;
|
|
77
|
+
lines.push(`└── ... and ${trace.tree.truncatedChildren} more callees`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Callers section
|
|
82
|
+
if (trace.callers && trace.callers.length > 0) {
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('CALLED BY:');
|
|
85
|
+
for (const c of trace.callers) {
|
|
86
|
+
lines.push(` ${c.name} - ${c.file}:${c.line}`);
|
|
87
|
+
lines.push(` ${c.expression}`);
|
|
88
|
+
}
|
|
89
|
+
if (trace.truncatedCallers) {
|
|
90
|
+
hasTruncation = true;
|
|
91
|
+
lines.push(` ... and ${trace.truncatedCallers} more callers`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (hasTruncation) {
|
|
96
|
+
const allHint = options.allHint || 'Use --all to show all.';
|
|
97
|
+
lines.push(`\nSome results truncated. ${allHint}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (trace.includeMethods === false) {
|
|
101
|
+
const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded — use --include-methods to include them';
|
|
102
|
+
lines.push(`\n${methodsHint}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format trace command output - JSON
|
|
110
|
+
*/
|
|
111
|
+
function formatTraceJson(trace) {
|
|
112
|
+
if (!trace) {
|
|
113
|
+
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
114
|
+
}
|
|
115
|
+
return JSON.stringify(trace, null, 2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Format blast command output - text
|
|
120
|
+
* Shows transitive blast radius (callers of callers)
|
|
121
|
+
*/
|
|
122
|
+
function formatBlast(blast, options = {}) {
|
|
123
|
+
if (!blast) {
|
|
124
|
+
return 'Function not found.';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lines = [];
|
|
128
|
+
|
|
129
|
+
// Header
|
|
130
|
+
lines.push(`Blast radius for ${blast.root}`);
|
|
131
|
+
lines.push('═'.repeat(60));
|
|
132
|
+
lines.push(`${blast.file}:${blast.line}`);
|
|
133
|
+
lines.push(`Depth: ${blast.maxDepth}`);
|
|
134
|
+
|
|
135
|
+
if (blast.warnings && blast.warnings.length > 0) {
|
|
136
|
+
for (const w of blast.warnings) {
|
|
137
|
+
lines.push(`Note: ${w.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push('');
|
|
142
|
+
|
|
143
|
+
// Render tree (same structure as trace but showing callers)
|
|
144
|
+
let hasTruncation = false;
|
|
145
|
+
const renderNode = (node, prefix = '', isLast = true) => {
|
|
146
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
147
|
+
const extension = isLast ? ' ' : '│ ';
|
|
148
|
+
|
|
149
|
+
let label = node.name;
|
|
150
|
+
if (node.file) {
|
|
151
|
+
label += ` (${node.file}:${node.line})`;
|
|
152
|
+
}
|
|
153
|
+
if (node.callSites && node.callSites > 1) {
|
|
154
|
+
label += ` ${node.callSites}x`;
|
|
155
|
+
}
|
|
156
|
+
if (node.alreadyShown) {
|
|
157
|
+
label += ' (see above)';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lines.push(prefix + connector + label);
|
|
161
|
+
|
|
162
|
+
if (node.children && !node.alreadyShown) {
|
|
163
|
+
const hasMore = node.truncatedChildren > 0;
|
|
164
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
165
|
+
const isChildLast = !hasMore && i === node.children.length - 1;
|
|
166
|
+
renderNode(node.children[i], prefix + extension, isChildLast);
|
|
167
|
+
}
|
|
168
|
+
if (hasMore) {
|
|
169
|
+
hasTruncation = true;
|
|
170
|
+
lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callers`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Root node
|
|
176
|
+
lines.push(blast.root);
|
|
177
|
+
if (blast.tree && blast.tree.children) {
|
|
178
|
+
const rootHasMore = blast.tree.truncatedChildren > 0;
|
|
179
|
+
for (let i = 0; i < blast.tree.children.length; i++) {
|
|
180
|
+
const isLast = !rootHasMore && i === blast.tree.children.length - 1;
|
|
181
|
+
renderNode(blast.tree.children[i], '', isLast);
|
|
182
|
+
}
|
|
183
|
+
if (rootHasMore) {
|
|
184
|
+
hasTruncation = true;
|
|
185
|
+
lines.push(`└── ... and ${blast.tree.truncatedChildren} more callers`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Summary
|
|
190
|
+
if (blast.summary) {
|
|
191
|
+
lines.push('');
|
|
192
|
+
const { totalAffected, totalFiles } = blast.summary;
|
|
193
|
+
if (totalAffected > 0) {
|
|
194
|
+
lines.push(`Summary: 1 function changed → ${totalAffected} function${totalAffected !== 1 ? 's' : ''} affected across ${totalFiles} file${totalFiles !== 1 ? 's' : ''}`);
|
|
195
|
+
} else {
|
|
196
|
+
lines.push('Summary: No callers found — this function is a root/entry point.');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (hasTruncation) {
|
|
201
|
+
const allHint = options.allHint || 'Use --all to show all.';
|
|
202
|
+
lines.push(`\nSome results truncated. ${allHint}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (blast.includeMethods === false) {
|
|
206
|
+
lines.push('\nNote: obj.method() calls excluded. Use --include-methods to include them.');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return lines.join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Format blast command output - JSON
|
|
214
|
+
*/
|
|
215
|
+
function formatBlastJson(blast) {
|
|
216
|
+
if (!blast) {
|
|
217
|
+
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
218
|
+
}
|
|
219
|
+
return JSON.stringify(blast, null, 2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Format reverse-trace command output - text
|
|
224
|
+
* Shows upward call chain to entry points
|
|
225
|
+
*/
|
|
226
|
+
function formatReverseTrace(result, options = {}) {
|
|
227
|
+
if (!result) {
|
|
228
|
+
return 'Function not found.';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const lines = [];
|
|
232
|
+
|
|
233
|
+
// Header
|
|
234
|
+
lines.push(`Reverse trace for ${result.root}`);
|
|
235
|
+
lines.push('═'.repeat(60));
|
|
236
|
+
lines.push(`${result.file}:${result.line}`);
|
|
237
|
+
lines.push(`Depth: ${result.maxDepth}`);
|
|
238
|
+
|
|
239
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
240
|
+
for (const w of result.warnings) {
|
|
241
|
+
lines.push(`Note: ${w.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
lines.push('');
|
|
246
|
+
|
|
247
|
+
// Render tree
|
|
248
|
+
let hasTruncation = false;
|
|
249
|
+
const renderNode = (node, prefix = '', isLast = true) => {
|
|
250
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
251
|
+
const extension = isLast ? ' ' : '│ ';
|
|
252
|
+
|
|
253
|
+
let label = node.name;
|
|
254
|
+
if (node.file) {
|
|
255
|
+
label += ` (${node.file}:${node.line})`;
|
|
256
|
+
}
|
|
257
|
+
if (node.callSites && node.callSites > 1) {
|
|
258
|
+
label += ` ${node.callSites}x`;
|
|
259
|
+
}
|
|
260
|
+
if (node.entryPoint) {
|
|
261
|
+
label += ' ★ entry point';
|
|
262
|
+
}
|
|
263
|
+
if (node.alreadyShown) {
|
|
264
|
+
label += ' (see above)';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push(prefix + connector + label);
|
|
268
|
+
|
|
269
|
+
if (node.children && !node.alreadyShown) {
|
|
270
|
+
const hasMore = node.truncatedChildren > 0;
|
|
271
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
272
|
+
const isChildLast = !hasMore && i === node.children.length - 1;
|
|
273
|
+
renderNode(node.children[i], prefix + extension, isChildLast);
|
|
274
|
+
}
|
|
275
|
+
if (hasMore) {
|
|
276
|
+
hasTruncation = true;
|
|
277
|
+
lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callers`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Root node
|
|
283
|
+
let rootLabel = result.root;
|
|
284
|
+
if (result.tree && result.tree.entryPoint) {
|
|
285
|
+
rootLabel += ' ★ entry point (no callers)';
|
|
286
|
+
}
|
|
287
|
+
lines.push(rootLabel);
|
|
288
|
+
if (result.tree && result.tree.children) {
|
|
289
|
+
const rootHasMore = result.tree.truncatedChildren > 0;
|
|
290
|
+
for (let i = 0; i < result.tree.children.length; i++) {
|
|
291
|
+
const isLast = !rootHasMore && i === result.tree.children.length - 1;
|
|
292
|
+
renderNode(result.tree.children[i], '', isLast);
|
|
293
|
+
}
|
|
294
|
+
if (rootHasMore) {
|
|
295
|
+
hasTruncation = true;
|
|
296
|
+
lines.push(`└── ... and ${result.tree.truncatedChildren} more callers`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Entry points summary
|
|
301
|
+
if (result.entryPoints && result.entryPoints.length > 0) {
|
|
302
|
+
lines.push('');
|
|
303
|
+
lines.push(`Entry points (${result.entryPoints.length}):`);
|
|
304
|
+
for (const ep of result.entryPoints) {
|
|
305
|
+
lines.push(` ★ ${ep.name} (${ep.file}:${ep.line})`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Summary
|
|
310
|
+
if (result.summary) {
|
|
311
|
+
lines.push('');
|
|
312
|
+
const { totalEntryPoints, totalFunctions } = result.summary;
|
|
313
|
+
if (totalFunctions > 0) {
|
|
314
|
+
lines.push(`Summary: ${totalEntryPoints} entry point${totalEntryPoints !== 1 ? 's' : ''} reach${totalEntryPoints === 1 ? 'es' : ''} ${result.root} through ${totalFunctions} intermediate function${totalFunctions !== 1 ? 's' : ''}`);
|
|
315
|
+
} else {
|
|
316
|
+
lines.push('Summary: No callers found — this function is itself an entry point.');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (hasTruncation) {
|
|
321
|
+
const allHint = options.allHint || 'Use --all to show all.';
|
|
322
|
+
lines.push(`\nSome results truncated. ${allHint}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (result.includeMethods === false) {
|
|
326
|
+
lines.push('\nNote: obj.method() calls excluded. Use --include-methods to include them.');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return lines.join('\n');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Format reverse-trace command output - JSON
|
|
334
|
+
*/
|
|
335
|
+
function formatReverseTraceJson(result) {
|
|
336
|
+
if (!result) {
|
|
337
|
+
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
338
|
+
}
|
|
339
|
+
return JSON.stringify(result, null, 2);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Format affected-tests command output - text
|
|
344
|
+
*/
|
|
345
|
+
function formatAffectedTests(result, options = {}) {
|
|
346
|
+
if (!result) return 'Function not found.';
|
|
347
|
+
|
|
348
|
+
const lines = [];
|
|
349
|
+
const { summary } = result;
|
|
350
|
+
|
|
351
|
+
lines.push(`affected-tests: ${result.root}`);
|
|
352
|
+
lines.push('═'.repeat(60));
|
|
353
|
+
lines.push(`${result.file}:${result.line}`);
|
|
354
|
+
lines.push(`1 function changed → ${summary.totalAffected} functions affected (depth ${result.depth})`);
|
|
355
|
+
lines.push('');
|
|
356
|
+
|
|
357
|
+
if (result.testFiles.length === 0) {
|
|
358
|
+
lines.push('No test files found for any affected function.');
|
|
359
|
+
} else {
|
|
360
|
+
const MAX_TEST_FILES = options.all ? Infinity : 30;
|
|
361
|
+
const displayFiles = result.testFiles.slice(0, MAX_TEST_FILES);
|
|
362
|
+
const truncatedFiles = result.testFiles.length - displayFiles.length;
|
|
363
|
+
lines.push(`Test files to run (${summary.totalTestFiles}):`);
|
|
364
|
+
lines.push('');
|
|
365
|
+
for (const tf of displayFiles) {
|
|
366
|
+
lines.push(` ${tf.file} (covers: ${tf.coveredFunctions.join(', ')})`);
|
|
367
|
+
// Show up to 5 key matches per file
|
|
368
|
+
const keyMatches = tf.matches
|
|
369
|
+
.filter(m => m.matchType === 'call' || m.matchType === 'test-case')
|
|
370
|
+
.slice(0, 5);
|
|
371
|
+
for (const m of keyMatches) {
|
|
372
|
+
lines.push(` L${m.line}: ${m.content} [${m.matchType}]`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (truncatedFiles > 0) {
|
|
376
|
+
lines.push(`\n ... ${truncatedFiles} more test files (use file= and exclude= to narrow scope)`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (result.uncovered.length > 0) {
|
|
381
|
+
lines.push('');
|
|
382
|
+
lines.push(`Uncovered (${result.uncovered.length}): ${result.uncovered.join(', ')}`);
|
|
383
|
+
lines.push(' ⚠ These affected functions have no test references');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
lines.push('');
|
|
387
|
+
const pct = summary.totalAffected > 0
|
|
388
|
+
? Math.round(summary.coveredFunctions / summary.totalAffected * 100)
|
|
389
|
+
: 0;
|
|
390
|
+
lines.push(`Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} test files, ${summary.coveredFunctions}/${summary.totalAffected} functions covered (${pct}%)`);
|
|
391
|
+
|
|
392
|
+
if (result.warnings?.length > 0) {
|
|
393
|
+
lines.push('');
|
|
394
|
+
for (const w of result.warnings) lines.push(`Note: ${w.message}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return lines.join('\n');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function formatAffectedTestsJson(result) {
|
|
401
|
+
if (!result) {
|
|
402
|
+
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
403
|
+
}
|
|
404
|
+
return JSON.stringify(result, null, 2);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
module.exports = {
|
|
408
|
+
formatTrace,
|
|
409
|
+
formatTraceJson,
|
|
410
|
+
formatBlast,
|
|
411
|
+
formatBlastJson,
|
|
412
|
+
formatReverseTrace,
|
|
413
|
+
formatReverseTraceJson,
|
|
414
|
+
formatAffectedTests,
|
|
415
|
+
formatAffectedTestsJson,
|
|
416
|
+
};
|