ucn 3.8.6 → 3.8.8
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/cli/index.js +4 -4
- package/core/entrypoints.js +136 -32
- package/core/execute.js +27 -10
- package/core/output.js +17 -4
- package/core/project.js +59 -23
- package/core/registry.js +1 -0
- package/languages/go.js +131 -0
- package/mcp/server.js +94 -75
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -715,7 +715,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
715
715
|
case 'affectedTests': {
|
|
716
716
|
const { ok, result, error } = execute(index, 'affectedTests', { name: arg, ...flags });
|
|
717
717
|
if (!ok) fail(error);
|
|
718
|
-
printOutput(result, output.formatAffectedTestsJson, output.formatAffectedTests);
|
|
718
|
+
printOutput(result, output.formatAffectedTestsJson, r => output.formatAffectedTests(r, { all: flags.all }));
|
|
719
719
|
break;
|
|
720
720
|
}
|
|
721
721
|
|
|
@@ -782,7 +782,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
782
782
|
case 'diffImpact': {
|
|
783
783
|
const { ok, result, error } = execute(index, 'diffImpact', { base: flags.base, staged: flags.staged, file: flags.file });
|
|
784
784
|
if (!ok) fail(error);
|
|
785
|
-
printOutput(result, output.formatDiffImpactJson, output.formatDiffImpact);
|
|
785
|
+
printOutput(result, output.formatDiffImpactJson, r => output.formatDiffImpact(r, { all: flags.all }));
|
|
786
786
|
break;
|
|
787
787
|
}
|
|
788
788
|
|
|
@@ -1504,7 +1504,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1504
1504
|
case 'affectedTests': {
|
|
1505
1505
|
const { ok, result, error } = execute(index, 'affectedTests', { name: arg, ...iflags });
|
|
1506
1506
|
if (!ok) { console.log(error); return; }
|
|
1507
|
-
console.log(output.formatAffectedTests(result));
|
|
1507
|
+
console.log(output.formatAffectedTests(result, { all: iflags.all }));
|
|
1508
1508
|
break;
|
|
1509
1509
|
}
|
|
1510
1510
|
|
|
@@ -1536,7 +1536,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1536
1536
|
case 'diffImpact': {
|
|
1537
1537
|
const { ok, result, error } = execute(index, 'diffImpact', iflags);
|
|
1538
1538
|
if (!ok) { console.log(error); return; }
|
|
1539
|
-
console.log(output.formatDiffImpact(result));
|
|
1539
|
+
console.log(output.formatDiffImpact(result, { all: iflags.all }));
|
|
1540
1540
|
break;
|
|
1541
1541
|
}
|
|
1542
1542
|
|
package/core/entrypoints.js
CHANGED
|
@@ -174,6 +174,55 @@ const FRAMEWORK_PATTERNS = [
|
|
|
174
174
|
pattern: /^tokio::main$/,
|
|
175
175
|
},
|
|
176
176
|
|
|
177
|
+
// ── Go Runtime Entry Points ─────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
// Go main function (program entry)
|
|
180
|
+
{
|
|
181
|
+
id: 'go-main',
|
|
182
|
+
languages: new Set(['go']),
|
|
183
|
+
type: 'runtime',
|
|
184
|
+
framework: 'go',
|
|
185
|
+
detection: 'namePattern',
|
|
186
|
+
pattern: /^main$/,
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Go init functions (package initialization, called by runtime)
|
|
190
|
+
{
|
|
191
|
+
id: 'go-init',
|
|
192
|
+
languages: new Set(['go']),
|
|
193
|
+
type: 'runtime',
|
|
194
|
+
framework: 'go',
|
|
195
|
+
detection: 'namePattern',
|
|
196
|
+
pattern: /^init$/,
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
// Go test functions (called by go test)
|
|
200
|
+
{
|
|
201
|
+
id: 'go-test',
|
|
202
|
+
languages: new Set(['go']),
|
|
203
|
+
type: 'test',
|
|
204
|
+
framework: 'go',
|
|
205
|
+
detection: 'namePattern',
|
|
206
|
+
pattern: /^(Test|Benchmark|Example|Fuzz)[A-Z_]/,
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// ── Go Framework Patterns ─────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
// Cobra CLI framework — RunE, Run, PreRunE etc. assigned to cobra.Command struct fields
|
|
212
|
+
// Detected via composite literal: &cobra.Command{RunE: handler}
|
|
213
|
+
{
|
|
214
|
+
id: 'cobra-command',
|
|
215
|
+
languages: new Set(['go']),
|
|
216
|
+
type: 'cli',
|
|
217
|
+
framework: 'cobra',
|
|
218
|
+
detection: 'compositePattern',
|
|
219
|
+
typePattern: /^cobra\.Command$/,
|
|
220
|
+
fieldPattern: /^(Run|RunE|PreRun|PreRunE|PostRun|PostRunE|PersistentPreRun|PersistentPreRunE|PersistentPostRun|PersistentPostRunE)$/,
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// Go goroutine launch — go func() or go handler()
|
|
224
|
+
// (detected separately in namePattern since it's a language feature)
|
|
225
|
+
|
|
177
226
|
// ── Catch-all fallbacks ─────────────────────────────────────────────
|
|
178
227
|
|
|
179
228
|
// Python: any decorator with '.' (attribute access) — framework registration heuristic
|
|
@@ -240,50 +289,77 @@ function matchDecoratorOrModifier(symbol, language) {
|
|
|
240
289
|
*/
|
|
241
290
|
function buildCallbackEntrypointMap(index) {
|
|
242
291
|
const callPatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'callPattern');
|
|
243
|
-
|
|
292
|
+
const compositePatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'compositePattern');
|
|
293
|
+
if (callPatterns.length === 0 && compositePatterns.length === 0) return new Map();
|
|
244
294
|
|
|
245
295
|
const result = new Map(); // name -> info
|
|
246
296
|
|
|
247
297
|
for (const [filePath, fileEntry] of index.files) {
|
|
248
298
|
const lang = fileEntry.language;
|
|
249
|
-
const relevantPatterns = callPatterns.filter(p => p.languages.has(lang));
|
|
250
|
-
if (relevantPatterns.length === 0) continue;
|
|
251
299
|
|
|
252
300
|
const calls = getCachedCalls(index, filePath);
|
|
253
301
|
if (!calls) continue;
|
|
254
302
|
|
|
255
|
-
// Pass 1:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
303
|
+
// Pass 1+2: call-pattern detection (e.g., app.GET("/", handler))
|
|
304
|
+
const relevantCallPatterns = callPatterns.filter(p => p.languages.has(lang));
|
|
305
|
+
if (relevantCallPatterns.length > 0) {
|
|
306
|
+
// Pass 1: find route-registration calls, index by line
|
|
307
|
+
const routeLines = new Map(); // line -> { pattern, call }
|
|
308
|
+
for (const call of calls) {
|
|
309
|
+
if (!call.receiver) continue;
|
|
310
|
+
for (const pattern of relevantCallPatterns) {
|
|
311
|
+
if (pattern.receiverPattern.test(call.receiver) &&
|
|
312
|
+
pattern.methodPattern.test(call.name)) {
|
|
313
|
+
routeLines.set(call.line, { pattern, call });
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (routeLines.size > 0) {
|
|
320
|
+
// Pass 2: find callbacks on route-registration lines
|
|
321
|
+
for (const call of calls) {
|
|
322
|
+
if (!call.isFunctionReference && !call.isPotentialCallback) continue;
|
|
323
|
+
const route = routeLines.get(call.line);
|
|
324
|
+
if (!route) continue;
|
|
325
|
+
|
|
326
|
+
if (!result.has(call.name)) {
|
|
327
|
+
result.set(call.name, {
|
|
328
|
+
framework: route.pattern.framework,
|
|
329
|
+
type: route.pattern.type,
|
|
330
|
+
patternId: route.pattern.id,
|
|
331
|
+
method: route.call.name.toUpperCase(),
|
|
332
|
+
file: filePath,
|
|
333
|
+
line: call.line,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
265
336
|
}
|
|
266
337
|
}
|
|
267
338
|
}
|
|
268
339
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
340
|
+
// Pass 3: composite literal patterns (e.g., &cobra.Command{RunE: handler})
|
|
341
|
+
const relevantCompositePatterns = compositePatterns.filter(p => p.languages.has(lang));
|
|
342
|
+
if (relevantCompositePatterns.length > 0) {
|
|
343
|
+
for (const call of calls) {
|
|
344
|
+
if (!call.compositeType) continue;
|
|
345
|
+
if (!call.isPotentialCallback && !call.isFunctionReference) continue;
|
|
346
|
+
|
|
347
|
+
for (const pattern of relevantCompositePatterns) {
|
|
348
|
+
if (pattern.typePattern.test(call.compositeType) &&
|
|
349
|
+
pattern.fieldPattern.test(call.fieldName)) {
|
|
350
|
+
if (!result.has(call.name)) {
|
|
351
|
+
result.set(call.name, {
|
|
352
|
+
framework: pattern.framework,
|
|
353
|
+
type: pattern.type,
|
|
354
|
+
patternId: pattern.id,
|
|
355
|
+
method: call.fieldName,
|
|
356
|
+
file: filePath,
|
|
357
|
+
line: call.line,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
287
363
|
}
|
|
288
364
|
}
|
|
289
365
|
}
|
|
@@ -308,12 +384,16 @@ function detectEntrypoints(index, options = {}) {
|
|
|
308
384
|
const results = [];
|
|
309
385
|
const seen = new Set(); // file:line:name dedup key
|
|
310
386
|
|
|
311
|
-
//
|
|
387
|
+
// Collect name-based patterns for efficient matching
|
|
388
|
+
const namePatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'namePattern');
|
|
389
|
+
|
|
390
|
+
// 1. Scan all symbols for decorator/modifier/name-based patterns
|
|
312
391
|
for (const [name, symbols] of index.symbols) {
|
|
313
392
|
for (const symbol of symbols) {
|
|
314
393
|
const fileEntry = index.files.get(symbol.file);
|
|
315
394
|
if (!fileEntry) continue;
|
|
316
395
|
|
|
396
|
+
// Check decorator/modifier-based patterns
|
|
317
397
|
const match = matchDecoratorOrModifier(symbol, fileEntry.language);
|
|
318
398
|
if (match) {
|
|
319
399
|
const key = `${symbol.file}:${symbol.startLine}:${name}`;
|
|
@@ -331,6 +411,30 @@ function detectEntrypoints(index, options = {}) {
|
|
|
331
411
|
evidence: [match.matchedOn],
|
|
332
412
|
confidence: 0.95,
|
|
333
413
|
});
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check name-based patterns (main, init, TestXxx, etc.)
|
|
418
|
+
for (const np of namePatterns) {
|
|
419
|
+
if (!np.languages.has(fileEntry.language)) continue;
|
|
420
|
+
if (np.pattern.test(name)) {
|
|
421
|
+
const key = `${symbol.file}:${symbol.startLine}:${name}`;
|
|
422
|
+
if (seen.has(key)) continue;
|
|
423
|
+
seen.add(key);
|
|
424
|
+
|
|
425
|
+
results.push({
|
|
426
|
+
name,
|
|
427
|
+
file: symbol.relativePath || symbol.file,
|
|
428
|
+
absoluteFile: symbol.file,
|
|
429
|
+
line: symbol.startLine,
|
|
430
|
+
type: np.type,
|
|
431
|
+
framework: np.framework,
|
|
432
|
+
patternId: np.id,
|
|
433
|
+
evidence: [`${name}() convention`],
|
|
434
|
+
confidence: 1.0,
|
|
435
|
+
});
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
334
438
|
}
|
|
335
439
|
}
|
|
336
440
|
}
|
package/core/execute.js
CHANGED
|
@@ -149,6 +149,23 @@ function checkFilePatternMatch(index, filePattern) {
|
|
|
149
149
|
for (const [, fileEntry] of index.files) {
|
|
150
150
|
if (fileEntry.relativePath.includes(filePattern)) return null;
|
|
151
151
|
}
|
|
152
|
+
// Suggest similar directories/files to help user refine
|
|
153
|
+
const patternLower = filePattern.toLowerCase();
|
|
154
|
+
const basename = filePattern.split('/').pop().toLowerCase();
|
|
155
|
+
const suggestions = new Set();
|
|
156
|
+
for (const [, fileEntry] of index.files) {
|
|
157
|
+
const rp = fileEntry.relativePath.toLowerCase();
|
|
158
|
+
// Check if any path component contains the last segment of the pattern
|
|
159
|
+
if (basename && rp.includes(basename)) {
|
|
160
|
+
// Extract the directory containing the match
|
|
161
|
+
const dir = fileEntry.relativePath.split('/').slice(0, -1).join('/');
|
|
162
|
+
if (dir) suggestions.add(dir);
|
|
163
|
+
if (suggestions.size >= 5) break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (suggestions.size > 0) {
|
|
167
|
+
return `No files matched pattern '${filePattern}'. Similar paths:\n${[...suggestions].map(s => ' ' + s).join('\n')}`;
|
|
168
|
+
}
|
|
152
169
|
return `No files matched pattern '${filePattern}'.`;
|
|
153
170
|
}
|
|
154
171
|
|
|
@@ -187,17 +204,16 @@ const HANDLERS = {
|
|
|
187
204
|
if (!result) {
|
|
188
205
|
// Give better error if file/className filter is the problem
|
|
189
206
|
if (p.file || p.className) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
includeMethods: p.includeMethods,
|
|
194
|
-
includeUncertain: p.includeUncertain || false,
|
|
195
|
-
exclude: toExcludeArray(p.exclude),
|
|
196
|
-
});
|
|
197
|
-
if (unfiltered && unfiltered.found !== false && unfiltered.symbol) {
|
|
198
|
-
const loc = `${unfiltered.symbol.file}:${unfiltered.symbol.startLine}`;
|
|
207
|
+
// Show ALL definitions so user can pick the right file= filter
|
|
208
|
+
const allDefs = index.symbols.get(p.name) || [];
|
|
209
|
+
if (allDefs.length > 0) {
|
|
199
210
|
const filterDesc = p.className ? `class "${p.className}"` : `file "${p.file}"`;
|
|
200
|
-
|
|
211
|
+
const locations = allDefs
|
|
212
|
+
.slice(0, 10)
|
|
213
|
+
.map(d => ` ${d.relativePath}:${d.startLine}${d.className ? ` (${d.className})` : ''}`)
|
|
214
|
+
.join('\n');
|
|
215
|
+
const more = allDefs.length > 10 ? `\n ... and ${allDefs.length - 10} more` : '';
|
|
216
|
+
return { ok: false, error: `Symbol "${p.name}" not found in ${filterDesc}. Found ${allDefs.length} definition(s) elsewhere:\n${locations}${more}\nUse file= with a path fragment from the list above to disambiguate.` };
|
|
201
217
|
}
|
|
202
218
|
}
|
|
203
219
|
return { ok: false, error: `Symbol "${p.name}" not found.` };
|
|
@@ -422,6 +438,7 @@ const HANDLERS = {
|
|
|
422
438
|
top: num(p.top, undefined),
|
|
423
439
|
file: p.file,
|
|
424
440
|
exclude: p.exclude,
|
|
441
|
+
in: p.in,
|
|
425
442
|
});
|
|
426
443
|
// Apply limit to detailed toc entries (symbols are in f.symbols.functions/classes arrays)
|
|
427
444
|
const limit = num(p.limit, undefined);
|
package/core/output.js
CHANGED
|
@@ -1084,7 +1084,7 @@ function formatReverseTraceJson(result) {
|
|
|
1084
1084
|
/**
|
|
1085
1085
|
* Format affected-tests command output - text
|
|
1086
1086
|
*/
|
|
1087
|
-
function formatAffectedTests(result) {
|
|
1087
|
+
function formatAffectedTests(result, options = {}) {
|
|
1088
1088
|
if (!result) return 'Function not found.';
|
|
1089
1089
|
|
|
1090
1090
|
const lines = [];
|
|
@@ -1099,9 +1099,12 @@ function formatAffectedTests(result) {
|
|
|
1099
1099
|
if (result.testFiles.length === 0) {
|
|
1100
1100
|
lines.push('No test files found for any affected function.');
|
|
1101
1101
|
} else {
|
|
1102
|
+
const MAX_TEST_FILES = options.all ? Infinity : 30;
|
|
1103
|
+
const displayFiles = result.testFiles.slice(0, MAX_TEST_FILES);
|
|
1104
|
+
const truncatedFiles = result.testFiles.length - displayFiles.length;
|
|
1102
1105
|
lines.push(`Test files to run (${summary.totalTestFiles}):`);
|
|
1103
1106
|
lines.push('');
|
|
1104
|
-
for (const tf of
|
|
1107
|
+
for (const tf of displayFiles) {
|
|
1105
1108
|
lines.push(` ${tf.file} (covers: ${tf.coveredFunctions.join(', ')})`);
|
|
1106
1109
|
// Show up to 5 key matches per file
|
|
1107
1110
|
const keyMatches = tf.matches
|
|
@@ -1111,6 +1114,9 @@ function formatAffectedTests(result) {
|
|
|
1111
1114
|
lines.push(` L${m.line}: ${m.content} [${m.matchType}]`);
|
|
1112
1115
|
}
|
|
1113
1116
|
}
|
|
1117
|
+
if (truncatedFiles > 0) {
|
|
1118
|
+
lines.push(`\n ... ${truncatedFiles} more test files (use file= and exclude= to narrow scope)`);
|
|
1119
|
+
}
|
|
1114
1120
|
}
|
|
1115
1121
|
|
|
1116
1122
|
if (result.uncovered.length > 0) {
|
|
@@ -2542,10 +2548,11 @@ function formatStats(stats, options = {}) {
|
|
|
2542
2548
|
// DIFF IMPACT
|
|
2543
2549
|
// ============================================================================
|
|
2544
2550
|
|
|
2545
|
-
function formatDiffImpact(result) {
|
|
2551
|
+
function formatDiffImpact(result, options = {}) {
|
|
2546
2552
|
if (!result) return 'No diff data.';
|
|
2547
2553
|
|
|
2548
2554
|
const lines = [];
|
|
2555
|
+
const MAX_CALLERS_PER_FN = options.all ? Infinity : 30;
|
|
2549
2556
|
|
|
2550
2557
|
lines.push(`Diff Impact Analysis (vs ${result.base})`);
|
|
2551
2558
|
lines.push('═'.repeat(60));
|
|
@@ -2574,12 +2581,17 @@ function formatDiffImpact(result) {
|
|
|
2574
2581
|
}
|
|
2575
2582
|
|
|
2576
2583
|
if (fn.callers.length > 0) {
|
|
2584
|
+
const displayCallers = fn.callers.slice(0, MAX_CALLERS_PER_FN);
|
|
2585
|
+
const truncated = fn.callers.length - displayCallers.length;
|
|
2577
2586
|
lines.push(` Callers (${fn.callers.length}):`);
|
|
2578
|
-
for (const c of
|
|
2587
|
+
for (const c of displayCallers) {
|
|
2579
2588
|
const caller = c.callerName ? `[${c.callerName}]` : '';
|
|
2580
2589
|
lines.push(` ${c.relativePath}:${c.line} ${caller}`);
|
|
2581
2590
|
lines.push(` ${c.content}`);
|
|
2582
2591
|
}
|
|
2592
|
+
if (truncated > 0) {
|
|
2593
|
+
lines.push(` ... ${truncated} more callers (use file= to scope diff to specific files, or use impact with class_name= for type-filtered results)`);
|
|
2594
|
+
}
|
|
2583
2595
|
} else {
|
|
2584
2596
|
lines.push(' Callers: none found');
|
|
2585
2597
|
}
|
|
@@ -3110,6 +3122,7 @@ function formatEntrypoints(results, options = {}) {
|
|
|
3110
3122
|
|
|
3111
3123
|
const typeLabels = {
|
|
3112
3124
|
http: 'HTTP Routes',
|
|
3125
|
+
cli: 'CLI Handlers',
|
|
3113
3126
|
di: 'Dependency Injection',
|
|
3114
3127
|
jobs: 'Job Schedulers',
|
|
3115
3128
|
test: 'Test Fixtures',
|
package/core/project.js
CHANGED
|
@@ -919,6 +919,10 @@ class ProjectIndex {
|
|
|
919
919
|
if (d.startLine && d.endLine && d.type === 'function') {
|
|
920
920
|
score += Math.min(d.endLine - d.startLine, 100);
|
|
921
921
|
}
|
|
922
|
+
// Prefer shallower paths (fewer directory levels = more central to project)
|
|
923
|
+
// Max bonus 50 for root-level files, decreasing with depth
|
|
924
|
+
const depth = (rp.match(/\//g) || []).length;
|
|
925
|
+
score += Math.max(0, 50 - depth * 10);
|
|
922
926
|
return { def: d, score };
|
|
923
927
|
});
|
|
924
928
|
|
|
@@ -969,10 +973,15 @@ class ProjectIndex {
|
|
|
969
973
|
// Build warnings
|
|
970
974
|
const warnings = [];
|
|
971
975
|
if (definitions.length > 1) {
|
|
976
|
+
const others = definitions.filter(d => d !== def);
|
|
977
|
+
const shown = others.slice(0, 5);
|
|
978
|
+
const extra = others.length - shown.length;
|
|
979
|
+
const alsoIn = shown.map(d => `${d.relativePath}:${d.startLine}`).join(', ');
|
|
980
|
+
const suffix = extra > 0 ? `, and ${extra} more` : '';
|
|
972
981
|
warnings.push({
|
|
973
982
|
type: 'ambiguous',
|
|
974
|
-
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Also in: ${
|
|
975
|
-
alternatives:
|
|
983
|
+
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Also in: ${alsoIn}${suffix}. Use file= to disambiguate.`,
|
|
984
|
+
alternatives: others.map(d => ({
|
|
976
985
|
file: d.relativePath,
|
|
977
986
|
line: d.startLine
|
|
978
987
|
}))
|
|
@@ -3255,11 +3264,29 @@ class ProjectIndex {
|
|
|
3255
3264
|
// like .close(), .get() etc. where many types have the same method.
|
|
3256
3265
|
if (def.className) {
|
|
3257
3266
|
const targetClassName = def.className;
|
|
3267
|
+
// Pre-compute how many types share this method name
|
|
3268
|
+
const _impMethodDefs = this.symbols.get(name);
|
|
3269
|
+
const _impClassNames = new Set();
|
|
3270
|
+
if (_impMethodDefs) {
|
|
3271
|
+
for (const d of _impMethodDefs) {
|
|
3272
|
+
if (d.className) _impClassNames.add(d.className);
|
|
3273
|
+
else if (d.receiver) _impClassNames.add(d.receiver.replace(/^\*/, ''));
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3258
3276
|
callerResults = callerResults.filter(c => {
|
|
3259
3277
|
// Keep non-method calls and self/this/cls calls (already resolved by findCallers)
|
|
3260
3278
|
if (!c.isMethod) return true;
|
|
3261
3279
|
const r = c.receiver;
|
|
3262
|
-
if (
|
|
3280
|
+
if (r && ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
3281
|
+
// Use receiverType from findCallers when available (Go/Java/Rust type inference)
|
|
3282
|
+
if (c.receiverType) {
|
|
3283
|
+
return c.receiverType === targetClassName;
|
|
3284
|
+
}
|
|
3285
|
+
// No receiver (chained/complex expression): only include if method is
|
|
3286
|
+
// unique or rare across types — otherwise too many false positives
|
|
3287
|
+
if (!r) {
|
|
3288
|
+
return _impClassNames.size <= 1;
|
|
3289
|
+
}
|
|
3263
3290
|
// Check if receiver matches the target class name (case-insensitive camelCase convention)
|
|
3264
3291
|
if (r.toLowerCase().includes(targetClassName.toLowerCase())) return true;
|
|
3265
3292
|
// Check if receiver is an instance of the target class using local variable type inference
|
|
@@ -3341,17 +3368,8 @@ class ProjectIndex {
|
|
|
3341
3368
|
}
|
|
3342
3369
|
// Unique method heuristic: if the called method exists on exactly one class/type
|
|
3343
3370
|
// and it matches the target, include the call (no other class could match)
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
const classNames = new Set();
|
|
3347
|
-
for (const d of methodDefs) {
|
|
3348
|
-
if (d.className) classNames.add(d.className);
|
|
3349
|
-
// Go/Rust: use receiver type as className equivalent
|
|
3350
|
-
else if (d.receiver) classNames.add(d.receiver.replace(/^\*/, ''));
|
|
3351
|
-
}
|
|
3352
|
-
if (classNames.size === 1 && classNames.has(targetClassName)) {
|
|
3353
|
-
return true;
|
|
3354
|
-
}
|
|
3371
|
+
if (_impClassNames.size === 1 && _impClassNames.has(targetClassName)) {
|
|
3372
|
+
return true;
|
|
3355
3373
|
}
|
|
3356
3374
|
// Type-scoped query but receiver type unknown — filter it out.
|
|
3357
3375
|
// Unknown receivers are likely unrelated.
|
|
@@ -4697,6 +4715,9 @@ class ProjectIndex {
|
|
|
4697
4715
|
if (options.exclude && options.exclude.length > 0) {
|
|
4698
4716
|
if (!this.matchesFilters(fileEntry.relativePath, { exclude: options.exclude })) continue;
|
|
4699
4717
|
}
|
|
4718
|
+
if (options.in) {
|
|
4719
|
+
if (!this.matchesFilters(fileEntry.relativePath, { in: options.in })) continue;
|
|
4720
|
+
}
|
|
4700
4721
|
let functions = fileEntry.symbols.filter(s =>
|
|
4701
4722
|
s.type === 'function' || s.type === 'method' || s.type === 'static' ||
|
|
4702
4723
|
s.type === 'constructor' || s.type === 'public' || s.type === 'abstract' ||
|
|
@@ -4789,6 +4810,7 @@ class ProjectIndex {
|
|
|
4789
4810
|
uncertain: 0,
|
|
4790
4811
|
projectLanguage: this._getPredominantLanguage(),
|
|
4791
4812
|
...(fileFilter && { filteredBy: options.file, matchedFiles: files.length }),
|
|
4813
|
+
...(options.in && { scopedTo: options.in }),
|
|
4792
4814
|
...(emptyFiles > 0 && fileFilter && { emptyFiles })
|
|
4793
4815
|
},
|
|
4794
4816
|
totals: {
|
|
@@ -5153,24 +5175,38 @@ class ProjectIndex {
|
|
|
5153
5175
|
const targetDef = targetDefs[0] || symbol;
|
|
5154
5176
|
if (targetDef.className && (lang === 'go' || lang === 'java' || lang === 'rust')) {
|
|
5155
5177
|
const targetClassName = targetDef.className;
|
|
5178
|
+
// Pre-compute how many types share this method name
|
|
5179
|
+
const methodDefs = this.symbols.get(symbol.name);
|
|
5180
|
+
const classNames = new Set();
|
|
5181
|
+
if (methodDefs) {
|
|
5182
|
+
for (const d of methodDefs) {
|
|
5183
|
+
if (d.className) classNames.add(d.className);
|
|
5184
|
+
else if (d.receiver) classNames.add(d.receiver.replace(/^\*/, ''));
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
const isWidelyShared = classNames.size > 3;
|
|
5156
5188
|
callers = callers.filter(c => {
|
|
5157
5189
|
if (!c.isMethod) return true;
|
|
5158
5190
|
const r = c.receiver;
|
|
5159
|
-
if (
|
|
5191
|
+
if (r && ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
5192
|
+
// No receiver (chained/complex expression): only include if method is
|
|
5193
|
+
// unique or rare across types — otherwise too many false positives
|
|
5194
|
+
if (!r) {
|
|
5195
|
+
return classNames.size <= 1;
|
|
5196
|
+
}
|
|
5160
5197
|
// Use receiverType from findCallers when available
|
|
5161
5198
|
if (c.receiverType) {
|
|
5162
5199
|
return c.receiverType === targetClassName ||
|
|
5163
5200
|
c.receiverType === targetDef.receiver?.replace(/^\*/, '');
|
|
5164
5201
|
}
|
|
5165
5202
|
// Unique method heuristic: if the method exists on exactly one class/type, include
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
if (classNames.size === 1 && classNames.has(targetClassName)) return true;
|
|
5203
|
+
if (classNames.size === 1 && classNames.has(targetClassName)) return true;
|
|
5204
|
+
// For widely shared method names (Get, Set, Run, etc.), require same-package
|
|
5205
|
+
// evidence when receiver type is unknown
|
|
5206
|
+
if (isWidelyShared) {
|
|
5207
|
+
const callerFile = c.file || '';
|
|
5208
|
+
const targetDir = path.dirname(change.filePath);
|
|
5209
|
+
return path.dirname(callerFile) === targetDir;
|
|
5174
5210
|
}
|
|
5175
5211
|
// Unknown receiver + multiple classes with this method → filter out
|
|
5176
5212
|
return false;
|
package/core/registry.js
CHANGED
package/languages/go.js
CHANGED
|
@@ -889,6 +889,137 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
889
889
|
}
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
// Detect function-value assignments:
|
|
893
|
+
// Pattern 1: sched.SchedulePod = sched.schedulePod (field = method reference)
|
|
894
|
+
// Pattern 2: sched.SchedulePod = schedulePod (field = function reference)
|
|
895
|
+
// Pattern 3: var handler = processEvent (short variable = function reference)
|
|
896
|
+
// The RHS is a function reference (not a call — no parentheses)
|
|
897
|
+
if (node.type === 'assignment_statement' || node.type === 'short_var_declaration') {
|
|
898
|
+
// Skip blank identifier assignments: _ = x (used to suppress unused warnings)
|
|
899
|
+
const left = node.childForFieldName('left');
|
|
900
|
+
const isBlankAssign = left && left.text.trim() === '_';
|
|
901
|
+
const right = isBlankAssign ? null : node.childForFieldName('right');
|
|
902
|
+
if (right) {
|
|
903
|
+
// Walk through the expression list (could be multiple assignments)
|
|
904
|
+
const rhsNodes = right.type === 'expression_list' ? right.namedChildren : [right];
|
|
905
|
+
for (const rhs of rhsNodes) {
|
|
906
|
+
// selector_expression: sched.schedulePod
|
|
907
|
+
if (rhs.type === 'selector_expression') {
|
|
908
|
+
const fieldNode = rhs.childForFieldName('field');
|
|
909
|
+
const operandNode = rhs.childForFieldName('operand');
|
|
910
|
+
if (fieldNode && operandNode) {
|
|
911
|
+
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
912
|
+
const receiverType = receiver ? getReceiverType(receiver) : undefined;
|
|
913
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
914
|
+
calls.push({
|
|
915
|
+
name: fieldNode.text,
|
|
916
|
+
line: rhs.startPosition.row + 1,
|
|
917
|
+
isMethod: true,
|
|
918
|
+
receiver,
|
|
919
|
+
...(receiverType && { receiverType }),
|
|
920
|
+
enclosingFunction,
|
|
921
|
+
isPotentialCallback: true,
|
|
922
|
+
uncertain: false
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// Plain identifier: schedulePod (standalone function reference)
|
|
927
|
+
if (rhs.type === 'identifier') {
|
|
928
|
+
const name = rhs.text;
|
|
929
|
+
if (!GO_SKIP_IDENTS.has(name) && !GO_BUILTINS.has(name) && !importAliases.has(name) && /^[a-zA-Z]/.test(name)) {
|
|
930
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
931
|
+
calls.push({
|
|
932
|
+
name,
|
|
933
|
+
line: rhs.startPosition.row + 1,
|
|
934
|
+
isMethod: false,
|
|
935
|
+
isFunctionReference: true,
|
|
936
|
+
isPotentialCallback: true,
|
|
937
|
+
enclosingFunction,
|
|
938
|
+
uncertain: false
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Detect function references in composite literal fields:
|
|
947
|
+
// ResourceEventHandlerFuncs{AddFunc: addNodeToCache, UpdateFunc: updateNode}
|
|
948
|
+
// keyed_element → literal_element(key) ":" literal_element(value)
|
|
949
|
+
// Go wraps values in literal_element nodes — unwrap to get the actual expression
|
|
950
|
+
if (node.type === 'keyed_element') {
|
|
951
|
+
// The value (second child, unwrap literal_element if present)
|
|
952
|
+
let valueNode = node.namedChildCount >= 2 ? node.namedChild(node.namedChildCount - 1) : null;
|
|
953
|
+
if (valueNode && valueNode.type === 'literal_element') {
|
|
954
|
+
valueNode = valueNode.namedChildCount > 0 ? valueNode.namedChild(0) : null;
|
|
955
|
+
}
|
|
956
|
+
if (valueNode) {
|
|
957
|
+
// Extract field name (the key) and parent composite literal type
|
|
958
|
+
let keyNode = node.namedChildCount >= 1 ? node.namedChild(0) : null;
|
|
959
|
+
if (keyNode && keyNode.type === 'literal_element') {
|
|
960
|
+
keyNode = keyNode.namedChildCount > 0 ? keyNode.namedChild(0) : null;
|
|
961
|
+
}
|
|
962
|
+
const fieldName = keyNode ? keyNode.text : undefined;
|
|
963
|
+
|
|
964
|
+
let compositeType;
|
|
965
|
+
let compositeLit = node.parent; // literal_value
|
|
966
|
+
if (compositeLit && compositeLit.type === 'literal_value') {
|
|
967
|
+
compositeLit = compositeLit.parent; // composite_literal
|
|
968
|
+
}
|
|
969
|
+
if (compositeLit && compositeLit.type === 'composite_literal') {
|
|
970
|
+
const typeNode = compositeLit.childForFieldName('type');
|
|
971
|
+
if (typeNode) {
|
|
972
|
+
if (typeNode.type === 'qualified_type') {
|
|
973
|
+
const pkg = typeNode.childForFieldName('package')?.text;
|
|
974
|
+
const typeName = typeNode.childForFieldName('name')?.text;
|
|
975
|
+
compositeType = pkg && typeName ? `${pkg}.${typeName}` : typeNode.text;
|
|
976
|
+
} else if (typeNode.type === 'type_identifier') {
|
|
977
|
+
compositeType = typeNode.text;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (valueNode.type === 'identifier') {
|
|
983
|
+
const name = valueNode.text;
|
|
984
|
+
if (!GO_SKIP_IDENTS.has(name) && !GO_BUILTINS.has(name) && !importAliases.has(name) && /^[a-zA-Z]/.test(name)) {
|
|
985
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
986
|
+
calls.push({
|
|
987
|
+
name,
|
|
988
|
+
line: valueNode.startPosition.row + 1,
|
|
989
|
+
isMethod: false,
|
|
990
|
+
isFunctionReference: true,
|
|
991
|
+
isPotentialCallback: true,
|
|
992
|
+
enclosingFunction,
|
|
993
|
+
uncertain: false,
|
|
994
|
+
...(compositeType && { compositeType }),
|
|
995
|
+
...(fieldName && { fieldName }),
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (valueNode.type === 'selector_expression') {
|
|
1000
|
+
const fieldNode = valueNode.childForFieldName('field');
|
|
1001
|
+
const operandNode = valueNode.childForFieldName('operand');
|
|
1002
|
+
if (fieldNode && operandNode) {
|
|
1003
|
+
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
1004
|
+
const receiverType = receiver ? getReceiverType(receiver) : undefined;
|
|
1005
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1006
|
+
calls.push({
|
|
1007
|
+
name: fieldNode.text,
|
|
1008
|
+
line: valueNode.startPosition.row + 1,
|
|
1009
|
+
isMethod: true,
|
|
1010
|
+
receiver,
|
|
1011
|
+
...(receiverType && { receiverType }),
|
|
1012
|
+
enclosingFunction,
|
|
1013
|
+
isPotentialCallback: true,
|
|
1014
|
+
uncertain: false,
|
|
1015
|
+
...(compositeType && { compositeType }),
|
|
1016
|
+
...(fieldName && { fieldName }),
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
892
1023
|
return true;
|
|
893
1024
|
}, {
|
|
894
1025
|
onLeave: (node) => {
|
package/mcp/server.js
CHANGED
|
@@ -114,16 +114,29 @@ const server = new McpServer({
|
|
|
114
114
|
// TOOL HELPERS
|
|
115
115
|
// ============================================================================
|
|
116
116
|
|
|
117
|
-
const
|
|
117
|
+
const DEFAULT_OUTPUT_CHARS = 30000; // ~7.5K tokens — safe default for AI context
|
|
118
|
+
const MAX_OUTPUT_CHARS = 100000; // hard ceiling even with max_chars override
|
|
118
119
|
|
|
119
|
-
function toolResult(text) {
|
|
120
|
+
function toolResult(text, command, maxChars) {
|
|
120
121
|
if (!text) return { content: [{ type: 'text', text: '(no output)' }] };
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
const limit = Math.min(maxChars || DEFAULT_OUTPUT_CHARS, MAX_OUTPUT_CHARS);
|
|
123
|
+
if (text.length > limit) {
|
|
124
|
+
const fullSize = text.length;
|
|
125
|
+
const fullTokens = Math.round(fullSize / 4);
|
|
126
|
+
const truncated = text.substring(0, limit);
|
|
123
127
|
// Cut at last newline to avoid breaking mid-line
|
|
124
128
|
const lastNewline = truncated.lastIndexOf('\n');
|
|
125
|
-
const cleanCut = lastNewline >
|
|
126
|
-
|
|
129
|
+
const cleanCut = lastNewline > limit * 0.8 ? truncated.substring(0, lastNewline) : truncated;
|
|
130
|
+
// Command-specific narrowing hints
|
|
131
|
+
let narrow = 'Use file=/in=/exclude= to narrow scope.';
|
|
132
|
+
if (command === 'toc') {
|
|
133
|
+
narrow = 'Use in= to scope to a subdirectory, or detailed=false for compact view.';
|
|
134
|
+
} else if (command === 'diff_impact') {
|
|
135
|
+
narrow = 'Use file= to scope to specific files/directories.';
|
|
136
|
+
} else if (command === 'affected_tests') {
|
|
137
|
+
narrow = 'Use file= to scope, exclude= to skip patterns.';
|
|
138
|
+
}
|
|
139
|
+
return { content: [{ type: 'text', text: cleanCut + `\n\n... OUTPUT TRUNCATED: showing ${limit} of ${fullSize} chars. Full output would be ~${fullTokens} tokens. ${narrow} Or use all=true to see everything (warning: ~${fullTokens} tokens).` }] };
|
|
127
140
|
}
|
|
128
141
|
return { content: [{ type: 'text', text }] };
|
|
129
142
|
}
|
|
@@ -272,6 +285,7 @@ server.registerTool(
|
|
|
272
285
|
class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
|
|
273
286
|
limit: z.number().optional().describe('Max results to return (default: 500). Caps find, usages, search, deadcode, api, toc --detailed.'),
|
|
274
287
|
max_files: z.number().optional().describe('Max files to index (default: 10000). Use for very large codebases.'),
|
|
288
|
+
max_chars: z.number().optional().describe('Max output chars before truncation (default: 30000 ~7.5K tokens, max: 100000 ~25K tokens). Use all=true to bypass all caps, or set this for fine-grained control. Truncation message shows full size.'),
|
|
275
289
|
// Structural search flags (search command)
|
|
276
290
|
type: z.string().optional().describe('Symbol type filter for structural search: function, class, call, method, type. Triggers index-based search.'),
|
|
277
291
|
param: z.string().optional().describe('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
|
|
@@ -291,6 +305,11 @@ server.registerTool(
|
|
|
291
305
|
// This eliminates per-case param selection and prevents CLI/MCP drift.
|
|
292
306
|
const { command: _c, project_dir: _p, ...rawParams } = args;
|
|
293
307
|
const ep = normalizeParams(rawParams);
|
|
308
|
+
// all=true bypasses both formatter caps AND char truncation (parity with CLI --all)
|
|
309
|
+
const maxChars = ep.all ? MAX_OUTPUT_CHARS : ep.maxChars;
|
|
310
|
+
|
|
311
|
+
// Wrap toolResult to auto-inject maxChars from this request
|
|
312
|
+
const tr = (text, cmd) => toolResult(text, cmd, maxChars);
|
|
294
313
|
|
|
295
314
|
try {
|
|
296
315
|
switch (command) {
|
|
@@ -304,8 +323,8 @@ server.registerTool(
|
|
|
304
323
|
case 'about': {
|
|
305
324
|
const index = getIndex(project_dir, ep);
|
|
306
325
|
const { ok, result, error } = execute(index, 'about', ep);
|
|
307
|
-
if (!ok) return
|
|
308
|
-
return
|
|
326
|
+
if (!ok) return tr(error); // soft error — won't kill sibling calls
|
|
327
|
+
return tr(output.formatAbout(result, {
|
|
309
328
|
allHint: 'Repeat with all=true to show all.',
|
|
310
329
|
methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.',
|
|
311
330
|
showConfidence: ep.showConfidence,
|
|
@@ -315,27 +334,27 @@ server.registerTool(
|
|
|
315
334
|
case 'context': {
|
|
316
335
|
const index = getIndex(project_dir, ep);
|
|
317
336
|
const { ok, result: ctx, error } = execute(index, 'context', ep);
|
|
318
|
-
if (!ok) return
|
|
337
|
+
if (!ok) return tr(error); // context uses soft error (not toolError)
|
|
319
338
|
const { text, expandable } = output.formatContext(ctx, {
|
|
320
339
|
expandHint: 'Use expand command with item number to see code for any item.',
|
|
321
340
|
showConfidence: ep.showConfidence,
|
|
322
341
|
});
|
|
323
342
|
expandCacheInstance.save(index.root, ep.name, ep.file, expandable);
|
|
324
|
-
return
|
|
343
|
+
return tr(text);
|
|
325
344
|
}
|
|
326
345
|
|
|
327
346
|
case 'impact': {
|
|
328
347
|
const index = getIndex(project_dir, ep);
|
|
329
348
|
const { ok, result, error } = execute(index, 'impact', ep);
|
|
330
|
-
if (!ok) return
|
|
331
|
-
return
|
|
349
|
+
if (!ok) return tr(error); // soft error
|
|
350
|
+
return tr(output.formatImpact(result));
|
|
332
351
|
}
|
|
333
352
|
|
|
334
353
|
case 'blast': {
|
|
335
354
|
const index = getIndex(project_dir, ep);
|
|
336
355
|
const { ok, result, error } = execute(index, 'blast', ep);
|
|
337
|
-
if (!ok) return
|
|
338
|
-
return
|
|
356
|
+
if (!ok) return tr(error); // soft error
|
|
357
|
+
return tr(output.formatBlast(result, {
|
|
339
358
|
allHint: 'Set depth to expand all children.',
|
|
340
359
|
}));
|
|
341
360
|
}
|
|
@@ -343,15 +362,15 @@ server.registerTool(
|
|
|
343
362
|
case 'smart': {
|
|
344
363
|
const index = getIndex(project_dir, ep);
|
|
345
364
|
const { ok, result, error } = execute(index, 'smart', ep);
|
|
346
|
-
if (!ok) return
|
|
347
|
-
return
|
|
365
|
+
if (!ok) return tr(error); // soft error
|
|
366
|
+
return tr(output.formatSmart(result));
|
|
348
367
|
}
|
|
349
368
|
|
|
350
369
|
case 'trace': {
|
|
351
370
|
const index = getIndex(project_dir, ep);
|
|
352
371
|
const { ok, result, error } = execute(index, 'trace', ep);
|
|
353
|
-
if (!ok) return
|
|
354
|
-
return
|
|
372
|
+
if (!ok) return tr(error); // soft error
|
|
373
|
+
return tr(output.formatTrace(result, {
|
|
355
374
|
allHint: 'Set depth to expand all children.',
|
|
356
375
|
methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
|
|
357
376
|
}));
|
|
@@ -360,8 +379,8 @@ server.registerTool(
|
|
|
360
379
|
case 'reverse_trace': {
|
|
361
380
|
const index = getIndex(project_dir, ep);
|
|
362
381
|
const { ok, result, error } = execute(index, 'reverseTrace', ep);
|
|
363
|
-
if (!ok) return
|
|
364
|
-
return
|
|
382
|
+
if (!ok) return tr(error);
|
|
383
|
+
return tr(output.formatReverseTrace(result, {
|
|
365
384
|
allHint: 'Set depth to expand all children.',
|
|
366
385
|
}));
|
|
367
386
|
}
|
|
@@ -369,17 +388,17 @@ server.registerTool(
|
|
|
369
388
|
case 'example': {
|
|
370
389
|
const index = getIndex(project_dir, ep);
|
|
371
390
|
const { ok, result, error } = execute(index, 'example', ep);
|
|
372
|
-
if (!ok) return
|
|
373
|
-
if (!result) return
|
|
374
|
-
return
|
|
391
|
+
if (!ok) return tr(error);
|
|
392
|
+
if (!result) return tr(`No usage examples found for "${ep.name}".`);
|
|
393
|
+
return tr(output.formatExample(result, ep.name));
|
|
375
394
|
}
|
|
376
395
|
|
|
377
396
|
case 'related': {
|
|
378
397
|
const index = getIndex(project_dir, ep);
|
|
379
398
|
const { ok, result, error } = execute(index, 'related', ep);
|
|
380
|
-
if (!ok) return
|
|
381
|
-
if (!result) return
|
|
382
|
-
return
|
|
399
|
+
if (!ok) return tr(error);
|
|
400
|
+
if (!result) return tr(`Symbol "${ep.name}" not found.`);
|
|
401
|
+
return tr(output.formatRelated(result, {
|
|
383
402
|
all: ep.all || false, top: ep.top,
|
|
384
403
|
allHint: 'Repeat with all=true to show all.'
|
|
385
404
|
}));
|
|
@@ -390,60 +409,60 @@ server.registerTool(
|
|
|
390
409
|
case 'find': {
|
|
391
410
|
const index = getIndex(project_dir, ep);
|
|
392
411
|
const { ok, result, error, note } = execute(index, 'find', ep);
|
|
393
|
-
if (!ok) return
|
|
412
|
+
if (!ok) return tr(error); // soft error
|
|
394
413
|
let text = output.formatFind(result, ep.name, ep.top);
|
|
395
414
|
if (note) text += '\n\n' + note;
|
|
396
|
-
return
|
|
415
|
+
return tr(text);
|
|
397
416
|
}
|
|
398
417
|
|
|
399
418
|
case 'usages': {
|
|
400
419
|
const index = getIndex(project_dir, ep);
|
|
401
420
|
const { ok, result, error, note } = execute(index, 'usages', ep);
|
|
402
|
-
if (!ok) return
|
|
421
|
+
if (!ok) return tr(error); // soft error
|
|
403
422
|
let text = output.formatUsages(result, ep.name);
|
|
404
423
|
if (note) text += '\n\n' + note;
|
|
405
|
-
return
|
|
424
|
+
return tr(text);
|
|
406
425
|
}
|
|
407
426
|
|
|
408
427
|
case 'toc': {
|
|
409
428
|
const index = getIndex(project_dir, ep);
|
|
410
429
|
const { ok, result, error, note } = execute(index, 'toc', ep);
|
|
411
|
-
if (!ok) return
|
|
430
|
+
if (!ok) return tr(error); // soft error
|
|
412
431
|
let text = output.formatToc(result, {
|
|
413
432
|
topHint: 'Set top=N or use detailed=false for compact view.'
|
|
414
433
|
});
|
|
415
434
|
if (note) text += '\n\n' + note;
|
|
416
|
-
return
|
|
435
|
+
return tr(text, 'toc');
|
|
417
436
|
}
|
|
418
437
|
|
|
419
438
|
case 'search': {
|
|
420
439
|
const index = getIndex(project_dir, ep);
|
|
421
440
|
const { ok, result, error, structural } = execute(index, 'search', ep);
|
|
422
|
-
if (!ok) return
|
|
441
|
+
if (!ok) return tr(error); // soft error
|
|
423
442
|
if (structural) {
|
|
424
|
-
return
|
|
443
|
+
return tr(output.formatStructuralSearch(result));
|
|
425
444
|
}
|
|
426
|
-
return
|
|
445
|
+
return tr(output.formatSearch(result, ep.term));
|
|
427
446
|
}
|
|
428
447
|
|
|
429
448
|
case 'tests': {
|
|
430
449
|
const index = getIndex(project_dir, ep);
|
|
431
450
|
const { ok, result, error } = execute(index, 'tests', ep);
|
|
432
|
-
if (!ok) return
|
|
433
|
-
return
|
|
451
|
+
if (!ok) return tr(error); // soft error
|
|
452
|
+
return tr(output.formatTests(result, ep.name));
|
|
434
453
|
}
|
|
435
454
|
|
|
436
455
|
case 'affected_tests': {
|
|
437
456
|
const index = getIndex(project_dir, ep);
|
|
438
457
|
const { ok, result, error } = execute(index, 'affectedTests', ep);
|
|
439
|
-
if (!ok) return
|
|
440
|
-
return
|
|
458
|
+
if (!ok) return tr(error);
|
|
459
|
+
return tr(output.formatAffectedTests(result, { all: ep.all }), 'affected_tests');
|
|
441
460
|
}
|
|
442
461
|
|
|
443
462
|
case 'deadcode': {
|
|
444
463
|
const index = getIndex(project_dir, ep);
|
|
445
464
|
const { ok, result, error, note } = execute(index, 'deadcode', ep);
|
|
446
|
-
if (!ok) return
|
|
465
|
+
if (!ok) return tr(error); // soft error
|
|
447
466
|
const dcNote = note;
|
|
448
467
|
let dcText = output.formatDeadcode(result, {
|
|
449
468
|
top: ep.top || 0,
|
|
@@ -451,14 +470,14 @@ server.registerTool(
|
|
|
451
470
|
exportedHint: !ep.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use include_exported=true to audit them.` : undefined
|
|
452
471
|
});
|
|
453
472
|
if (dcNote) dcText += '\n\n' + dcNote;
|
|
454
|
-
return
|
|
473
|
+
return tr(dcText);
|
|
455
474
|
}
|
|
456
475
|
|
|
457
476
|
case 'entrypoints': {
|
|
458
477
|
const index = getIndex(project_dir, ep);
|
|
459
478
|
const { ok, result, error } = execute(index, 'entrypoints', ep);
|
|
460
|
-
if (!ok) return
|
|
461
|
-
return
|
|
479
|
+
if (!ok) return tr(error);
|
|
480
|
+
return tr(output.formatEntrypoints(result));
|
|
462
481
|
}
|
|
463
482
|
|
|
464
483
|
// ── File Dependencies ───────────────────────────────────────
|
|
@@ -466,29 +485,29 @@ server.registerTool(
|
|
|
466
485
|
case 'imports': {
|
|
467
486
|
const index = getIndex(project_dir, ep);
|
|
468
487
|
const { ok, result, error } = execute(index, 'imports', ep);
|
|
469
|
-
if (!ok) return
|
|
470
|
-
return
|
|
488
|
+
if (!ok) return tr(error); // soft error
|
|
489
|
+
return tr(output.formatImports(result, ep.file));
|
|
471
490
|
}
|
|
472
491
|
|
|
473
492
|
case 'exporters': {
|
|
474
493
|
const index = getIndex(project_dir, ep);
|
|
475
494
|
const { ok, result, error } = execute(index, 'exporters', ep);
|
|
476
|
-
if (!ok) return
|
|
477
|
-
return
|
|
495
|
+
if (!ok) return tr(error); // soft error
|
|
496
|
+
return tr(output.formatExporters(result, ep.file));
|
|
478
497
|
}
|
|
479
498
|
|
|
480
499
|
case 'file_exports': {
|
|
481
500
|
const index = getIndex(project_dir, ep);
|
|
482
501
|
const { ok, result, error } = execute(index, 'fileExports', ep);
|
|
483
|
-
if (!ok) return
|
|
484
|
-
return
|
|
502
|
+
if (!ok) return tr(error); // soft error
|
|
503
|
+
return tr(output.formatFileExports(result, ep.file));
|
|
485
504
|
}
|
|
486
505
|
|
|
487
506
|
case 'graph': {
|
|
488
507
|
const index = getIndex(project_dir, ep);
|
|
489
508
|
const { ok, result, error } = execute(index, 'graph', ep);
|
|
490
|
-
if (!ok) return
|
|
491
|
-
return
|
|
509
|
+
if (!ok) return tr(error); // soft error
|
|
510
|
+
return tr(output.formatGraph(result, {
|
|
492
511
|
showAll: ep.all || ep.depth !== undefined,
|
|
493
512
|
maxDepth: ep.depth ?? 2, file: ep.file,
|
|
494
513
|
depthHint: 'Set depth parameter for deeper graph.',
|
|
@@ -499,8 +518,8 @@ server.registerTool(
|
|
|
499
518
|
case 'circular_deps': {
|
|
500
519
|
const index = getIndex(project_dir, ep);
|
|
501
520
|
const { ok, result, error } = execute(index, 'circularDeps', ep);
|
|
502
|
-
if (!ok) return
|
|
503
|
-
return
|
|
521
|
+
if (!ok) return tr(error);
|
|
522
|
+
return tr(output.formatCircularDeps(result));
|
|
504
523
|
}
|
|
505
524
|
|
|
506
525
|
// ── Refactoring ─────────────────────────────────────────────
|
|
@@ -508,22 +527,22 @@ server.registerTool(
|
|
|
508
527
|
case 'verify': {
|
|
509
528
|
const index = getIndex(project_dir, ep);
|
|
510
529
|
const { ok, result, error } = execute(index, 'verify', ep);
|
|
511
|
-
if (!ok) return
|
|
512
|
-
return
|
|
530
|
+
if (!ok) return tr(error); // soft error
|
|
531
|
+
return tr(output.formatVerify(result));
|
|
513
532
|
}
|
|
514
533
|
|
|
515
534
|
case 'plan': {
|
|
516
535
|
const index = getIndex(project_dir, ep);
|
|
517
536
|
const { ok, result, error } = execute(index, 'plan', ep);
|
|
518
|
-
if (!ok) return
|
|
519
|
-
return
|
|
537
|
+
if (!ok) return tr(error); // soft error
|
|
538
|
+
return tr(output.formatPlan(result));
|
|
520
539
|
}
|
|
521
540
|
|
|
522
541
|
case 'diff_impact': {
|
|
523
542
|
const index = getIndex(project_dir, ep);
|
|
524
543
|
const { ok, result, error } = execute(index, 'diffImpact', ep);
|
|
525
|
-
if (!ok) return
|
|
526
|
-
return
|
|
544
|
+
if (!ok) return tr(error); // soft error — e.g. "not a git repo"
|
|
545
|
+
return tr(output.formatDiffImpact(result, { all: ep.all }), 'diff_impact');
|
|
527
546
|
}
|
|
528
547
|
|
|
529
548
|
// ── Other ───────────────────────────────────────────────────
|
|
@@ -531,31 +550,31 @@ server.registerTool(
|
|
|
531
550
|
case 'typedef': {
|
|
532
551
|
const index = getIndex(project_dir, ep);
|
|
533
552
|
const { ok, result, error } = execute(index, 'typedef', ep);
|
|
534
|
-
if (!ok) return
|
|
535
|
-
return
|
|
553
|
+
if (!ok) return tr(error); // soft error
|
|
554
|
+
return tr(output.formatTypedef(result, ep.name));
|
|
536
555
|
}
|
|
537
556
|
|
|
538
557
|
case 'stacktrace': {
|
|
539
558
|
const index = getIndex(project_dir, ep);
|
|
540
559
|
const { ok, result, error } = execute(index, 'stacktrace', ep);
|
|
541
|
-
if (!ok) return
|
|
542
|
-
return
|
|
560
|
+
if (!ok) return tr(error); // soft error
|
|
561
|
+
return tr(output.formatStackTrace(result));
|
|
543
562
|
}
|
|
544
563
|
|
|
545
564
|
case 'api': {
|
|
546
565
|
const index = getIndex(project_dir, ep);
|
|
547
566
|
const { ok, result, error, note } = execute(index, 'api', ep);
|
|
548
|
-
if (!ok) return
|
|
567
|
+
if (!ok) return tr(error); // soft error
|
|
549
568
|
let apiText = output.formatApi(result, ep.file || '.');
|
|
550
569
|
if (note) apiText += '\n\n' + note;
|
|
551
|
-
return
|
|
570
|
+
return tr(apiText);
|
|
552
571
|
}
|
|
553
572
|
|
|
554
573
|
case 'stats': {
|
|
555
574
|
const index = getIndex(project_dir, ep);
|
|
556
575
|
const { ok, result, error } = execute(index, 'stats', ep);
|
|
557
|
-
if (!ok) return
|
|
558
|
-
return
|
|
576
|
+
if (!ok) return tr(error); // soft error
|
|
577
|
+
return tr(output.formatStats(result, { top: ep.top || 0 }));
|
|
559
578
|
}
|
|
560
579
|
|
|
561
580
|
// ── Extracting Code (via execute) ────────────────────────────
|
|
@@ -565,14 +584,14 @@ server.registerTool(
|
|
|
565
584
|
if (err) return err;
|
|
566
585
|
const index = getIndex(project_dir, ep);
|
|
567
586
|
const { ok, result, error } = execute(index, 'fn', ep);
|
|
568
|
-
if (!ok) return
|
|
587
|
+
if (!ok) return tr(error); // soft error
|
|
569
588
|
// MCP path security: validate all result files are within project root
|
|
570
589
|
for (const entry of result.entries) {
|
|
571
590
|
const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
|
|
572
591
|
if (typeof check !== 'string') return check;
|
|
573
592
|
}
|
|
574
593
|
const notes = result.notes.length ? result.notes.map(n => 'Note: ' + n).join('\n') + '\n\n' : '';
|
|
575
|
-
return
|
|
594
|
+
return tr(notes + output.formatFnResult(result));
|
|
576
595
|
}
|
|
577
596
|
|
|
578
597
|
case 'class': {
|
|
@@ -583,24 +602,24 @@ server.registerTool(
|
|
|
583
602
|
}
|
|
584
603
|
const index = getIndex(project_dir, ep);
|
|
585
604
|
const { ok, result, error } = execute(index, 'class', ep);
|
|
586
|
-
if (!ok) return
|
|
605
|
+
if (!ok) return tr(error); // soft error (class not found)
|
|
587
606
|
// MCP path security: validate all result files are within project root
|
|
588
607
|
for (const entry of result.entries) {
|
|
589
608
|
const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
|
|
590
609
|
if (typeof check !== 'string') return check;
|
|
591
610
|
}
|
|
592
611
|
const notes = result.notes.length ? result.notes.map(n => 'Note: ' + n).join('\n') + '\n\n' : '';
|
|
593
|
-
return
|
|
612
|
+
return tr(notes + output.formatClassResult(result));
|
|
594
613
|
}
|
|
595
614
|
|
|
596
615
|
case 'lines': {
|
|
597
616
|
const index = getIndex(project_dir, ep);
|
|
598
617
|
const { ok, result, error } = execute(index, 'lines', ep);
|
|
599
|
-
if (!ok) return
|
|
618
|
+
if (!ok) return tr(error); // soft error
|
|
600
619
|
// MCP path security: validate file is within project root
|
|
601
620
|
const check = resolveAndValidatePath(index, result.relativePath);
|
|
602
621
|
if (typeof check !== 'string') return check;
|
|
603
|
-
return
|
|
622
|
+
return tr(output.formatLines(result));
|
|
604
623
|
}
|
|
605
624
|
|
|
606
625
|
case 'expand': {
|
|
@@ -614,8 +633,8 @@ server.registerTool(
|
|
|
614
633
|
itemCount: lookup.itemCount, symbolName: lookup.symbolName,
|
|
615
634
|
validateRoot: true
|
|
616
635
|
});
|
|
617
|
-
if (!ok) return
|
|
618
|
-
return
|
|
636
|
+
if (!ok) return tr(error); // soft error
|
|
637
|
+
return tr(result.text);
|
|
619
638
|
}
|
|
620
639
|
|
|
621
640
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.8",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
|
|
6
6
|
"main": "index.js",
|