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,355 @@
1
+ /**
2
+ * core/output/find.js - Find/usages/symbol lookup formatters
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const {
7
+ formatFunctionSignature,
8
+ formatClassSignature,
9
+ computeConfidence,
10
+ } = require('./shared');
11
+
12
+ /**
13
+ * Format find command output
14
+ */
15
+ function formatFind(symbols, query, top) {
16
+ if (symbols.length === 0) {
17
+ return `No symbols found for "${query}"`;
18
+ }
19
+
20
+ const lines = [];
21
+ const limit = (top && top > 0) ? Math.min(symbols.length, top) : Math.min(symbols.length, 10);
22
+ const hidden = symbols.length - limit;
23
+
24
+ if (hidden > 0) {
25
+ lines.push(`Found ${symbols.length} match(es) for "${query}" (showing top ${limit}):`);
26
+ } else {
27
+ lines.push(`Found ${symbols.length} match(es) for "${query}":`);
28
+ }
29
+ lines.push('─'.repeat(60));
30
+
31
+ for (let i = 0; i < limit; i++) {
32
+ const s = symbols[i];
33
+ const sig = s.params !== undefined
34
+ ? formatFunctionSignature(s)
35
+ : formatClassSignature(s);
36
+ lines.push(`${s.relativePath}:${s.startLine} ${sig}`);
37
+ if (s.usageCounts !== undefined) {
38
+ const c = s.usageCounts;
39
+ const parts = [];
40
+ if (c.calls > 0) parts.push(`${c.calls} calls`);
41
+ if (c.definitions > 0) parts.push(`${c.definitions} def`);
42
+ if (c.imports > 0) parts.push(`${c.imports} imports`);
43
+ if (c.references > 0) parts.push(`${c.references} refs`);
44
+ lines.push(parts.length > 0
45
+ ? ` (${c.total} usages: ${parts.join(', ')})`
46
+ : ` (${c.total} usages)`);
47
+ } else if (s.usageCount !== undefined) {
48
+ lines.push(` (${s.usageCount} usages)`);
49
+ }
50
+ }
51
+
52
+ if (hidden > 0) {
53
+ lines.push(`... ${hidden} more result(s).`);
54
+ }
55
+
56
+ return lines.join('\n');
57
+ }
58
+
59
+ function formatFindJson(items) {
60
+ return JSON.stringify({
61
+ meta: { command: 'find', count: items.length },
62
+ data: items.map(m => ({
63
+ name: m.name,
64
+ type: m.type,
65
+ file: m.relativePath || m.file,
66
+ line: m.startLine,
67
+ endLine: m.endLine,
68
+ ...(m.className && { className: m.className }),
69
+ ...(m.receiver && { receiver: m.receiver }),
70
+ })),
71
+ }, null, 2);
72
+ }
73
+
74
+ /**
75
+ * Format find results with depth/confidence features (detailed view).
76
+ * Returns a string. Used by CLI and interactive mode.
77
+ *
78
+ * @param {Array} symbols - Find result array
79
+ * @param {string} query - Original search query
80
+ * @param {object} options - { depth, top, all }
81
+ */
82
+ function formatFindDetailed(symbols, query, options = {}) {
83
+ const { depth, top, all } = options;
84
+ const DEFAULT_LIMIT = 5;
85
+
86
+ if (symbols.length === 0) {
87
+ return `No symbols found for "${query}"`;
88
+ }
89
+
90
+ const lines = [];
91
+ const limit = all ? symbols.length : (top > 0 ? top : DEFAULT_LIMIT);
92
+ const showing = Math.min(limit, symbols.length);
93
+ const hidden = symbols.length - showing;
94
+
95
+ if (hidden > 0) {
96
+ lines.push(`Found ${symbols.length} match(es) for "${query}" (showing top ${showing}):`);
97
+ } else {
98
+ lines.push(`Found ${symbols.length} match(es) for "${query}":`);
99
+ }
100
+ lines.push('─'.repeat(60));
101
+
102
+ for (let i = 0; i < showing; i++) {
103
+ const s = symbols[i];
104
+ // Depth 0: just location
105
+ if (depth === '0') {
106
+ lines.push(`${s.relativePath}:${s.startLine}`);
107
+ continue;
108
+ }
109
+
110
+ // Depth 1 (default): location + signature
111
+ const sig = s.params !== undefined
112
+ ? formatFunctionSignature(s)
113
+ : formatClassSignature(s);
114
+
115
+ const confidence = computeConfidence(s);
116
+ const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
117
+
118
+ lines.push(`${s.relativePath}:${s.startLine} ${sig}${confStr}`);
119
+ if (s.usageCounts !== undefined) {
120
+ const c = s.usageCounts;
121
+ const parts = [];
122
+ if (c.calls > 0) parts.push(`${c.calls} calls`);
123
+ if (c.definitions > 0) parts.push(`${c.definitions} def`);
124
+ if (c.imports > 0) parts.push(`${c.imports} imports`);
125
+ if (c.references > 0) parts.push(`${c.references} refs`);
126
+ lines.push(` (${c.total} usages: ${parts.join(', ')})`);
127
+ } else if (s.usageCount !== undefined) {
128
+ lines.push(` (${s.usageCount} usages)`);
129
+ }
130
+
131
+ if (confidence.level !== 'high' && confidence.reasons.length > 0) {
132
+ lines.push(` ⚠ ${confidence.reasons.join(', ')}`);
133
+ }
134
+
135
+ // Depth 2: + first 10 lines of code
136
+ if (depth === '2' || depth === 'full') {
137
+ try {
138
+ const content = fs.readFileSync(s.file, 'utf-8');
139
+ const fileLines = content.split('\n');
140
+ const maxLines = depth === 'full' ? (s.endLine - s.startLine + 1) : 10;
141
+ const endLine = Math.min(s.startLine + maxLines - 1, s.endLine);
142
+ lines.push(' ───');
143
+ for (let j = s.startLine - 1; j < endLine; j++) {
144
+ lines.push(` ${fileLines[j]}`);
145
+ }
146
+ if (depth === '2' && s.endLine > endLine) {
147
+ lines.push(` ... (${s.endLine - endLine} more lines)`);
148
+ }
149
+ } catch (e) {
150
+ // Skip code extraction on error
151
+ }
152
+ }
153
+ lines.push('');
154
+ }
155
+
156
+ if (hidden > 0) {
157
+ lines.push(`... ${hidden} more result(s). Use --all to see all, or --top=N to see more.`);
158
+ }
159
+
160
+ return lines.join('\n');
161
+ }
162
+
163
+ /**
164
+ * Format symbol search results as JSON
165
+ */
166
+ function formatSymbolJson(symbols, query) {
167
+ return JSON.stringify({
168
+ meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
169
+ data: {
170
+ query,
171
+ count: symbols.length,
172
+ results: symbols.map(s => ({
173
+ name: s.name,
174
+ type: s.type,
175
+ file: s.relativePath || s.file,
176
+ startLine: s.startLine,
177
+ endLine: s.endLine,
178
+ ...(s.params && { params: s.params }), // FULL params
179
+ ...(s.paramsStructured && { paramsStructured: s.paramsStructured }),
180
+ ...(s.returnType && { returnType: s.returnType }),
181
+ ...(s.modifiers && { modifiers: s.modifiers }),
182
+ ...(s.usageCount !== undefined && { usageCount: s.usageCount }),
183
+ ...(s.usageCounts !== undefined && { usageCounts: s.usageCounts })
184
+ }))
185
+ }
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Format usages as JSON - FULL expressions, never truncated
191
+ */
192
+ function formatUsagesJson(usages, name) {
193
+ const definitions = usages.filter(u => u.isDefinition);
194
+ const refs = usages.filter(u => !u.isDefinition);
195
+
196
+ const calls = refs.filter(u => u.usageType === 'call');
197
+ const imports = refs.filter(u => u.usageType === 'import');
198
+ const references = refs.filter(u => u.usageType === 'reference');
199
+
200
+ const formatUsage = (u) => ({
201
+ file: u.relativePath || u.file,
202
+ line: u.line,
203
+ expression: u.content, // FULL expression - key improvement
204
+ ...(u.args && { args: u.args }), // Parsed arguments
205
+ ...(u.before && u.before.length > 0 && { before: u.before }),
206
+ ...(u.after && u.after.length > 0 && { after: u.after })
207
+ });
208
+
209
+ return JSON.stringify({
210
+ meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
211
+ data: {
212
+ symbol: name,
213
+ definitionCount: definitions.length,
214
+ callCount: calls.length,
215
+ importCount: imports.length,
216
+ referenceCount: references.length,
217
+ totalUsages: refs.length,
218
+ definitions: definitions.map(d => ({
219
+ file: d.relativePath || d.file,
220
+ line: d.line,
221
+ signature: d.signature || null, // FULL signature
222
+ type: d.type || null,
223
+ ...(d.returnType && { returnType: d.returnType }),
224
+ ...(d.before && d.before.length > 0 && { before: d.before }),
225
+ ...(d.after && d.after.length > 0 && { after: d.after })
226
+ })),
227
+ calls: calls.map(formatUsage),
228
+ imports: imports.map(formatUsage),
229
+ references: references.map(formatUsage)
230
+ }
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Format usages command output
236
+ */
237
+ function formatUsages(usages, name) {
238
+ const defs = usages.filter(u => u.isDefinition);
239
+ const calls = usages.filter(u => u.usageType === 'call');
240
+ const imports = usages.filter(u => u.usageType === 'import');
241
+ const refs = usages.filter(u => !u.isDefinition && u.usageType === 'reference');
242
+
243
+ const lines = [];
244
+ lines.push(`Usages of "${name}": ${defs.length} definitions, ${calls.length} calls, ${imports.length} imports, ${refs.length} references`);
245
+ lines.push('═'.repeat(60));
246
+
247
+ function renderContextLines(usage) {
248
+ if (usage.before && usage.before.length > 0) {
249
+ for (const line of usage.before) {
250
+ lines.push(` ${line}`);
251
+ }
252
+ }
253
+ }
254
+
255
+ function renderAfterLines(usage) {
256
+ if (usage.after && usage.after.length > 0) {
257
+ for (const line of usage.after) {
258
+ lines.push(` ${line}`);
259
+ }
260
+ }
261
+ }
262
+
263
+ if (defs.length > 0) {
264
+ lines.push('\nDEFINITIONS:');
265
+ for (const d of defs) {
266
+ lines.push(` ${d.relativePath}:${d.line || d.startLine}`);
267
+ if (d.signature) lines.push(` ${d.signature}`);
268
+ }
269
+ }
270
+
271
+ if (calls.length > 0) {
272
+ lines.push('\nCALLS:');
273
+ for (const c of calls) {
274
+ lines.push(` ${c.relativePath}:${c.line}`);
275
+ renderContextLines(c);
276
+ lines.push(` ${c.content.trim()}`);
277
+ renderAfterLines(c);
278
+ }
279
+ }
280
+
281
+ if (imports.length > 0) {
282
+ lines.push('\nIMPORTS:');
283
+ for (const i of imports) {
284
+ lines.push(` ${i.relativePath}:${i.line}`);
285
+ lines.push(` ${i.content.trim()}`);
286
+ }
287
+ }
288
+
289
+ if (refs.length > 0) {
290
+ lines.push('\nREFERENCES:');
291
+ for (const r of refs) {
292
+ lines.push(` ${r.relativePath}:${r.line}`);
293
+ renderContextLines(r);
294
+ lines.push(` ${r.content.trim()}`);
295
+ renderAfterLines(r);
296
+ }
297
+ }
298
+
299
+ return lines.join('\n');
300
+ }
301
+
302
+ /**
303
+ * Format disambiguation prompt - text
304
+ */
305
+ function formatDisambiguation(matches, name, command) {
306
+ const lines = [`Multiple matches for "${name}":\n`];
307
+
308
+ for (const m of matches) {
309
+ const sig = m.params !== undefined
310
+ ? formatFunctionSignature(m)
311
+ : formatClassSignature(m);
312
+ lines.push(` ${m.relativePath}:${m.startLine} ${sig}`);
313
+ if (m.usageCount !== undefined) {
314
+ lines.push(` (${m.usageCount} usages)`);
315
+ }
316
+ }
317
+
318
+ lines.push('');
319
+ lines.push(`Use: ucn . ${command} ${name} --file <pattern>`);
320
+
321
+ return lines.join('\n');
322
+ }
323
+
324
+ /**
325
+ * Format disambiguation prompt - JSON
326
+ */
327
+ function formatDisambiguationJson(matches, name, command) {
328
+ return JSON.stringify({
329
+ query: name,
330
+ command,
331
+ count: matches.length,
332
+ matches: matches.map(m => ({
333
+ name: m.name,
334
+ type: m.type,
335
+ file: m.relativePath,
336
+ startLine: m.startLine,
337
+ endLine: m.endLine,
338
+ ...(m.params !== undefined && { params: m.params }),
339
+ ...(m.className && { className: m.className }),
340
+ ...(m.usageCount !== undefined && { usageCount: m.usageCount }),
341
+ })),
342
+ hint: `Use: ucn . ${command} ${name} --file <pattern>`
343
+ }, null, 2);
344
+ }
345
+
346
+ module.exports = {
347
+ formatFind,
348
+ formatFindJson,
349
+ formatFindDetailed,
350
+ formatSymbolJson,
351
+ formatUsages,
352
+ formatUsagesJson,
353
+ formatDisambiguation,
354
+ formatDisambiguationJson,
355
+ };