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
package/core/output.js CHANGED
@@ -1,3300 +1,22 @@
1
1
  /**
2
- * core/output.js - Output formatting utilities
2
+ * core/output.js - Re-export facade
3
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
- * Format dynamic imports note with language-appropriate terminology.
13
- * Go doesn't have "dynamic imports" — uses "blank/dot imports" instead.
14
- */
15
- function dynamicImportsNote(count, meta) {
16
- if (!count) return null;
17
- if (meta?.projectLanguage === 'go') {
18
- return `${count} blank/dot import(s)`;
19
- }
20
- return `${count} dynamic import(s)`;
21
- }
22
-
23
- const FILE_ERROR_MESSAGES = {
24
- 'file-not-found': 'File not found in project',
25
- 'file-ambiguous': 'Ambiguous file match'
26
- };
27
-
28
- function formatFileError(errorObj, fallbackPath) {
29
- const msg = FILE_ERROR_MESSAGES[errorObj.error] || errorObj.error;
30
- const file = errorObj.filePath || fallbackPath || '';
31
- return `Error: ${msg}: ${file}`;
32
- }
33
-
34
- /**
35
- * Normalize parameters for display
36
- * Collapses multiline params to single line
37
- * @param {string} params - Raw params string
38
- * @returns {string} - Normalized params (NO truncation)
39
- */
40
- function normalizeParams(params) {
41
- if (!params || params === '...') return params || '...';
42
- // Collapse whitespace (newlines, tabs, multiple spaces) to single space
43
- return params.replace(/\s+/g, ' ').trim();
44
- }
45
-
46
- /**
47
- * Format a line number for display
48
- * @param {number} line - 1-indexed line number
49
- * @param {number} width - Padding width
50
- * @returns {string}
51
- */
52
- function lineNum(line, width = 4) {
53
- return String(line).padStart(width);
54
- }
55
-
56
- /**
57
- * Format a line range
58
- * @param {number} start - 1-indexed start line
59
- * @param {number} end - 1-indexed end line
60
- * @returns {string}
61
- */
62
- function lineRange(start, end) {
63
- return `[${lineNum(start)}-${lineNum(end)}]`;
64
- }
65
-
66
- /**
67
- * Format a single line location
68
- * @param {number} line - 1-indexed line number
69
- * @returns {string}
70
- */
71
- function lineLoc(line) {
72
- return `[${lineNum(line)}]`;
73
- }
74
-
75
- // ============================================================================
76
- // TEXT FORMATTERS
77
- // ============================================================================
78
-
79
- /**
80
- * Format function signature for TOC display
81
- * @param {object} fn - Function definition
82
- * @returns {string}
83
- */
84
- function formatFunctionSignature(fn) {
85
- const prefix = [];
86
-
87
- // Modifiers
88
- if (fn.modifiers && fn.modifiers.length > 0) {
89
- prefix.push(fn.modifiers.join(' '));
90
- }
91
-
92
- // Generator marker
93
- if (fn.isGenerator) prefix.push('*');
94
-
95
- // Name + generics + params (concatenated without spaces)
96
- let sig = fn.name;
97
- if (fn.generics) sig += fn.generics;
98
- const params = normalizeParams(fn.params);
99
- sig += `(${params})`;
100
-
101
- // Return type
102
- if (fn.returnType) sig += `: ${fn.returnType}`;
103
-
104
- // Arrow indicator
105
- if (fn.isArrow) sig += ' =>';
106
-
107
- if (prefix.length > 0) {
108
- return prefix.join(' ') + ' ' + sig;
109
- }
110
- return sig;
111
- }
112
-
113
- /**
114
- * Format class/type signature for TOC display
115
- */
116
- function formatClassSignature(cls) {
117
- const parts = [cls.type, cls.name];
118
-
119
- if (cls.generics) parts.push(cls.generics);
120
- if (cls.extends) parts.push(`extends ${cls.extends}`);
121
- if (cls.implements && cls.implements.length > 0) {
122
- parts.push(`implements ${cls.implements.join(', ')}`);
123
- }
124
-
125
- return parts.join(' ');
126
- }
127
-
128
- /**
129
- * Format class member for TOC display
130
- */
131
- function formatMemberSignature(member) {
132
- const parts = [];
133
-
134
- // Member type (static, get, set, private, etc.)
135
- if (member.memberType && member.memberType !== 'method') {
136
- parts.push(member.memberType);
137
- }
138
-
139
- // Async
140
- if (member.isAsync) parts.push('async');
141
-
142
- // Generator
143
- if (member.isGenerator) parts.push('*');
144
-
145
- // Name + Parameters (no space between name and parens)
146
- if (member.params !== undefined) {
147
- const params = normalizeParams(member.params);
148
- parts.push(`${member.name}(${params})`);
149
- } else {
150
- parts.push(member.name);
151
- }
152
-
153
- // Return type
154
- if (member.returnType) parts.push(`: ${member.returnType}`);
155
-
156
- return parts.join(' ').replace(/\s+/g, ' ').trim();
157
- }
158
-
159
- /**
160
- * Print section header
161
- */
162
- function header(title, char = '═') {
163
- console.log(title);
164
- console.log(char.repeat(60));
165
- }
166
-
167
- /**
168
- * Print subheader
169
- */
170
- function subheader(title) {
171
- console.log(title);
172
- console.log('─'.repeat(40));
173
- }
174
-
175
- /**
176
- * Print a usage/call site - FULL expression, never truncated
177
- * @param {object} usage - Usage object
178
- * @param {string} [relativePath] - Relative file path
179
- */
180
- function printUsage(usage, relativePath) {
181
- const file = relativePath || usage.file;
182
- // Context before the match
183
- if (usage.before && usage.before.length > 0) {
184
- for (const line of usage.before) {
185
- console.log(` ... ${line.trim()}`);
186
- }
187
- }
188
- // FULL content - this is the key improvement
189
- console.log(` ${file}:${usage.line}`);
190
- console.log(` ${usage.content.trim()}`);
191
- // Context after the match
192
- if (usage.after && usage.after.length > 0) {
193
- for (const line of usage.after) {
194
- console.log(` ... ${line.trim()}`);
195
- }
196
- }
197
- }
198
-
199
- /**
200
- * Print definition with full signature
201
- */
202
- function printDefinition(def, relativePath) {
203
- const file = relativePath || def.file;
204
- console.log(` ${file}:${def.line}`);
205
- if (def.signature) {
206
- console.log(` ${def.signature}`);
207
- }
208
- }
209
-
210
- // ============================================================================
211
- // JSON FORMATTERS
212
- // ============================================================================
213
-
214
- /**
215
- * Format TOC data as JSON
216
- */
217
- function formatTocJson(data) {
218
- const obj = {
219
- meta: data.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
220
- totals: data.totals,
221
- summary: data.summary,
222
- files: data.files
223
- };
224
- if (data.hiddenFiles > 0) obj.hiddenFiles = data.hiddenFiles;
225
- return JSON.stringify(obj);
226
- }
227
-
228
- /**
229
- * Format symbol search results as JSON
230
- */
231
- function formatSymbolJson(symbols, query) {
232
- return JSON.stringify({
233
- meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
234
- data: {
235
- query,
236
- count: symbols.length,
237
- results: symbols.map(s => ({
238
- name: s.name,
239
- type: s.type,
240
- file: s.relativePath || s.file,
241
- startLine: s.startLine,
242
- endLine: s.endLine,
243
- ...(s.params && { params: s.params }), // FULL params
244
- ...(s.paramsStructured && { paramsStructured: s.paramsStructured }),
245
- ...(s.returnType && { returnType: s.returnType }),
246
- ...(s.modifiers && { modifiers: s.modifiers }),
247
- ...(s.usageCount !== undefined && { usageCount: s.usageCount }),
248
- ...(s.usageCounts !== undefined && { usageCounts: s.usageCounts })
249
- }))
250
- }
251
- });
252
- }
253
-
254
- /**
255
- * Format usages as JSON - FULL expressions, never truncated
256
- */
257
- function formatUsagesJson(usages, name) {
258
- const definitions = usages.filter(u => u.isDefinition);
259
- const refs = usages.filter(u => !u.isDefinition);
260
-
261
- const calls = refs.filter(u => u.usageType === 'call');
262
- const imports = refs.filter(u => u.usageType === 'import');
263
- const references = refs.filter(u => u.usageType === 'reference');
264
-
265
- const formatUsage = (u) => ({
266
- file: u.relativePath || u.file,
267
- line: u.line,
268
- expression: u.content, // FULL expression - key improvement
269
- ...(u.args && { args: u.args }), // Parsed arguments
270
- ...(u.before && u.before.length > 0 && { before: u.before }),
271
- ...(u.after && u.after.length > 0 && { after: u.after })
272
- });
273
-
274
- return JSON.stringify({
275
- meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
276
- data: {
277
- symbol: name,
278
- definitionCount: definitions.length,
279
- callCount: calls.length,
280
- importCount: imports.length,
281
- referenceCount: references.length,
282
- totalUsages: refs.length,
283
- definitions: definitions.map(d => ({
284
- file: d.relativePath || d.file,
285
- line: d.line,
286
- signature: d.signature || null, // FULL signature
287
- type: d.type || null,
288
- ...(d.returnType && { returnType: d.returnType }),
289
- ...(d.before && d.before.length > 0 && { before: d.before }),
290
- ...(d.after && d.after.length > 0 && { after: d.after })
291
- })),
292
- calls: calls.map(formatUsage),
293
- imports: imports.map(formatUsage),
294
- references: references.map(formatUsage)
295
- }
296
- });
297
- }
298
-
299
- /**
300
- * Format context (callers + callees) as JSON
301
- */
302
- function formatContextJson(context) {
303
- const meta = context.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 };
304
- // Handle struct/interface types differently
305
- if (context.type && ['class', 'struct', 'interface', 'type'].includes(context.type)) {
306
- const callers = context.callers || [];
307
- const methods = context.methods || [];
308
- return JSON.stringify({
309
- meta,
310
- data: {
311
- type: context.type,
312
- name: context.name,
313
- file: context.file,
314
- startLine: context.startLine,
315
- endLine: context.endLine,
316
- methodCount: methods.length,
317
- usageCount: callers.length,
318
- methods: methods.map(m => ({
319
- name: m.name,
320
- file: m.file,
321
- line: m.line,
322
- params: m.params,
323
- returnType: m.returnType,
324
- receiver: m.receiver
325
- })),
326
- usages: callers.map(c => ({
327
- file: c.relativePath || c.file,
328
- line: c.line,
329
- expression: c.content,
330
- callerName: c.callerName
331
- })),
332
- ...(context.warnings && { warnings: context.warnings })
333
- }
334
- });
335
- }
336
-
337
- // Standard function/method context
338
- const callers = context.callers || [];
339
- const callees = context.callees || [];
340
- return JSON.stringify({
341
- meta,
342
- data: {
343
- function: context.function,
344
- file: context.file,
345
- callerCount: callers.length,
346
- calleeCount: callees.length,
347
- callers: callers.map(c => ({
348
- file: c.relativePath || c.file,
349
- line: c.line,
350
- expression: c.content, // FULL expression
351
- callerName: c.callerName,
352
- ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
353
- })),
354
- callees: callees.map(c => ({
355
- name: c.name,
356
- type: c.type,
357
- file: c.relativePath || c.file,
358
- line: c.startLine,
359
- params: c.params, // FULL params
360
- weight: c.weight || 'normal', // Dependency weight: core, setup, utility
361
- ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
362
- })),
363
- ...(context.warnings && { warnings: context.warnings })
364
- }
365
- });
366
- }
367
-
368
- /**
369
- * Format extracted function as JSON
370
- */
371
- function formatFunctionJson(fn, code) {
372
- return JSON.stringify({
373
- name: fn.name,
374
- params: fn.params, // FULL params
375
- paramsStructured: fn.paramsStructured || [],
376
- startLine: fn.startLine,
377
- endLine: fn.endLine,
378
- modifiers: fn.modifiers || [],
379
- ...(fn.returnType && { returnType: fn.returnType }),
380
- ...(fn.generics && { generics: fn.generics }),
381
- ...(fn.docstring && { docstring: fn.docstring }),
382
- ...(fn.isArrow && { isArrow: true }),
383
- ...(fn.isGenerator && { isGenerator: true }),
384
- code // FULL code
385
- }, null, 2);
386
- }
387
-
388
- /**
389
- * Format search results as JSON
390
- */
391
- function formatSearchJson(results, term) {
392
- const meta = results.meta;
393
- const obj = {
394
- term,
395
- totalMatches: (meta && meta.totalMatches != null) ? meta.totalMatches : results.reduce((sum, r) => sum + r.matches.length, 0),
396
- files: results.map(r => ({
397
- file: r.file,
398
- matchCount: r.matches.length,
399
- matches: r.matches.map(m => ({
400
- line: m.line,
401
- content: m.content // FULL content
402
- }))
403
- }))
404
- };
405
- if (meta) {
406
- obj.filesScanned = meta.filesScanned;
407
- obj.filesSkipped = meta.filesSkipped;
408
- obj.totalFiles = meta.totalFiles;
409
- if (meta.regexFallback) obj.regexFallback = meta.regexFallback;
410
- if (meta.truncatedMatches > 0) obj.truncatedMatches = meta.truncatedMatches;
411
- }
412
- return JSON.stringify(obj, null, 2);
413
- }
414
-
415
- /**
416
- * Format imports as JSON
417
- */
418
- function formatImportsJson(imports, filePath) {
419
- if (imports?.error) return JSON.stringify({ found: false, error: imports.error, file: imports.filePath || filePath }, null, 2);
420
- return JSON.stringify({
421
- file: filePath,
422
- importCount: imports.length,
423
- imports: imports.map(i => ({
424
- module: i.module,
425
- names: i.names,
426
- type: i.type,
427
- resolved: i.resolved || null,
428
- isDynamic: !!i.isDynamic
429
- }))
430
- }, null, 2);
431
- }
432
-
433
- /**
434
- * Format project stats as JSON
435
- */
436
- function formatStatsJson(stats) {
437
- return JSON.stringify(stats, null, 2);
438
- }
439
-
440
- /**
441
- * Format dependency graph as JSON
442
- */
443
- function formatGraphJson(graph) {
444
- if (graph?.error) return JSON.stringify({ found: false, error: graph.error, file: graph.filePath }, null, 2);
445
- const result = {
446
- root: graph.root,
447
- direction: graph.direction,
448
- nodes: graph.nodes,
449
- edges: graph.edges
450
- };
451
- if (graph.imports) result.imports = graph.imports;
452
- if (graph.importers) result.importers = graph.importers;
453
- return JSON.stringify(result, null, 2);
454
- }
455
-
456
- /**
457
- * Format smart extraction result as JSON
458
- * Includes function + all dependencies
459
- */
460
- function formatSmartJson(result) {
461
- if (!result) return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
462
- const meta = result.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 };
463
- return JSON.stringify({
464
- meta,
465
- data: {
466
- target: {
467
- name: result.target.name,
468
- file: result.target.file,
469
- startLine: result.target.startLine,
470
- endLine: result.target.endLine,
471
- params: result.target.params,
472
- returnType: result.target.returnType,
473
- code: result.target.code
474
- },
475
- dependencies: result.dependencies.map(d => ({
476
- name: d.name,
477
- type: d.type,
478
- file: d.file,
479
- startLine: d.startLine,
480
- endLine: d.endLine,
481
- params: d.params,
482
- weight: d.weight, // core, setup, utility
483
- callCount: d.callCount,
484
- code: d.code
485
- })),
486
- types: result.types || []
487
- }
488
- });
489
- }
490
-
491
- // ============================================================================
492
- // NEW FORMATTERS (v2 Migration)
493
- // ============================================================================
494
-
495
- /**
496
- * Format imports command output - text
497
- */
498
- function formatImports(imports, filePath) {
499
- if (imports?.error) return formatFileError(imports, filePath);
500
- const lines = [`Imports in ${filePath}:\n`];
501
-
502
- const internal = imports.filter(i => !i.isExternal && !i.isDynamic);
503
- const external = imports.filter(i => i.isExternal && !i.isDynamic);
504
- const dynamic = imports.filter(i => i.isDynamic);
505
-
506
- if (internal.length > 0) {
507
- lines.push('INTERNAL:');
508
- for (const imp of internal) {
509
- lines.push(` ${imp.module}`);
510
- if (imp.resolved) {
511
- lines.push(` -> ${imp.resolved}`);
512
- }
513
- if (imp.names && imp.names.length > 0 && imp.names[0] !== '*') {
514
- lines.push(` ${imp.names.join(', ')}`);
515
- }
516
- }
517
- }
518
-
519
- if (external.length > 0) {
520
- if (internal.length > 0) lines.push('');
521
- lines.push('EXTERNAL:');
522
- for (const imp of external) {
523
- lines.push(` ${imp.module}`);
524
- if (imp.names && imp.names.length > 0) {
525
- lines.push(` ${imp.names.join(', ')}`);
526
- }
527
- }
528
- }
529
-
530
- if (dynamic.length > 0) {
531
- if (internal.length > 0 || external.length > 0) lines.push('');
532
- lines.push('DYNAMIC (unresolved):');
533
- for (const imp of dynamic) {
534
- lines.push(` ${imp.module || '(variable)'}`);
535
- if (imp.names && imp.names.length > 0) {
536
- lines.push(` ${imp.names.join(', ')}`);
537
- }
538
- }
539
- }
540
-
541
- return lines.join('\n');
542
- }
543
-
544
- /**
545
- * Format exporters command output - text
546
- */
547
- function formatExporters(exporters, filePath) {
548
- if (exporters?.error) return formatFileError(exporters, filePath);
549
- const lines = [`Files that import ${filePath}:\n`];
550
-
551
- if (exporters.length === 0) {
552
- lines.push(' (none found)');
553
- } else {
554
- for (const exp of exporters) {
555
- if (exp.importLine) {
556
- lines.push(` ${exp.file}:${exp.importLine}`);
557
- } else {
558
- lines.push(` ${exp.file}`);
559
- }
560
- }
561
- }
562
-
563
- return lines.join('\n');
564
- }
565
-
566
- /**
567
- * Format typedef command output - text
568
- */
569
- function formatTypedef(types, name) {
570
- const lines = [`Type definitions for "${name}":\n`];
571
-
572
- if (types.length === 0) {
573
- lines.push(' (none found)');
574
- } else {
575
- for (const t of types) {
576
- lines.push(`${t.relativePath}:${t.startLine} ${t.type} ${t.name}`);
577
- if (t.usageCount !== undefined) {
578
- lines.push(` (${t.usageCount} usages)`);
579
- }
580
- if (t.code) {
581
- lines.push('');
582
- lines.push('─── CODE ───');
583
- lines.push(t.code);
584
- lines.push('');
585
- }
586
- }
587
- }
588
-
589
- return lines.join('\n');
590
- }
591
-
592
- /**
593
- * Format tests command output - text
594
- */
595
- function formatTests(tests, name) {
596
- const lines = [`Tests for "${name}":\n`];
597
-
598
- if (tests.length === 0) {
599
- lines.push(' (no tests found)');
600
- } else {
601
- const totalMatches = tests.reduce((sum, t) => sum + t.matches.length, 0);
602
- lines.push(`Found ${totalMatches} matches in ${tests.length} test file(s):\n`);
603
-
604
- for (const testFile of tests) {
605
- lines.push(testFile.file);
606
- for (const match of testFile.matches) {
607
- const typeLabel = match.matchType === 'test-case' ? '[test]' :
608
- match.matchType === 'import' ? '[import]' :
609
- match.matchType === 'call' ? '[call]' :
610
- match.matchType === 'string-ref' ? '[string]' : '[ref]';
611
- lines.push(` ${match.line}: ${typeLabel} ${match.content}`);
612
- }
613
- lines.push('');
614
- }
615
- }
616
-
617
- return lines.join('\n');
618
- }
619
-
620
- /**
621
- * Format api command output - text
622
- */
623
- function formatApi(symbols, filePath) {
624
- const title = filePath
625
- ? `Exports from ${filePath}:`
626
- : 'Project API (exported symbols):';
627
- const lines = [title + '\n'];
628
-
629
- if (symbols.length === 0) {
630
- lines.push(' (none found)');
631
- if (filePath && filePath.endsWith('.py')) {
632
- lines.push('');
633
- lines.push('Note: Python requires __all__ for export detection. Use \'toc\' command to see all functions/classes.');
634
- }
635
- } else {
636
- // Group by file
637
- const byFile = new Map();
638
- for (const sym of symbols) {
639
- if (!byFile.has(sym.file)) {
640
- byFile.set(sym.file, []);
641
- }
642
- byFile.get(sym.file).push(sym);
643
- }
644
-
645
- for (const [file, syms] of byFile) {
646
- lines.push(file);
647
- for (const s of syms) {
648
- const sig = s.signature || `${s.type} ${s.name}`;
649
- lines.push(` ${lineRange(s.startLine, s.endLine)} ${sig}`);
650
- }
651
- lines.push('');
652
- }
653
- }
654
-
655
- return lines.join('\n');
656
- }
657
-
658
- /**
659
- * Format disambiguation prompt - text
660
- */
661
- function formatDisambiguation(matches, name, command) {
662
- const lines = [`Multiple matches for "${name}":\n`];
663
-
664
- for (const m of matches) {
665
- const sig = m.params !== undefined
666
- ? formatFunctionSignature(m)
667
- : formatClassSignature(m);
668
- lines.push(` ${m.relativePath}:${m.startLine} ${sig}`);
669
- if (m.usageCount !== undefined) {
670
- lines.push(` (${m.usageCount} usages)`);
671
- }
672
- }
673
-
674
- lines.push('');
675
- lines.push(`Use: ucn . ${command} ${name} --file <pattern>`);
676
-
677
- return lines.join('\n');
678
- }
679
-
680
- // ============================================================================
681
- // NEW JSON FORMATTERS
682
- // ============================================================================
683
-
684
- /**
685
- * Format exporters as JSON
686
- */
687
- function formatExportersJson(exporters, filePath) {
688
- if (exporters?.error) return JSON.stringify({ found: false, error: exporters.error, file: exporters.filePath || filePath }, null, 2);
689
- return JSON.stringify({
690
- file: filePath,
691
- importerCount: exporters.length,
692
- importers: exporters
693
- }, null, 2);
694
- }
695
-
696
- /**
697
- * Format typedef as JSON
698
- */
699
- function formatTypedefJson(types, name) {
700
- return JSON.stringify({
701
- query: name,
702
- count: types.length,
703
- types: types.map(t => ({
704
- name: t.name,
705
- type: t.type,
706
- file: t.relativePath || t.file,
707
- startLine: t.startLine,
708
- endLine: t.endLine,
709
- ...(t.usageCount !== undefined && { usageCount: t.usageCount }),
710
- ...(t.code && { code: t.code })
711
- }))
712
- }, null, 2);
713
- }
714
-
715
- /**
716
- * Format tests as JSON
717
- */
718
- function formatTestsJson(tests, name) {
719
- return JSON.stringify({
720
- query: name,
721
- testFileCount: tests.length,
722
- totalMatches: tests.reduce((sum, t) => sum + t.matches.length, 0),
723
- testFiles: tests
724
- }, null, 2);
725
- }
726
-
727
- /**
728
- * Format api as JSON
729
- */
730
- function formatApiJson(symbols, filePath) {
731
- return JSON.stringify({
732
- ...(filePath && { file: filePath }),
733
- exportCount: symbols.length,
734
- exports: symbols.map(s => ({
735
- name: s.name,
736
- type: s.type,
737
- file: s.file,
738
- startLine: s.startLine,
739
- endLine: s.endLine,
740
- ...(s.params && { params: s.params }),
741
- ...(s.returnType && { returnType: s.returnType }),
742
- ...(s.signature && { signature: s.signature })
743
- }))
744
- }, null, 2);
745
- }
746
-
747
- /**
748
- * Format trace command output - text
749
- * Shows call tree visualization
750
- */
751
- function formatTrace(trace, options = {}) {
752
- if (!trace) {
753
- return 'Function not found.';
754
- }
755
-
756
- const lines = [];
757
-
758
- // Header
759
- lines.push(`Call tree for ${trace.root}`);
760
- lines.push('═'.repeat(60));
761
- lines.push(`${trace.file}:${trace.line}`);
762
- lines.push(`Direction: ${trace.direction}, Max depth: ${trace.maxDepth}`);
763
-
764
- if (trace.warnings && trace.warnings.length > 0) {
765
- for (const w of trace.warnings) {
766
- lines.push(`Note: ${w.message}`);
767
- }
768
- }
769
-
770
- lines.push('');
771
-
772
- // Render tree
773
- let hasTruncation = false;
774
- const renderNode = (node, prefix = '', isLast = true) => {
775
- const connector = isLast ? '└── ' : '├── ';
776
- const extension = isLast ? ' ' : '│ ';
777
-
778
- let label = node.name;
779
- if (node.external) {
780
- label += ' [external]';
781
- } else if (node.file) {
782
- label += ` (${node.file}:${node.line})`;
783
- }
784
- if (node.weight && node.weight !== 'normal') {
785
- label += ` [${node.weight}]`;
786
- }
787
- if (node.callCount) {
788
- label += ` ${node.callCount}x`;
789
- }
790
- if (node.alreadyShown) {
791
- label += ' (see above)';
792
- }
793
-
794
- lines.push(prefix + connector + label);
795
-
796
- if (node.children && !node.alreadyShown) {
797
- const hasMore = node.truncatedChildren > 0;
798
- for (let i = 0; i < node.children.length; i++) {
799
- const isChildLast = !hasMore && i === node.children.length - 1;
800
- renderNode(node.children[i], prefix + extension, isChildLast);
801
- }
802
- if (hasMore) {
803
- hasTruncation = true;
804
- lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callees`);
805
- }
806
- }
807
- };
808
-
809
- // Root node
810
- lines.push(trace.root);
811
- if (trace.tree && trace.tree.children) {
812
- const rootHasMore = trace.tree.truncatedChildren > 0;
813
- for (let i = 0; i < trace.tree.children.length; i++) {
814
- const isLast = !rootHasMore && i === trace.tree.children.length - 1;
815
- renderNode(trace.tree.children[i], '', isLast);
816
- }
817
- if (rootHasMore) {
818
- hasTruncation = true;
819
- lines.push(`└── ... and ${trace.tree.truncatedChildren} more callees`);
820
- }
821
- }
822
-
823
- // Callers section
824
- if (trace.callers && trace.callers.length > 0) {
825
- lines.push('');
826
- lines.push('CALLED BY:');
827
- for (const c of trace.callers) {
828
- lines.push(` ${c.name} - ${c.file}:${c.line}`);
829
- lines.push(` ${c.expression}`);
830
- }
831
- if (trace.truncatedCallers) {
832
- hasTruncation = true;
833
- lines.push(` ... and ${trace.truncatedCallers} more callers`);
834
- }
835
- }
836
-
837
- if (hasTruncation) {
838
- const allHint = options.allHint || 'Use --all to show all.';
839
- lines.push(`\nSome results truncated. ${allHint}`);
840
- }
841
-
842
- if (trace.includeMethods === false) {
843
- const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded — use --include-methods to include them';
844
- lines.push(`\n${methodsHint}`);
845
- }
846
-
847
- return lines.join('\n');
848
- }
849
-
850
- /**
851
- * Format trace command output - JSON
852
- */
853
- function formatTraceJson(trace) {
854
- if (!trace) {
855
- return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
856
- }
857
- return JSON.stringify(trace, null, 2);
858
- }
859
-
860
- /**
861
- * Format blast command output - text
862
- * Shows transitive blast radius (callers of callers)
863
- */
864
- function formatBlast(blast, options = {}) {
865
- if (!blast) {
866
- return 'Function not found.';
867
- }
868
-
869
- const lines = [];
870
-
871
- // Header
872
- lines.push(`Blast radius for ${blast.root}`);
873
- lines.push('═'.repeat(60));
874
- lines.push(`${blast.file}:${blast.line}`);
875
- lines.push(`Depth: ${blast.maxDepth}`);
876
-
877
- if (blast.warnings && blast.warnings.length > 0) {
878
- for (const w of blast.warnings) {
879
- lines.push(`Note: ${w.message}`);
880
- }
881
- }
882
-
883
- lines.push('');
884
-
885
- // Render tree (same structure as trace but showing callers)
886
- let hasTruncation = false;
887
- const renderNode = (node, prefix = '', isLast = true) => {
888
- const connector = isLast ? '└── ' : '├── ';
889
- const extension = isLast ? ' ' : '│ ';
890
-
891
- let label = node.name;
892
- if (node.file) {
893
- label += ` (${node.file}:${node.line})`;
894
- }
895
- if (node.callSites && node.callSites > 1) {
896
- label += ` ${node.callSites}x`;
897
- }
898
- if (node.alreadyShown) {
899
- label += ' (see above)';
900
- }
901
-
902
- lines.push(prefix + connector + label);
903
-
904
- if (node.children && !node.alreadyShown) {
905
- const hasMore = node.truncatedChildren > 0;
906
- for (let i = 0; i < node.children.length; i++) {
907
- const isChildLast = !hasMore && i === node.children.length - 1;
908
- renderNode(node.children[i], prefix + extension, isChildLast);
909
- }
910
- if (hasMore) {
911
- hasTruncation = true;
912
- lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callers`);
913
- }
914
- }
915
- };
916
-
917
- // Root node
918
- lines.push(blast.root);
919
- if (blast.tree && blast.tree.children) {
920
- const rootHasMore = blast.tree.truncatedChildren > 0;
921
- for (let i = 0; i < blast.tree.children.length; i++) {
922
- const isLast = !rootHasMore && i === blast.tree.children.length - 1;
923
- renderNode(blast.tree.children[i], '', isLast);
924
- }
925
- if (rootHasMore) {
926
- hasTruncation = true;
927
- lines.push(`└── ... and ${blast.tree.truncatedChildren} more callers`);
928
- }
929
- }
930
-
931
- // Summary
932
- if (blast.summary) {
933
- lines.push('');
934
- const { totalAffected, totalFiles } = blast.summary;
935
- if (totalAffected > 0) {
936
- lines.push(`Summary: 1 function changed → ${totalAffected} function${totalAffected !== 1 ? 's' : ''} affected across ${totalFiles} file${totalFiles !== 1 ? 's' : ''}`);
937
- } else {
938
- lines.push('Summary: No callers found — this function is a root/entry point.');
939
- }
940
- }
941
-
942
- if (hasTruncation) {
943
- const allHint = options.allHint || 'Use --all to show all.';
944
- lines.push(`\nSome results truncated. ${allHint}`);
945
- }
946
-
947
- if (blast.includeMethods === false) {
948
- lines.push('\nNote: obj.method() calls excluded. Use --include-methods to include them.');
949
- }
950
-
951
- return lines.join('\n');
952
- }
953
-
954
- /**
955
- * Format blast command output - JSON
956
- */
957
- function formatBlastJson(blast) {
958
- if (!blast) {
959
- return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
960
- }
961
- return JSON.stringify(blast, null, 2);
962
- }
963
-
964
- /**
965
- * Format reverse-trace command output - text
966
- * Shows upward call chain to entry points
967
- */
968
- function formatReverseTrace(result, options = {}) {
969
- if (!result) {
970
- return 'Function not found.';
971
- }
972
-
973
- const lines = [];
974
-
975
- // Header
976
- lines.push(`Reverse trace for ${result.root}`);
977
- lines.push('═'.repeat(60));
978
- lines.push(`${result.file}:${result.line}`);
979
- lines.push(`Depth: ${result.maxDepth}`);
980
-
981
- if (result.warnings && result.warnings.length > 0) {
982
- for (const w of result.warnings) {
983
- lines.push(`Note: ${w.message}`);
984
- }
985
- }
986
-
987
- lines.push('');
988
-
989
- // Render tree
990
- let hasTruncation = false;
991
- const renderNode = (node, prefix = '', isLast = true) => {
992
- const connector = isLast ? '└── ' : '├── ';
993
- const extension = isLast ? ' ' : '│ ';
994
-
995
- let label = node.name;
996
- if (node.file) {
997
- label += ` (${node.file}:${node.line})`;
998
- }
999
- if (node.callSites && node.callSites > 1) {
1000
- label += ` ${node.callSites}x`;
1001
- }
1002
- if (node.entryPoint) {
1003
- label += ' ★ entry point';
1004
- }
1005
- if (node.alreadyShown) {
1006
- label += ' (see above)';
1007
- }
1008
-
1009
- lines.push(prefix + connector + label);
1010
-
1011
- if (node.children && !node.alreadyShown) {
1012
- const hasMore = node.truncatedChildren > 0;
1013
- for (let i = 0; i < node.children.length; i++) {
1014
- const isChildLast = !hasMore && i === node.children.length - 1;
1015
- renderNode(node.children[i], prefix + extension, isChildLast);
1016
- }
1017
- if (hasMore) {
1018
- hasTruncation = true;
1019
- lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callers`);
1020
- }
1021
- }
1022
- };
1023
-
1024
- // Root node
1025
- let rootLabel = result.root;
1026
- if (result.tree && result.tree.entryPoint) {
1027
- rootLabel += ' ★ entry point (no callers)';
1028
- }
1029
- lines.push(rootLabel);
1030
- if (result.tree && result.tree.children) {
1031
- const rootHasMore = result.tree.truncatedChildren > 0;
1032
- for (let i = 0; i < result.tree.children.length; i++) {
1033
- const isLast = !rootHasMore && i === result.tree.children.length - 1;
1034
- renderNode(result.tree.children[i], '', isLast);
1035
- }
1036
- if (rootHasMore) {
1037
- hasTruncation = true;
1038
- lines.push(`└── ... and ${result.tree.truncatedChildren} more callers`);
1039
- }
1040
- }
1041
-
1042
- // Entry points summary
1043
- if (result.entryPoints && result.entryPoints.length > 0) {
1044
- lines.push('');
1045
- lines.push(`Entry points (${result.entryPoints.length}):`);
1046
- for (const ep of result.entryPoints) {
1047
- lines.push(` ★ ${ep.name} (${ep.file}:${ep.line})`);
1048
- }
1049
- }
1050
-
1051
- // Summary
1052
- if (result.summary) {
1053
- lines.push('');
1054
- const { totalEntryPoints, totalFunctions } = result.summary;
1055
- if (totalFunctions > 0) {
1056
- lines.push(`Summary: ${totalEntryPoints} entry point${totalEntryPoints !== 1 ? 's' : ''} reach${totalEntryPoints === 1 ? 'es' : ''} ${result.root} through ${totalFunctions} intermediate function${totalFunctions !== 1 ? 's' : ''}`);
1057
- } else {
1058
- lines.push('Summary: No callers found — this function is itself an entry point.');
1059
- }
1060
- }
1061
-
1062
- if (hasTruncation) {
1063
- const allHint = options.allHint || 'Use --all to show all.';
1064
- lines.push(`\nSome results truncated. ${allHint}`);
1065
- }
1066
-
1067
- if (result.includeMethods === false) {
1068
- lines.push('\nNote: obj.method() calls excluded. Use --include-methods to include them.');
1069
- }
1070
-
1071
- return lines.join('\n');
1072
- }
1073
-
1074
- /**
1075
- * Format reverse-trace command output - JSON
1076
- */
1077
- function formatReverseTraceJson(result) {
1078
- if (!result) {
1079
- return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
1080
- }
1081
- return JSON.stringify(result, null, 2);
1082
- }
1083
-
1084
- /**
1085
- * Format affected-tests command output - text
1086
- */
1087
- function formatAffectedTests(result, options = {}) {
1088
- if (!result) return 'Function not found.';
1089
-
1090
- const lines = [];
1091
- const { summary } = result;
1092
-
1093
- lines.push(`affected-tests: ${result.root}`);
1094
- lines.push('═'.repeat(60));
1095
- lines.push(`${result.file}:${result.line}`);
1096
- lines.push(`1 function changed → ${summary.totalAffected} functions affected (depth ${result.depth})`);
1097
- lines.push('');
1098
-
1099
- if (result.testFiles.length === 0) {
1100
- lines.push('No test files found for any affected function.');
1101
- } else {
1102
- const MAX_TEST_FILES = options.all ? Infinity : 30;
1103
- const displayFiles = result.testFiles.slice(0, MAX_TEST_FILES);
1104
- const truncatedFiles = result.testFiles.length - displayFiles.length;
1105
- lines.push(`Test files to run (${summary.totalTestFiles}):`);
1106
- lines.push('');
1107
- for (const tf of displayFiles) {
1108
- lines.push(` ${tf.file} (covers: ${tf.coveredFunctions.join(', ')})`);
1109
- // Show up to 5 key matches per file
1110
- const keyMatches = tf.matches
1111
- .filter(m => m.matchType === 'call' || m.matchType === 'test-case')
1112
- .slice(0, 5);
1113
- for (const m of keyMatches) {
1114
- lines.push(` L${m.line}: ${m.content} [${m.matchType}]`);
1115
- }
1116
- }
1117
- if (truncatedFiles > 0) {
1118
- lines.push(`\n ... ${truncatedFiles} more test files (use file= and exclude= to narrow scope)`);
1119
- }
1120
- }
1121
-
1122
- if (result.uncovered.length > 0) {
1123
- lines.push('');
1124
- lines.push(`Uncovered (${result.uncovered.length}): ${result.uncovered.join(', ')}`);
1125
- lines.push(' ⚠ These affected functions have no test references');
1126
- }
1127
-
1128
- lines.push('');
1129
- const pct = summary.totalAffected > 0
1130
- ? Math.round(summary.coveredFunctions / summary.totalAffected * 100)
1131
- : 0;
1132
- lines.push(`Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} test files, ${summary.coveredFunctions}/${summary.totalAffected} functions covered (${pct}%)`);
1133
-
1134
- if (result.warnings?.length > 0) {
1135
- lines.push('');
1136
- for (const w of result.warnings) lines.push(`Note: ${w.message}`);
1137
- }
1138
-
1139
- return lines.join('\n');
1140
- }
1141
-
1142
- function formatAffectedTestsJson(result) {
1143
- if (!result) {
1144
- return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
1145
- }
1146
- return JSON.stringify(result, null, 2);
1147
- }
1148
-
1149
- /**
1150
- * Format related command output - text
1151
- */
1152
- function formatRelated(related, options = {}) {
1153
- if (!related) {
1154
- return 'Function not found.';
1155
- }
1156
-
1157
- const lines = [];
1158
-
1159
- // Header
1160
- lines.push(`Related to ${related.target.name}`);
1161
- lines.push('═'.repeat(60));
1162
- lines.push(`${related.target.file}:${related.target.line}`);
1163
- lines.push('');
1164
-
1165
- // Same file
1166
- let relatedTruncated = false;
1167
- if (related.sameFile.length > 0) {
1168
- const maxSameFile = options.top || (options.all ? Infinity : 8);
1169
- lines.push(`SAME FILE (${related.sameFile.length}):`);
1170
- for (const f of related.sameFile.slice(0, maxSameFile)) {
1171
- const params = f.params ? `(${f.params})` : '';
1172
- lines.push(` :${f.line} ${f.name}${params}`);
1173
- }
1174
- if (related.sameFile.length > maxSameFile) {
1175
- relatedTruncated = true;
1176
- lines.push(` ... and ${related.sameFile.length - maxSameFile} more`);
1177
- }
1178
- lines.push('');
1179
- }
1180
-
1181
- // Similar names
1182
- if (related.similarNames.length > 0) {
1183
- lines.push(`SIMILAR NAMES (${related.similarNames.length}):`);
1184
- for (const s of related.similarNames) {
1185
- lines.push(` ${s.name} - ${s.file}:${s.line}`);
1186
- lines.push(` shared: ${s.sharedParts.join(', ')}`);
1187
- }
1188
- lines.push('');
1189
- }
1190
-
1191
- // Shared callers
1192
- if (related.sharedCallers.length > 0) {
1193
- lines.push(`CALLED BY SAME FUNCTIONS (${related.sharedCallers.length}):`);
1194
- for (const s of related.sharedCallers) {
1195
- lines.push(` ${s.name} - ${s.file}:${s.line} (${s.sharedCallerCount} shared callers)`);
1196
- }
1197
- lines.push('');
1198
- }
1199
-
1200
- // Shared callees
1201
- if (related.sharedCallees.length > 0) {
1202
- lines.push(`CALLS SAME FUNCTIONS (${related.sharedCallees.length}):`);
1203
- for (const s of related.sharedCallees) {
1204
- lines.push(` ${s.name} - ${s.file}:${s.line} (${s.sharedCalleeCount} shared callees)`);
1205
- }
1206
- }
1207
-
1208
- if (relatedTruncated) {
1209
- const allHint = options.allHint || 'Use --all to show all.';
1210
- lines.push(`\nSome sections truncated. ${allHint}`);
1211
- }
1212
-
1213
- return lines.join('\n');
1214
- }
1215
-
1216
- /**
1217
- * Format related command output - JSON
1218
- */
1219
- function formatRelatedJson(related) {
1220
- if (!related) {
1221
- return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
1222
- }
1223
- return JSON.stringify(related, null, 2);
1224
- }
1225
-
1226
- /**
1227
- * Format impact command output - text
1228
- * Shows what would need updating if a function signature changes
1229
- */
1230
- function formatImpact(impact, options = {}) {
1231
- if (!impact) {
1232
- return 'Function not found.';
1233
- }
1234
-
1235
- const lines = [];
1236
-
1237
- // Header
1238
- lines.push(`Impact analysis for ${impact.function}`);
1239
- lines.push('═'.repeat(60));
1240
- lines.push(`${impact.file}:${impact.startLine}`);
1241
- lines.push(impact.signature);
1242
- lines.push('');
1243
-
1244
- // Summary
1245
- if (impact.shownCallSites !== undefined && impact.shownCallSites < impact.totalCallSites) {
1246
- lines.push(`CALL SITES: ${impact.shownCallSites} shown of ${impact.totalCallSites} total`);
1247
- } else {
1248
- lines.push(`CALL SITES: ${impact.totalCallSites}`);
1249
- }
1250
- lines.push(` Files affected: ${impact.byFile.length}`);
1251
-
1252
- // Patterns
1253
- const p = impact.patterns;
1254
- if (p) {
1255
- const patternParts = [];
1256
- if (p.constantArgs > 0) patternParts.push(`${p.constantArgs} with literals`);
1257
- if (p.variableArgs > 0) patternParts.push(`${p.variableArgs} with variables`);
1258
- if (p.awaitedCalls > 0) patternParts.push(`${p.awaitedCalls} awaited`);
1259
- if (p.chainedCalls > 0) patternParts.push(`${p.chainedCalls} chained`);
1260
- if (p.spreadCalls > 0) patternParts.push(`${p.spreadCalls} with spread`);
1261
- if (patternParts.length > 0) {
1262
- lines.push(` Patterns: ${patternParts.join(', ')}`);
1263
- }
1264
- }
1265
-
1266
- // Scope pollution warning
1267
- if (impact.scopeWarning) {
1268
- lines.push(` Note: ${impact.scopeWarning.hint}`);
1269
- }
1270
-
1271
- // By file
1272
- lines.push('');
1273
- lines.push('BY FILE:');
1274
- for (const fileGroup of impact.byFile) {
1275
- lines.push(`\n${fileGroup.file} (${fileGroup.count} calls)`);
1276
- for (const site of fileGroup.sites) {
1277
- const caller = site.callerName ? `[${site.callerName}]` : '';
1278
- lines.push(` :${site.line} ${caller}`);
1279
- lines.push(` ${site.expression}`);
1280
- if (site.args && site.args.length > 0) {
1281
- lines.push(` args: ${site.args.join(', ')}`);
1282
- }
1283
- }
1284
- }
1285
-
1286
- return lines.join('\n');
1287
- }
1288
-
1289
- /**
1290
- * Format impact command output - JSON
1291
- */
1292
- function formatImpactJson(impact) {
1293
- if (!impact) {
1294
- return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
1295
- }
1296
- return JSON.stringify(impact, null, 2);
1297
- }
1298
-
1299
- /**
1300
- * Format plan command output - text
1301
- * Shows before/after signatures and all changes needed
1302
- */
1303
- function formatPlan(plan, options = {}) {
1304
- if (!plan) {
1305
- return 'Function not found.';
1306
- }
1307
- if (!plan.found) {
1308
- return `Function "${plan.function}" not found.`;
1309
- }
1310
- if (plan.error) {
1311
- return `Error: ${plan.error}\nCurrent parameters: ${plan.currentParams?.join(', ') || 'none'}`;
1312
- }
1313
-
1314
- const lines = [];
1315
-
1316
- // Header
1317
- lines.push(`Refactoring plan: ${plan.operation}`);
1318
- lines.push('═'.repeat(60));
1319
- lines.push(`${plan.file}:${plan.startLine}`);
1320
- lines.push('');
1321
-
1322
- // Before/After
1323
- lines.push('SIGNATURE CHANGE:');
1324
- lines.push(` Before: ${plan.before.signature}`);
1325
- lines.push(` After: ${plan.after.signature}`);
1326
- lines.push('');
1327
-
1328
- // Summary
1329
- lines.push(`CHANGES NEEDED: ${plan.totalChanges}`);
1330
- lines.push(` Files affected: ${plan.filesAffected}`);
1331
- if (plan.scopeWarning) {
1332
- lines.push(` Note: ${plan.scopeWarning.hint}`);
1333
- }
1334
- lines.push('');
1335
-
1336
- // Group by file
1337
- const byFile = new Map();
1338
- for (const change of plan.changes) {
1339
- if (!byFile.has(change.file)) {
1340
- byFile.set(change.file, []);
1341
- }
1342
- byFile.get(change.file).push(change);
1343
- }
1344
-
1345
- lines.push('BY FILE:');
1346
- for (const [file, changes] of byFile) {
1347
- lines.push(`\n${file} (${changes.length} changes)`);
1348
- for (const change of changes) {
1349
- lines.push(` :${change.line}`);
1350
- lines.push(` ${change.expression}`);
1351
- lines.push(` → ${change.suggestion}`);
1352
- }
1353
- }
1354
-
1355
- return lines.join('\n');
1356
- }
1357
-
1358
- /**
1359
- * Format stack trace command output - text
1360
- * Shows code context for each stack frame
1361
- */
1362
- function formatStackTrace(result) {
1363
- if (!result || result.frameCount === 0) {
1364
- return 'No stack frames found in input.';
1365
- }
1366
-
1367
- const lines = [];
1368
- lines.push(`Stack trace: ${result.frameCount} frames`);
1369
- lines.push('═'.repeat(60));
1370
-
1371
- for (let i = 0; i < result.frames.length; i++) {
1372
- const frame = result.frames[i];
1373
- lines.push('');
1374
- lines.push(`Frame ${i}: ${frame.function || '(anonymous)'}`);
1375
- lines.push('─'.repeat(40));
1376
-
1377
- if (frame.found) {
1378
- lines.push(` ${frame.resolvedFile}:${frame.line}`);
1379
-
1380
- // Show code context
1381
- if (frame.context) {
1382
- lines.push('');
1383
- for (const ctx of frame.context) {
1384
- const marker = ctx.isCurrent ? '→ ' : ' ';
1385
- const lineNum = ctx.line.toString().padStart(4);
1386
- lines.push(` ${marker}${lineNum} │ ${ctx.code}`);
1387
- }
1388
- }
1389
-
1390
- // Show function info if available
1391
- if (frame.functionInfo) {
1392
- lines.push('');
1393
- lines.push(` In: ${frame.functionInfo.name}(${frame.functionInfo.params || ''})`);
1394
- lines.push(` Range: ${frame.functionInfo.startLine}-${frame.functionInfo.endLine}`);
1395
- }
1396
- } else {
1397
- lines.push(` ${frame.file}:${frame.line} (file not found in project)`);
1398
- lines.push(` Raw: ${frame.raw}`);
1399
- }
1400
- }
1401
-
1402
- return lines.join('\n');
1403
- }
1404
-
1405
- /**
1406
- * Format verify command output - text
1407
- * Shows call site validation results
1408
- */
1409
- function formatVerify(result, options = {}) {
1410
- if (!result) {
1411
- return 'Function not found.';
1412
- }
1413
- if (!result.found) {
1414
- return `Function "${result.function}" not found.`;
1415
- }
1416
-
1417
- const lines = [];
1418
-
1419
- // Header
1420
- lines.push(`Verification: ${result.function}`);
1421
- lines.push('═'.repeat(60));
1422
- lines.push(`${result.file}:${result.startLine}`);
1423
- lines.push(result.signature);
1424
- lines.push('');
1425
-
1426
- // Expected args
1427
- const { min, max } = result.expectedArgs;
1428
- const expectedStr = min === max ? `${min}` : `${min}-${max}`;
1429
- lines.push(`Expected arguments: ${expectedStr}`);
1430
- lines.push('');
1431
-
1432
- // Summary
1433
- const status = result.mismatches === 0 ? '✓ All calls valid' : '✗ Mismatches found';
1434
- lines.push(`STATUS: ${status}`);
1435
- lines.push(` Total calls: ${result.totalCalls}`);
1436
- lines.push(` Valid: ${result.valid}`);
1437
- lines.push(` Mismatches: ${result.mismatches}`);
1438
- lines.push(` Uncertain: ${result.uncertain}`);
1439
- if (result.scopeWarning) {
1440
- lines.push(` Note: ${result.scopeWarning.hint}`);
1441
- }
1442
-
1443
- // Show mismatches
1444
- if (result.mismatchDetails.length > 0) {
1445
- lines.push('');
1446
- lines.push('MISMATCHES:');
1447
- for (const m of result.mismatchDetails) {
1448
- lines.push(` ${m.file}:${m.line}`);
1449
- lines.push(` ${m.expression}`);
1450
- lines.push(` Expected ${m.expected}, got ${m.actual}: [${m.args?.join(', ') || ''}]`);
1451
- }
1452
- }
1453
-
1454
- // Show uncertain
1455
- if (result.uncertainDetails.length > 0) {
1456
- lines.push('');
1457
- lines.push('UNCERTAIN (manual check needed):');
1458
- for (const u of result.uncertainDetails) {
1459
- lines.push(` ${u.file}:${u.line}`);
1460
- lines.push(` ${u.expression}`);
1461
- lines.push(` Reason: ${u.reason}`);
1462
- }
1463
- }
1464
-
1465
- return lines.join('\n');
1466
- }
1467
-
1468
- /**
1469
- * Format about command output - text
1470
- * The "tell me everything" output for AI agents
1471
- */
1472
- function formatAbout(about, options = {}) {
1473
- if (!about) {
1474
- return 'Symbol not found.';
1475
- }
1476
- if (!about.found) {
1477
- const lines = ['Symbol not found.\n'];
1478
- if (about.suggestions && about.suggestions.length > 0) {
1479
- lines.push('Did you mean:');
1480
- for (const s of about.suggestions) {
1481
- lines.push(` ${s.name} (${s.type}) - ${s.file}:${s.line}`);
1482
- lines.push(` ${s.usageCount} usages`);
1483
- }
1484
- }
1485
- return lines.join('\n');
1486
- }
1487
-
1488
- const lines = [];
1489
- const sym = about.symbol;
1490
- const { expand, root, depth } = options;
1491
-
1492
- // Depth=0: location only
1493
- if (depth !== null && depth !== undefined && Number(depth) === 0) {
1494
- return `${sym.file}:${sym.startLine}`;
1495
- }
1496
-
1497
- // Depth=1: location + signature + usage counts
1498
- if (depth !== null && depth !== undefined && Number(depth) === 1) {
1499
- lines.push(`${sym.file}:${sym.startLine}`);
1500
- if (sym.signature) {
1501
- lines.push(sym.signature);
1502
- }
1503
- lines.push(`(${about.totalUsages} usages: ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} refs)`);
1504
- return lines.join('\n');
1505
- }
1506
-
1507
- // Header with signature
1508
- lines.push(`${sym.name} (${sym.type})`);
1509
- lines.push('═'.repeat(60));
1510
- lines.push(`${sym.file}:${sym.startLine}-${sym.endLine}`);
1511
- if (sym.signature) {
1512
- lines.push(sym.signature);
1513
- }
1514
- if (sym.docstring) {
1515
- lines.push(`"${sym.docstring}"`);
1516
- }
1517
-
1518
- // Warnings (show early for visibility)
1519
- if (about.warnings && about.warnings.length > 0) {
1520
- for (const w of about.warnings) {
1521
- lines.push(` Note: ${w.message}`);
1522
- }
1523
- }
1524
- if (about.confidenceFiltered) {
1525
- lines.push(` Note: ${about.confidenceFiltered} edge(s) below confidence threshold hidden`);
1526
- }
1527
-
1528
- // Usage summary
1529
- lines.push('');
1530
- lines.push(`USAGES: ${about.totalUsages} total`);
1531
- lines.push(` ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} references`);
1532
-
1533
- // Callers
1534
- const showConf = options.showConfidence || false;
1535
- let aboutTruncated = false;
1536
- if (about.callers.total > 0) {
1537
- lines.push('');
1538
- if (about.callers.total > about.callers.top.length) {
1539
- lines.push(`CALLERS (showing ${about.callers.top.length} of ${about.callers.total}):`);
1540
- aboutTruncated = true;
1541
- } else {
1542
- lines.push(`CALLERS (${about.callers.total}):`);
1543
- }
1544
- for (const c of about.callers.top) {
1545
- const caller = c.callerName ? `[${c.callerName}]` : '';
1546
- lines.push(` ${c.file}:${c.line} ${caller}`);
1547
- lines.push(` ${c.expression}`);
1548
- if (showConf && c.confidence != null) {
1549
- lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
1550
- }
1551
- }
1552
- }
1553
-
1554
- // Callees
1555
- if (about.callees.total > 0) {
1556
- lines.push('');
1557
- if (about.callees.total > about.callees.top.length) {
1558
- lines.push(`CALLEES (showing ${about.callees.top.length} of ${about.callees.total}):`);
1559
- aboutTruncated = true;
1560
- } else {
1561
- lines.push(`CALLEES (${about.callees.total}):`);
1562
- }
1563
- for (const c of about.callees.top) {
1564
- const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
1565
- lines.push(` ${c.name}${weight} - ${c.file}:${c.line} (${c.callCount}x)`);
1566
- if (showConf && c.confidence != null) {
1567
- lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
1568
- }
1569
-
1570
- // Inline expansion: show first 3 lines of callee code
1571
- if (expand && root && c.file && c.startLine) {
1572
- try {
1573
- const filePath = path.isAbsolute(c.file) ? c.file : path.join(root, c.file);
1574
- const content = fs.readFileSync(filePath, 'utf-8');
1575
- const fileLines = content.split('\n');
1576
- const endLine = c.endLine || c.startLine + 5;
1577
- const previewLines = Math.min(3, endLine - c.startLine + 1);
1578
- for (let i = 0; i < previewLines && c.startLine - 1 + i < fileLines.length; i++) {
1579
- const codeLine = fileLines[c.startLine - 1 + i];
1580
- lines.push(` │ ${codeLine}`);
1581
- }
1582
- if (endLine - c.startLine + 1 > 3) {
1583
- lines.push(` │ ... (${endLine - c.startLine - 2} more lines)`);
1584
- }
1585
- } catch (e) {
1586
- // Skip expansion on error
1587
- }
1588
- }
1589
- }
1590
- }
1591
-
1592
- // Tests
1593
- if (about.tests.totalMatches > 0) {
1594
- lines.push('');
1595
- if (about.tests.fileCount > about.tests.files.length) {
1596
- lines.push(`TESTS: ${about.tests.totalMatches} matches in ${about.tests.fileCount} file(s), showing ${about.tests.files.length}:`);
1597
- aboutTruncated = true;
1598
- } else {
1599
- lines.push(`TESTS: ${about.tests.totalMatches} matches in ${about.tests.fileCount} file(s)`);
1600
- }
1601
- for (const f of about.tests.files) {
1602
- lines.push(` ${f}`);
1603
- }
1604
- }
1605
-
1606
- // Other definitions
1607
- if (about.otherDefinitions.length > 0) {
1608
- lines.push('');
1609
- lines.push(`OTHER DEFINITIONS (${about.otherDefinitions.length}):`);
1610
- for (const d of about.otherDefinitions) {
1611
- lines.push(` ${d.file}:${d.line} (${d.usageCount} usages)`);
1612
- }
1613
- }
1614
-
1615
- // Types
1616
- if (about.types && about.types.length > 0) {
1617
- lines.push('');
1618
- lines.push('TYPES:');
1619
- for (const t of about.types) {
1620
- lines.push(` ${t.name} (${t.type}) - ${t.file}:${t.line}`);
1621
- }
1622
- }
1623
-
1624
- // Completeness warnings (condensed single line)
1625
- if (about.completeness && about.completeness.warnings && about.completeness.warnings.length > 0) {
1626
- const parts = about.completeness.warnings.map(w => `${w.count} ${w.type.replace('_', ' ')}`);
1627
- lines.push('');
1628
- lines.push(`Note: Results may be incomplete (${parts.join(', ')} in project)`);
1629
- }
1630
-
1631
- // Code
1632
- if (about.code) {
1633
- lines.push('');
1634
- lines.push('─── CODE ───');
1635
- lines.push(about.code);
1636
- }
1637
-
1638
- if (aboutTruncated) {
1639
- const allHint = options.allHint || 'Use --all to show all.';
1640
- lines.push(`\nSome sections truncated. ${allHint}`);
1641
- }
1642
-
1643
- if (about.includeMethods === false) {
1644
- const methodsHint = options.methodsHint || 'Note: obj.method() callers/callees excluded — use --include-methods to include them';
1645
- lines.push(`\n${methodsHint}`);
1646
- }
1647
-
1648
- return lines.join('\n');
1649
- }
1650
-
1651
- /**
1652
- * Format about command output - JSON
1653
- */
1654
- function formatAboutJson(about) {
1655
- if (!about) {
1656
- return JSON.stringify({ found: false, error: 'Symbol not found' }, null, 2);
1657
- }
1658
- return JSON.stringify(about, null, 2);
1659
- }
1660
-
1661
- /**
1662
- * Format example result as text
1663
- */
1664
- function formatExample(result, name) {
1665
- if (!result || !result.best) return `No call examples found for "${name}"`;
1666
-
1667
- const best = result.best;
1668
- const lines = [];
1669
- lines.push(`Best example of "${name}":`);
1670
- lines.push('═'.repeat(60));
1671
- lines.push(`${best.relativePath}:${best.line}`);
1672
- lines.push('');
1673
-
1674
- if (best.before) {
1675
- for (let i = 0; i < best.before.length; i++) {
1676
- const ln = best.line - best.before.length + i;
1677
- lines.push(`${ln.toString().padStart(4)}| ${best.before[i]}`);
1678
- }
1679
- }
1680
-
1681
- lines.push(`${best.line.toString().padStart(4)}| ${best.content} <--`);
1682
-
1683
- if (best.after) {
1684
- for (let i = 0; i < best.after.length; i++) {
1685
- const ln = best.line + i + 1;
1686
- lines.push(`${ln.toString().padStart(4)}| ${best.after[i]}`);
1687
- }
1688
- }
1689
-
1690
- lines.push('');
1691
- lines.push(`Score: ${best.score} (${result.totalCalls} total calls)`);
1692
- lines.push(`Why: ${best.reasons.length > 0 ? best.reasons.join(', ') : 'first available call'}`);
1693
-
1694
- return lines.join('\n');
1695
- }
1696
-
1697
- // ============================================================================
1698
- // TEXT FORMATTERS (shared between CLI and MCP)
1699
- // ============================================================================
1700
-
1701
- /**
1702
- * Format toc command output
1703
- * @param {object} toc - TOC data
1704
- * @param {object} [options] - Formatting options
1705
- * @param {string} [options.detailedHint] - Custom hint text for non-detailed mode
1706
- * @param {string} [options.uncertainHint] - Custom hint text for uncertain references
1707
- */
1708
- function formatToc(toc, options = {}) {
1709
- const lines = [];
1710
- const t = toc.totals;
1711
- lines.push(`PROJECT: ${t.files} files, ${t.lines} lines`);
1712
- lines.push(` ${t.functions} functions, ${t.classes} types (classes/interfaces/enums), ${t.state} state objects`);
1713
-
1714
- const meta = toc.meta || {};
1715
- if (meta.filteredBy) {
1716
- lines.push(` Filtered by: --file=${meta.filteredBy} (${meta.matchedFiles} files matched)`);
1717
- if (meta.emptyFiles) {
1718
- lines.push(` Note: ${meta.emptyFiles} file(s) have no detected symbols (may be generated or data files)`);
1719
- }
1720
- }
1721
- const warnings = [];
1722
- if (meta.dynamicImports) { const dn = dynamicImportsNote(meta.dynamicImports, meta); if (dn) warnings.push(dn); }
1723
- if (meta.uncertain) warnings.push(`${meta.uncertain} uncertain reference(s)`);
1724
- if (warnings.length) {
1725
- const uncertainSuffix = meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
1726
- lines.push(` Note: ${warnings.join(', ')}${uncertainSuffix}`);
1727
- }
1728
-
1729
- if (toc.summary) {
1730
- if (toc.summary.topFunctionFiles?.length) {
1731
- const hint = toc.summary.topFunctionFiles.map(f => `${f.file} (${f.functions})`).join(', ');
1732
- lines.push(` Most functions: ${hint}`);
1733
- }
1734
- if (toc.summary.topLineFiles?.length) {
1735
- const hint = toc.summary.topLineFiles.map(f => `${f.file} (${f.lines})`).join(', ');
1736
- lines.push(` Largest files: ${hint}`);
1737
- }
1738
- if (toc.summary.entryFiles?.length) {
1739
- lines.push(` Entry points: ${toc.summary.entryFiles.join(', ')}`);
1740
- }
1741
- }
1742
-
1743
- lines.push('═'.repeat(60));
1744
- const hasDetail = toc.files.some(f => f.symbols);
1745
- for (const file of toc.files) {
1746
- const parts = [`${file.lines} lines`];
1747
- if (file.functions) parts.push(`${file.functions} fn`);
1748
- if (file.classes) parts.push(`${file.classes} types`);
1749
- if (file.state) parts.push(`${file.state} state`);
1750
-
1751
- if (hasDetail) {
1752
- lines.push(`\n${file.file} (${parts.join(', ')})`);
1753
- if (file.symbols) {
1754
- for (const fn of file.symbols.functions) {
1755
- lines.push(` ${lineRange(fn.startLine, fn.endLine)} ${formatFunctionSignature(fn)}`);
1756
- }
1757
- for (const cls of file.symbols.classes) {
1758
- lines.push(` ${lineRange(cls.startLine, cls.endLine)} ${formatClassSignature(cls)}`);
1759
- }
1760
- }
1761
- } else {
1762
- lines.push(` ${file.file} — ${parts.join(', ')}`);
1763
- }
1764
- }
1765
-
1766
- if (!hasDetail) {
1767
- const hint = options.detailedHint || 'Use detailed=true to list all functions and classes.';
1768
- lines.push(`\n${hint}`);
1769
- }
1770
-
1771
- if (toc.hiddenFiles > 0) {
1772
- const topHint = options.topHint || 'Use --top=N or --all to show more.';
1773
- lines.push(`\n... and ${toc.hiddenFiles} more files. ${topHint}`);
1774
- }
1775
-
1776
- return lines.join('\n');
1777
- }
1778
-
1779
- /**
1780
- * Format find command output
1781
- */
1782
- function formatFind(symbols, query, top) {
1783
- if (symbols.length === 0) {
1784
- return `No symbols found for "${query}"`;
1785
- }
1786
-
1787
- const lines = [];
1788
- const limit = (top && top > 0) ? Math.min(symbols.length, top) : Math.min(symbols.length, 10);
1789
- const hidden = symbols.length - limit;
1790
-
1791
- if (hidden > 0) {
1792
- lines.push(`Found ${symbols.length} match(es) for "${query}" (showing top ${limit}):`);
1793
- } else {
1794
- lines.push(`Found ${symbols.length} match(es) for "${query}":`);
1795
- }
1796
- lines.push('─'.repeat(60));
1797
-
1798
- for (let i = 0; i < limit; i++) {
1799
- const s = symbols[i];
1800
- const sig = s.params !== undefined
1801
- ? formatFunctionSignature(s)
1802
- : formatClassSignature(s);
1803
- lines.push(`${s.relativePath}:${s.startLine} ${sig}`);
1804
- if (s.usageCounts !== undefined) {
1805
- const c = s.usageCounts;
1806
- const parts = [];
1807
- if (c.calls > 0) parts.push(`${c.calls} calls`);
1808
- if (c.definitions > 0) parts.push(`${c.definitions} def`);
1809
- if (c.imports > 0) parts.push(`${c.imports} imports`);
1810
- if (c.references > 0) parts.push(`${c.references} refs`);
1811
- lines.push(parts.length > 0
1812
- ? ` (${c.total} usages: ${parts.join(', ')})`
1813
- : ` (${c.total} usages)`);
1814
- } else if (s.usageCount !== undefined) {
1815
- lines.push(` (${s.usageCount} usages)`);
1816
- }
1817
- }
1818
-
1819
- if (hidden > 0) {
1820
- lines.push(`... ${hidden} more result(s).`);
1821
- }
1822
-
1823
- return lines.join('\n');
1824
- }
1825
-
1826
- /**
1827
- * Format usages command output
1828
- */
1829
- function formatUsages(usages, name) {
1830
- const defs = usages.filter(u => u.isDefinition);
1831
- const calls = usages.filter(u => u.usageType === 'call');
1832
- const imports = usages.filter(u => u.usageType === 'import');
1833
- const refs = usages.filter(u => !u.isDefinition && u.usageType === 'reference');
1834
-
1835
- const lines = [];
1836
- lines.push(`Usages of "${name}": ${defs.length} definitions, ${calls.length} calls, ${imports.length} imports, ${refs.length} references`);
1837
- lines.push('═'.repeat(60));
1838
-
1839
- function renderContextLines(usage) {
1840
- if (usage.before && usage.before.length > 0) {
1841
- for (const line of usage.before) {
1842
- lines.push(` ${line}`);
1843
- }
1844
- }
1845
- }
1846
-
1847
- function renderAfterLines(usage) {
1848
- if (usage.after && usage.after.length > 0) {
1849
- for (const line of usage.after) {
1850
- lines.push(` ${line}`);
1851
- }
1852
- }
1853
- }
1854
-
1855
- if (defs.length > 0) {
1856
- lines.push('\nDEFINITIONS:');
1857
- for (const d of defs) {
1858
- lines.push(` ${d.relativePath}:${d.line || d.startLine}`);
1859
- if (d.signature) lines.push(` ${d.signature}`);
1860
- }
1861
- }
1862
-
1863
- if (calls.length > 0) {
1864
- lines.push('\nCALLS:');
1865
- for (const c of calls) {
1866
- lines.push(` ${c.relativePath}:${c.line}`);
1867
- renderContextLines(c);
1868
- lines.push(` ${c.content.trim()}`);
1869
- renderAfterLines(c);
1870
- }
1871
- }
1872
-
1873
- if (imports.length > 0) {
1874
- lines.push('\nIMPORTS:');
1875
- for (const i of imports) {
1876
- lines.push(` ${i.relativePath}:${i.line}`);
1877
- lines.push(` ${i.content.trim()}`);
1878
- }
1879
- }
1880
-
1881
- if (refs.length > 0) {
1882
- lines.push('\nREFERENCES:');
1883
- for (const r of refs) {
1884
- lines.push(` ${r.relativePath}:${r.line}`);
1885
- renderContextLines(r);
1886
- lines.push(` ${r.content.trim()}`);
1887
- renderAfterLines(r);
1888
- }
1889
- }
1890
-
1891
- return lines.join('\n');
1892
- }
1893
-
1894
- /**
1895
- * Format context command output
1896
- * Returns { text, expandable } where expandable is an array of items for expand
1897
- * @param {object} ctx - Context data
1898
- * @param {object} [options] - Formatting options
1899
- * @param {string} [options.methodsHint] - Custom hint for excluded method calls
1900
- * @param {string} [options.expandHint] - Custom hint for expand command
1901
- * @param {string} [options.uncertainHint] - Custom hint for uncertain calls
1902
- */
1903
- function formatContext(ctx, options = {}) {
1904
- if (!ctx) return { text: 'Symbol not found.', expandable: [] };
1905
-
1906
- const expandHint = options.expandHint || 'Use ucn_expand with item number to see code for any item.';
1907
- const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded. Use include_methods=true to include them.';
1908
-
1909
- const lines = [];
1910
- const expandable = [];
1911
- let itemNum = 1;
1912
-
1913
- // Handle struct/interface types
1914
- if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
1915
- lines.push(`Context for ${ctx.type} ${ctx.name}:`);
1916
- lines.push('═'.repeat(60));
1917
-
1918
- if (ctx.warnings && ctx.warnings.length > 0) {
1919
- for (const w of ctx.warnings) {
1920
- lines.push(` Note: ${w.message}`);
1921
- }
1922
- }
1923
-
1924
- const methods = ctx.methods || [];
1925
- lines.push(`\nMETHODS (${methods.length}):`);
1926
- for (const m of methods) {
1927
- const receiver = m.receiver ? `(${m.receiver}) ` : '';
1928
- const params = m.params || '...';
1929
- const returnType = m.returnType ? `: ${m.returnType}` : '';
1930
- lines.push(` [${itemNum}] ${receiver}${m.name}(${params})${returnType}`);
1931
- lines.push(` ${m.file}:${m.line}`);
1932
- expandable.push({
1933
- num: itemNum++,
1934
- type: 'method',
1935
- name: m.name,
1936
- file: null,
1937
- relativePath: m.file,
1938
- startLine: m.line,
1939
- endLine: m.endLine || m.line
1940
- });
1941
- }
1942
-
1943
- const callers = ctx.callers || [];
1944
- lines.push(`\nCALLERS (${callers.length}):`);
1945
- for (const c of callers) {
1946
- const callerName = c.callerName ? ` [${c.callerName}]` : '';
1947
- lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
1948
- lines.push(` ${c.content.trim()}`);
1949
- expandable.push({
1950
- num: itemNum++,
1951
- type: 'caller',
1952
- name: c.callerName || '(module level)',
1953
- file: c.callerFile || c.file,
1954
- relativePath: c.relativePath,
1955
- line: c.line,
1956
- startLine: c.callerStartLine || c.line,
1957
- endLine: c.callerEndLine || c.line
1958
- });
1959
- }
1960
-
1961
- if (expandable.length > 0) {
1962
- lines.push(`\n${expandHint}`);
1963
- }
1964
-
1965
- return { text: lines.join('\n'), expandable };
1966
- }
1967
-
1968
- // Standard function/method context
1969
- lines.push(`Context for ${ctx.function}:`);
1970
- lines.push('═'.repeat(60));
1971
-
1972
- if (ctx.meta) {
1973
- const notes = [];
1974
- if (ctx.meta.dynamicImports) { const dn = dynamicImportsNote(ctx.meta.dynamicImports, ctx.meta); if (dn) notes.push(dn); }
1975
- if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
1976
- if (ctx.meta.confidenceFiltered) notes.push(`${ctx.meta.confidenceFiltered} edge(s) below confidence threshold hidden`);
1977
- if (notes.length) {
1978
- const uncertainSuffix = ctx.meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
1979
- lines.push(` Note: ${notes.join(', ')}${uncertainSuffix}`);
1980
- }
1981
- }
1982
-
1983
- if (ctx.meta && ctx.meta.includeMethods === false) {
1984
- lines.push(` ${methodsHint}`);
1985
- }
1986
-
1987
- if (ctx.warnings && ctx.warnings.length > 0) {
1988
- for (const w of ctx.warnings) {
1989
- lines.push(` Note: ${w.message}`);
1990
- }
1991
- }
1992
-
1993
- const showConf = options.showConfidence || false;
1994
- const callers = ctx.callers || [];
1995
- lines.push(`\nCALLERS (${callers.length}):`);
1996
- for (const c of callers) {
1997
- const callerName = c.callerName ? ` [${c.callerName}]` : '';
1998
- lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
1999
- lines.push(` ${c.content.trim()}`);
2000
- if (showConf && c.confidence != null) {
2001
- lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
2002
- }
2003
- expandable.push({
2004
- num: itemNum++,
2005
- type: 'caller',
2006
- name: c.callerName || '(module level)',
2007
- file: c.callerFile || c.file,
2008
- relativePath: c.relativePath,
2009
- line: c.line,
2010
- startLine: c.callerStartLine || c.line,
2011
- endLine: c.callerEndLine || c.line
2012
- });
2013
- }
2014
-
2015
- // Structural hint: class methods may have callers through constructed/injected instances
2016
- // that static analysis can't track. Only show when caller count is low (≤3) to avoid noise.
2017
- if (ctx.meta && (ctx.meta.isMethod || ctx.meta.className || ctx.meta.receiver) && callers.length <= 3) {
2018
- lines.push(` Note: ${ctx.function} is a class/struct method — additional callers through constructed or injected instances are not tracked by static analysis.`);
2019
- }
2020
-
2021
- const callees = ctx.callees || [];
2022
- lines.push(`\nCALLEES (${callees.length}):`);
2023
- for (const c of callees) {
2024
- const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
2025
- lines.push(` [${itemNum}] ${c.name}${weight} - ${c.relativePath}:${c.startLine}`);
2026
- if (showConf && c.confidence != null) {
2027
- lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
2028
- }
2029
- expandable.push({
2030
- num: itemNum++,
2031
- type: 'callee',
2032
- name: c.name,
2033
- file: c.file,
2034
- relativePath: c.relativePath,
2035
- startLine: c.startLine,
2036
- endLine: c.endLine
2037
- });
2038
- }
2039
-
2040
- if (expandable.length > 0) {
2041
- lines.push(`\n${expandHint}`);
2042
- }
2043
-
2044
- return { text: lines.join('\n'), expandable };
2045
- }
2046
-
2047
- /**
2048
- * Format smart command output
2049
- * @param {object} smart - Smart extraction result
2050
- * @param {object} [options] - Formatting options
2051
- * @param {string} [options.uncertainHint] - Custom hint for uncertain calls
2052
- */
2053
- function formatSmart(smart, options = {}) {
2054
- if (!smart) return 'Function not found.';
2055
-
2056
- const lines = [];
2057
- lines.push(`${smart.target.name} (${smart.target.file}:${smart.target.startLine})`);
2058
- lines.push('═'.repeat(60));
2059
-
2060
- if (smart.meta) {
2061
- const notes = [];
2062
- if (smart.meta.dynamicImports) { const dn = dynamicImportsNote(smart.meta.dynamicImports, smart.meta); if (dn) notes.push(dn); }
2063
- if (smart.meta.uncertain) notes.push(`${smart.meta.uncertain} uncertain call(s) skipped`);
2064
- if (notes.length) {
2065
- const uncertainSuffix = smart.meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
2066
- lines.push(` Note: ${notes.join(', ')}${uncertainSuffix}`);
2067
- }
2068
- }
2069
-
2070
- lines.push(smart.target.code);
2071
-
2072
- if (smart.dependencies.length > 0) {
2073
- lines.push('\n─── DEPENDENCIES ───');
2074
- for (const dep of smart.dependencies) {
2075
- const weight = dep.weight && dep.weight !== 'normal' ? ` [${dep.weight}]` : '';
2076
- lines.push(`\n// ${dep.name}${weight} (${dep.relativePath}:${dep.startLine})`);
2077
- lines.push(dep.code);
2078
- }
2079
- }
2080
-
2081
- if (smart.types && smart.types.length > 0) {
2082
- lines.push('\n─── TYPES ───');
2083
- for (const t of smart.types) {
2084
- lines.push(`\n// ${t.name} (${t.relativePath}:${t.startLine})`);
2085
- lines.push(t.code);
2086
- }
2087
- }
2088
-
2089
- return lines.join('\n');
2090
- }
2091
-
2092
- /**
2093
- * Format deadcode command output
2094
- * @param {Array} results - Dead code results
2095
- * @param {object} [options] - Formatting options
2096
- * @param {string} [options.exportedHint] - Hint about exported symbols exclusion
2097
- */
2098
- function formatDeadcode(results, options = {}) {
2099
- if (results.length === 0 && !results.excludedDecorated && !results.excludedExported) {
2100
- return 'No dead code found.';
2101
- }
2102
-
2103
- const lines = [];
2104
- const top = options.top > 0 ? options.top : 0;
2105
- const showing = top > 0 ? results.slice(0, top) : results;
2106
- const hidden = results.length - showing.length;
2107
-
2108
- if (results.length > 0) {
2109
- if (hidden > 0) {
2110
- lines.push(`Dead code: ${results.length} unused symbol(s) (showing ${showing.length})\n`);
2111
- } else {
2112
- lines.push(`Dead code: ${results.length} unused symbol(s)\n`);
2113
- }
2114
- }
2115
-
2116
- let currentFile = null;
2117
- for (const item of showing) {
2118
- if (item.file !== currentFile) {
2119
- currentFile = item.file;
2120
- lines.push(item.file);
2121
- }
2122
- const exported = item.isExported ? ' [exported]' : '';
2123
- // Surface decorators/annotations — structural hint that a framework may invoke this
2124
- const hints = [];
2125
- if (item.decorators && item.decorators.length > 0) {
2126
- hints.push(...item.decorators.map(d => `@${d}`));
2127
- }
2128
- if (item.annotations && item.annotations.length > 0) {
2129
- hints.push(...item.annotations.map(a => `@${a}`));
2130
- }
2131
- const hintStr = hints.length > 0 ? ` [has ${hints.join(', ')}]` : '';
2132
- lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}`);
2133
- }
2134
-
2135
- if (hidden > 0) {
2136
- lines.push(`\n${hidden} more result(s) not shown. Use --top=${results.length} or --all to see all.`);
2137
- }
2138
-
2139
- // Show counts of excluded items with expansion hints
2140
- if (results.length === 0) {
2141
- lines.push('No dead code found.');
2142
- }
2143
- if (results.excludedDecorated > 0) {
2144
- const decoratedHint = options.decoratedHint || `${results.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.`;
2145
- lines.push(`\n${decoratedHint}`);
2146
- }
2147
- if (results.excludedExported > 0) {
2148
- const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.`;
2149
- lines.push(`\n${exportedHint}`);
2150
- }
2151
-
2152
- if (lines.length === 0) {
2153
- return 'No dead code found.';
2154
- }
2155
-
2156
- return lines.join('\n');
2157
- }
2158
-
2159
- /**
2160
- * Format fn command output
2161
- */
2162
- function formatFn(match, fnCode) {
2163
- const lines = [];
2164
- lines.push(`${match.relativePath}:${match.startLine}`);
2165
- lines.push(`${lineRange(match.startLine, match.endLine)} ${formatFunctionSignature(match)}`);
2166
- lines.push('─'.repeat(60));
2167
- lines.push(fnCode);
2168
- return lines.join('\n');
2169
- }
2170
-
2171
- /**
2172
- * Format class command output
2173
- */
2174
- function formatClass(cls, clsCode) {
2175
- const lines = [];
2176
- lines.push(`${cls.relativePath || cls.file}:${cls.startLine}`);
2177
- lines.push(`${lineRange(cls.startLine, cls.endLine)} ${formatClassSignature(cls)}`);
2178
- lines.push('─'.repeat(60));
2179
- lines.push(clsCode);
2180
- return lines.join('\n');
2181
- }
2182
-
2183
- /**
2184
- * Format graph command output
2185
- * @param {object} graph - Graph data
2186
- * @param {object} [options] - Formatting options
2187
- * @param {boolean} [options.showAll] - Show all children (no truncation)
2188
- * @param {number} [options.maxDepth] - Maximum depth for tree traversal
2189
- */
2190
- function formatGraph(graph, options = {}) {
2191
- // Support legacy signature: formatGraph(graph, showAll)
2192
- if (typeof options === 'boolean') {
2193
- options = { showAll: options };
2194
- }
2195
- if (graph?.error) return formatFileError(graph);
2196
- if (graph.nodes.length === 0) {
2197
- const file = options.file || graph.root || '';
2198
- return file ? `File not found: ${file}` : 'File not found.';
2199
- }
2200
-
2201
- const rootEntry = graph.nodes.find(n => n.file === graph.root);
2202
- const rootRelPath = rootEntry ? rootEntry.relativePath : graph.root;
2203
- const lines = [];
2204
-
2205
- const showAll = options.showAll || false;
2206
- const maxChildren = showAll ? Infinity : 8;
2207
- const maxDepth = options.maxDepth !== undefined ? options.maxDepth : Infinity;
2208
-
2209
- function printTree(nodes, edges, rootFile) {
2210
- const visited = new Set(); // all nodes ever printed (for diamond dep detection)
2211
- const ancestors = new Set(); // current path from root (for true circular detection)
2212
- let truncatedNodes = 0;
2213
- let depthLimited = false;
2214
-
2215
- function printNode(file, indent = 0, isLast = true) {
2216
- const fileEntry = nodes.find(n => n.file === file);
2217
- const relPath = fileEntry ? fileEntry.relativePath : file;
2218
- const connector = isLast ? '└── ' : '├── ';
2219
- const prefix = indent === 0 ? '' : ' '.repeat(indent - 1) + connector;
2220
-
2221
- if (ancestors.has(file)) {
2222
- lines.push(`${prefix}${relPath} (circular)`);
2223
- return;
2224
- }
2225
- if (visited.has(file)) {
2226
- lines.push(`${prefix}${relPath} (already shown)`);
2227
- return;
2228
- }
2229
- visited.add(file);
2230
-
2231
- if (indent > maxDepth) {
2232
- depthLimited = true;
2233
- lines.push(`${prefix}${relPath} ...`);
2234
- return;
2235
- }
2236
-
2237
- lines.push(`${prefix}${relPath}`);
2238
-
2239
- ancestors.add(file);
2240
- const fileEdges = edges.filter(e => e.from === file);
2241
- const displayEdges = fileEdges.slice(0, maxChildren);
2242
- const hiddenCount = fileEdges.length - displayEdges.length;
2243
-
2244
- for (let i = 0; i < displayEdges.length; i++) {
2245
- const childIsLast = i === displayEdges.length - 1 && hiddenCount === 0;
2246
- printNode(displayEdges[i].to, indent + 1, childIsLast);
2247
- }
2248
- ancestors.delete(file);
2249
-
2250
- if (hiddenCount > 0) {
2251
- truncatedNodes += hiddenCount;
2252
- lines.push(`${' '.repeat(indent)}└── ... and ${hiddenCount} more`);
2253
- }
2254
- }
2255
-
2256
- printNode(rootFile);
2257
- return { truncatedNodes, depthLimited };
2258
- }
2259
-
2260
- if (graph.direction === 'both' && graph.imports && graph.importers) {
2261
- const importCount = graph.imports.edges.filter(e => e.from === graph.root).length;
2262
- const importerCount = graph.importers.edges.filter(e => e.from === graph.root).length;
2263
-
2264
- lines.push(`Dependency graph for ${rootRelPath}`);
2265
- lines.push('═'.repeat(60));
2266
-
2267
- let totalTruncated = 0;
2268
- let anyDepthLimited = false;
2269
-
2270
- lines.push(`\nIMPORTS (what this file depends on): ${importCount} files`);
2271
- if (importCount > 0) {
2272
- const r = printTree(graph.imports.nodes, graph.imports.edges, graph.root);
2273
- totalTruncated += r.truncatedNodes;
2274
- anyDepthLimited = anyDepthLimited || r.depthLimited;
2275
- } else {
2276
- lines.push(' (none)');
2277
- }
2278
-
2279
- lines.push(`\nIMPORTERS (what depends on this file): ${importerCount} files`);
2280
- if (importerCount > 0) {
2281
- const r = printTree(graph.importers.nodes, graph.importers.edges, graph.root);
2282
- totalTruncated += r.truncatedNodes;
2283
- anyDepthLimited = anyDepthLimited || r.depthLimited;
2284
- } else {
2285
- lines.push(' (none)');
2286
- }
2287
-
2288
- if (anyDepthLimited || totalTruncated > 0) {
2289
- lines.push('\n' + '─'.repeat(60));
2290
- if (anyDepthLimited) {
2291
- const depthHint = options.depthHint || `Use --depth=N for deeper graph.`;
2292
- lines.push(`Depth limited to ${maxDepth}. ${depthHint}`);
2293
- }
2294
- if (totalTruncated > 0) {
2295
- const allHint = options.allHint || 'Use --all to show all children.';
2296
- lines.push(`${totalTruncated} nodes hidden. ${allHint}`);
2297
- }
2298
- }
2299
- } else {
2300
- lines.push(`Dependency graph for ${rootRelPath}`);
2301
- lines.push('═'.repeat(60));
2302
-
2303
- const { truncatedNodes, depthLimited } = printTree(graph.nodes, graph.edges, graph.root);
2304
-
2305
- if (depthLimited || truncatedNodes > 0) {
2306
- lines.push('\n' + '─'.repeat(60));
2307
- if (depthLimited) {
2308
- const depthHint = options.depthHint || `Use --depth=N for deeper graph.`;
2309
- lines.push(`Depth limited to ${maxDepth}. ${depthHint}`);
2310
- }
2311
- if (truncatedNodes > 0) {
2312
- const allHint = options.allHint || 'Use --all to show all children.';
2313
- lines.push(`${truncatedNodes} nodes hidden. ${allHint} Graph has ${graph.nodes.length} total files.`);
2314
- }
2315
- }
2316
- }
2317
-
2318
- return lines.join('\n');
2319
- }
2320
-
2321
- function formatCircularDeps(result) {
2322
- if (!result) return 'No results.';
2323
- const lines = [];
2324
-
2325
- lines.push('Circular dependencies');
2326
- lines.push('═'.repeat(60));
2327
-
2328
- if (result.fileFilter) {
2329
- lines.push(`Filtered to cycles involving: ${result.fileFilter}`);
2330
- }
2331
-
2332
- const scannedCount = result.filesWithImports != null ? result.filesWithImports : result.totalFiles;
2333
-
2334
- if (result.cycles.length === 0) {
2335
- lines.push('');
2336
- lines.push('No circular dependencies found.');
2337
- lines.push(`Scanned ${scannedCount} files with import relationships.`);
2338
- return lines.join('\n');
2339
- }
2340
-
2341
- for (let i = 0; i < result.cycles.length; i++) {
2342
- const cycle = result.cycles[i];
2343
- lines.push('');
2344
- lines.push(`Cycle ${i + 1} (${cycle.length} files):`);
2345
- lines.push(` ${cycle.files.join(' → ')} → ${cycle.files[0]}`);
2346
- }
2347
-
2348
- lines.push('');
2349
- const { totalCycles, filesInCycles } = result.summary;
2350
- lines.push(`Summary: ${totalCycles} circular dependency chain${totalCycles !== 1 ? 's' : ''} involving ${filesInCycles} file${filesInCycles !== 1 ? 's' : ''} (${scannedCount} files with imports scanned).`);
2351
-
2352
- return lines.join('\n');
2353
- }
2354
-
2355
- function formatCircularDepsJson(result) {
2356
- if (!result) return JSON.stringify({ error: 'No results' }, null, 2);
2357
- return JSON.stringify(result, null, 2);
2358
- }
2359
-
2360
- /**
2361
- * Detect common double-escaping patterns in regex search terms.
2362
- * When MCP/JSON transport is involved, agents often write \\. when they mean \. (literal dot).
2363
- * Returns a hint string if double-escaping is suspected, empty string otherwise.
2364
- */
2365
- function detectDoubleEscaping(term) {
2366
- // Look for \\. \\d \\w \\s \\b \\D \\W \\S \\B \\( \\) \\[ \\] — common double-escaped sequences
2367
- const doubleEscaped = term.match(/\\\\[.dDwWsSbB()\[\]*+?^${}|]/g);
2368
- if (!doubleEscaped) return '';
2369
- const examples = [...new Set(doubleEscaped)].slice(0, 3);
2370
- const fixed = examples.map(e => e.slice(1)); // remove one backslash
2371
- return `\nHint: Pattern contains ${examples.join(', ')} which matches literal backslash(es). If you meant ${fixed.join(', ')}, use a single backslash (MCP/JSON parameters are already raw strings).`;
2372
- }
2373
-
2374
- /**
2375
- * Format search command output
2376
- */
2377
- function formatSearch(results, term) {
2378
- const meta = results.meta;
2379
- const fallbackNote = meta && meta.regexFallback
2380
- ? `\nNote: Invalid regex (${meta.regexFallback}). Fell back to plain text search.`
2381
- : '';
2382
-
2383
- const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
2384
- if (totalMatches === 0) {
2385
- if (meta) {
2386
- const scope = meta.filesSkipped > 0
2387
- ? `Searched ${meta.filesScanned} of ${meta.totalFiles} file${meta.totalFiles === 1 ? '' : 's'} (${meta.filesSkipped} excluded by filters).`
2388
- : `Searched ${meta.filesScanned} file${meta.filesScanned === 1 ? '' : 's'}.`;
2389
- const escapingHint = detectDoubleEscaping(term);
2390
- return `No matches found for "${term}". ${scope}${fallbackNote}${escapingHint}`;
2391
- }
2392
- return `No matches found for "${term}"${fallbackNote}`;
2393
- }
2394
-
2395
- const lines = [];
2396
- const fileWord = results.length === 1 ? 'file' : 'files';
2397
- lines.push(`Found ${totalMatches} match${totalMatches === 1 ? '' : 'es'} for "${term}" in ${results.length} ${fileWord}:`);
2398
- if (fallbackNote) lines.push(fallbackNote.trim());
2399
- lines.push('═'.repeat(60));
2400
-
2401
- for (const result of results) {
2402
- lines.push(`\n${result.file}`);
2403
- for (const m of result.matches) {
2404
- if (m.before && m.before.length > 0) {
2405
- for (const line of m.before) {
2406
- lines.push(` ... ${line.trim()}`);
2407
- }
2408
- }
2409
- lines.push(` ${m.line}: ${m.content.trim()}`);
2410
- if (m.after && m.after.length > 0) {
2411
- for (const line of m.after) {
2412
- lines.push(` ... ${line.trim()}`);
2413
- }
2414
- }
2415
- }
2416
- }
2417
-
2418
- if (meta && meta.truncatedMatches > 0) {
2419
- lines.push(`\n${results.reduce((s, r) => s + r.matches.length, 0)} shown of ${meta.totalMatches} total matches. Use top= to see more.`);
2420
- }
2421
-
2422
- if (meta && meta.testsExcluded && meta.filesSkipped > 0) {
2423
- lines.push(`\nNote: ${meta.filesSkipped} test file${meta.filesSkipped === 1 ? '' : 's'} hidden by default (use include_tests=true to include).`);
2424
- }
2425
-
2426
- return lines.join('\n');
2427
- }
2428
-
2429
- /**
2430
- * Format structural search results (index-based queries)
2431
- */
2432
- function formatStructuralSearch(result) {
2433
- const { results, meta } = result;
2434
- const lines = [];
2435
-
2436
- // Build query description
2437
- const parts = [];
2438
- if (meta.query.type) parts.push(`type=${meta.query.type}`);
2439
- if (meta.query.term) parts.push(`name="${meta.query.term}"`);
2440
- if (meta.query.param) parts.push(`param="${meta.query.param}"`);
2441
- if (meta.query.receiver) parts.push(`receiver="${meta.query.receiver}"`);
2442
- if (meta.query.returns) parts.push(`returns="${meta.query.returns}"`);
2443
- if (meta.query.decorator) parts.push(`decorator="${meta.query.decorator}"`);
2444
- if (meta.query.exported) parts.push('exported');
2445
- if (meta.query.unused) parts.push('unused');
2446
- const queryStr = parts.join(', ');
2447
-
2448
- lines.push(`Structural search: ${queryStr}`);
2449
- lines.push('═'.repeat(60));
2450
-
2451
- if (results.length === 0) {
2452
- lines.push('No matches found.');
2453
- return lines.join('\n');
2454
- }
2455
-
2456
- lines.push(`Found ${meta.totalMatched} match${meta.totalMatched === 1 ? '' : 'es'}${meta.shown < meta.totalMatched ? ` (showing ${meta.shown})` : ''}:`);
2457
- lines.push('');
2458
-
2459
- // Group by file
2460
- let currentFile = null;
2461
- for (const r of results) {
2462
- if (r.file !== currentFile) {
2463
- currentFile = r.file;
2464
- lines.push(`${r.file}`);
2465
- }
2466
-
2467
- if (r.kind === 'call') {
2468
- lines.push(` ${r.line}: ${r.name}()${r.isMethod ? ' [method]' : ''}`);
2469
- } else {
2470
- let sig = ` ${r.line}: ${r.kind} ${r.name}`;
2471
- if (r.params) sig += `(${r.params})`;
2472
- if (r.returnType) sig += ` → ${r.returnType}`;
2473
- if (r.className) sig += ` [${r.className}]`;
2474
- if (r.decorators) sig += ` @${r.decorators.join(', @')}`;
2475
- lines.push(sig);
2476
- }
2477
- }
2478
-
2479
- if (meta.shown < meta.totalMatched) {
2480
- lines.push(`\n${meta.shown} of ${meta.totalMatched} shown. Use top= to see more.`);
2481
- }
2482
-
2483
- return lines.join('\n');
2484
- }
2485
-
2486
- function formatStructuralSearchJson(result) {
2487
- return JSON.stringify(result, null, 2);
2488
- }
2489
-
2490
- /**
2491
- * Format file-exports command output
2492
- */
2493
- function formatFileExports(exports, filePath) {
2494
- if (exports?.error) return formatFileError(exports, filePath);
2495
- if (exports.length === 0) return `No exports found in ${filePath}`;
2496
-
2497
- const lines = [];
2498
- lines.push(`Exports from ${filePath}:\n`);
2499
- for (const exp of exports) {
2500
- lines.push(` ${lineRange(exp.startLine, exp.endLine)} ${exp.signature || exp.name}`);
2501
- }
2502
- return lines.join('\n');
2503
- }
2504
-
2505
- /**
2506
- * Format stats command output
2507
- */
2508
- function formatStats(stats, options = {}) {
2509
- const lines = [];
2510
- lines.push('PROJECT STATISTICS');
2511
- lines.push('═'.repeat(60));
2512
- lines.push(`Root: ${stats.root}`);
2513
- if (stats.truncated) {
2514
- lines.push(`Files: ${stats.files} (truncated at ${stats.truncated.maxFiles} — use --max-files to increase)`);
2515
- } else {
2516
- lines.push(`Files: ${stats.files}`);
2517
- }
2518
- lines.push(`Symbols: ${stats.symbols}`);
2519
- lines.push(`Build time: ${stats.buildTime}ms`);
2520
-
2521
- lines.push('\nBy Language:');
2522
- for (const [lang, info] of Object.entries(stats.byLanguage)) {
2523
- lines.push(` ${lang}: ${info.files} files, ${info.lines} lines, ${info.symbols} symbols`);
2524
- }
2525
-
2526
- lines.push('\nBy Type:');
2527
- for (const [type, count] of Object.entries(stats.byType)) {
2528
- lines.push(` ${type}: ${count}`);
2529
- }
2530
-
2531
- if (stats.functions) {
2532
- const top = options.top || 30;
2533
- const shown = stats.functions.slice(0, top);
2534
- lines.push(`\nFunctions by line count (top ${shown.length} of ${stats.functions.length}):`);
2535
- for (const fn of shown) {
2536
- const loc = `${fn.file}:${fn.startLine}`;
2537
- lines.push(` ${String(fn.lines).padStart(5)} lines ${fn.name} (${loc})`);
2538
- }
2539
- if (stats.functions.length > top) {
2540
- lines.push(` ... ${stats.functions.length - top} more (use --top=N to show more)`);
2541
- }
2542
- }
2543
-
2544
- return lines.join('\n');
2545
- }
2546
-
2547
- // ============================================================================
2548
- // DIFF IMPACT
2549
- // ============================================================================
2550
-
2551
- function formatDiffImpact(result, options = {}) {
2552
- if (!result) return 'No diff data.';
2553
-
2554
- const lines = [];
2555
- const MAX_CALLERS_PER_FN = options.all ? Infinity : 30;
2556
-
2557
- lines.push(`Diff Impact Analysis (vs ${result.base})`);
2558
- lines.push('═'.repeat(60));
2559
-
2560
- const s = result.summary || {};
2561
- const parts = [];
2562
- if (s.modifiedFunctions > 0) parts.push(`${s.modifiedFunctions} modified`);
2563
- if (s.deletedFunctions > 0) parts.push(`${s.deletedFunctions} deleted`);
2564
- if (s.newFunctions > 0) parts.push(`${s.newFunctions} new`);
2565
- parts.push(`${s.totalCallSites || 0} call sites across ${s.affectedFiles || 0} files`);
2566
- lines.push(parts.join(', '));
2567
- lines.push('');
2568
-
2569
- // Modified functions
2570
- if (result.functions.length > 0) {
2571
- lines.push('MODIFIED FUNCTIONS:');
2572
- for (const fn of result.functions) {
2573
- lines.push(`\n ${fn.name}`);
2574
- lines.push(` ${fn.relativePath}:${fn.startLine}`);
2575
- lines.push(` ${fn.signature}`);
2576
- if (fn.addedLines.length > 0) {
2577
- lines.push(` Lines added: ${formatLineRanges(fn.addedLines)}`);
2578
- }
2579
- if (fn.deletedLines.length > 0) {
2580
- lines.push(` Lines deleted: ${formatLineRanges(fn.deletedLines)}`);
2581
- }
2582
-
2583
- if (fn.callers.length > 0) {
2584
- const displayCallers = fn.callers.slice(0, MAX_CALLERS_PER_FN);
2585
- const truncated = fn.callers.length - displayCallers.length;
2586
- lines.push(` Callers (${fn.callers.length}):`);
2587
- for (const c of displayCallers) {
2588
- const caller = c.callerName ? `[${c.callerName}]` : '';
2589
- lines.push(` ${c.relativePath}:${c.line} ${caller}`);
2590
- lines.push(` ${c.content}`);
2591
- }
2592
- if (truncated > 0) {
2593
- lines.push(` ... ${truncated} more callers (use file= to scope diff to specific files, or use impact with class_name= for type-filtered results)`);
2594
- }
2595
- } else {
2596
- lines.push(' Callers: none found');
2597
- }
2598
- }
2599
- }
2600
-
2601
- // New functions
2602
- if (result.newFunctions.length > 0) {
2603
- lines.push('\nNEW FUNCTIONS:');
2604
- for (const fn of result.newFunctions) {
2605
- lines.push(` ${fn.name} — ${fn.relativePath}:${fn.startLine}`);
2606
- lines.push(` ${fn.signature}`);
2607
- }
2608
- }
2609
-
2610
- // Deleted functions
2611
- if (result.deletedFunctions.length > 0) {
2612
- lines.push('\nDELETED FUNCTIONS:');
2613
- for (const fn of result.deletedFunctions) {
2614
- lines.push(` ${fn.name} — ${fn.relativePath}:${fn.startLine}`);
2615
- }
2616
- }
2617
-
2618
- // Module-level changes
2619
- if (result.moduleLevelChanges.length > 0) {
2620
- lines.push('\nMODULE-LEVEL CHANGES:');
2621
- for (const m of result.moduleLevelChanges) {
2622
- const changeParts = [];
2623
- if (m.addedLines.length > 0) changeParts.push(`+${m.addedLines.length} lines`);
2624
- if (m.deletedLines.length > 0) changeParts.push(`-${m.deletedLines.length} lines`);
2625
- lines.push(` ${m.relativePath}: ${changeParts.join(', ')}`);
2626
- }
2627
- }
2628
-
2629
- return lines.join('\n');
2630
- }
2631
-
2632
- /**
2633
- * Compact display of line numbers, collapsing consecutive ranges
2634
- */
2635
- function formatLineRanges(lineNums) {
2636
- if (lineNums.length === 0) return '';
2637
- const sorted = [...lineNums].sort((a, b) => a - b);
2638
- const ranges = [];
2639
- let start = sorted[0], end = sorted[0];
2640
- for (let i = 1; i < sorted.length; i++) {
2641
- if (sorted[i] === end + 1) {
2642
- end = sorted[i];
2643
- } else {
2644
- ranges.push(start === end ? `${start}` : `${start}-${end}`);
2645
- start = end = sorted[i];
2646
- }
2647
- }
2648
- ranges.push(start === end ? `${start}` : `${start}-${end}`);
2649
- return ranges.join(', ');
2650
- }
2651
-
2652
- /**
2653
- * Format plan command output - JSON
2654
- */
2655
- function formatPlanJson(plan) {
2656
- if (!plan) {
2657
- return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
2658
- }
2659
- if (!plan.found) {
2660
- return JSON.stringify({
2661
- found: false,
2662
- error: plan.error || `Function "${plan.function}" not found.`,
2663
- ...(plan.currentParams && { currentParams: plan.currentParams })
2664
- }, null, 2);
2665
- }
2666
- if (plan.error) {
2667
- return JSON.stringify({
2668
- found: true,
2669
- error: plan.error,
2670
- ...(plan.currentParams && { currentParams: plan.currentParams })
2671
- }, null, 2);
2672
- }
2673
-
2674
- return JSON.stringify({
2675
- found: true,
2676
- function: plan.function,
2677
- file: plan.file,
2678
- startLine: plan.startLine,
2679
- operation: plan.operation,
2680
- before: { signature: plan.before.signature },
2681
- after: { signature: plan.after.signature },
2682
- totalChanges: plan.totalChanges,
2683
- filesAffected: plan.filesAffected,
2684
- changes: plan.changes.map(c => ({
2685
- file: c.file,
2686
- line: c.line,
2687
- expression: c.expression,
2688
- suggestion: c.suggestion
2689
- }))
2690
- }, null, 2);
2691
- }
2692
-
2693
- /**
2694
- * Format stack trace command output - JSON
2695
- */
2696
- function formatStackTraceJson(result) {
2697
- if (!result || result.frameCount === 0) {
2698
- return JSON.stringify({ frameCount: 0, frames: [] }, null, 2);
2699
- }
2700
-
2701
- return JSON.stringify({
2702
- frameCount: result.frameCount,
2703
- frames: result.frames.map(f => ({
2704
- function: f.function || null,
2705
- file: f.file,
2706
- line: f.line,
2707
- found: !!f.found,
2708
- ...(f.resolvedFile && { resolvedFile: f.resolvedFile }),
2709
- ...(f.context && { context: f.context.map(c => ({
2710
- line: c.line,
2711
- code: c.code,
2712
- isCurrent: !!c.isCurrent
2713
- })) }),
2714
- ...(f.functionInfo && { functionInfo: {
2715
- name: f.functionInfo.name,
2716
- params: f.functionInfo.params || null,
2717
- startLine: f.functionInfo.startLine,
2718
- endLine: f.functionInfo.endLine
2719
- } }),
2720
- ...(f.raw && { raw: f.raw })
2721
- }))
2722
- }, null, 2);
2723
- }
2724
-
2725
- /**
2726
- * Format verify command output - JSON
2727
- */
2728
- function formatVerifyJson(result) {
2729
- if (!result) {
2730
- return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
2731
- }
2732
- if (!result.found) {
2733
- return JSON.stringify({ found: false, error: `Function "${result.function}" not found.` }, null, 2);
2734
- }
2735
-
2736
- return JSON.stringify({
2737
- found: true,
2738
- function: result.function,
2739
- file: result.file,
2740
- startLine: result.startLine,
2741
- signature: result.signature,
2742
- expectedArgs: result.expectedArgs,
2743
- totalCalls: result.totalCalls,
2744
- valid: result.valid,
2745
- mismatches: result.mismatches,
2746
- uncertain: result.uncertain,
2747
- mismatchDetails: result.mismatchDetails.map(m => ({
2748
- file: m.file,
2749
- line: m.line,
2750
- expression: m.expression,
2751
- expected: m.expected,
2752
- actual: m.actual,
2753
- args: m.args || []
2754
- })),
2755
- uncertainDetails: result.uncertainDetails.map(u => ({
2756
- file: u.file,
2757
- line: u.line,
2758
- expression: u.expression,
2759
- reason: u.reason
2760
- }))
2761
- }, null, 2);
2762
- }
2763
-
2764
- /**
2765
- * Format example command output - JSON
2766
- */
2767
- function formatExampleJson(result, name) {
2768
- if (!result || !result.best) {
2769
- return JSON.stringify({ found: false, query: name, error: `No call examples found for "${name}"` }, null, 2);
2770
- }
2771
-
2772
- const best = result.best;
2773
- return JSON.stringify({
2774
- found: true,
2775
- query: name,
2776
- totalCalls: result.totalCalls,
2777
- best: {
2778
- file: best.relativePath || best.file,
2779
- line: best.line,
2780
- content: best.content,
2781
- score: best.score,
2782
- reasons: best.reasons || [],
2783
- ...(best.before && best.before.length > 0 && { before: best.before }),
2784
- ...(best.after && best.after.length > 0 && { after: best.after })
2785
- }
2786
- }, null, 2);
2787
- }
2788
-
2789
- /**
2790
- * Format deadcode command output - JSON
2791
- */
2792
- function formatDeadcodeJson(results) {
2793
- return JSON.stringify({
2794
- count: results.length,
2795
- ...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
2796
- ...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
2797
- symbols: results.map(item => ({
2798
- name: item.name,
2799
- type: item.type,
2800
- file: item.file,
2801
- startLine: item.startLine,
2802
- endLine: item.endLine,
2803
- ...(item.isExported && { isExported: true }),
2804
- ...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
2805
- ...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations })
2806
- }))
2807
- }, null, 2);
2808
- }
2809
-
2810
- function formatDiffImpactJson(result) {
2811
- return JSON.stringify(result, null, 2);
2812
- }
2813
-
2814
- // ============================================================================
2815
- // Extraction command formatters (fn, class, lines)
2816
- // ============================================================================
2817
-
2818
- /**
2819
- * Format fn handler result (from execute.js).
2820
- * Notes are NOT included — surfaces render those separately (e.g. stderr for CLI).
2821
- * @param {{ entries: Array<{match, code}>, notes: string[] }} result
2822
- */
2823
- function formatFnResult(result) {
2824
- const parts = [];
2825
- for (const { match, code } of result.entries) {
2826
- parts.push(formatFn(match, code));
2827
- }
2828
- const separator = result.entries.length > 1 ? '\n\n' + '═'.repeat(60) + '\n\n' : '';
2829
- return parts.join(separator);
2830
- }
2831
-
2832
- /**
2833
- * Format fn handler result as JSON.
2834
- */
2835
- function formatFnResultJson(result) {
2836
- if (result.entries.length === 1) {
2837
- return formatFunctionJson(result.entries[0].match, result.entries[0].code);
2838
- }
2839
- const arr = result.entries.map(({ match, code }) => ({
2840
- name: match.name,
2841
- params: match.params,
2842
- paramsStructured: match.paramsStructured || [],
2843
- startLine: match.startLine,
2844
- endLine: match.endLine,
2845
- modifiers: match.modifiers || [],
2846
- ...(match.returnType && { returnType: match.returnType }),
2847
- ...(match.generics && { generics: match.generics }),
2848
- ...(match.docstring && { docstring: match.docstring }),
2849
- ...(match.isArrow && { isArrow: true }),
2850
- ...(match.isGenerator && { isGenerator: true }),
2851
- file: match.relativePath || match.file,
2852
- code,
2853
- }));
2854
- return JSON.stringify(arr, null, 2);
2855
- }
2856
-
2857
- /**
2858
- * Format class handler result (from execute.js).
2859
- * @param {{ entries: Array<{match, code, methods?, summaryMode, truncated, totalLines, maxLines?}>, notes: string[] }} result
2860
- */
2861
- function formatClassResult(result) {
2862
- const parts = [];
2863
- for (const entry of result.entries) {
2864
- if (entry.summaryMode) {
2865
- // Large class summary
2866
- const lines = [];
2867
- lines.push(`${entry.match.relativePath}:${entry.match.startLine}`);
2868
- lines.push(`${lineRange(entry.match.startLine, entry.match.endLine)} ${formatClassSignature(entry.match)}`);
2869
- lines.push('\u2500'.repeat(60));
2870
- if (entry.methods && entry.methods.length > 0) {
2871
- lines.push(`\nMethods (${entry.methods.length}):`);
2872
- for (const m of entry.methods) {
2873
- lines.push(` ${formatFunctionSignature(m)} [line ${m.startLine}]`);
2874
- }
2875
- }
2876
- lines.push(`\nClass is ${entry.totalLines} lines. Use --max-lines=N to see source, or "fn <method>" for individual methods.`);
2877
- parts.push(lines.join('\n'));
2878
- } else if (entry.truncated) {
2879
- parts.push(formatClass(entry.match, entry.code) + `\n\n... showing ${entry.maxLines} of ${entry.totalLines} lines`);
2880
- } else {
2881
- parts.push(formatClass(entry.match, entry.code));
2882
- }
2883
- }
2884
-
2885
- return parts.join('\n\n');
2886
- }
2887
-
2888
- /**
2889
- * Format class handler result as JSON.
2890
- */
2891
- function formatClassResultJson(result) {
2892
- if (result.entries.length === 1) {
2893
- const entry = result.entries[0];
2894
- return JSON.stringify({
2895
- ...entry.match,
2896
- code: entry.code,
2897
- ...(entry.summaryMode && { summaryMode: true }),
2898
- ...(entry.methods && { methods: entry.methods }),
2899
- ...(entry.truncated && { truncated: true }),
2900
- totalLines: entry.totalLines,
2901
- }, null, 2);
2902
- }
2903
- const arr = result.entries.map(entry => ({
2904
- ...entry.match,
2905
- code: entry.code,
2906
- ...(entry.summaryMode && { summaryMode: true }),
2907
- ...(entry.methods && { methods: entry.methods }),
2908
- ...(entry.truncated && { truncated: true }),
2909
- totalLines: entry.totalLines,
2910
- }));
2911
- return JSON.stringify(arr, null, 2);
2912
- }
2913
-
2914
- /**
2915
- * Format lines handler result (from execute.js).
2916
- * @param {{ relativePath: string, lines: string[], startLine: number, endLine: number }} result
2917
- */
2918
- function formatLines(result) {
2919
- const lines = [];
2920
- lines.push(`${result.relativePath}:${result.startLine}-${result.endLine}`);
2921
- lines.push('\u2500'.repeat(60));
2922
- for (let i = 0; i < result.lines.length; i++) {
2923
- lines.push(`${lineNum(result.startLine + i)} \u2502 ${result.lines[i]}`);
2924
- }
2925
- return lines.join('\n');
2926
- }
2927
-
2928
- // ============================================================================
2929
- // FIND DETAILED (moved from CLI — depth/confidence features)
2930
- // ============================================================================
2931
-
2932
- /**
2933
- * Count depth of nested generic brackets.
2934
- */
2935
- function countNestedGenerics(str) {
2936
- let maxDepth = 0;
2937
- let depth = 0;
2938
- for (const char of str) {
2939
- if (char === '<') {
2940
- depth++;
2941
- maxDepth = Math.max(maxDepth, depth);
2942
- } else if (char === '>') {
2943
- depth--;
2944
- }
2945
- }
2946
- return maxDepth;
2947
- }
2948
-
2949
- /**
2950
- * Compute confidence level for a symbol match.
2951
- * @returns {{ level: 'high'|'medium'|'low', reasons: string[] }}
2952
- */
2953
- function computeConfidence(symbol) {
2954
- const reasons = [];
2955
- let score = 100;
2956
-
2957
- const span = (symbol.endLine || symbol.startLine) - symbol.startLine;
2958
- if (span > 500) {
2959
- score -= 30;
2960
- reasons.push('very long function (>500 lines)');
2961
- } else if (span > 200) {
2962
- score -= 15;
2963
- reasons.push('long function (>200 lines)');
2964
- }
2965
-
2966
- const params = Array.isArray(symbol.params) ? symbol.params : [];
2967
- const signature = params.map(p => p.type || '').join(' ') + (symbol.returnType || '');
2968
- const genericDepth = countNestedGenerics(signature);
2969
- if (genericDepth > 3) {
2970
- score -= 20;
2971
- reasons.push('complex nested generics');
2972
- } else if (genericDepth > 2) {
2973
- score -= 10;
2974
- reasons.push('nested generics');
2975
- }
2976
-
2977
- if (symbol.file) {
2978
- try {
2979
- const stats = fs.statSync(symbol.file);
2980
- const sizeKB = stats.size / 1024;
2981
- if (sizeKB > 500) {
2982
- score -= 20;
2983
- reasons.push('very large file (>500KB)');
2984
- } else if (sizeKB > 200) {
2985
- score -= 10;
2986
- reasons.push('large file (>200KB)');
2987
- }
2988
- } catch (e) {
2989
- // Skip file size check on error
2990
- }
2991
- }
2992
-
2993
- let level = 'high';
2994
- if (score < 50) level = 'low';
2995
- else if (score < 80) level = 'medium';
2996
-
2997
- return { level, reasons };
2998
- }
2999
-
3000
- /**
3001
- * Format find results with depth/confidence features (detailed view).
3002
- * Returns a string. Used by CLI and interactive mode.
4
+ * All formatters are split into domain files under core/output/.
5
+ * This file re-exports everything so consumers don't need to change.
3003
6
  *
3004
- * @param {Array} symbols - Find result array
3005
- * @param {string} query - Original search query
3006
- * @param {object} options - { depth, top, all }
3007
- */
3008
- function formatFindDetailed(symbols, query, options = {}) {
3009
- const { depth, top, all } = options;
3010
- const DEFAULT_LIMIT = 5;
3011
-
3012
- if (symbols.length === 0) {
3013
- return `No symbols found for "${query}"`;
3014
- }
3015
-
3016
- const lines = [];
3017
- const limit = all ? symbols.length : (top > 0 ? top : DEFAULT_LIMIT);
3018
- const showing = Math.min(limit, symbols.length);
3019
- const hidden = symbols.length - showing;
3020
-
3021
- if (hidden > 0) {
3022
- lines.push(`Found ${symbols.length} match(es) for "${query}" (showing top ${showing}):`);
3023
- } else {
3024
- lines.push(`Found ${symbols.length} match(es) for "${query}":`);
3025
- }
3026
- lines.push('─'.repeat(60));
3027
-
3028
- for (let i = 0; i < showing; i++) {
3029
- const s = symbols[i];
3030
- // Depth 0: just location
3031
- if (depth === '0') {
3032
- lines.push(`${s.relativePath}:${s.startLine}`);
3033
- continue;
3034
- }
3035
-
3036
- // Depth 1 (default): location + signature
3037
- const sig = s.params !== undefined
3038
- ? formatFunctionSignature(s)
3039
- : formatClassSignature(s);
3040
-
3041
- const confidence = computeConfidence(s);
3042
- const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
3043
-
3044
- lines.push(`${s.relativePath}:${s.startLine} ${sig}${confStr}`);
3045
- if (s.usageCounts !== undefined) {
3046
- const c = s.usageCounts;
3047
- const parts = [];
3048
- if (c.calls > 0) parts.push(`${c.calls} calls`);
3049
- if (c.definitions > 0) parts.push(`${c.definitions} def`);
3050
- if (c.imports > 0) parts.push(`${c.imports} imports`);
3051
- if (c.references > 0) parts.push(`${c.references} refs`);
3052
- lines.push(` (${c.total} usages: ${parts.join(', ')})`);
3053
- } else if (s.usageCount !== undefined) {
3054
- lines.push(` (${s.usageCount} usages)`);
3055
- }
3056
-
3057
- if (confidence.level !== 'high' && confidence.reasons.length > 0) {
3058
- lines.push(` ⚠ ${confidence.reasons.join(', ')}`);
3059
- }
3060
-
3061
- // Depth 2: + first 10 lines of code
3062
- if (depth === '2' || depth === 'full') {
3063
- try {
3064
- const content = fs.readFileSync(s.file, 'utf-8');
3065
- const fileLines = content.split('\n');
3066
- const maxLines = depth === 'full' ? (s.endLine - s.startLine + 1) : 10;
3067
- const endLine = Math.min(s.startLine + maxLines - 1, s.endLine);
3068
- lines.push(' ───');
3069
- for (let j = s.startLine - 1; j < endLine; j++) {
3070
- lines.push(` ${fileLines[j]}`);
3071
- }
3072
- if (depth === '2' && s.endLine > endLine) {
3073
- lines.push(` ... (${s.endLine - endLine} more lines)`);
3074
- }
3075
- } catch (e) {
3076
- // Skip code extraction on error
3077
- }
3078
- }
3079
- lines.push('');
3080
- }
3081
-
3082
- if (hidden > 0) {
3083
- lines.push(`... ${hidden} more result(s). Use --all to see all, or --top=N to see more.`);
3084
- }
3085
-
3086
- return lines.join('\n');
3087
- }
3088
-
3089
- /**
3090
- * Format lines handler result as JSON.
3091
- */
3092
- function formatLinesJson(result) {
3093
- return JSON.stringify({
3094
- file: result.relativePath,
3095
- startLine: result.startLine,
3096
- endLine: result.endLine,
3097
- lines: result.lines,
3098
- }, null, 2);
3099
- }
3100
-
3101
- // ============================================================================
3102
- // Entrypoints command formatters
3103
- // ============================================================================
3104
-
3105
- /**
3106
- * Format entrypoints command output (text)
3107
- */
3108
- function formatEntrypoints(results, options = {}) {
3109
- if (!results || results.length === 0) {
3110
- return 'No framework entry points detected.';
3111
- }
3112
-
3113
- const lines = [];
3114
- lines.push(`Framework Entry Points: ${results.length} detected\n`);
3115
-
3116
- // Group by type
3117
- const byType = new Map();
3118
- for (const ep of results) {
3119
- if (!byType.has(ep.type)) byType.set(ep.type, []);
3120
- byType.get(ep.type).push(ep);
3121
- }
3122
-
3123
- const typeLabels = {
3124
- http: 'HTTP Routes',
3125
- cli: 'CLI Handlers',
3126
- di: 'Dependency Injection',
3127
- jobs: 'Job Schedulers',
3128
- test: 'Test Fixtures',
3129
- runtime: 'Runtime Entry Points',
3130
- ui: 'UI Handlers',
3131
- events: 'Event Handlers',
3132
- };
3133
-
3134
- let itemNum = 0;
3135
- for (const [type, entries] of byType) {
3136
- const label = typeLabels[type] || type;
3137
- lines.push(`${label} (${entries.length}):`);
3138
-
3139
- let currentFile = null;
3140
- for (const ep of entries) {
3141
- if (ep.file !== currentFile) {
3142
- currentFile = ep.file;
3143
- lines.push(` ${ep.file}`);
3144
- }
3145
- itemNum++;
3146
- const evidence = ep.evidence.join(', ');
3147
- lines.push(` [${itemNum}] ${ep.name} (${ep.framework}) — ${evidence}${' '.repeat(Math.max(0, 40 - ep.name.length - ep.framework.length - evidence.length))}:${ep.line}`);
3148
- }
3149
- lines.push('');
3150
- }
3151
-
3152
- return lines.join('\n').trimEnd();
3153
- }
3154
-
3155
- /**
3156
- * Format entrypoints command output (JSON)
7
+ * KEY PRINCIPLE: Never truncate critical information.
8
+ * Full expressions, full signatures, full context.
3157
9
  */
3158
- function formatEntrypointsJson(results) {
3159
- return JSON.stringify({
3160
- meta: { total: results.length },
3161
- data: {
3162
- entrypoints: results.map(ep => ({
3163
- name: ep.name,
3164
- file: ep.file,
3165
- line: ep.line,
3166
- type: ep.type,
3167
- framework: ep.framework,
3168
- patternId: ep.patternId,
3169
- evidence: ep.evidence,
3170
- confidence: ep.confidence,
3171
- }))
3172
- }
3173
- }, null, 2);
3174
- }
3175
10
 
3176
11
  module.exports = {
3177
- // Utilities
3178
- normalizeParams,
3179
- lineNum,
3180
- lineRange,
3181
- lineLoc,
3182
- formatFunctionSignature,
3183
- formatClassSignature,
3184
- formatMemberSignature,
3185
-
3186
- // Text output
3187
- header,
3188
- subheader,
3189
- printUsage,
3190
- printDefinition,
3191
-
3192
- // JSON formatters
3193
- formatTocJson,
3194
- formatSymbolJson,
3195
- formatUsagesJson,
3196
- formatContextJson,
3197
- formatFunctionJson,
3198
- formatSearchJson,
3199
- formatImportsJson,
3200
- formatStatsJson,
3201
- formatGraphJson,
3202
- formatSmartJson,
3203
-
3204
- // New formatters (v2 migration)
3205
- formatImports,
3206
- formatExporters,
3207
- formatTypedef,
3208
- formatTests,
3209
- formatApi,
3210
- formatDisambiguation,
3211
- formatExportersJson,
3212
- formatTypedefJson,
3213
- formatTestsJson,
3214
- formatApiJson,
3215
-
3216
- // About command
3217
- formatAbout,
3218
- formatAboutJson,
3219
-
3220
- // Impact command
3221
- formatImpact,
3222
- formatImpactJson,
3223
-
3224
- // Plan command
3225
- formatPlan,
3226
- formatPlanJson,
3227
-
3228
- // Stack trace command
3229
- formatStackTrace,
3230
- formatStackTraceJson,
3231
-
3232
- // Verify command
3233
- formatVerify,
3234
- formatVerifyJson,
3235
-
3236
- // Trace command
3237
- formatTrace,
3238
- formatTraceJson,
3239
-
3240
- // Blast command
3241
- formatBlast,
3242
- formatBlastJson,
3243
-
3244
- // Reverse trace command
3245
- formatReverseTrace,
3246
- formatReverseTraceJson,
3247
-
3248
- // Affected tests command
3249
- formatAffectedTests,
3250
- formatAffectedTestsJson,
3251
-
3252
- // Related command
3253
- formatRelated,
3254
- formatRelatedJson,
3255
-
3256
- // Example command
3257
- formatExample,
3258
- formatExampleJson,
3259
-
3260
- // Deadcode command
3261
- formatDeadcodeJson,
3262
-
3263
- // Shared text formatters (CLI + MCP)
3264
- formatToc,
3265
- formatFind,
3266
- formatUsages,
3267
- formatContext,
3268
- formatSmart,
3269
- formatDeadcode,
3270
- formatFn,
3271
- formatClass,
3272
- formatGraph,
3273
- formatCircularDeps,
3274
- formatCircularDepsJson,
3275
- formatSearch,
3276
- formatStructuralSearch,
3277
- formatStructuralSearchJson,
3278
- detectDoubleEscaping,
3279
- formatFileExports,
3280
- formatStats,
3281
-
3282
- // Diff impact command
3283
- formatDiffImpact,
3284
- formatDiffImpactJson,
3285
-
3286
- // Find detailed (depth/confidence)
3287
- formatFindDetailed,
3288
-
3289
- // Extraction commands (fn, class, lines)
3290
- formatFnResult,
3291
- formatFnResultJson,
3292
- formatClassResult,
3293
- formatClassResultJson,
3294
- formatLines,
3295
- formatLinesJson,
3296
-
3297
- // Entrypoints command
3298
- formatEntrypoints,
3299
- formatEntrypointsJson,
12
+ ...require('./output/shared'),
13
+ ...require('./output/tracing'),
14
+ ...require('./output/analysis'),
15
+ ...require('./output/analysis-ext'),
16
+ ...require('./output/find'),
17
+ ...require('./output/search'),
18
+ ...require('./output/graph'),
19
+ ...require('./output/extraction'),
20
+ ...require('./output/reporting'),
21
+ ...require('./output/refactoring'),
3300
22
  };