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,331 @@
1
+ /**
2
+ * core/output/reporting.js - Stats/TOC/deadcode/entrypoints formatters
3
+ */
4
+
5
+ const {
6
+ lineRange,
7
+ dynamicImportsNote,
8
+ formatFunctionSignature,
9
+ formatClassSignature,
10
+ } = require('./shared');
11
+
12
+ /**
13
+ * Format toc command output
14
+ * @param {object} toc - TOC data
15
+ * @param {object} [options] - Formatting options
16
+ * @param {string} [options.detailedHint] - Custom hint text for non-detailed mode
17
+ * @param {string} [options.uncertainHint] - Custom hint text for uncertain references
18
+ */
19
+ function formatToc(toc, options = {}) {
20
+ const lines = [];
21
+ const t = toc.totals;
22
+ lines.push(`PROJECT: ${t.files} files, ${t.lines} lines`);
23
+ lines.push(` ${t.functions} functions, ${t.classes} types (classes/interfaces/enums), ${t.state} state objects`);
24
+
25
+ const meta = toc.meta || {};
26
+ if (meta.filteredBy) {
27
+ lines.push(` Filtered by: --file=${meta.filteredBy} (${meta.matchedFiles} files matched)`);
28
+ if (meta.emptyFiles) {
29
+ lines.push(` Note: ${meta.emptyFiles} file(s) have no detected symbols (may be generated or data files)`);
30
+ }
31
+ }
32
+ const warnings = [];
33
+ if (meta.dynamicImports) { const dn = dynamicImportsNote(meta.dynamicImports, meta); if (dn) warnings.push(dn); }
34
+ if (meta.uncertain) warnings.push(`${meta.uncertain} uncertain reference(s)`);
35
+ if (warnings.length) {
36
+ const uncertainSuffix = meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
37
+ lines.push(` Note: ${warnings.join(', ')}${uncertainSuffix}`);
38
+ }
39
+
40
+ if (toc.summary) {
41
+ if (toc.summary.topFunctionFiles?.length) {
42
+ const hint = toc.summary.topFunctionFiles.map(f => `${f.file} (${f.functions})`).join(', ');
43
+ lines.push(` Most functions: ${hint}`);
44
+ }
45
+ if (toc.summary.topLineFiles?.length) {
46
+ const hint = toc.summary.topLineFiles.map(f => `${f.file} (${f.lines})`).join(', ');
47
+ lines.push(` Largest files: ${hint}`);
48
+ }
49
+ if (toc.summary.entryFiles?.length) {
50
+ lines.push(` Entry points: ${toc.summary.entryFiles.join(', ')}`);
51
+ }
52
+ }
53
+
54
+ lines.push('═'.repeat(60));
55
+ const hasDetail = toc.files.some(f => f.symbols);
56
+ for (const file of toc.files) {
57
+ const parts = [`${file.lines} lines`];
58
+ if (file.functions) parts.push(`${file.functions} fn`);
59
+ if (file.classes) parts.push(`${file.classes} types`);
60
+ if (file.state) parts.push(`${file.state} state`);
61
+
62
+ if (hasDetail) {
63
+ lines.push(`\n${file.file} (${parts.join(', ')})`);
64
+ if (file.symbols) {
65
+ for (const fn of file.symbols.functions) {
66
+ lines.push(` ${lineRange(fn.startLine, fn.endLine)} ${formatFunctionSignature(fn)}`);
67
+ }
68
+ for (const cls of file.symbols.classes) {
69
+ lines.push(` ${lineRange(cls.startLine, cls.endLine)} ${formatClassSignature(cls)}`);
70
+ }
71
+ }
72
+ } else {
73
+ lines.push(` ${file.file} — ${parts.join(', ')}`);
74
+ }
75
+ }
76
+
77
+ if (!hasDetail) {
78
+ const hint = options.detailedHint || 'Use detailed=true to list all functions and classes.';
79
+ lines.push(`\n${hint}`);
80
+ }
81
+
82
+ if (toc.hiddenFiles > 0) {
83
+ const topHint = options.topHint || 'Use --top=N or --all to show more.';
84
+ lines.push(`\n... and ${toc.hiddenFiles} more files. ${topHint}`);
85
+ }
86
+
87
+ return lines.join('\n');
88
+ }
89
+
90
+ /**
91
+ * Format TOC data as JSON
92
+ */
93
+ function formatTocJson(data) {
94
+ const obj = {
95
+ meta: data.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
96
+ totals: data.totals,
97
+ summary: data.summary,
98
+ files: data.files
99
+ };
100
+ if (data.hiddenFiles > 0) obj.hiddenFiles = data.hiddenFiles;
101
+ return JSON.stringify(obj);
102
+ }
103
+
104
+ /**
105
+ * Format stats command output
106
+ */
107
+ function formatStats(stats, options = {}) {
108
+ const lines = [];
109
+ lines.push('PROJECT STATISTICS');
110
+ lines.push('═'.repeat(60));
111
+ lines.push(`Root: ${stats.root}`);
112
+ if (stats.truncated) {
113
+ lines.push(`Files: ${stats.files} (truncated at ${stats.truncated.maxFiles} — use --max-files to increase)`);
114
+ } else {
115
+ lines.push(`Files: ${stats.files}`);
116
+ }
117
+ lines.push(`Symbols: ${stats.symbols}`);
118
+ lines.push(`Build time: ${stats.buildTime}ms`);
119
+
120
+ lines.push('\nBy Language:');
121
+ for (const [lang, info] of Object.entries(stats.byLanguage)) {
122
+ lines.push(` ${lang}: ${info.files} files, ${info.lines} lines, ${info.symbols} symbols`);
123
+ }
124
+
125
+ lines.push('\nBy Type:');
126
+ for (const [type, count] of Object.entries(stats.byType)) {
127
+ lines.push(` ${type}: ${count}`);
128
+ }
129
+
130
+ if (stats.warnings) {
131
+ lines.push(`\nWarnings: ${stats.warnings.count} file(s) failed to parse:`);
132
+ for (const f of stats.warnings.failedFiles.slice(0, 10)) {
133
+ lines.push(` ${f}`);
134
+ }
135
+ if (stats.warnings.count > 10) {
136
+ lines.push(` ... and ${stats.warnings.count - 10} more`);
137
+ }
138
+ }
139
+
140
+ if (stats.functions) {
141
+ const top = options.top || 30;
142
+ const shown = stats.functions.slice(0, top);
143
+ lines.push(`\nFunctions by line count (top ${shown.length} of ${stats.functions.length}):`);
144
+ for (const fn of shown) {
145
+ const loc = `${fn.file}:${fn.startLine}`;
146
+ lines.push(` ${String(fn.lines).padStart(5)} lines ${fn.name} (${loc})`);
147
+ }
148
+ if (stats.functions.length > top) {
149
+ lines.push(` ... ${stats.functions.length - top} more (use --top=N to show more)`);
150
+ }
151
+ }
152
+
153
+ return lines.join('\n');
154
+ }
155
+
156
+ /**
157
+ * Format project stats as JSON
158
+ */
159
+ function formatStatsJson(stats) {
160
+ return JSON.stringify(stats, null, 2);
161
+ }
162
+
163
+ /**
164
+ * Format deadcode command output
165
+ * @param {Array} results - Dead code results
166
+ * @param {object} [options] - Formatting options
167
+ * @param {string} [options.exportedHint] - Hint about exported symbols exclusion
168
+ */
169
+ function formatDeadcode(results, options = {}) {
170
+ if (results.length === 0 && !results.excludedDecorated && !results.excludedExported) {
171
+ return 'No dead code found.';
172
+ }
173
+
174
+ const lines = [];
175
+ const top = options.top > 0 ? options.top : 0;
176
+ const showing = top > 0 ? results.slice(0, top) : results;
177
+ const hidden = results.length - showing.length;
178
+
179
+ if (results.length > 0) {
180
+ if (hidden > 0) {
181
+ lines.push(`Dead code: ${results.length} unused symbol(s) (showing ${showing.length})\n`);
182
+ } else {
183
+ lines.push(`Dead code: ${results.length} unused symbol(s)\n`);
184
+ }
185
+ }
186
+
187
+ let currentFile = null;
188
+ for (const item of showing) {
189
+ if (item.file !== currentFile) {
190
+ currentFile = item.file;
191
+ lines.push(item.file);
192
+ }
193
+ const exported = item.isExported ? ' [exported]' : '';
194
+ // Surface decorators/annotations — structural hint that a framework may invoke this
195
+ const hints = [];
196
+ if (item.decorators && item.decorators.length > 0) {
197
+ hints.push(...item.decorators.map(d => `@${d}`));
198
+ }
199
+ if (item.annotations && item.annotations.length > 0) {
200
+ hints.push(...item.annotations.map(a => `@${a}`));
201
+ }
202
+ const hintStr = hints.length > 0 ? ` [has ${hints.join(', ')}]` : '';
203
+ lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}`);
204
+ }
205
+
206
+ if (hidden > 0) {
207
+ lines.push(`\n${hidden} more result(s) not shown. Use --top=${results.length} or --all to see all.`);
208
+ }
209
+
210
+ // Show counts of excluded items with expansion hints
211
+ if (results.length === 0) {
212
+ lines.push('No dead code found.');
213
+ }
214
+ if (results.excludedDecorated > 0) {
215
+ const decoratedHint = options.decoratedHint || `${results.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.`;
216
+ lines.push(`\n${decoratedHint}`);
217
+ }
218
+ if (results.excludedExported > 0) {
219
+ const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.`;
220
+ lines.push(`\n${exportedHint}`);
221
+ }
222
+
223
+ if (lines.length === 0) {
224
+ return 'No dead code found.';
225
+ }
226
+
227
+ return lines.join('\n');
228
+ }
229
+
230
+ /**
231
+ * Format deadcode command output - JSON
232
+ */
233
+ function formatDeadcodeJson(results) {
234
+ return JSON.stringify({
235
+ count: results.length,
236
+ ...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
237
+ ...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
238
+ symbols: results.map(item => ({
239
+ name: item.name,
240
+ type: item.type,
241
+ file: item.file,
242
+ startLine: item.startLine,
243
+ endLine: item.endLine,
244
+ ...(item.isExported && { isExported: true }),
245
+ ...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
246
+ ...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations })
247
+ }))
248
+ }, null, 2);
249
+ }
250
+
251
+ /**
252
+ * Format entrypoints command output (text)
253
+ */
254
+ function formatEntrypoints(results, options = {}) {
255
+ if (!results || results.length === 0) {
256
+ return 'No framework entry points detected.';
257
+ }
258
+
259
+ const lines = [];
260
+ lines.push(`Framework Entry Points: ${results.length} detected\n`);
261
+
262
+ // Group by type
263
+ const byType = new Map();
264
+ for (const ep of results) {
265
+ if (!byType.has(ep.type)) byType.set(ep.type, []);
266
+ byType.get(ep.type).push(ep);
267
+ }
268
+
269
+ const typeLabels = {
270
+ http: 'HTTP Routes',
271
+ cli: 'CLI Handlers',
272
+ di: 'Dependency Injection',
273
+ jobs: 'Job Schedulers',
274
+ test: 'Test Fixtures',
275
+ runtime: 'Runtime Entry Points',
276
+ ui: 'UI Handlers',
277
+ events: 'Event Handlers',
278
+ };
279
+
280
+ let itemNum = 0;
281
+ for (const [type, entries] of byType) {
282
+ const label = typeLabels[type] || type;
283
+ lines.push(`${label} (${entries.length}):`);
284
+
285
+ let currentFile = null;
286
+ for (const ep of entries) {
287
+ if (ep.file !== currentFile) {
288
+ currentFile = ep.file;
289
+ lines.push(` ${ep.file}`);
290
+ }
291
+ itemNum++;
292
+ const evidence = ep.evidence.join(', ');
293
+ lines.push(` [${itemNum}] ${ep.name} (${ep.framework}) — ${evidence}${' '.repeat(Math.max(0, 40 - ep.name.length - ep.framework.length - evidence.length))}:${ep.line}`);
294
+ }
295
+ lines.push('');
296
+ }
297
+
298
+ return lines.join('\n').trimEnd();
299
+ }
300
+
301
+ /**
302
+ * Format entrypoints command output (JSON)
303
+ */
304
+ function formatEntrypointsJson(results) {
305
+ return JSON.stringify({
306
+ meta: { total: results.length },
307
+ data: {
308
+ entrypoints: results.map(ep => ({
309
+ name: ep.name,
310
+ file: ep.file,
311
+ line: ep.line,
312
+ type: ep.type,
313
+ framework: ep.framework,
314
+ patternId: ep.patternId,
315
+ evidence: ep.evidence,
316
+ confidence: ep.confidence,
317
+ }))
318
+ }
319
+ }, null, 2);
320
+ }
321
+
322
+ module.exports = {
323
+ formatToc,
324
+ formatTocJson,
325
+ formatStats,
326
+ formatStatsJson,
327
+ formatDeadcode,
328
+ formatDeadcodeJson,
329
+ formatEntrypoints,
330
+ formatEntrypointsJson,
331
+ };
@@ -0,0 +1,307 @@
1
+ /**
2
+ * core/output/search.js - Text search, structural search, example, typedef, tests formatters
3
+ */
4
+
5
+ const { detectDoubleEscaping } = require('./shared');
6
+
7
+ /**
8
+ * Format search command output
9
+ */
10
+ function formatSearch(results, term) {
11
+ const meta = results.meta;
12
+ const fallbackNote = meta && meta.regexFallback
13
+ ? `\nNote: Invalid regex (${meta.regexFallback}). Fell back to plain text search.`
14
+ : '';
15
+
16
+ const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
17
+ if (totalMatches === 0) {
18
+ if (meta) {
19
+ const scope = meta.filesSkipped > 0
20
+ ? `Searched ${meta.filesScanned} of ${meta.totalFiles} file${meta.totalFiles === 1 ? '' : 's'} (${meta.filesSkipped} excluded by filters).`
21
+ : `Searched ${meta.filesScanned} file${meta.filesScanned === 1 ? '' : 's'}.`;
22
+ const escapingHint = detectDoubleEscaping(term);
23
+ return `No matches found for "${term}". ${scope}${fallbackNote}${escapingHint}`;
24
+ }
25
+ return `No matches found for "${term}"${fallbackNote}`;
26
+ }
27
+
28
+ const lines = [];
29
+ const fileWord = results.length === 1 ? 'file' : 'files';
30
+ lines.push(`Found ${totalMatches} match${totalMatches === 1 ? '' : 'es'} for "${term}" in ${results.length} ${fileWord}:`);
31
+ if (fallbackNote) lines.push(fallbackNote.trim());
32
+ lines.push('═'.repeat(60));
33
+
34
+ for (const result of results) {
35
+ lines.push(`\n${result.file}`);
36
+ for (const m of result.matches) {
37
+ if (m.before && m.before.length > 0) {
38
+ for (const line of m.before) {
39
+ lines.push(` ... ${line.trim()}`);
40
+ }
41
+ }
42
+ lines.push(` ${m.line}: ${m.content.trim()}`);
43
+ if (m.after && m.after.length > 0) {
44
+ for (const line of m.after) {
45
+ lines.push(` ... ${line.trim()}`);
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ if (meta && meta.truncatedMatches > 0) {
52
+ lines.push(`\n${results.reduce((s, r) => s + r.matches.length, 0)} shown of ${meta.totalMatches} total matches. Use top= to see more.`);
53
+ }
54
+
55
+ if (meta && meta.testsExcluded && meta.filesSkipped > 0) {
56
+ lines.push(`\nNote: ${meta.filesSkipped} test file${meta.filesSkipped === 1 ? '' : 's'} hidden by default (use include_tests=true to include).`);
57
+ }
58
+
59
+ return lines.join('\n');
60
+ }
61
+
62
+ /**
63
+ * Format search results as JSON
64
+ */
65
+ function formatSearchJson(results, term) {
66
+ const meta = results.meta;
67
+ const obj = {
68
+ term,
69
+ totalMatches: (meta && meta.totalMatches != null) ? meta.totalMatches : results.reduce((sum, r) => sum + r.matches.length, 0),
70
+ files: results.map(r => ({
71
+ file: r.file,
72
+ matchCount: r.matches.length,
73
+ matches: r.matches.map(m => ({
74
+ line: m.line,
75
+ content: m.content // FULL content
76
+ }))
77
+ }))
78
+ };
79
+ if (meta) {
80
+ obj.filesScanned = meta.filesScanned;
81
+ obj.filesSkipped = meta.filesSkipped;
82
+ obj.totalFiles = meta.totalFiles;
83
+ if (meta.regexFallback) obj.regexFallback = meta.regexFallback;
84
+ if (meta.truncatedMatches > 0) obj.truncatedMatches = meta.truncatedMatches;
85
+ }
86
+ return JSON.stringify(obj, null, 2);
87
+ }
88
+
89
+ /**
90
+ * Format structural search results (index-based queries)
91
+ */
92
+ function formatStructuralSearch(result) {
93
+ const { results, meta } = result;
94
+ const lines = [];
95
+
96
+ // Build query description
97
+ const parts = [];
98
+ if (meta.query.type) parts.push(`type=${meta.query.type}`);
99
+ if (meta.query.term) parts.push(`name="${meta.query.term}"`);
100
+ if (meta.query.param) parts.push(`param="${meta.query.param}"`);
101
+ if (meta.query.receiver) parts.push(`receiver="${meta.query.receiver}"`);
102
+ if (meta.query.returns) parts.push(`returns="${meta.query.returns}"`);
103
+ if (meta.query.decorator) parts.push(`decorator="${meta.query.decorator}"`);
104
+ if (meta.query.exported) parts.push('exported');
105
+ if (meta.query.unused) parts.push('unused');
106
+ const queryStr = parts.join(', ');
107
+
108
+ lines.push(`Structural search: ${queryStr}`);
109
+ lines.push('═'.repeat(60));
110
+
111
+ if (results.length === 0) {
112
+ lines.push('No matches found.');
113
+ return lines.join('\n');
114
+ }
115
+
116
+ lines.push(`Found ${meta.totalMatched} match${meta.totalMatched === 1 ? '' : 'es'}${meta.shown < meta.totalMatched ? ` (showing ${meta.shown})` : ''}:`);
117
+ lines.push('');
118
+
119
+ // Group by file
120
+ let currentFile = null;
121
+ for (const r of results) {
122
+ if (r.file !== currentFile) {
123
+ currentFile = r.file;
124
+ lines.push(`${r.file}`);
125
+ }
126
+
127
+ if (r.kind === 'call') {
128
+ lines.push(` ${r.line}: ${r.name}()${r.isMethod ? ' [method]' : ''}`);
129
+ } else {
130
+ let sig = ` ${r.line}: ${r.kind} ${r.name}`;
131
+ if (r.params) sig += `(${r.params})`;
132
+ if (r.returnType) sig += ` → ${r.returnType}`;
133
+ if (r.className) sig += ` [${r.className}]`;
134
+ if (r.decorators) sig += ` @${r.decorators.join(', @')}`;
135
+ lines.push(sig);
136
+ }
137
+ }
138
+
139
+ if (meta.shown < meta.totalMatched) {
140
+ lines.push(`\n${meta.shown} of ${meta.totalMatched} shown. Use top= to see more.`);
141
+ }
142
+
143
+ return lines.join('\n');
144
+ }
145
+
146
+ function formatStructuralSearchJson(result) {
147
+ return JSON.stringify(result, null, 2);
148
+ }
149
+
150
+ /**
151
+ * Format example result as text
152
+ */
153
+ function formatExample(result, name) {
154
+ if (!result || !result.best) return `No call examples found for "${name}"`;
155
+
156
+ const best = result.best;
157
+ const lines = [];
158
+ lines.push(`Best example of "${name}":`);
159
+ lines.push('═'.repeat(60));
160
+ lines.push(`${best.relativePath}:${best.line}`);
161
+ lines.push('');
162
+
163
+ if (best.before) {
164
+ for (let i = 0; i < best.before.length; i++) {
165
+ const ln = best.line - best.before.length + i;
166
+ lines.push(`${ln.toString().padStart(4)}| ${best.before[i]}`);
167
+ }
168
+ }
169
+
170
+ lines.push(`${best.line.toString().padStart(4)}| ${best.content} <--`);
171
+
172
+ if (best.after) {
173
+ for (let i = 0; i < best.after.length; i++) {
174
+ const ln = best.line + i + 1;
175
+ lines.push(`${ln.toString().padStart(4)}| ${best.after[i]}`);
176
+ }
177
+ }
178
+
179
+ lines.push('');
180
+ lines.push(`Score: ${best.score} (${result.totalCalls} total calls)`);
181
+ lines.push(`Why: ${best.reasons.length > 0 ? best.reasons.join(', ') : 'first available call'}`);
182
+
183
+ return lines.join('\n');
184
+ }
185
+
186
+ /**
187
+ * Format example command output - JSON
188
+ */
189
+ function formatExampleJson(result, name) {
190
+ if (!result || !result.best) {
191
+ return JSON.stringify({ found: false, query: name, error: `No call examples found for "${name}"` }, null, 2);
192
+ }
193
+
194
+ const best = result.best;
195
+ return JSON.stringify({
196
+ found: true,
197
+ query: name,
198
+ totalCalls: result.totalCalls,
199
+ best: {
200
+ file: best.relativePath || best.file,
201
+ line: best.line,
202
+ content: best.content,
203
+ score: best.score,
204
+ reasons: best.reasons || [],
205
+ ...(best.before && best.before.length > 0 && { before: best.before }),
206
+ ...(best.after && best.after.length > 0 && { after: best.after })
207
+ }
208
+ }, null, 2);
209
+ }
210
+
211
+ /**
212
+ * Format typedef command output - text
213
+ */
214
+ function formatTypedef(types, name) {
215
+ const lines = [`Type definitions for "${name}":\n`];
216
+
217
+ if (types.length === 0) {
218
+ lines.push(' (none found)');
219
+ } else {
220
+ for (const t of types) {
221
+ lines.push(`${t.relativePath}:${t.startLine} ${t.type} ${t.name}`);
222
+ if (t.usageCount !== undefined) {
223
+ lines.push(` (${t.usageCount} usages)`);
224
+ }
225
+ if (t.code) {
226
+ lines.push('');
227
+ lines.push('─── CODE ───');
228
+ lines.push(t.code);
229
+ lines.push('');
230
+ }
231
+ }
232
+ }
233
+
234
+ return lines.join('\n');
235
+ }
236
+
237
+ /**
238
+ * Format typedef as JSON
239
+ */
240
+ function formatTypedefJson(types, name) {
241
+ return JSON.stringify({
242
+ query: name,
243
+ count: types.length,
244
+ types: types.map(t => ({
245
+ name: t.name,
246
+ type: t.type,
247
+ file: t.relativePath || t.file,
248
+ startLine: t.startLine,
249
+ endLine: t.endLine,
250
+ ...(t.usageCount !== undefined && { usageCount: t.usageCount }),
251
+ ...(t.code && { code: t.code })
252
+ }))
253
+ }, null, 2);
254
+ }
255
+
256
+ /**
257
+ * Format tests command output - text
258
+ */
259
+ function formatTests(tests, name) {
260
+ const lines = [`Tests for "${name}":\n`];
261
+
262
+ if (tests.length === 0) {
263
+ lines.push(' (no tests found)');
264
+ } else {
265
+ const totalMatches = tests.reduce((sum, t) => sum + t.matches.length, 0);
266
+ lines.push(`Found ${totalMatches} matches in ${tests.length} test file(s):\n`);
267
+
268
+ for (const testFile of tests) {
269
+ lines.push(testFile.file);
270
+ for (const match of testFile.matches) {
271
+ const typeLabel = match.matchType === 'test-case' ? '[test]' :
272
+ match.matchType === 'import' ? '[import]' :
273
+ match.matchType === 'call' ? '[call]' :
274
+ match.matchType === 'string-ref' ? '[string]' : '[ref]';
275
+ lines.push(` ${match.line}: ${typeLabel} ${match.content}`);
276
+ }
277
+ lines.push('');
278
+ }
279
+ }
280
+
281
+ return lines.join('\n');
282
+ }
283
+
284
+ /**
285
+ * Format tests as JSON
286
+ */
287
+ function formatTestsJson(tests, name) {
288
+ return JSON.stringify({
289
+ query: name,
290
+ testFileCount: tests.length,
291
+ totalMatches: tests.reduce((sum, t) => sum + t.matches.length, 0),
292
+ testFiles: tests
293
+ }, null, 2);
294
+ }
295
+
296
+ module.exports = {
297
+ formatSearch,
298
+ formatSearchJson,
299
+ formatStructuralSearch,
300
+ formatStructuralSearchJson,
301
+ formatExample,
302
+ formatExampleJson,
303
+ formatTypedef,
304
+ formatTypedefJson,
305
+ formatTests,
306
+ formatTestsJson,
307
+ };