ucn 3.8.12 → 3.8.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude/skills/ucn/SKILL.md +3 -1
  2. package/.github/workflows/ci.yml +15 -3
  3. package/.github/workflows/publish.yml +20 -8
  4. package/README.md +1 -0
  5. package/cli/index.js +165 -246
  6. package/core/analysis.js +1400 -0
  7. package/core/build-worker.js +194 -0
  8. package/core/cache.js +105 -7
  9. package/core/callers.js +194 -64
  10. package/core/deadcode.js +22 -66
  11. package/core/discovery.js +9 -54
  12. package/core/execute.js +139 -54
  13. package/core/graph.js +615 -0
  14. package/core/output/analysis-ext.js +271 -0
  15. package/core/output/analysis.js +491 -0
  16. package/core/output/extraction.js +188 -0
  17. package/core/output/find.js +355 -0
  18. package/core/output/graph.js +399 -0
  19. package/core/output/refactoring.js +293 -0
  20. package/core/output/reporting.js +331 -0
  21. package/core/output/search.js +307 -0
  22. package/core/output/shared.js +271 -0
  23. package/core/output/tracing.js +416 -0
  24. package/core/output.js +15 -3293
  25. package/core/parallel-build.js +165 -0
  26. package/core/project.js +299 -3633
  27. package/core/registry.js +59 -0
  28. package/core/reporting.js +258 -0
  29. package/core/search.js +890 -0
  30. package/core/stacktrace.js +1 -1
  31. package/core/tracing.js +631 -0
  32. package/core/verify.js +10 -13
  33. package/eslint.config.js +43 -0
  34. package/jsconfig.json +10 -0
  35. package/languages/go.js +21 -2
  36. package/languages/html.js +8 -0
  37. package/languages/index.js +102 -40
  38. package/languages/java.js +13 -0
  39. package/languages/javascript.js +17 -1
  40. package/languages/python.js +14 -0
  41. package/languages/rust.js +13 -0
  42. package/languages/utils.js +1 -1
  43. package/mcp/server.js +45 -28
  44. package/package.json +8 -3
@@ -0,0 +1,491 @@
1
+ /**
2
+ * core/output/analysis.js - Understanding/analysis formatters
3
+ */
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { langTraits } = require('../../languages');
7
+ const { dynamicImportsNote } = require('./shared');
8
+
9
+ /** Format context (callers + callees) as JSON */
10
+ function formatContextJson(context) {
11
+ const meta = context.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 };
12
+ // Handle struct/interface types differently
13
+ if (context.type && ['class', 'struct', 'interface', 'type'].includes(context.type)) {
14
+ const callers = context.callers || [];
15
+ const methods = context.methods || [];
16
+ return JSON.stringify({
17
+ meta,
18
+ data: {
19
+ type: context.type,
20
+ name: context.name,
21
+ file: context.file,
22
+ startLine: context.startLine,
23
+ endLine: context.endLine,
24
+ methodCount: methods.length,
25
+ usageCount: callers.length,
26
+ methods: methods.map(m => ({
27
+ name: m.name,
28
+ file: m.file,
29
+ line: m.line,
30
+ params: m.params,
31
+ returnType: m.returnType,
32
+ receiver: m.receiver
33
+ })),
34
+ usages: callers.map(c => ({
35
+ file: c.relativePath || c.file,
36
+ line: c.line,
37
+ expression: c.content,
38
+ callerName: c.callerName
39
+ })),
40
+ ...(context.warnings && { warnings: context.warnings })
41
+ }
42
+ });
43
+ }
44
+
45
+ // Standard function/method context
46
+ const callers = context.callers || [];
47
+ const callees = context.callees || [];
48
+ return JSON.stringify({
49
+ meta,
50
+ data: {
51
+ function: context.function,
52
+ file: context.file,
53
+ callerCount: callers.length,
54
+ calleeCount: callees.length,
55
+ callers: callers.map(c => ({
56
+ file: c.relativePath || c.file,
57
+ line: c.line,
58
+ expression: c.content, // FULL expression
59
+ callerName: c.callerName,
60
+ ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
61
+ })),
62
+ callees: callees.map(c => ({
63
+ name: c.name,
64
+ type: c.type,
65
+ file: c.relativePath || c.file,
66
+ line: c.startLine,
67
+ params: c.params, // FULL params
68
+ weight: c.weight || 'normal', // Dependency weight: core, setup, utility
69
+ ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
70
+ })),
71
+ ...(context.warnings && { warnings: context.warnings })
72
+ }
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Format context command output.
78
+ * Returns { text, expandable } where expandable is an array of items for expand.
79
+ */
80
+ function formatContext(ctx, options = {}) {
81
+ if (!ctx) return { text: 'Symbol not found.', expandable: [] };
82
+
83
+ const expandHint = options.expandHint || 'Use ucn_expand with item number to see code for any item.';
84
+ const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded. Use include_methods=true to include them.';
85
+
86
+ const lines = [];
87
+ const expandable = [];
88
+ let itemNum = 1;
89
+
90
+ // Handle struct/interface types
91
+ if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
92
+ lines.push(`Context for ${ctx.type} ${ctx.name}:`);
93
+ lines.push('═'.repeat(60));
94
+
95
+ if (ctx.warnings && ctx.warnings.length > 0) {
96
+ for (const w of ctx.warnings) {
97
+ lines.push(` Note: ${w.message}`);
98
+ }
99
+ }
100
+
101
+ const methods = ctx.methods || [];
102
+ lines.push(`\nMETHODS (${methods.length}):`);
103
+ for (const m of methods) {
104
+ const receiver = m.receiver ? `(${m.receiver}) ` : '';
105
+ const params = m.params || '...';
106
+ const returnType = m.returnType ? `: ${m.returnType}` : '';
107
+ lines.push(` [${itemNum}] ${receiver}${m.name}(${params})${returnType}`);
108
+ lines.push(` ${m.file}:${m.line}`);
109
+ expandable.push({
110
+ num: itemNum++,
111
+ type: 'method',
112
+ name: m.name,
113
+ file: null,
114
+ relativePath: m.file,
115
+ startLine: m.line,
116
+ endLine: m.endLine || m.line
117
+ });
118
+ }
119
+
120
+ const callers = ctx.callers || [];
121
+ lines.push(`\nCALLERS (${callers.length}):`);
122
+ for (const c of callers) {
123
+ const callerName = c.callerName ? ` [${c.callerName}]` : '';
124
+ lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
125
+ lines.push(` ${c.content.trim()}`);
126
+ expandable.push({
127
+ num: itemNum++,
128
+ type: 'caller',
129
+ name: c.callerName || '(module level)',
130
+ file: c.callerFile || c.file,
131
+ relativePath: c.relativePath,
132
+ line: c.line,
133
+ startLine: c.callerStartLine || c.line,
134
+ endLine: c.callerEndLine || c.line
135
+ });
136
+ }
137
+
138
+ if (expandable.length > 0) {
139
+ lines.push(`\n${expandHint}`);
140
+ }
141
+
142
+ return { text: lines.join('\n'), expandable };
143
+ }
144
+
145
+ // Standard function/method context
146
+ lines.push(`Context for ${ctx.function}:`);
147
+ lines.push('═'.repeat(60));
148
+
149
+ if (ctx.meta) {
150
+ const notes = [];
151
+ if (ctx.meta.dynamicImports) { const dn = dynamicImportsNote(ctx.meta.dynamicImports, ctx.meta); if (dn) notes.push(dn); }
152
+ if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
153
+ if (ctx.meta.confidenceFiltered) notes.push(`${ctx.meta.confidenceFiltered} edge(s) below confidence threshold hidden`);
154
+ if (notes.length) {
155
+ const uncertainSuffix = ctx.meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
156
+ lines.push(` Note: ${notes.join(', ')}${uncertainSuffix}`);
157
+ }
158
+ }
159
+
160
+ if (ctx.meta && ctx.meta.includeMethods === false) {
161
+ lines.push(` ${methodsHint}`);
162
+ }
163
+
164
+ if (ctx.warnings && ctx.warnings.length > 0) {
165
+ for (const w of ctx.warnings) {
166
+ lines.push(` Note: ${w.message}`);
167
+ }
168
+ }
169
+
170
+ const showConf = options.showConfidence || false;
171
+ const callers = ctx.callers || [];
172
+ lines.push(`\nCALLERS (${callers.length}):`);
173
+ for (const c of callers) {
174
+ const callerName = c.callerName ? ` [${c.callerName}]` : '';
175
+ lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
176
+ lines.push(` ${c.content.trim()}`);
177
+ if (showConf && c.confidence != null) {
178
+ lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
179
+ }
180
+ expandable.push({
181
+ num: itemNum++,
182
+ type: 'caller',
183
+ name: c.callerName || '(module level)',
184
+ file: c.callerFile || c.file,
185
+ relativePath: c.relativePath,
186
+ line: c.line,
187
+ startLine: c.callerStartLine || c.line,
188
+ endLine: c.callerEndLine || c.line
189
+ });
190
+ }
191
+
192
+ // Structural hint: class methods may have callers through constructed/injected instances
193
+ // that static analysis can't track. Only show when caller count is low (≤3) to avoid noise.
194
+ if (ctx.meta && (ctx.meta.isMethod || ctx.meta.className || ctx.meta.receiver) && callers.length <= 3) {
195
+ lines.push(` Note: ${ctx.function} is a class/struct method — additional callers through constructed or injected instances are not tracked by static analysis.`);
196
+ }
197
+
198
+ const callees = ctx.callees || [];
199
+ lines.push(`\nCALLEES (${callees.length}):`);
200
+ for (const c of callees) {
201
+ const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
202
+ lines.push(` [${itemNum}] ${c.name}${weight} - ${c.relativePath}:${c.startLine}`);
203
+ if (showConf && c.confidence != null) {
204
+ lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
205
+ }
206
+ expandable.push({
207
+ num: itemNum++,
208
+ type: 'callee',
209
+ name: c.name,
210
+ file: c.file,
211
+ relativePath: c.relativePath,
212
+ startLine: c.startLine,
213
+ endLine: c.endLine
214
+ });
215
+ }
216
+
217
+ if (expandable.length > 0) {
218
+ lines.push(`\n${expandHint}`);
219
+ }
220
+
221
+ return { text: lines.join('\n'), expandable };
222
+ }
223
+
224
+ /** Format impact command output - text. Shows what would need updating if a function signature changes. */
225
+ function formatImpact(impact, options = {}) {
226
+ if (!impact) {
227
+ return 'Function not found.';
228
+ }
229
+
230
+ const lines = [];
231
+
232
+ // Header
233
+ lines.push(`Impact analysis for ${impact.function}`);
234
+ lines.push('═'.repeat(60));
235
+ lines.push(`${impact.file}:${impact.startLine}`);
236
+ lines.push(impact.signature);
237
+ lines.push('');
238
+
239
+ // Summary
240
+ if (impact.shownCallSites !== undefined && impact.shownCallSites < impact.totalCallSites) {
241
+ lines.push(`CALL SITES: ${impact.shownCallSites} shown of ${impact.totalCallSites} total`);
242
+ } else {
243
+ lines.push(`CALL SITES: ${impact.totalCallSites}`);
244
+ }
245
+ lines.push(` Files affected: ${impact.byFile.length}`);
246
+
247
+ // Patterns
248
+ const p = impact.patterns;
249
+ if (p) {
250
+ const patternParts = [];
251
+ if (p.constantArgs > 0) patternParts.push(`${p.constantArgs} with literals`);
252
+ if (p.variableArgs > 0) patternParts.push(`${p.variableArgs} with variables`);
253
+ if (p.awaitedCalls > 0) patternParts.push(`${p.awaitedCalls} awaited`);
254
+ if (p.chainedCalls > 0) patternParts.push(`${p.chainedCalls} chained`);
255
+ if (p.spreadCalls > 0) patternParts.push(`${p.spreadCalls} with spread`);
256
+ if (patternParts.length > 0) {
257
+ lines.push(` Patterns: ${patternParts.join(', ')}`);
258
+ }
259
+ }
260
+
261
+ // Scope pollution warning
262
+ if (impact.scopeWarning) {
263
+ lines.push(` Note: ${impact.scopeWarning.hint}`);
264
+ }
265
+
266
+ // By file
267
+ lines.push('');
268
+ lines.push('BY FILE:');
269
+ for (const fileGroup of impact.byFile) {
270
+ lines.push(`\n${fileGroup.file} (${fileGroup.count} calls)`);
271
+ for (const site of fileGroup.sites) {
272
+ const caller = site.callerName ? `[${site.callerName}]` : '';
273
+ lines.push(` :${site.line} ${caller}`);
274
+ lines.push(` ${site.expression}`);
275
+ if (site.args && site.args.length > 0) {
276
+ lines.push(` args: ${site.args.join(', ')}`);
277
+ }
278
+ }
279
+ }
280
+
281
+ return lines.join('\n');
282
+ }
283
+
284
+ /** Format impact command output - JSON */
285
+ function formatImpactJson(impact) {
286
+ if (!impact) {
287
+ return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
288
+ }
289
+ return JSON.stringify(impact, null, 2);
290
+ }
291
+
292
+ /** Format about command output - text. The "tell me everything" output for AI agents. */
293
+ function formatAbout(about, options = {}) {
294
+ if (!about) {
295
+ return 'Symbol not found.';
296
+ }
297
+ if (!about.found) {
298
+ const lines = ['Symbol not found.\n'];
299
+ if (about.suggestions && about.suggestions.length > 0) {
300
+ lines.push('Did you mean:');
301
+ for (const s of about.suggestions) {
302
+ lines.push(` ${s.name} (${s.type}) - ${s.file}:${s.line}`);
303
+ lines.push(` ${s.usageCount} usages`);
304
+ }
305
+ }
306
+ return lines.join('\n');
307
+ }
308
+
309
+ const lines = [];
310
+ const sym = about.symbol;
311
+ const { expand, root, depth } = options;
312
+
313
+ // Depth=0: location only
314
+ if (depth !== null && depth !== undefined && Number(depth) === 0) {
315
+ return `${sym.file}:${sym.startLine}`;
316
+ }
317
+
318
+ // Depth=1: location + signature + usage counts
319
+ if (depth !== null && depth !== undefined && Number(depth) === 1) {
320
+ lines.push(`${sym.file}:${sym.startLine}`);
321
+ if (sym.signature) {
322
+ lines.push(sym.signature);
323
+ }
324
+ lines.push(`(${about.totalUsages} usages: ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} refs)`);
325
+ return lines.join('\n');
326
+ }
327
+
328
+ // Header with signature
329
+ lines.push(`${sym.name} (${sym.type})`);
330
+ lines.push('═'.repeat(60));
331
+ lines.push(`${sym.file}:${sym.startLine}-${sym.endLine}`);
332
+ if (sym.signature) {
333
+ lines.push(sym.signature);
334
+ }
335
+ if (sym.docstring) {
336
+ lines.push(`"${sym.docstring}"`);
337
+ }
338
+
339
+ // Warnings (show early for visibility)
340
+ if (about.warnings && about.warnings.length > 0) {
341
+ for (const w of about.warnings) {
342
+ lines.push(` Note: ${w.message}`);
343
+ }
344
+ }
345
+ if (about.confidenceFiltered) {
346
+ lines.push(` Note: ${about.confidenceFiltered} edge(s) below confidence threshold hidden`);
347
+ }
348
+
349
+ // Usage summary
350
+ lines.push('');
351
+ lines.push(`USAGES: ${about.totalUsages} total`);
352
+ lines.push(` ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} references`);
353
+
354
+ // Callers
355
+ const showConf = options.showConfidence || false;
356
+ let aboutTruncated = false;
357
+ if (about.callers.total > 0) {
358
+ lines.push('');
359
+ if (about.callers.total > about.callers.top.length) {
360
+ lines.push(`CALLERS (showing ${about.callers.top.length} of ${about.callers.total}):`);
361
+ aboutTruncated = true;
362
+ } else {
363
+ lines.push(`CALLERS (${about.callers.total}):`);
364
+ }
365
+ for (const c of about.callers.top) {
366
+ const caller = c.callerName ? `[${c.callerName}]` : '';
367
+ lines.push(` ${c.file}:${c.line} ${caller}`);
368
+ lines.push(` ${c.expression}`);
369
+ if (showConf && c.confidence != null) {
370
+ lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
371
+ }
372
+ }
373
+ }
374
+
375
+ // Callees
376
+ if (about.callees.total > 0) {
377
+ lines.push('');
378
+ if (about.callees.total > about.callees.top.length) {
379
+ lines.push(`CALLEES (showing ${about.callees.top.length} of ${about.callees.total}):`);
380
+ aboutTruncated = true;
381
+ } else {
382
+ lines.push(`CALLEES (${about.callees.total}):`);
383
+ }
384
+ for (const c of about.callees.top) {
385
+ const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
386
+ lines.push(` ${c.name}${weight} - ${c.file}:${c.line} (${c.callCount}x)`);
387
+ if (showConf && c.confidence != null) {
388
+ lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
389
+ }
390
+
391
+ // Inline expansion: show first 3 lines of callee code
392
+ if (expand && root && c.file && c.startLine) {
393
+ try {
394
+ const filePath = path.isAbsolute(c.file) ? c.file : path.join(root, c.file);
395
+ const content = fs.readFileSync(filePath, 'utf-8');
396
+ const fileLines = content.split('\n');
397
+ const endLine = c.endLine || c.startLine + 5;
398
+ const previewLines = Math.min(3, endLine - c.startLine + 1);
399
+ for (let i = 0; i < previewLines && c.startLine - 1 + i < fileLines.length; i++) {
400
+ const codeLine = fileLines[c.startLine - 1 + i];
401
+ lines.push(` │ ${codeLine}`);
402
+ }
403
+ if (endLine - c.startLine + 1 > 3) {
404
+ lines.push(` │ ... (${endLine - c.startLine - 2} more lines)`);
405
+ }
406
+ } catch (e) {
407
+ // Skip expansion on error
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ // Tests
414
+ if (about.tests.totalMatches > 0) {
415
+ lines.push('');
416
+ if (about.tests.fileCount > about.tests.files.length) {
417
+ lines.push(`TESTS: ${about.tests.totalMatches} matches in ${about.tests.fileCount} file(s), showing ${about.tests.files.length}:`);
418
+ aboutTruncated = true;
419
+ } else {
420
+ lines.push(`TESTS: ${about.tests.totalMatches} matches in ${about.tests.fileCount} file(s)`);
421
+ }
422
+ for (const f of about.tests.files) {
423
+ lines.push(` ${f}`);
424
+ }
425
+ }
426
+
427
+ // Other definitions
428
+ if (about.otherDefinitions.length > 0) {
429
+ lines.push('');
430
+ lines.push(`OTHER DEFINITIONS (${about.otherDefinitions.length}):`);
431
+ for (const d of about.otherDefinitions) {
432
+ lines.push(` ${d.file}:${d.line} (${d.usageCount} usages)`);
433
+ }
434
+ }
435
+
436
+ // Types
437
+ if (about.types && about.types.length > 0) {
438
+ lines.push('');
439
+ lines.push('TYPES:');
440
+ for (const t of about.types) {
441
+ lines.push(` ${t.name} (${t.type}) - ${t.file}:${t.line}`);
442
+ }
443
+ }
444
+
445
+ // Completeness warnings (condensed single line)
446
+ if (about.completeness && about.completeness.warnings && about.completeness.warnings.length > 0) {
447
+ const lang = about.completeness?.projectLanguage;
448
+ const parts = about.completeness.warnings.map(w => {
449
+ if (w.type === 'dynamic_imports' && lang && !langTraits(lang)?.hasDynamicImports) return `${w.count} blank/dot import(s)`;
450
+ return `${w.count} ${w.type.replace('_', ' ')}`;
451
+ });
452
+ lines.push('');
453
+ lines.push(`Note: Results may be incomplete (${parts.join(', ')} in project)`);
454
+ }
455
+
456
+ // Code
457
+ if (about.code) {
458
+ lines.push('');
459
+ lines.push('─── CODE ───');
460
+ lines.push(about.code);
461
+ }
462
+
463
+ if (aboutTruncated) {
464
+ const allHint = options.allHint || 'Use --all to show all.';
465
+ lines.push(`\nSome sections truncated. ${allHint}`);
466
+ }
467
+
468
+ if (about.includeMethods === false) {
469
+ const methodsHint = options.methodsHint || 'Note: obj.method() callers/callees excluded — use --include-methods to include them';
470
+ lines.push(`\n${methodsHint}`);
471
+ }
472
+
473
+ return lines.join('\n');
474
+ }
475
+
476
+ /** Format about command output - JSON */
477
+ function formatAboutJson(about) {
478
+ if (!about) {
479
+ return JSON.stringify({ found: false, error: 'Symbol not found' }, null, 2);
480
+ }
481
+ return JSON.stringify(about, null, 2);
482
+ }
483
+
484
+ module.exports = {
485
+ formatContext,
486
+ formatContextJson,
487
+ formatImpact,
488
+ formatImpactJson,
489
+ formatAbout,
490
+ formatAboutJson,
491
+ };
@@ -0,0 +1,188 @@
1
+ /**
2
+ * core/output/extraction.js - Code extraction formatters (fn, class, lines)
3
+ */
4
+
5
+ const {
6
+ lineNum,
7
+ lineRange,
8
+ formatFunctionSignature,
9
+ formatClassSignature,
10
+ } = require('./shared');
11
+
12
+ /**
13
+ * Format fn command output
14
+ */
15
+ function formatFn(match, fnCode) {
16
+ const lines = [];
17
+ lines.push(`${match.relativePath}:${match.startLine}`);
18
+ lines.push(`${lineRange(match.startLine, match.endLine)} ${formatFunctionSignature(match)}`);
19
+ lines.push('─'.repeat(60));
20
+ lines.push(fnCode);
21
+ return lines.join('\n');
22
+ }
23
+
24
+ /**
25
+ * Format class command output
26
+ */
27
+ function formatClass(cls, clsCode) {
28
+ const lines = [];
29
+ lines.push(`${cls.relativePath || cls.file}:${cls.startLine}`);
30
+ lines.push(`${lineRange(cls.startLine, cls.endLine)} ${formatClassSignature(cls)}`);
31
+ lines.push('─'.repeat(60));
32
+ lines.push(clsCode);
33
+ return lines.join('\n');
34
+ }
35
+
36
+ /**
37
+ * Format extracted function as JSON
38
+ */
39
+ function formatFunctionJson(fn, code) {
40
+ return JSON.stringify({
41
+ name: fn.name,
42
+ params: fn.params, // FULL params
43
+ paramsStructured: fn.paramsStructured || [],
44
+ startLine: fn.startLine,
45
+ endLine: fn.endLine,
46
+ modifiers: fn.modifiers || [],
47
+ ...(fn.returnType && { returnType: fn.returnType }),
48
+ ...(fn.generics && { generics: fn.generics }),
49
+ ...(fn.docstring && { docstring: fn.docstring }),
50
+ ...(fn.isArrow && { isArrow: true }),
51
+ ...(fn.isGenerator && { isGenerator: true }),
52
+ code // FULL code
53
+ }, null, 2);
54
+ }
55
+
56
+ /**
57
+ * Format fn handler result (from execute.js).
58
+ * Notes are NOT included — surfaces render those separately (e.g. stderr for CLI).
59
+ * @param {{ entries: Array<{match, code}>, notes: string[] }} result
60
+ */
61
+ function formatFnResult(result) {
62
+ const parts = [];
63
+ for (const { match, code } of result.entries) {
64
+ parts.push(formatFn(match, code));
65
+ }
66
+ const separator = result.entries.length > 1 ? '\n\n' + '═'.repeat(60) + '\n\n' : '';
67
+ return parts.join(separator);
68
+ }
69
+
70
+ /**
71
+ * Format fn handler result as JSON.
72
+ */
73
+ function formatFnResultJson(result) {
74
+ if (result.entries.length === 1) {
75
+ return formatFunctionJson(result.entries[0].match, result.entries[0].code);
76
+ }
77
+ const arr = result.entries.map(({ match, code }) => ({
78
+ name: match.name,
79
+ params: match.params,
80
+ paramsStructured: match.paramsStructured || [],
81
+ startLine: match.startLine,
82
+ endLine: match.endLine,
83
+ modifiers: match.modifiers || [],
84
+ ...(match.returnType && { returnType: match.returnType }),
85
+ ...(match.generics && { generics: match.generics }),
86
+ ...(match.docstring && { docstring: match.docstring }),
87
+ ...(match.isArrow && { isArrow: true }),
88
+ ...(match.isGenerator && { isGenerator: true }),
89
+ file: match.relativePath || match.file,
90
+ code,
91
+ }));
92
+ return JSON.stringify(arr, null, 2);
93
+ }
94
+
95
+ /**
96
+ * Format class handler result (from execute.js).
97
+ * @param {{ entries: Array<{match, code, methods?, summaryMode, truncated, totalLines, maxLines?}>, notes: string[] }} result
98
+ */
99
+ function formatClassResult(result) {
100
+ const parts = [];
101
+ for (const entry of result.entries) {
102
+ if (entry.summaryMode) {
103
+ // Large class summary
104
+ const lines = [];
105
+ lines.push(`${entry.match.relativePath}:${entry.match.startLine}`);
106
+ lines.push(`${lineRange(entry.match.startLine, entry.match.endLine)} ${formatClassSignature(entry.match)}`);
107
+ lines.push('\u2500'.repeat(60));
108
+ if (entry.methods && entry.methods.length > 0) {
109
+ lines.push(`\nMethods (${entry.methods.length}):`);
110
+ for (const m of entry.methods) {
111
+ lines.push(` ${formatFunctionSignature(m)} [line ${m.startLine}]`);
112
+ }
113
+ }
114
+ lines.push(`\nClass is ${entry.totalLines} lines. Use --max-lines=N to see source, or "fn <method>" for individual methods.`);
115
+ parts.push(lines.join('\n'));
116
+ } else if (entry.truncated) {
117
+ parts.push(formatClass(entry.match, entry.code) + `\n\n... showing ${entry.maxLines} of ${entry.totalLines} lines`);
118
+ } else {
119
+ parts.push(formatClass(entry.match, entry.code));
120
+ }
121
+ }
122
+
123
+ return parts.join('\n\n');
124
+ }
125
+
126
+ /**
127
+ * Format class handler result as JSON.
128
+ */
129
+ function formatClassResultJson(result) {
130
+ if (result.entries.length === 1) {
131
+ const entry = result.entries[0];
132
+ return JSON.stringify({
133
+ ...entry.match,
134
+ code: entry.code,
135
+ ...(entry.summaryMode && { summaryMode: true }),
136
+ ...(entry.methods && { methods: entry.methods }),
137
+ ...(entry.truncated && { truncated: true }),
138
+ totalLines: entry.totalLines,
139
+ }, null, 2);
140
+ }
141
+ const arr = result.entries.map(entry => ({
142
+ ...entry.match,
143
+ code: entry.code,
144
+ ...(entry.summaryMode && { summaryMode: true }),
145
+ ...(entry.methods && { methods: entry.methods }),
146
+ ...(entry.truncated && { truncated: true }),
147
+ totalLines: entry.totalLines,
148
+ }));
149
+ return JSON.stringify(arr, null, 2);
150
+ }
151
+
152
+ /**
153
+ * Format lines handler result (from execute.js).
154
+ * @param {{ relativePath: string, lines: string[], startLine: number, endLine: number }} result
155
+ */
156
+ function formatLines(result) {
157
+ const lines = [];
158
+ lines.push(`${result.relativePath}:${result.startLine}-${result.endLine}`);
159
+ lines.push('\u2500'.repeat(60));
160
+ for (let i = 0; i < result.lines.length; i++) {
161
+ lines.push(`${lineNum(result.startLine + i)} \u2502 ${result.lines[i]}`);
162
+ }
163
+ return lines.join('\n');
164
+ }
165
+
166
+ /**
167
+ * Format lines handler result as JSON.
168
+ */
169
+ function formatLinesJson(result) {
170
+ return JSON.stringify({
171
+ file: result.relativePath,
172
+ startLine: result.startLine,
173
+ endLine: result.endLine,
174
+ lines: result.lines,
175
+ }, null, 2);
176
+ }
177
+
178
+ module.exports = {
179
+ formatFn,
180
+ formatClass,
181
+ formatFunctionJson,
182
+ formatFnResult,
183
+ formatFnResultJson,
184
+ formatClassResult,
185
+ formatClassResultJson,
186
+ formatLines,
187
+ formatLinesJson,
188
+ };