ucn 3.8.12 → 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 +15 -3
- package/.github/workflows/publish.yml +20 -8
- 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,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/graph.js - File dependency formatters
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { lineRange, formatFileError } = require('./shared');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format imports command output - text
|
|
9
|
+
*/
|
|
10
|
+
function formatImports(imports, filePath) {
|
|
11
|
+
if (imports?.error) return formatFileError(imports, filePath);
|
|
12
|
+
const lines = [`Imports in ${filePath}:\n`];
|
|
13
|
+
|
|
14
|
+
const internal = imports.filter(i => !i.isExternal && !i.isDynamic);
|
|
15
|
+
const external = imports.filter(i => i.isExternal && !i.isDynamic);
|
|
16
|
+
const dynamic = imports.filter(i => i.isDynamic);
|
|
17
|
+
|
|
18
|
+
if (internal.length > 0) {
|
|
19
|
+
lines.push('INTERNAL:');
|
|
20
|
+
for (const imp of internal) {
|
|
21
|
+
lines.push(` ${imp.module}`);
|
|
22
|
+
if (imp.resolved) {
|
|
23
|
+
lines.push(` -> ${imp.resolved}`);
|
|
24
|
+
}
|
|
25
|
+
if (imp.names && imp.names.length > 0 && imp.names[0] !== '*') {
|
|
26
|
+
lines.push(` ${imp.names.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (external.length > 0) {
|
|
32
|
+
if (internal.length > 0) lines.push('');
|
|
33
|
+
lines.push('EXTERNAL:');
|
|
34
|
+
for (const imp of external) {
|
|
35
|
+
lines.push(` ${imp.module}`);
|
|
36
|
+
if (imp.names && imp.names.length > 0) {
|
|
37
|
+
lines.push(` ${imp.names.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (dynamic.length > 0) {
|
|
43
|
+
if (internal.length > 0 || external.length > 0) lines.push('');
|
|
44
|
+
lines.push('DYNAMIC (unresolved):');
|
|
45
|
+
for (const imp of dynamic) {
|
|
46
|
+
lines.push(` ${imp.module || '(variable)'}`);
|
|
47
|
+
if (imp.names && imp.names.length > 0) {
|
|
48
|
+
lines.push(` ${imp.names.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format imports as JSON
|
|
58
|
+
*/
|
|
59
|
+
function formatImportsJson(imports, filePath) {
|
|
60
|
+
if (imports?.error) return JSON.stringify({ found: false, error: imports.error, file: imports.filePath || filePath }, null, 2);
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
file: filePath,
|
|
63
|
+
importCount: imports.length,
|
|
64
|
+
imports: imports.map(i => ({
|
|
65
|
+
module: i.module,
|
|
66
|
+
names: i.names,
|
|
67
|
+
type: i.type,
|
|
68
|
+
resolved: i.resolved || null,
|
|
69
|
+
isDynamic: !!i.isDynamic
|
|
70
|
+
}))
|
|
71
|
+
}, null, 2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format exporters command output - text
|
|
76
|
+
*/
|
|
77
|
+
function formatExporters(exporters, filePath) {
|
|
78
|
+
if (exporters?.error) return formatFileError(exporters, filePath);
|
|
79
|
+
const lines = [`Files that import ${filePath}:\n`];
|
|
80
|
+
|
|
81
|
+
if (exporters.length === 0) {
|
|
82
|
+
lines.push(' (none found)');
|
|
83
|
+
} else {
|
|
84
|
+
for (const exp of exporters) {
|
|
85
|
+
if (exp.importLine) {
|
|
86
|
+
lines.push(` ${exp.file}:${exp.importLine}`);
|
|
87
|
+
} else {
|
|
88
|
+
lines.push(` ${exp.file}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Format exporters as JSON
|
|
98
|
+
*/
|
|
99
|
+
function formatExportersJson(exporters, filePath) {
|
|
100
|
+
if (exporters?.error) return JSON.stringify({ found: false, error: exporters.error, file: exporters.filePath || filePath }, null, 2);
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
file: filePath,
|
|
103
|
+
importerCount: exporters.length,
|
|
104
|
+
importers: exporters
|
|
105
|
+
}, null, 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format file-exports command output
|
|
110
|
+
*/
|
|
111
|
+
function formatFileExports(exports, filePath) {
|
|
112
|
+
if (exports?.error) return formatFileError(exports, filePath);
|
|
113
|
+
if (exports.length === 0) return `No exports found in ${filePath}`;
|
|
114
|
+
|
|
115
|
+
const lines = [];
|
|
116
|
+
lines.push(`Exports from ${filePath}:\n`);
|
|
117
|
+
for (const exp of exports) {
|
|
118
|
+
lines.push(` ${lineRange(exp.startLine, exp.endLine)} ${exp.signature || exp.name}`);
|
|
119
|
+
}
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatFileExportsJson(result) {
|
|
124
|
+
if (!result) return JSON.stringify({ found: false });
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
meta: { command: 'fileExports', file: result.file },
|
|
127
|
+
data: {
|
|
128
|
+
file: result.file,
|
|
129
|
+
exports: result.exports || [],
|
|
130
|
+
reExports: result.reExports || [],
|
|
131
|
+
},
|
|
132
|
+
}, null, 2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format api command output - text
|
|
137
|
+
*/
|
|
138
|
+
function formatApi(symbols, filePath) {
|
|
139
|
+
const title = filePath
|
|
140
|
+
? `Exports from ${filePath}:`
|
|
141
|
+
: 'Project API (exported symbols):';
|
|
142
|
+
const lines = [title + '\n'];
|
|
143
|
+
|
|
144
|
+
if (symbols.length === 0) {
|
|
145
|
+
lines.push(' (none found)');
|
|
146
|
+
if (filePath && filePath.endsWith('.py')) {
|
|
147
|
+
lines.push('');
|
|
148
|
+
lines.push('Note: Python requires __all__ for export detection. Use \'toc\' command to see all functions/classes.');
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// Group by file
|
|
152
|
+
const byFile = new Map();
|
|
153
|
+
for (const sym of symbols) {
|
|
154
|
+
if (!byFile.has(sym.file)) {
|
|
155
|
+
byFile.set(sym.file, []);
|
|
156
|
+
}
|
|
157
|
+
byFile.get(sym.file).push(sym);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const [file, syms] of byFile) {
|
|
161
|
+
lines.push(file);
|
|
162
|
+
for (const s of syms) {
|
|
163
|
+
const sig = s.signature || `${s.type} ${s.name}`;
|
|
164
|
+
lines.push(` ${lineRange(s.startLine, s.endLine)} ${sig}`);
|
|
165
|
+
}
|
|
166
|
+
lines.push('');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return lines.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Format api as JSON
|
|
175
|
+
*/
|
|
176
|
+
function formatApiJson(symbols, filePath) {
|
|
177
|
+
return JSON.stringify({
|
|
178
|
+
...(filePath && { file: filePath }),
|
|
179
|
+
exportCount: symbols.length,
|
|
180
|
+
exports: symbols.map(s => ({
|
|
181
|
+
name: s.name,
|
|
182
|
+
type: s.type,
|
|
183
|
+
file: s.file,
|
|
184
|
+
startLine: s.startLine,
|
|
185
|
+
endLine: s.endLine,
|
|
186
|
+
...(s.params && { params: s.params }),
|
|
187
|
+
...(s.returnType && { returnType: s.returnType }),
|
|
188
|
+
...(s.signature && { signature: s.signature })
|
|
189
|
+
}))
|
|
190
|
+
}, null, 2);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Format graph command output
|
|
195
|
+
* @param {object} graph - Graph data
|
|
196
|
+
* @param {object} [options] - Formatting options
|
|
197
|
+
* @param {boolean} [options.showAll] - Show all children (no truncation)
|
|
198
|
+
* @param {number} [options.maxDepth] - Maximum depth for tree traversal
|
|
199
|
+
*/
|
|
200
|
+
function formatGraph(graph, options = {}) {
|
|
201
|
+
// Support legacy signature: formatGraph(graph, showAll)
|
|
202
|
+
if (typeof options === 'boolean') {
|
|
203
|
+
options = { showAll: options };
|
|
204
|
+
}
|
|
205
|
+
if (graph?.error) return formatFileError(graph);
|
|
206
|
+
if (graph.nodes.length === 0) {
|
|
207
|
+
const file = options.file || graph.root || '';
|
|
208
|
+
return file ? `File not found: ${file}` : 'File not found.';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const rootEntry = graph.nodes.find(n => n.file === graph.root);
|
|
212
|
+
const rootRelPath = rootEntry ? rootEntry.relativePath : graph.root;
|
|
213
|
+
const lines = [];
|
|
214
|
+
|
|
215
|
+
const showAll = options.showAll || false;
|
|
216
|
+
const maxChildren = showAll ? Infinity : 8;
|
|
217
|
+
const maxDepth = options.maxDepth !== undefined ? options.maxDepth : Infinity;
|
|
218
|
+
|
|
219
|
+
function printTree(nodes, edges, rootFile) {
|
|
220
|
+
const visited = new Set(); // all nodes ever printed (for diamond dep detection)
|
|
221
|
+
const ancestors = new Set(); // current path from root (for true circular detection)
|
|
222
|
+
let truncatedNodes = 0;
|
|
223
|
+
let depthLimited = false;
|
|
224
|
+
|
|
225
|
+
function printNode(file, indent = 0, isLast = true) {
|
|
226
|
+
const fileEntry = nodes.find(n => n.file === file);
|
|
227
|
+
const relPath = fileEntry ? fileEntry.relativePath : file;
|
|
228
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
229
|
+
const prefix = indent === 0 ? '' : ' '.repeat(indent - 1) + connector;
|
|
230
|
+
|
|
231
|
+
if (ancestors.has(file)) {
|
|
232
|
+
lines.push(`${prefix}${relPath} (circular)`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (visited.has(file)) {
|
|
236
|
+
lines.push(`${prefix}${relPath} (already shown)`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
visited.add(file);
|
|
240
|
+
|
|
241
|
+
if (indent > maxDepth) {
|
|
242
|
+
depthLimited = true;
|
|
243
|
+
lines.push(`${prefix}${relPath} ...`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
lines.push(`${prefix}${relPath}`);
|
|
248
|
+
|
|
249
|
+
ancestors.add(file);
|
|
250
|
+
const fileEdges = edges.filter(e => e.from === file);
|
|
251
|
+
const displayEdges = fileEdges.slice(0, maxChildren);
|
|
252
|
+
const hiddenCount = fileEdges.length - displayEdges.length;
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < displayEdges.length; i++) {
|
|
255
|
+
const childIsLast = i === displayEdges.length - 1 && hiddenCount === 0;
|
|
256
|
+
printNode(displayEdges[i].to, indent + 1, childIsLast);
|
|
257
|
+
}
|
|
258
|
+
ancestors.delete(file);
|
|
259
|
+
|
|
260
|
+
if (hiddenCount > 0) {
|
|
261
|
+
truncatedNodes += hiddenCount;
|
|
262
|
+
lines.push(`${' '.repeat(indent)}└── ... and ${hiddenCount} more`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
printNode(rootFile);
|
|
267
|
+
return { truncatedNodes, depthLimited };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (graph.direction === 'both' && graph.imports && graph.importers) {
|
|
271
|
+
const importCount = graph.imports.edges.filter(e => e.from === graph.root).length;
|
|
272
|
+
const importerCount = graph.importers.edges.filter(e => e.from === graph.root).length;
|
|
273
|
+
|
|
274
|
+
lines.push(`Dependency graph for ${rootRelPath}`);
|
|
275
|
+
lines.push('═'.repeat(60));
|
|
276
|
+
|
|
277
|
+
let totalTruncated = 0;
|
|
278
|
+
let anyDepthLimited = false;
|
|
279
|
+
|
|
280
|
+
lines.push(`\nIMPORTS (what this file depends on): ${importCount} files`);
|
|
281
|
+
if (importCount > 0) {
|
|
282
|
+
const r = printTree(graph.imports.nodes, graph.imports.edges, graph.root);
|
|
283
|
+
totalTruncated += r.truncatedNodes;
|
|
284
|
+
anyDepthLimited = anyDepthLimited || r.depthLimited;
|
|
285
|
+
} else {
|
|
286
|
+
lines.push(' (none)');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
lines.push(`\nIMPORTERS (what depends on this file): ${importerCount} files`);
|
|
290
|
+
if (importerCount > 0) {
|
|
291
|
+
const r = printTree(graph.importers.nodes, graph.importers.edges, graph.root);
|
|
292
|
+
totalTruncated += r.truncatedNodes;
|
|
293
|
+
anyDepthLimited = anyDepthLimited || r.depthLimited;
|
|
294
|
+
} else {
|
|
295
|
+
lines.push(' (none)');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (anyDepthLimited || totalTruncated > 0) {
|
|
299
|
+
lines.push('\n' + '─'.repeat(60));
|
|
300
|
+
if (anyDepthLimited) {
|
|
301
|
+
const depthHint = options.depthHint || `Use --depth=N for deeper graph.`;
|
|
302
|
+
lines.push(`Depth limited to ${maxDepth}. ${depthHint}`);
|
|
303
|
+
}
|
|
304
|
+
if (totalTruncated > 0) {
|
|
305
|
+
const allHint = options.allHint || 'Use --all to show all children.';
|
|
306
|
+
lines.push(`${totalTruncated} nodes hidden. ${allHint}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
lines.push(`Dependency graph for ${rootRelPath}`);
|
|
311
|
+
lines.push('═'.repeat(60));
|
|
312
|
+
|
|
313
|
+
const { truncatedNodes, depthLimited } = printTree(graph.nodes, graph.edges, graph.root);
|
|
314
|
+
|
|
315
|
+
if (depthLimited || truncatedNodes > 0) {
|
|
316
|
+
lines.push('\n' + '─'.repeat(60));
|
|
317
|
+
if (depthLimited) {
|
|
318
|
+
const depthHint = options.depthHint || `Use --depth=N for deeper graph.`;
|
|
319
|
+
lines.push(`Depth limited to ${maxDepth}. ${depthHint}`);
|
|
320
|
+
}
|
|
321
|
+
if (truncatedNodes > 0) {
|
|
322
|
+
const allHint = options.allHint || 'Use --all to show all children.';
|
|
323
|
+
lines.push(`${truncatedNodes} nodes hidden. ${allHint} Graph has ${graph.nodes.length} total files.`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return lines.join('\n');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Format dependency graph as JSON
|
|
333
|
+
*/
|
|
334
|
+
function formatGraphJson(graph) {
|
|
335
|
+
if (graph?.error) return JSON.stringify({ found: false, error: graph.error, file: graph.filePath }, null, 2);
|
|
336
|
+
const result = {
|
|
337
|
+
root: graph.root,
|
|
338
|
+
direction: graph.direction,
|
|
339
|
+
nodes: graph.nodes,
|
|
340
|
+
edges: graph.edges
|
|
341
|
+
};
|
|
342
|
+
if (graph.imports) result.imports = graph.imports;
|
|
343
|
+
if (graph.importers) result.importers = graph.importers;
|
|
344
|
+
return JSON.stringify(result, null, 2);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function formatCircularDeps(result) {
|
|
348
|
+
if (!result) return 'No results.';
|
|
349
|
+
const lines = [];
|
|
350
|
+
|
|
351
|
+
lines.push('Circular dependencies');
|
|
352
|
+
lines.push('═'.repeat(60));
|
|
353
|
+
|
|
354
|
+
if (result.fileFilter) {
|
|
355
|
+
lines.push(`Filtered to cycles involving: ${result.fileFilter}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const scannedCount = result.filesWithImports != null ? result.filesWithImports : result.totalFiles;
|
|
359
|
+
|
|
360
|
+
if (result.cycles.length === 0) {
|
|
361
|
+
lines.push('');
|
|
362
|
+
lines.push('No circular dependencies found.');
|
|
363
|
+
lines.push(`Scanned ${scannedCount} files with import relationships.`);
|
|
364
|
+
return lines.join('\n');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (let i = 0; i < result.cycles.length; i++) {
|
|
368
|
+
const cycle = result.cycles[i];
|
|
369
|
+
lines.push('');
|
|
370
|
+
lines.push(`Cycle ${i + 1} (${cycle.length} files):`);
|
|
371
|
+
lines.push(` ${cycle.files.join(' → ')} → ${cycle.files[0]}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
lines.push('');
|
|
375
|
+
const { totalCycles, filesInCycles } = result.summary;
|
|
376
|
+
lines.push(`Summary: ${totalCycles} circular dependency chain${totalCycles !== 1 ? 's' : ''} involving ${filesInCycles} file${filesInCycles !== 1 ? 's' : ''} (${scannedCount} files with imports scanned).`);
|
|
377
|
+
|
|
378
|
+
return lines.join('\n');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function formatCircularDepsJson(result) {
|
|
382
|
+
if (!result) return JSON.stringify({ error: 'No results' }, null, 2);
|
|
383
|
+
return JSON.stringify(result, null, 2);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = {
|
|
387
|
+
formatImports,
|
|
388
|
+
formatImportsJson,
|
|
389
|
+
formatExporters,
|
|
390
|
+
formatExportersJson,
|
|
391
|
+
formatFileExports,
|
|
392
|
+
formatFileExportsJson,
|
|
393
|
+
formatApi,
|
|
394
|
+
formatApiJson,
|
|
395
|
+
formatGraph,
|
|
396
|
+
formatGraphJson,
|
|
397
|
+
formatCircularDeps,
|
|
398
|
+
formatCircularDepsJson,
|
|
399
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/refactoring.js - Verify/plan/stacktrace formatters
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format plan command output - text
|
|
7
|
+
* Shows before/after signatures and all changes needed
|
|
8
|
+
*/
|
|
9
|
+
function formatPlan(plan, options = {}) {
|
|
10
|
+
if (!plan) {
|
|
11
|
+
return 'Function not found.';
|
|
12
|
+
}
|
|
13
|
+
if (!plan.found) {
|
|
14
|
+
return `Function "${plan.function}" not found.`;
|
|
15
|
+
}
|
|
16
|
+
if (plan.error) {
|
|
17
|
+
return `Error: ${plan.error}\nCurrent parameters: ${plan.currentParams?.join(', ') || 'none'}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lines = [];
|
|
21
|
+
|
|
22
|
+
// Header
|
|
23
|
+
lines.push(`Refactoring plan: ${plan.operation}`);
|
|
24
|
+
lines.push('═'.repeat(60));
|
|
25
|
+
lines.push(`${plan.file}:${plan.startLine}`);
|
|
26
|
+
lines.push('');
|
|
27
|
+
|
|
28
|
+
// Before/After
|
|
29
|
+
lines.push('SIGNATURE CHANGE:');
|
|
30
|
+
lines.push(` Before: ${plan.before.signature}`);
|
|
31
|
+
lines.push(` After: ${plan.after.signature}`);
|
|
32
|
+
lines.push('');
|
|
33
|
+
|
|
34
|
+
// Summary
|
|
35
|
+
lines.push(`CHANGES NEEDED: ${plan.totalChanges}`);
|
|
36
|
+
lines.push(` Files affected: ${plan.filesAffected}`);
|
|
37
|
+
if (plan.scopeWarning) {
|
|
38
|
+
lines.push(` Note: ${plan.scopeWarning.hint}`);
|
|
39
|
+
}
|
|
40
|
+
lines.push('');
|
|
41
|
+
|
|
42
|
+
// Group by file
|
|
43
|
+
const byFile = new Map();
|
|
44
|
+
for (const change of plan.changes) {
|
|
45
|
+
if (!byFile.has(change.file)) {
|
|
46
|
+
byFile.set(change.file, []);
|
|
47
|
+
}
|
|
48
|
+
byFile.get(change.file).push(change);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lines.push('BY FILE:');
|
|
52
|
+
for (const [file, changes] of byFile) {
|
|
53
|
+
lines.push(`\n${file} (${changes.length} changes)`);
|
|
54
|
+
for (const change of changes) {
|
|
55
|
+
lines.push(` :${change.line}`);
|
|
56
|
+
lines.push(` ${change.expression}`);
|
|
57
|
+
lines.push(` → ${change.suggestion}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format plan command output - JSON
|
|
66
|
+
*/
|
|
67
|
+
function formatPlanJson(plan) {
|
|
68
|
+
if (!plan) {
|
|
69
|
+
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
70
|
+
}
|
|
71
|
+
if (!plan.found) {
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
found: false,
|
|
74
|
+
error: plan.error || `Function "${plan.function}" not found.`,
|
|
75
|
+
...(plan.currentParams && { currentParams: plan.currentParams })
|
|
76
|
+
}, null, 2);
|
|
77
|
+
}
|
|
78
|
+
if (plan.error) {
|
|
79
|
+
return JSON.stringify({
|
|
80
|
+
found: true,
|
|
81
|
+
error: plan.error,
|
|
82
|
+
...(plan.currentParams && { currentParams: plan.currentParams })
|
|
83
|
+
}, null, 2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return JSON.stringify({
|
|
87
|
+
found: true,
|
|
88
|
+
function: plan.function,
|
|
89
|
+
file: plan.file,
|
|
90
|
+
startLine: plan.startLine,
|
|
91
|
+
operation: plan.operation,
|
|
92
|
+
before: { signature: plan.before.signature },
|
|
93
|
+
after: { signature: plan.after.signature },
|
|
94
|
+
totalChanges: plan.totalChanges,
|
|
95
|
+
filesAffected: plan.filesAffected,
|
|
96
|
+
changes: plan.changes.map(c => ({
|
|
97
|
+
file: c.file,
|
|
98
|
+
line: c.line,
|
|
99
|
+
expression: c.expression,
|
|
100
|
+
suggestion: c.suggestion
|
|
101
|
+
}))
|
|
102
|
+
}, null, 2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format verify command output - text
|
|
107
|
+
* Shows call site validation results
|
|
108
|
+
*/
|
|
109
|
+
function formatVerify(result, options = {}) {
|
|
110
|
+
if (!result) {
|
|
111
|
+
return 'Function not found.';
|
|
112
|
+
}
|
|
113
|
+
if (!result.found) {
|
|
114
|
+
return `Function "${result.function}" not found.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lines = [];
|
|
118
|
+
|
|
119
|
+
// Header
|
|
120
|
+
lines.push(`Verification: ${result.function}`);
|
|
121
|
+
lines.push('═'.repeat(60));
|
|
122
|
+
lines.push(`${result.file}:${result.startLine}`);
|
|
123
|
+
lines.push(result.signature);
|
|
124
|
+
lines.push('');
|
|
125
|
+
|
|
126
|
+
// Expected args
|
|
127
|
+
const { min, max } = result.expectedArgs;
|
|
128
|
+
const expectedStr = min === max ? `${min}` : `${min}-${max}`;
|
|
129
|
+
lines.push(`Expected arguments: ${expectedStr}`);
|
|
130
|
+
lines.push('');
|
|
131
|
+
|
|
132
|
+
// Summary
|
|
133
|
+
const status = result.mismatches === 0 ? '✓ All calls valid' : '✗ Mismatches found';
|
|
134
|
+
lines.push(`STATUS: ${status}`);
|
|
135
|
+
lines.push(` Total calls: ${result.totalCalls}`);
|
|
136
|
+
lines.push(` Valid: ${result.valid}`);
|
|
137
|
+
lines.push(` Mismatches: ${result.mismatches}`);
|
|
138
|
+
lines.push(` Uncertain: ${result.uncertain}`);
|
|
139
|
+
if (result.scopeWarning) {
|
|
140
|
+
lines.push(` Note: ${result.scopeWarning.hint}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Show mismatches
|
|
144
|
+
if (result.mismatchDetails.length > 0) {
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push('MISMATCHES:');
|
|
147
|
+
for (const m of result.mismatchDetails) {
|
|
148
|
+
lines.push(` ${m.file}:${m.line}`);
|
|
149
|
+
lines.push(` ${m.expression}`);
|
|
150
|
+
lines.push(` Expected ${m.expected}, got ${m.actual}: [${m.args?.join(', ') || ''}]`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Show uncertain
|
|
155
|
+
if (result.uncertainDetails.length > 0) {
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('UNCERTAIN (manual check needed):');
|
|
158
|
+
for (const u of result.uncertainDetails) {
|
|
159
|
+
lines.push(` ${u.file}:${u.line}`);
|
|
160
|
+
lines.push(` ${u.expression}`);
|
|
161
|
+
lines.push(` Reason: ${u.reason}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format verify command output - JSON
|
|
170
|
+
*/
|
|
171
|
+
function formatVerifyJson(result) {
|
|
172
|
+
if (!result) {
|
|
173
|
+
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
174
|
+
}
|
|
175
|
+
if (!result.found) {
|
|
176
|
+
return JSON.stringify({ found: false, error: `Function "${result.function}" not found.` }, null, 2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return JSON.stringify({
|
|
180
|
+
found: true,
|
|
181
|
+
function: result.function,
|
|
182
|
+
file: result.file,
|
|
183
|
+
startLine: result.startLine,
|
|
184
|
+
signature: result.signature,
|
|
185
|
+
expectedArgs: result.expectedArgs,
|
|
186
|
+
totalCalls: result.totalCalls,
|
|
187
|
+
valid: result.valid,
|
|
188
|
+
mismatches: result.mismatches,
|
|
189
|
+
uncertain: result.uncertain,
|
|
190
|
+
mismatchDetails: result.mismatchDetails.map(m => ({
|
|
191
|
+
file: m.file,
|
|
192
|
+
line: m.line,
|
|
193
|
+
expression: m.expression,
|
|
194
|
+
expected: m.expected,
|
|
195
|
+
actual: m.actual,
|
|
196
|
+
args: m.args || []
|
|
197
|
+
})),
|
|
198
|
+
uncertainDetails: result.uncertainDetails.map(u => ({
|
|
199
|
+
file: u.file,
|
|
200
|
+
line: u.line,
|
|
201
|
+
expression: u.expression,
|
|
202
|
+
reason: u.reason
|
|
203
|
+
}))
|
|
204
|
+
}, null, 2);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Format stack trace command output - text
|
|
209
|
+
* Shows code context for each stack frame
|
|
210
|
+
*/
|
|
211
|
+
function formatStackTrace(result) {
|
|
212
|
+
if (!result || result.frameCount === 0) {
|
|
213
|
+
return 'No stack frames found in input.';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const lines = [];
|
|
217
|
+
lines.push(`Stack trace: ${result.frameCount} frames`);
|
|
218
|
+
lines.push('═'.repeat(60));
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < result.frames.length; i++) {
|
|
221
|
+
const frame = result.frames[i];
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push(`Frame ${i}: ${frame.function || '(anonymous)'}`);
|
|
224
|
+
lines.push('─'.repeat(40));
|
|
225
|
+
|
|
226
|
+
if (frame.found) {
|
|
227
|
+
lines.push(` ${frame.resolvedFile}:${frame.line}`);
|
|
228
|
+
|
|
229
|
+
// Show code context
|
|
230
|
+
if (frame.context) {
|
|
231
|
+
lines.push('');
|
|
232
|
+
for (const ctx of frame.context) {
|
|
233
|
+
const marker = ctx.isCurrent ? '→ ' : ' ';
|
|
234
|
+
const lineNum = ctx.line.toString().padStart(4);
|
|
235
|
+
lines.push(` ${marker}${lineNum} │ ${ctx.code}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Show function info if available
|
|
240
|
+
if (frame.functionInfo) {
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push(` In: ${frame.functionInfo.name}(${frame.functionInfo.params || ''})`);
|
|
243
|
+
lines.push(` Range: ${frame.functionInfo.startLine}-${frame.functionInfo.endLine}`);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
lines.push(` ${frame.file}:${frame.line} (file not found in project)`);
|
|
247
|
+
lines.push(` Raw: ${frame.raw}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return lines.join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Format stack trace command output - JSON
|
|
256
|
+
*/
|
|
257
|
+
function formatStackTraceJson(result) {
|
|
258
|
+
if (!result || result.frameCount === 0) {
|
|
259
|
+
return JSON.stringify({ frameCount: 0, frames: [] }, null, 2);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return JSON.stringify({
|
|
263
|
+
frameCount: result.frameCount,
|
|
264
|
+
frames: result.frames.map(f => ({
|
|
265
|
+
function: f.function || null,
|
|
266
|
+
file: f.file,
|
|
267
|
+
line: f.line,
|
|
268
|
+
found: !!f.found,
|
|
269
|
+
...(f.resolvedFile && { resolvedFile: f.resolvedFile }),
|
|
270
|
+
...(f.context && { context: f.context.map(c => ({
|
|
271
|
+
line: c.line,
|
|
272
|
+
code: c.code,
|
|
273
|
+
isCurrent: !!c.isCurrent
|
|
274
|
+
})) }),
|
|
275
|
+
...(f.functionInfo && { functionInfo: {
|
|
276
|
+
name: f.functionInfo.name,
|
|
277
|
+
params: f.functionInfo.params || null,
|
|
278
|
+
startLine: f.functionInfo.startLine,
|
|
279
|
+
endLine: f.functionInfo.endLine
|
|
280
|
+
} }),
|
|
281
|
+
...(f.raw && { raw: f.raw })
|
|
282
|
+
}))
|
|
283
|
+
}, null, 2);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
formatPlan,
|
|
288
|
+
formatPlanJson,
|
|
289
|
+
formatVerify,
|
|
290
|
+
formatVerifyJson,
|
|
291
|
+
formatStackTrace,
|
|
292
|
+
formatStackTraceJson,
|
|
293
|
+
};
|