ucn 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ucn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/.claude/skills/ucn/SKILL.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +135 -0
  4. package/cli/index.js +2437 -0
  5. package/core/discovery.js +513 -0
  6. package/core/imports.js +558 -0
  7. package/core/output.js +1274 -0
  8. package/core/parser.js +279 -0
  9. package/core/project.js +3261 -0
  10. package/index.js +52 -0
  11. package/languages/go.js +653 -0
  12. package/languages/index.js +267 -0
  13. package/languages/java.js +826 -0
  14. package/languages/javascript.js +1346 -0
  15. package/languages/python.js +667 -0
  16. package/languages/rust.js +950 -0
  17. package/languages/utils.js +457 -0
  18. package/package.json +42 -0
  19. package/test/fixtures/go/go.mod +3 -0
  20. package/test/fixtures/go/main.go +257 -0
  21. package/test/fixtures/go/service.go +187 -0
  22. package/test/fixtures/java/DataService.java +279 -0
  23. package/test/fixtures/java/Main.java +287 -0
  24. package/test/fixtures/java/Utils.java +199 -0
  25. package/test/fixtures/java/pom.xml +6 -0
  26. package/test/fixtures/javascript/main.js +109 -0
  27. package/test/fixtures/javascript/package.json +1 -0
  28. package/test/fixtures/javascript/service.js +88 -0
  29. package/test/fixtures/javascript/utils.js +67 -0
  30. package/test/fixtures/python/main.py +198 -0
  31. package/test/fixtures/python/pyproject.toml +3 -0
  32. package/test/fixtures/python/service.py +166 -0
  33. package/test/fixtures/python/utils.py +118 -0
  34. package/test/fixtures/rust/Cargo.toml +3 -0
  35. package/test/fixtures/rust/main.rs +253 -0
  36. package/test/fixtures/rust/service.rs +210 -0
  37. package/test/fixtures/rust/utils.rs +154 -0
  38. package/test/fixtures/typescript/main.ts +154 -0
  39. package/test/fixtures/typescript/package.json +1 -0
  40. package/test/fixtures/typescript/repository.ts +149 -0
  41. package/test/fixtures/typescript/types.ts +114 -0
  42. package/test/parser.test.js +3661 -0
  43. package/test/public-repos-test.js +477 -0
  44. package/test/systematic-test.js +619 -0
  45. package/ucn.js +8 -0
package/core/output.js ADDED
@@ -0,0 +1,1274 @@
1
+ /**
2
+ * core/output.js - Output formatting utilities
3
+ *
4
+ * KEY PRINCIPLE: Never truncate critical information.
5
+ * Full expressions, full signatures, full context.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Normalize parameters for display
13
+ * Collapses multiline params to single line
14
+ * @param {string} params - Raw params string
15
+ * @returns {string} - Normalized params (NO truncation)
16
+ */
17
+ function normalizeParams(params) {
18
+ if (!params || params === '...') return params || '...';
19
+ // Collapse whitespace (newlines, tabs, multiple spaces) to single space
20
+ return params.replace(/\s+/g, ' ').trim();
21
+ }
22
+
23
+ /**
24
+ * Format a line number for display
25
+ * @param {number} line - 1-indexed line number
26
+ * @param {number} width - Padding width
27
+ * @returns {string}
28
+ */
29
+ function lineNum(line, width = 4) {
30
+ return String(line).padStart(width);
31
+ }
32
+
33
+ /**
34
+ * Format a line range
35
+ * @param {number} start - 1-indexed start line
36
+ * @param {number} end - 1-indexed end line
37
+ * @returns {string}
38
+ */
39
+ function lineRange(start, end) {
40
+ return `[${lineNum(start)}-${lineNum(end)}]`;
41
+ }
42
+
43
+ /**
44
+ * Format a single line location
45
+ * @param {number} line - 1-indexed line number
46
+ * @returns {string}
47
+ */
48
+ function lineLoc(line) {
49
+ return `[${lineNum(line)}]`;
50
+ }
51
+
52
+ // ============================================================================
53
+ // TEXT FORMATTERS
54
+ // ============================================================================
55
+
56
+ /**
57
+ * Format function signature for TOC display
58
+ * @param {object} fn - Function definition
59
+ * @returns {string}
60
+ */
61
+ function formatFunctionSignature(fn) {
62
+ const parts = [];
63
+
64
+ // Modifiers
65
+ if (fn.modifiers && fn.modifiers.length > 0) {
66
+ parts.push(fn.modifiers.join(' '));
67
+ }
68
+
69
+ // Generator marker
70
+ if (fn.isGenerator) parts.push('*');
71
+
72
+ // Name
73
+ parts.push(fn.name);
74
+
75
+ // Generics
76
+ if (fn.generics) parts.push(fn.generics);
77
+
78
+ // Parameters - FULL, never truncated
79
+ const params = normalizeParams(fn.params);
80
+ parts.push(`(${params})`);
81
+
82
+ // Return type
83
+ if (fn.returnType) parts.push(`: ${fn.returnType}`);
84
+
85
+ // Arrow indicator
86
+ if (fn.isArrow) parts.push(' =>');
87
+
88
+ return parts.filter(p => p).join('');
89
+ }
90
+
91
+ /**
92
+ * Format class/type signature for TOC display
93
+ */
94
+ function formatClassSignature(cls) {
95
+ const parts = [cls.type, cls.name];
96
+
97
+ if (cls.generics) parts.push(cls.generics);
98
+ if (cls.extends) parts.push(`extends ${cls.extends}`);
99
+ if (cls.implements && cls.implements.length > 0) {
100
+ parts.push(`implements ${cls.implements.join(', ')}`);
101
+ }
102
+
103
+ return parts.join(' ');
104
+ }
105
+
106
+ /**
107
+ * Format class member for TOC display
108
+ */
109
+ function formatMemberSignature(member) {
110
+ const parts = [];
111
+
112
+ // Member type (static, get, set, private, etc.)
113
+ if (member.memberType && member.memberType !== 'method') {
114
+ parts.push(member.memberType);
115
+ }
116
+
117
+ // Async
118
+ if (member.isAsync) parts.push('async');
119
+
120
+ // Generator
121
+ if (member.isGenerator) parts.push('*');
122
+
123
+ // Name
124
+ parts.push(member.name);
125
+
126
+ // Parameters
127
+ if (member.params !== undefined) {
128
+ const params = normalizeParams(member.params);
129
+ parts.push(`(${params})`);
130
+ }
131
+
132
+ // Return type
133
+ if (member.returnType) parts.push(`: ${member.returnType}`);
134
+
135
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
136
+ }
137
+
138
+ /**
139
+ * Print section header
140
+ */
141
+ function header(title, char = '═') {
142
+ console.log(title);
143
+ console.log(char.repeat(60));
144
+ }
145
+
146
+ /**
147
+ * Print subheader
148
+ */
149
+ function subheader(title) {
150
+ console.log(title);
151
+ console.log('─'.repeat(40));
152
+ }
153
+
154
+ /**
155
+ * Print a usage/call site - FULL expression, never truncated
156
+ * @param {object} usage - Usage object
157
+ * @param {string} [relativePath] - Relative file path
158
+ */
159
+ function printUsage(usage, relativePath) {
160
+ const file = relativePath || usage.file;
161
+ // FULL content - this is the key improvement
162
+ console.log(` ${file}:${usage.line}`);
163
+ console.log(` ${usage.content.trim()}`);
164
+
165
+ // Context lines if provided
166
+ if (usage.before && usage.before.length > 0) {
167
+ for (const line of usage.before) {
168
+ console.log(` ... ${line.trim()}`);
169
+ }
170
+ }
171
+ if (usage.after && usage.after.length > 0) {
172
+ for (const line of usage.after) {
173
+ console.log(` ... ${line.trim()}`);
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Print definition with full signature
180
+ */
181
+ function printDefinition(def, relativePath) {
182
+ const file = relativePath || def.file;
183
+ console.log(` ${file}:${def.line}`);
184
+ if (def.signature) {
185
+ console.log(` ${def.signature}`);
186
+ }
187
+ }
188
+
189
+ // ============================================================================
190
+ // JSON FORMATTERS
191
+ // ============================================================================
192
+
193
+ /**
194
+ * Format TOC data as JSON
195
+ */
196
+ function formatTocJson(data) {
197
+ return JSON.stringify({
198
+ files: data.totalFiles,
199
+ lines: data.totalLines,
200
+ functions: data.totalFunctions,
201
+ classes: data.totalClasses,
202
+ state: data.totalState,
203
+ byFile: data.byFile.map(f => ({
204
+ file: f.file,
205
+ language: f.language,
206
+ lines: f.lines,
207
+ functions: f.functions.map(fn => ({
208
+ name: fn.name,
209
+ params: fn.params, // FULL params
210
+ paramsStructured: fn.paramsStructured || [],
211
+ startLine: fn.startLine,
212
+ endLine: fn.endLine,
213
+ modifiers: fn.modifiers || [],
214
+ ...(fn.returnType && { returnType: fn.returnType }),
215
+ ...(fn.generics && { generics: fn.generics }),
216
+ ...(fn.docstring && { docstring: fn.docstring }),
217
+ ...(fn.isArrow && { isArrow: true }),
218
+ ...(fn.isGenerator && { isGenerator: true })
219
+ })),
220
+ classes: f.classes.map(c => ({
221
+ name: c.name,
222
+ type: c.type,
223
+ startLine: c.startLine,
224
+ endLine: c.endLine,
225
+ modifiers: c.modifiers || [],
226
+ members: c.members || [],
227
+ ...(c.extends && { extends: c.extends }),
228
+ ...(c.implements && { implements: c.implements }),
229
+ ...(c.generics && { generics: c.generics }),
230
+ ...(c.docstring && { docstring: c.docstring })
231
+ })),
232
+ state: f.state.map(s => ({
233
+ name: s.name,
234
+ startLine: s.startLine,
235
+ endLine: s.endLine
236
+ }))
237
+ }))
238
+ }, null, 2);
239
+ }
240
+
241
+ /**
242
+ * Format symbol search results as JSON
243
+ */
244
+ function formatSymbolJson(symbols, query) {
245
+ return JSON.stringify({
246
+ query,
247
+ count: symbols.length,
248
+ results: symbols.map(s => ({
249
+ name: s.name,
250
+ type: s.type,
251
+ file: s.relativePath || s.file,
252
+ startLine: s.startLine,
253
+ endLine: s.endLine,
254
+ ...(s.params && { params: s.params }), // FULL params
255
+ ...(s.paramsStructured && { paramsStructured: s.paramsStructured }),
256
+ ...(s.returnType && { returnType: s.returnType }),
257
+ ...(s.modifiers && { modifiers: s.modifiers }),
258
+ ...(s.usageCount !== undefined && { usageCount: s.usageCount }),
259
+ ...(s.usageCounts !== undefined && { usageCounts: s.usageCounts })
260
+ }))
261
+ }, null, 2);
262
+ }
263
+
264
+ /**
265
+ * Format usages as JSON - FULL expressions, never truncated
266
+ */
267
+ function formatUsagesJson(usages, name) {
268
+ const definitions = usages.filter(u => u.isDefinition);
269
+ const refs = usages.filter(u => !u.isDefinition);
270
+
271
+ const calls = refs.filter(u => u.usageType === 'call');
272
+ const imports = refs.filter(u => u.usageType === 'import');
273
+ const references = refs.filter(u => u.usageType === 'reference');
274
+
275
+ const formatUsage = (u) => ({
276
+ file: u.relativePath || u.file,
277
+ line: u.line,
278
+ expression: u.content, // FULL expression - key improvement
279
+ ...(u.args && { args: u.args }), // Parsed arguments
280
+ ...(u.before && u.before.length > 0 && { before: u.before }),
281
+ ...(u.after && u.after.length > 0 && { after: u.after })
282
+ });
283
+
284
+ return JSON.stringify({
285
+ symbol: name,
286
+ definitionCount: definitions.length,
287
+ callCount: calls.length,
288
+ importCount: imports.length,
289
+ referenceCount: references.length,
290
+ totalUsages: refs.length,
291
+ definitions: definitions.map(d => ({
292
+ file: d.relativePath || d.file,
293
+ line: d.line,
294
+ signature: d.signature || null, // FULL signature
295
+ type: d.type || null,
296
+ ...(d.returnType && { returnType: d.returnType }),
297
+ ...(d.before && d.before.length > 0 && { before: d.before }),
298
+ ...(d.after && d.after.length > 0 && { after: d.after })
299
+ })),
300
+ calls: calls.map(formatUsage),
301
+ imports: imports.map(formatUsage),
302
+ references: references.map(formatUsage)
303
+ }, null, 2);
304
+ }
305
+
306
+ /**
307
+ * Format context (callers + callees) as JSON
308
+ */
309
+ function formatContextJson(context) {
310
+ return JSON.stringify({
311
+ function: context.function,
312
+ file: context.file,
313
+ callerCount: context.callers.length,
314
+ calleeCount: context.callees.length,
315
+ callers: context.callers.map(c => ({
316
+ file: c.relativePath || c.file,
317
+ line: c.line,
318
+ expression: c.content, // FULL expression
319
+ callerName: c.callerName
320
+ })),
321
+ callees: context.callees.map(c => ({
322
+ name: c.name,
323
+ type: c.type,
324
+ file: c.relativePath || c.file,
325
+ line: c.startLine,
326
+ params: c.params, // FULL params
327
+ weight: c.weight || 'normal' // Dependency weight: core, setup, utility
328
+ }))
329
+ }, null, 2);
330
+ }
331
+
332
+ /**
333
+ * Format extracted function as JSON
334
+ */
335
+ function formatFunctionJson(fn, code) {
336
+ return JSON.stringify({
337
+ name: fn.name,
338
+ params: fn.params, // FULL params
339
+ paramsStructured: fn.paramsStructured || [],
340
+ startLine: fn.startLine,
341
+ endLine: fn.endLine,
342
+ modifiers: fn.modifiers || [],
343
+ ...(fn.returnType && { returnType: fn.returnType }),
344
+ ...(fn.generics && { generics: fn.generics }),
345
+ ...(fn.docstring && { docstring: fn.docstring }),
346
+ ...(fn.isArrow && { isArrow: true }),
347
+ ...(fn.isGenerator && { isGenerator: true }),
348
+ code // FULL code
349
+ }, null, 2);
350
+ }
351
+
352
+ /**
353
+ * Format search results as JSON
354
+ */
355
+ function formatSearchJson(results, term) {
356
+ return JSON.stringify({
357
+ term,
358
+ totalMatches: results.reduce((sum, r) => sum + r.matches.length, 0),
359
+ files: results.map(r => ({
360
+ file: r.file,
361
+ matchCount: r.matches.length,
362
+ matches: r.matches.map(m => ({
363
+ line: m.line,
364
+ content: m.content // FULL content
365
+ }))
366
+ }))
367
+ }, null, 2);
368
+ }
369
+
370
+ /**
371
+ * Format imports as JSON
372
+ */
373
+ function formatImportsJson(imports, filePath) {
374
+ return JSON.stringify({
375
+ file: filePath,
376
+ importCount: imports.length,
377
+ imports: imports.map(i => ({
378
+ module: i.module,
379
+ names: i.names,
380
+ type: i.type,
381
+ resolved: i.resolved || null
382
+ }))
383
+ }, null, 2);
384
+ }
385
+
386
+ /**
387
+ * Format project stats as JSON
388
+ */
389
+ function formatStatsJson(stats) {
390
+ return JSON.stringify(stats, null, 2);
391
+ }
392
+
393
+ /**
394
+ * Format dependency graph as JSON
395
+ */
396
+ function formatGraphJson(graph) {
397
+ return JSON.stringify({
398
+ file: graph.file,
399
+ depth: graph.depth,
400
+ dependencies: graph.dependencies
401
+ }, null, 2);
402
+ }
403
+
404
+ /**
405
+ * Format smart extraction result as JSON
406
+ * Includes function + all dependencies
407
+ */
408
+ function formatSmartJson(result) {
409
+ return JSON.stringify({
410
+ target: {
411
+ name: result.target.name,
412
+ file: result.target.file,
413
+ startLine: result.target.startLine,
414
+ endLine: result.target.endLine,
415
+ params: result.target.params,
416
+ returnType: result.target.returnType,
417
+ code: result.target.code
418
+ },
419
+ dependencies: result.dependencies.map(d => ({
420
+ name: d.name,
421
+ type: d.type,
422
+ file: d.file,
423
+ startLine: d.startLine,
424
+ endLine: d.endLine,
425
+ params: d.params,
426
+ weight: d.weight, // core, setup, utility
427
+ callCount: d.callCount,
428
+ code: d.code
429
+ })),
430
+ types: result.types || []
431
+ }, null, 2);
432
+ }
433
+
434
+ // ============================================================================
435
+ // NEW FORMATTERS (v2 Migration)
436
+ // ============================================================================
437
+
438
+ /**
439
+ * Format imports command output - text
440
+ */
441
+ function formatImports(imports, filePath) {
442
+ const lines = [`Imports in ${filePath}:\n`];
443
+
444
+ const internal = imports.filter(i => !i.isExternal);
445
+ const external = imports.filter(i => i.isExternal);
446
+
447
+ if (internal.length > 0) {
448
+ lines.push('INTERNAL:');
449
+ for (const imp of internal) {
450
+ lines.push(` ${imp.module}`);
451
+ if (imp.resolved) {
452
+ lines.push(` -> ${imp.resolved}`);
453
+ }
454
+ if (imp.names && imp.names.length > 0 && imp.names[0] !== '*') {
455
+ lines.push(` ${imp.names.join(', ')}`);
456
+ }
457
+ }
458
+ }
459
+
460
+ if (external.length > 0) {
461
+ if (internal.length > 0) lines.push('');
462
+ lines.push('EXTERNAL:');
463
+ for (const imp of external) {
464
+ lines.push(` ${imp.module}`);
465
+ if (imp.names && imp.names.length > 0) {
466
+ lines.push(` ${imp.names.join(', ')}`);
467
+ }
468
+ }
469
+ }
470
+
471
+ return lines.join('\n');
472
+ }
473
+
474
+ /**
475
+ * Format exporters command output - text
476
+ */
477
+ function formatExporters(exporters, filePath) {
478
+ const lines = [`Files that import ${filePath}:\n`];
479
+
480
+ if (exporters.length === 0) {
481
+ lines.push(' (none found)');
482
+ } else {
483
+ for (const exp of exporters) {
484
+ if (exp.importLine) {
485
+ lines.push(` ${exp.file}:${exp.importLine}`);
486
+ } else {
487
+ lines.push(` ${exp.file}`);
488
+ }
489
+ }
490
+ }
491
+
492
+ return lines.join('\n');
493
+ }
494
+
495
+ /**
496
+ * Format typedef command output - text
497
+ */
498
+ function formatTypedef(types, name) {
499
+ const lines = [`Type definitions for "${name}":\n`];
500
+
501
+ if (types.length === 0) {
502
+ lines.push(' (none found)');
503
+ } else {
504
+ for (const t of types) {
505
+ lines.push(` ${t.relativePath}:${t.startLine} ${t.type} ${t.name}`);
506
+ if (t.usageCount !== undefined) {
507
+ lines.push(` (${t.usageCount} usages)`);
508
+ }
509
+ }
510
+ }
511
+
512
+ return lines.join('\n');
513
+ }
514
+
515
+ /**
516
+ * Format tests command output - text
517
+ */
518
+ function formatTests(tests, name) {
519
+ const lines = [`Tests for "${name}":\n`];
520
+
521
+ if (tests.length === 0) {
522
+ lines.push(' (no tests found)');
523
+ } else {
524
+ const totalMatches = tests.reduce((sum, t) => sum + t.matches.length, 0);
525
+ lines.push(`Found ${totalMatches} matches in ${tests.length} test file(s):\n`);
526
+
527
+ for (const testFile of tests) {
528
+ lines.push(testFile.file);
529
+ for (const match of testFile.matches) {
530
+ const typeLabel = match.matchType === 'test-case' ? '[test]' :
531
+ match.matchType === 'import' ? '[import]' :
532
+ match.matchType === 'call' ? '[call]' : '';
533
+ lines.push(` ${match.line}: ${typeLabel} ${match.content}`);
534
+ }
535
+ lines.push('');
536
+ }
537
+ }
538
+
539
+ return lines.join('\n');
540
+ }
541
+
542
+ /**
543
+ * Format api command output - text
544
+ */
545
+ function formatApi(symbols, filePath) {
546
+ const title = filePath
547
+ ? `Exports from ${filePath}:`
548
+ : 'Project API (exported symbols):';
549
+ const lines = [title + '\n'];
550
+
551
+ if (symbols.length === 0) {
552
+ lines.push(' (none found)');
553
+ } else {
554
+ // Group by file
555
+ const byFile = new Map();
556
+ for (const sym of symbols) {
557
+ if (!byFile.has(sym.file)) {
558
+ byFile.set(sym.file, []);
559
+ }
560
+ byFile.get(sym.file).push(sym);
561
+ }
562
+
563
+ for (const [file, syms] of byFile) {
564
+ lines.push(file);
565
+ for (const s of syms) {
566
+ const sig = s.signature || `${s.type} ${s.name}`;
567
+ lines.push(` ${lineRange(s.startLine, s.endLine)} ${sig}`);
568
+ }
569
+ lines.push('');
570
+ }
571
+ }
572
+
573
+ return lines.join('\n');
574
+ }
575
+
576
+ /**
577
+ * Format disambiguation prompt - text
578
+ */
579
+ function formatDisambiguation(matches, name, command) {
580
+ const lines = [`Multiple matches for "${name}":\n`];
581
+
582
+ for (const m of matches) {
583
+ const sig = m.params !== undefined
584
+ ? formatFunctionSignature(m)
585
+ : formatClassSignature(m);
586
+ lines.push(` ${m.relativePath}:${m.startLine} ${sig}`);
587
+ if (m.usageCount !== undefined) {
588
+ lines.push(` (${m.usageCount} usages)`);
589
+ }
590
+ }
591
+
592
+ lines.push('');
593
+ lines.push(`Use: ucn . ${command} ${name} --file <pattern>`);
594
+
595
+ return lines.join('\n');
596
+ }
597
+
598
+ // ============================================================================
599
+ // NEW JSON FORMATTERS
600
+ // ============================================================================
601
+
602
+ /**
603
+ * Format exporters as JSON
604
+ */
605
+ function formatExportersJson(exporters, filePath) {
606
+ return JSON.stringify({
607
+ file: filePath,
608
+ importerCount: exporters.length,
609
+ importers: exporters
610
+ }, null, 2);
611
+ }
612
+
613
+ /**
614
+ * Format typedef as JSON
615
+ */
616
+ function formatTypedefJson(types, name) {
617
+ return JSON.stringify({
618
+ query: name,
619
+ count: types.length,
620
+ types: types.map(t => ({
621
+ name: t.name,
622
+ type: t.type,
623
+ file: t.relativePath || t.file,
624
+ startLine: t.startLine,
625
+ endLine: t.endLine,
626
+ ...(t.usageCount !== undefined && { usageCount: t.usageCount })
627
+ }))
628
+ }, null, 2);
629
+ }
630
+
631
+ /**
632
+ * Format tests as JSON
633
+ */
634
+ function formatTestsJson(tests, name) {
635
+ return JSON.stringify({
636
+ query: name,
637
+ testFileCount: tests.length,
638
+ totalMatches: tests.reduce((sum, t) => sum + t.matches.length, 0),
639
+ testFiles: tests
640
+ }, null, 2);
641
+ }
642
+
643
+ /**
644
+ * Format api as JSON
645
+ */
646
+ function formatApiJson(symbols, filePath) {
647
+ return JSON.stringify({
648
+ ...(filePath && { file: filePath }),
649
+ exportCount: symbols.length,
650
+ exports: symbols.map(s => ({
651
+ name: s.name,
652
+ type: s.type,
653
+ file: s.file,
654
+ startLine: s.startLine,
655
+ endLine: s.endLine,
656
+ ...(s.params && { params: s.params }),
657
+ ...(s.returnType && { returnType: s.returnType }),
658
+ ...(s.signature && { signature: s.signature })
659
+ }))
660
+ }, null, 2);
661
+ }
662
+
663
+ /**
664
+ * Format trace command output - text
665
+ * Shows call tree visualization
666
+ */
667
+ function formatTrace(trace) {
668
+ if (!trace) {
669
+ return 'Function not found.';
670
+ }
671
+
672
+ const lines = [];
673
+
674
+ // Header
675
+ lines.push(`Call tree for ${trace.root}`);
676
+ lines.push('═'.repeat(60));
677
+ lines.push(`${trace.file}:${trace.line}`);
678
+ lines.push(`Direction: ${trace.direction}, Max depth: ${trace.maxDepth}`);
679
+ lines.push('');
680
+
681
+ // Render tree
682
+ const renderNode = (node, prefix = '', isLast = true) => {
683
+ const connector = isLast ? '└── ' : '├── ';
684
+ const extension = isLast ? ' ' : '│ ';
685
+
686
+ let label = node.name;
687
+ if (node.external) {
688
+ label += ' [external]';
689
+ } else if (node.file) {
690
+ label += ` (${node.file}:${node.line})`;
691
+ }
692
+ if (node.weight && node.weight !== 'normal') {
693
+ label += ` [${node.weight}]`;
694
+ }
695
+ if (node.callCount) {
696
+ label += ` ${node.callCount}x`;
697
+ }
698
+
699
+ lines.push(prefix + connector + label);
700
+
701
+ if (node.children) {
702
+ for (let i = 0; i < node.children.length; i++) {
703
+ const isChildLast = i === node.children.length - 1;
704
+ renderNode(node.children[i], prefix + extension, isChildLast);
705
+ }
706
+ }
707
+ };
708
+
709
+ // Root node
710
+ lines.push(trace.root);
711
+ if (trace.tree && trace.tree.children) {
712
+ for (let i = 0; i < trace.tree.children.length; i++) {
713
+ const isLast = i === trace.tree.children.length - 1;
714
+ renderNode(trace.tree.children[i], '', isLast);
715
+ }
716
+ }
717
+
718
+ // Callers section
719
+ if (trace.callers && trace.callers.length > 0) {
720
+ lines.push('');
721
+ lines.push('CALLED BY:');
722
+ for (const c of trace.callers) {
723
+ lines.push(` ${c.name} - ${c.file}:${c.line}`);
724
+ lines.push(` ${c.expression}`);
725
+ }
726
+ }
727
+
728
+ return lines.join('\n');
729
+ }
730
+
731
+ /**
732
+ * Format trace command output - JSON
733
+ */
734
+ function formatTraceJson(trace) {
735
+ if (!trace) {
736
+ return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
737
+ }
738
+ return JSON.stringify(trace, null, 2);
739
+ }
740
+
741
+ /**
742
+ * Format related command output - text
743
+ */
744
+ function formatRelated(related) {
745
+ if (!related) {
746
+ return 'Function not found.';
747
+ }
748
+
749
+ const lines = [];
750
+
751
+ // Header
752
+ lines.push(`Related to ${related.target.name}`);
753
+ lines.push('═'.repeat(60));
754
+ lines.push(`${related.target.file}:${related.target.line}`);
755
+ lines.push('');
756
+
757
+ // Same file
758
+ if (related.sameFile.length > 0) {
759
+ lines.push(`SAME FILE (${related.sameFile.length}):`);
760
+ for (const f of related.sameFile.slice(0, 8)) {
761
+ const params = f.params ? `(${f.params})` : '';
762
+ lines.push(` :${f.line} ${f.name}${params}`);
763
+ }
764
+ if (related.sameFile.length > 8) {
765
+ lines.push(` ... and ${related.sameFile.length - 8} more`);
766
+ }
767
+ lines.push('');
768
+ }
769
+
770
+ // Similar names
771
+ if (related.similarNames.length > 0) {
772
+ lines.push(`SIMILAR NAMES (${related.similarNames.length}):`);
773
+ for (const s of related.similarNames) {
774
+ lines.push(` ${s.name} - ${s.file}:${s.line}`);
775
+ lines.push(` shared: ${s.sharedParts.join(', ')}`);
776
+ }
777
+ lines.push('');
778
+ }
779
+
780
+ // Shared callers
781
+ if (related.sharedCallers.length > 0) {
782
+ lines.push(`CALLED BY SAME FUNCTIONS (${related.sharedCallers.length}):`);
783
+ for (const s of related.sharedCallers) {
784
+ lines.push(` ${s.name} - ${s.file}:${s.line} (${s.sharedCallerCount} shared callers)`);
785
+ }
786
+ lines.push('');
787
+ }
788
+
789
+ // Shared callees
790
+ if (related.sharedCallees.length > 0) {
791
+ lines.push(`CALLS SAME FUNCTIONS (${related.sharedCallees.length}):`);
792
+ for (const s of related.sharedCallees) {
793
+ lines.push(` ${s.name} - ${s.file}:${s.line} (${s.sharedCalleeCount} shared callees)`);
794
+ }
795
+ }
796
+
797
+ return lines.join('\n');
798
+ }
799
+
800
+ /**
801
+ * Format related command output - JSON
802
+ */
803
+ function formatRelatedJson(related) {
804
+ if (!related) {
805
+ return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
806
+ }
807
+ return JSON.stringify(related, null, 2);
808
+ }
809
+
810
+ /**
811
+ * Format impact command output - text
812
+ * Shows what would need updating if a function signature changes
813
+ */
814
+ function formatImpact(impact) {
815
+ if (!impact) {
816
+ return 'Function not found.';
817
+ }
818
+
819
+ const lines = [];
820
+
821
+ // Header
822
+ lines.push(`Impact analysis for ${impact.function}`);
823
+ lines.push('═'.repeat(60));
824
+ lines.push(`${impact.file}:${impact.startLine}`);
825
+ lines.push(impact.signature);
826
+ lines.push('');
827
+
828
+ // Summary
829
+ lines.push(`CALL SITES: ${impact.totalCallSites}`);
830
+ lines.push(` Files affected: ${impact.byFile.length}`);
831
+
832
+ // Patterns
833
+ const p = impact.patterns;
834
+ if (p) {
835
+ const patternParts = [];
836
+ if (p.constantArgs > 0) patternParts.push(`${p.constantArgs} with literals`);
837
+ if (p.variableArgs > 0) patternParts.push(`${p.variableArgs} with variables`);
838
+ if (p.awaitedCalls > 0) patternParts.push(`${p.awaitedCalls} awaited`);
839
+ if (p.chainedCalls > 0) patternParts.push(`${p.chainedCalls} chained`);
840
+ if (p.spreadCalls > 0) patternParts.push(`${p.spreadCalls} with spread`);
841
+ if (patternParts.length > 0) {
842
+ lines.push(` Patterns: ${patternParts.join(', ')}`);
843
+ }
844
+ }
845
+
846
+ // By file
847
+ lines.push('');
848
+ lines.push('BY FILE:');
849
+ for (const fileGroup of impact.byFile) {
850
+ lines.push(`\n${fileGroup.file} (${fileGroup.count} calls)`);
851
+ for (const site of fileGroup.sites.slice(0, 10)) { // Limit to 10 per file
852
+ const caller = site.callerName ? `[${site.callerName}]` : '';
853
+ lines.push(` :${site.line} ${caller}`);
854
+ lines.push(` ${site.expression}`);
855
+ if (site.args && site.args.length > 0) {
856
+ lines.push(` args: ${site.args.join(', ')}`);
857
+ }
858
+ }
859
+ if (fileGroup.count > 10) {
860
+ lines.push(` ... and ${fileGroup.count - 10} more`);
861
+ }
862
+ }
863
+
864
+ return lines.join('\n');
865
+ }
866
+
867
+ /**
868
+ * Format impact command output - JSON
869
+ */
870
+ function formatImpactJson(impact) {
871
+ if (!impact) {
872
+ return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
873
+ }
874
+ return JSON.stringify(impact, null, 2);
875
+ }
876
+
877
+ /**
878
+ * Format plan command output - text
879
+ * Shows before/after signatures and all changes needed
880
+ */
881
+ function formatPlan(plan) {
882
+ if (!plan) {
883
+ return 'Function not found.';
884
+ }
885
+ if (!plan.found) {
886
+ if (plan.error) {
887
+ return `Error: ${plan.error}\nCurrent parameters: ${plan.currentParams?.join(', ') || 'none'}`;
888
+ }
889
+ return `Function "${plan.function}" not found.`;
890
+ }
891
+
892
+ const lines = [];
893
+
894
+ // Header
895
+ lines.push(`Refactoring plan: ${plan.operation}`);
896
+ lines.push('═'.repeat(60));
897
+ lines.push(`${plan.file}:${plan.startLine}`);
898
+ lines.push('');
899
+
900
+ // Before/After
901
+ lines.push('SIGNATURE CHANGE:');
902
+ lines.push(` Before: ${plan.before.signature}`);
903
+ lines.push(` After: ${plan.after.signature}`);
904
+ lines.push('');
905
+
906
+ // Summary
907
+ lines.push(`CHANGES NEEDED: ${plan.totalChanges}`);
908
+ lines.push(` Files affected: ${plan.filesAffected}`);
909
+ lines.push('');
910
+
911
+ // Group by file
912
+ const byFile = new Map();
913
+ for (const change of plan.changes) {
914
+ if (!byFile.has(change.file)) {
915
+ byFile.set(change.file, []);
916
+ }
917
+ byFile.get(change.file).push(change);
918
+ }
919
+
920
+ lines.push('BY FILE:');
921
+ for (const [file, changes] of byFile) {
922
+ lines.push(`\n${file} (${changes.length} changes)`);
923
+ for (const change of changes.slice(0, 10)) {
924
+ lines.push(` :${change.line}`);
925
+ lines.push(` ${change.expression}`);
926
+ lines.push(` → ${change.suggestion}`);
927
+ }
928
+ if (changes.length > 10) {
929
+ lines.push(` ... and ${changes.length - 10} more`);
930
+ }
931
+ }
932
+
933
+ return lines.join('\n');
934
+ }
935
+
936
+ /**
937
+ * Format stack trace command output - text
938
+ * Shows code context for each stack frame
939
+ */
940
+ function formatStackTrace(result) {
941
+ if (!result || result.frameCount === 0) {
942
+ return 'No stack frames found in input.';
943
+ }
944
+
945
+ const lines = [];
946
+ lines.push(`Stack trace: ${result.frameCount} frames`);
947
+ lines.push('═'.repeat(60));
948
+
949
+ for (let i = 0; i < result.frames.length; i++) {
950
+ const frame = result.frames[i];
951
+ lines.push('');
952
+ lines.push(`Frame ${i}: ${frame.function || '(anonymous)'}`);
953
+ lines.push('─'.repeat(40));
954
+
955
+ if (frame.found) {
956
+ lines.push(` ${frame.resolvedFile}:${frame.line}`);
957
+
958
+ // Show code context
959
+ if (frame.context) {
960
+ lines.push('');
961
+ for (const ctx of frame.context) {
962
+ const marker = ctx.isCurrent ? '→ ' : ' ';
963
+ const lineNum = ctx.line.toString().padStart(4);
964
+ lines.push(` ${marker}${lineNum} │ ${ctx.code}`);
965
+ }
966
+ }
967
+
968
+ // Show function info if available
969
+ if (frame.functionInfo) {
970
+ lines.push('');
971
+ lines.push(` In: ${frame.functionInfo.name}(${frame.functionInfo.params || ''})`);
972
+ lines.push(` Range: ${frame.functionInfo.startLine}-${frame.functionInfo.endLine}`);
973
+ }
974
+ } else {
975
+ lines.push(` ${frame.file}:${frame.line} (file not found in project)`);
976
+ lines.push(` Raw: ${frame.raw}`);
977
+ }
978
+ }
979
+
980
+ return lines.join('\n');
981
+ }
982
+
983
+ /**
984
+ * Format verify command output - text
985
+ * Shows call site validation results
986
+ */
987
+ function formatVerify(result) {
988
+ if (!result) {
989
+ return 'Function not found.';
990
+ }
991
+ if (!result.found) {
992
+ return `Function "${result.function}" not found.`;
993
+ }
994
+
995
+ const lines = [];
996
+
997
+ // Header
998
+ lines.push(`Verification: ${result.function}`);
999
+ lines.push('═'.repeat(60));
1000
+ lines.push(`${result.file}:${result.startLine}`);
1001
+ lines.push(result.signature);
1002
+ lines.push('');
1003
+
1004
+ // Expected args
1005
+ const { min, max } = result.expectedArgs;
1006
+ const expectedStr = min === max ? `${min}` : `${min}-${max}`;
1007
+ lines.push(`Expected arguments: ${expectedStr}`);
1008
+ lines.push('');
1009
+
1010
+ // Summary
1011
+ const status = result.mismatches === 0 ? '✓ All calls valid' : '✗ Mismatches found';
1012
+ lines.push(`STATUS: ${status}`);
1013
+ lines.push(` Total calls: ${result.totalCalls}`);
1014
+ lines.push(` Valid: ${result.valid}`);
1015
+ lines.push(` Mismatches: ${result.mismatches}`);
1016
+ lines.push(` Uncertain: ${result.uncertain}`);
1017
+
1018
+ // Show mismatches
1019
+ if (result.mismatchDetails.length > 0) {
1020
+ lines.push('');
1021
+ lines.push('MISMATCHES:');
1022
+ for (const m of result.mismatchDetails.slice(0, 10)) {
1023
+ lines.push(` ${m.file}:${m.line}`);
1024
+ lines.push(` ${m.expression}`);
1025
+ lines.push(` Expected ${m.expected}, got ${m.actual}: [${m.args?.join(', ') || ''}]`);
1026
+ }
1027
+ if (result.mismatchDetails.length > 10) {
1028
+ lines.push(` ... and ${result.mismatchDetails.length - 10} more`);
1029
+ }
1030
+ }
1031
+
1032
+ // Show uncertain
1033
+ if (result.uncertainDetails.length > 0) {
1034
+ lines.push('');
1035
+ lines.push('UNCERTAIN (manual check needed):');
1036
+ for (const u of result.uncertainDetails.slice(0, 5)) {
1037
+ lines.push(` ${u.file}:${u.line}`);
1038
+ lines.push(` ${u.expression}`);
1039
+ lines.push(` Reason: ${u.reason}`);
1040
+ }
1041
+ if (result.uncertainDetails.length > 5) {
1042
+ lines.push(` ... and ${result.uncertainDetails.length - 5} more`);
1043
+ }
1044
+ }
1045
+
1046
+ return lines.join('\n');
1047
+ }
1048
+
1049
+ /**
1050
+ * Format about command output - text
1051
+ * The "tell me everything" output for AI agents
1052
+ */
1053
+ function formatAbout(about, options = {}) {
1054
+ if (!about) {
1055
+ return 'Symbol not found.';
1056
+ }
1057
+ if (!about.found) {
1058
+ const lines = ['Symbol not found.\n'];
1059
+ if (about.suggestions && about.suggestions.length > 0) {
1060
+ lines.push('Did you mean:');
1061
+ for (const s of about.suggestions) {
1062
+ lines.push(` ${s.name} (${s.type}) - ${s.file}:${s.line}`);
1063
+ lines.push(` ${s.usageCount} usages`);
1064
+ }
1065
+ }
1066
+ return lines.join('\n');
1067
+ }
1068
+
1069
+ const lines = [];
1070
+ const sym = about.symbol;
1071
+ const { expand, root, depth } = options;
1072
+
1073
+ // Depth=0: location only
1074
+ if (depth === '0') {
1075
+ return `${sym.file}:${sym.startLine}`;
1076
+ }
1077
+
1078
+ // Depth=1: location + signature + usage counts
1079
+ if (depth === '1') {
1080
+ lines.push(`${sym.file}:${sym.startLine}`);
1081
+ if (sym.signature) {
1082
+ lines.push(sym.signature);
1083
+ }
1084
+ lines.push(`(${about.totalUsages} usages: ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} refs)`);
1085
+ return lines.join('\n');
1086
+ }
1087
+
1088
+ // Header with signature
1089
+ lines.push(`${sym.name} (${sym.type})`);
1090
+ lines.push('═'.repeat(60));
1091
+ lines.push(`${sym.file}:${sym.startLine}-${sym.endLine}`);
1092
+ if (sym.signature) {
1093
+ lines.push(sym.signature);
1094
+ }
1095
+ if (sym.docstring) {
1096
+ lines.push(`"${sym.docstring}"`);
1097
+ }
1098
+
1099
+ // Warnings (show early for visibility)
1100
+ if (about.warnings && about.warnings.length > 0) {
1101
+ lines.push('');
1102
+ lines.push('⚠️ WARNINGS:');
1103
+ for (const w of about.warnings) {
1104
+ lines.push(` ${w.message}`);
1105
+ }
1106
+ }
1107
+
1108
+ // Usage summary
1109
+ lines.push('');
1110
+ lines.push(`USAGES: ${about.totalUsages} total`);
1111
+ lines.push(` ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} references`);
1112
+
1113
+ // Callers
1114
+ if (about.callers.total > 0) {
1115
+ lines.push('');
1116
+ lines.push(`CALLERS (${about.callers.total} total, showing top ${about.callers.top.length}):`);
1117
+ for (const c of about.callers.top) {
1118
+ const caller = c.callerName ? `[${c.callerName}]` : '';
1119
+ lines.push(` ${c.file}:${c.line} ${caller}`);
1120
+ lines.push(` ${c.expression}`);
1121
+ }
1122
+ }
1123
+
1124
+ // Callees
1125
+ if (about.callees.total > 0) {
1126
+ lines.push('');
1127
+ lines.push(`CALLEES (${about.callees.total} total, showing top ${about.callees.top.length}):`);
1128
+ for (const c of about.callees.top) {
1129
+ const weight = c.weight !== 'normal' ? `[${c.weight}]` : '';
1130
+ lines.push(` ${c.name} ${weight} - ${c.file}:${c.line} (${c.callCount}x)`);
1131
+
1132
+ // Inline expansion: show first 3 lines of callee code
1133
+ if (expand && root && c.file && c.startLine) {
1134
+ try {
1135
+ const filePath = path.isAbsolute(c.file) ? c.file : path.join(root, c.file);
1136
+ const content = fs.readFileSync(filePath, 'utf-8');
1137
+ const fileLines = content.split('\n');
1138
+ const endLine = c.endLine || c.startLine + 5;
1139
+ const previewLines = Math.min(3, endLine - c.startLine + 1);
1140
+ for (let i = 0; i < previewLines && c.startLine - 1 + i < fileLines.length; i++) {
1141
+ const codeLine = fileLines[c.startLine - 1 + i];
1142
+ lines.push(` │ ${codeLine}`);
1143
+ }
1144
+ if (endLine - c.startLine + 1 > 3) {
1145
+ lines.push(` │ ... (${endLine - c.startLine - 2} more lines)`);
1146
+ }
1147
+ } catch (e) {
1148
+ // Skip expansion on error
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ // Tests
1155
+ if (about.tests.totalMatches > 0) {
1156
+ lines.push('');
1157
+ lines.push(`TESTS: ${about.tests.totalMatches} matches in ${about.tests.fileCount} file(s)`);
1158
+ for (const f of about.tests.files) {
1159
+ lines.push(` ${f}`);
1160
+ }
1161
+ }
1162
+
1163
+ // Other definitions
1164
+ if (about.otherDefinitions.length > 0) {
1165
+ lines.push('');
1166
+ lines.push(`OTHER DEFINITIONS (${about.otherDefinitions.length}):`);
1167
+ for (const d of about.otherDefinitions) {
1168
+ lines.push(` ${d.file}:${d.line} (${d.usageCount} usages)`);
1169
+ }
1170
+ }
1171
+
1172
+ // Types
1173
+ if (about.types && about.types.length > 0) {
1174
+ lines.push('');
1175
+ lines.push('TYPES:');
1176
+ for (const t of about.types) {
1177
+ lines.push(` ${t.name} (${t.type}) - ${t.file}:${t.line}`);
1178
+ }
1179
+ }
1180
+
1181
+ // Completeness warnings
1182
+ if (about.completeness && about.completeness.warnings && about.completeness.warnings.length > 0) {
1183
+ lines.push('');
1184
+ lines.push('⚠ COMPLETENESS:');
1185
+ for (const w of about.completeness.warnings) {
1186
+ lines.push(` ${w.message}`);
1187
+ }
1188
+ }
1189
+
1190
+ // Code
1191
+ if (about.code) {
1192
+ lines.push('');
1193
+ lines.push('─── CODE ───');
1194
+ lines.push(about.code);
1195
+ }
1196
+
1197
+ return lines.join('\n');
1198
+ }
1199
+
1200
+ /**
1201
+ * Format about command output - JSON
1202
+ */
1203
+ function formatAboutJson(about) {
1204
+ if (!about) {
1205
+ return JSON.stringify({ found: false, error: 'Symbol not found' }, null, 2);
1206
+ }
1207
+ return JSON.stringify(about, null, 2);
1208
+ }
1209
+
1210
+ module.exports = {
1211
+ // Utilities
1212
+ normalizeParams,
1213
+ lineNum,
1214
+ lineRange,
1215
+ lineLoc,
1216
+ formatFunctionSignature,
1217
+ formatClassSignature,
1218
+ formatMemberSignature,
1219
+
1220
+ // Text output
1221
+ header,
1222
+ subheader,
1223
+ printUsage,
1224
+ printDefinition,
1225
+
1226
+ // JSON formatters
1227
+ formatTocJson,
1228
+ formatSymbolJson,
1229
+ formatUsagesJson,
1230
+ formatContextJson,
1231
+ formatFunctionJson,
1232
+ formatSearchJson,
1233
+ formatImportsJson,
1234
+ formatStatsJson,
1235
+ formatGraphJson,
1236
+ formatSmartJson,
1237
+
1238
+ // New formatters (v2 migration)
1239
+ formatImports,
1240
+ formatExporters,
1241
+ formatTypedef,
1242
+ formatTests,
1243
+ formatApi,
1244
+ formatDisambiguation,
1245
+ formatExportersJson,
1246
+ formatTypedefJson,
1247
+ formatTestsJson,
1248
+ formatApiJson,
1249
+
1250
+ // About command
1251
+ formatAbout,
1252
+ formatAboutJson,
1253
+
1254
+ // Impact command
1255
+ formatImpact,
1256
+ formatImpactJson,
1257
+
1258
+ // Plan command
1259
+ formatPlan,
1260
+
1261
+ // Stack trace command
1262
+ formatStackTrace,
1263
+
1264
+ // Verify command
1265
+ formatVerify,
1266
+
1267
+ // Trace command
1268
+ formatTrace,
1269
+ formatTraceJson,
1270
+
1271
+ // Related command
1272
+ formatRelated,
1273
+ formatRelatedJson
1274
+ };