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.
- package/.claude/skills/ucn/SKILL.md +3 -1
- package/.github/workflows/ci.yml +15 -3
- package/.github/workflows/publish.yml +20 -8
- package/README.md +1 -0
- package/cli/index.js +165 -246
- package/core/analysis.js +1400 -0
- package/core/build-worker.js +194 -0
- package/core/cache.js +105 -7
- package/core/callers.js +194 -64
- package/core/deadcode.js +22 -66
- package/core/discovery.js +9 -54
- package/core/execute.js +139 -54
- package/core/graph.js +615 -0
- package/core/output/analysis-ext.js +271 -0
- package/core/output/analysis.js +491 -0
- package/core/output/extraction.js +188 -0
- package/core/output/find.js +355 -0
- package/core/output/graph.js +399 -0
- package/core/output/refactoring.js +293 -0
- package/core/output/reporting.js +331 -0
- package/core/output/search.js +307 -0
- package/core/output/shared.js +271 -0
- package/core/output/tracing.js +416 -0
- package/core/output.js +15 -3293
- package/core/parallel-build.js +165 -0
- package/core/project.js +299 -3633
- package/core/registry.js +59 -0
- package/core/reporting.js +258 -0
- package/core/search.js +890 -0
- package/core/stacktrace.js +1 -1
- package/core/tracing.js +631 -0
- package/core/verify.js +10 -13
- package/eslint.config.js +43 -0
- package/jsconfig.json +10 -0
- package/languages/go.js +21 -2
- package/languages/html.js +8 -0
- package/languages/index.js +102 -40
- package/languages/java.js +13 -0
- package/languages/javascript.js +17 -1
- package/languages/python.js +14 -0
- package/languages/rust.js +13 -0
- package/languages/utils.js +1 -1
- package/mcp/server.js +45 -28
- 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
|
+
};
|