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.
Files changed (44) hide show
  1. package/.claude/skills/ucn/SKILL.md +3 -1
  2. package/.github/workflows/ci.yml +15 -3
  3. package/.github/workflows/publish.yml +20 -8
  4. package/README.md +1 -0
  5. package/cli/index.js +165 -246
  6. package/core/analysis.js +1400 -0
  7. package/core/build-worker.js +194 -0
  8. package/core/cache.js +105 -7
  9. package/core/callers.js +194 -64
  10. package/core/deadcode.js +22 -66
  11. package/core/discovery.js +9 -54
  12. package/core/execute.js +139 -54
  13. package/core/graph.js +615 -0
  14. package/core/output/analysis-ext.js +271 -0
  15. package/core/output/analysis.js +491 -0
  16. package/core/output/extraction.js +188 -0
  17. package/core/output/find.js +355 -0
  18. package/core/output/graph.js +399 -0
  19. package/core/output/refactoring.js +293 -0
  20. package/core/output/reporting.js +331 -0
  21. package/core/output/search.js +307 -0
  22. package/core/output/shared.js +271 -0
  23. package/core/output/tracing.js +416 -0
  24. package/core/output.js +15 -3293
  25. package/core/parallel-build.js +165 -0
  26. package/core/project.js +299 -3633
  27. package/core/registry.js +59 -0
  28. package/core/reporting.js +258 -0
  29. package/core/search.js +890 -0
  30. package/core/stacktrace.js +1 -1
  31. package/core/tracing.js +631 -0
  32. package/core/verify.js +10 -13
  33. package/eslint.config.js +43 -0
  34. package/jsconfig.json +10 -0
  35. package/languages/go.js +21 -2
  36. package/languages/html.js +8 -0
  37. package/languages/index.js +102 -40
  38. package/languages/java.js +13 -0
  39. package/languages/javascript.js +17 -1
  40. package/languages/python.js +14 -0
  41. package/languages/rust.js +13 -0
  42. package/languages/utils.js +1 -1
  43. package/mcp/server.js +45 -28
  44. 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
+ };