ucn 3.8.6 → 3.8.7

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 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
 
@@ -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 call-pattern: cobra.Command composite literals with function value fields
213
+ {
214
+ id: 'cobra-command',
215
+ languages: new Set(['go']),
216
+ type: 'cli',
217
+ framework: 'cobra',
218
+ detection: 'callPattern',
219
+ receiverPattern: /^cobra$/i,
220
+ methodPattern: /^Command$/,
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
@@ -308,12 +357,16 @@ function detectEntrypoints(index, options = {}) {
308
357
  const results = [];
309
358
  const seen = new Set(); // file:line:name dedup key
310
359
 
311
- // 1. Scan all symbols for decorator/modifier-based patterns
360
+ // Collect name-based patterns for efficient matching
361
+ const namePatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'namePattern');
362
+
363
+ // 1. Scan all symbols for decorator/modifier/name-based patterns
312
364
  for (const [name, symbols] of index.symbols) {
313
365
  for (const symbol of symbols) {
314
366
  const fileEntry = index.files.get(symbol.file);
315
367
  if (!fileEntry) continue;
316
368
 
369
+ // Check decorator/modifier-based patterns
317
370
  const match = matchDecoratorOrModifier(symbol, fileEntry.language);
318
371
  if (match) {
319
372
  const key = `${symbol.file}:${symbol.startLine}:${name}`;
@@ -331,6 +384,30 @@ function detectEntrypoints(index, options = {}) {
331
384
  evidence: [match.matchedOn],
332
385
  confidence: 0.95,
333
386
  });
387
+ continue;
388
+ }
389
+
390
+ // Check name-based patterns (main, init, TestXxx, etc.)
391
+ for (const np of namePatterns) {
392
+ if (!np.languages.has(fileEntry.language)) continue;
393
+ if (np.pattern.test(name)) {
394
+ const key = `${symbol.file}:${symbol.startLine}:${name}`;
395
+ if (seen.has(key)) continue;
396
+ seen.add(key);
397
+
398
+ results.push({
399
+ name,
400
+ file: symbol.relativePath || symbol.file,
401
+ absoluteFile: symbol.file,
402
+ line: symbol.startLine,
403
+ type: np.type,
404
+ framework: np.framework,
405
+ patternId: np.id,
406
+ evidence: [`${name}() convention`],
407
+ confidence: 1.0,
408
+ });
409
+ break;
410
+ }
334
411
  }
335
412
  }
336
413
  }
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
- const unfiltered = index.about(p.name, {
191
- withTypes: p.withTypes || false,
192
- all: false,
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
- return { ok: false, error: `Symbol "${p.name}" not found in ${filterDesc}. Found in: ${loc}. Use the correct --file or Class.method syntax.` };
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 result.testFiles) {
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 fn.callers) {
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
  }
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: ${definitions.filter(d => d !== def).map(d => `${d.relativePath}:${d.startLine}`).join(', ')}. Specify a file to disambiguate.`,
975
- alternatives: definitions.filter(d => d !== def).map(d => ({
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 (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
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
- const methodDefs = this.symbols.get(name);
3345
- if (methodDefs) {
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 (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
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
- const methodDefs = this.symbols.get(symbol.name);
5167
- if (methodDefs) {
5168
- const classNames = new Set();
5169
- for (const d of methodDefs) {
5170
- if (d.className) classNames.add(d.className);
5171
- else if (d.receiver) classNames.add(d.receiver.replace(/^\*/, ''));
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
@@ -83,6 +83,7 @@ const PARAM_MAP = {
83
83
  default_value: 'defaultValue',
84
84
  top_level: 'topLevel',
85
85
  max_files: 'maxFiles',
86
+ max_chars: 'maxChars',
86
87
  };
87
88
 
88
89
  // ============================================================================
package/languages/go.js CHANGED
@@ -889,6 +889,108 @@ 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
+ if (valueNode.type === 'identifier') {
958
+ const name = valueNode.text;
959
+ if (!GO_SKIP_IDENTS.has(name) && !GO_BUILTINS.has(name) && !importAliases.has(name) && /^[a-zA-Z]/.test(name)) {
960
+ const enclosingFunction = getCurrentEnclosingFunction();
961
+ calls.push({
962
+ name,
963
+ line: valueNode.startPosition.row + 1,
964
+ isMethod: false,
965
+ isFunctionReference: true,
966
+ isPotentialCallback: true,
967
+ enclosingFunction,
968
+ uncertain: false
969
+ });
970
+ }
971
+ }
972
+ if (valueNode.type === 'selector_expression') {
973
+ const fieldNode = valueNode.childForFieldName('field');
974
+ const operandNode = valueNode.childForFieldName('operand');
975
+ if (fieldNode && operandNode) {
976
+ const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
977
+ const receiverType = receiver ? getReceiverType(receiver) : undefined;
978
+ const enclosingFunction = getCurrentEnclosingFunction();
979
+ calls.push({
980
+ name: fieldNode.text,
981
+ line: valueNode.startPosition.row + 1,
982
+ isMethod: true,
983
+ receiver,
984
+ ...(receiverType && { receiverType }),
985
+ enclosingFunction,
986
+ isPotentialCallback: true,
987
+ uncertain: false
988
+ });
989
+ }
990
+ }
991
+ }
992
+ }
993
+
892
994
  return true;
893
995
  }, {
894
996
  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 MAX_OUTPUT_CHARS = 100000; // ~100KB, safe for all MCP clients
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
- if (text.length > MAX_OUTPUT_CHARS) {
122
- const truncated = text.substring(0, MAX_OUTPUT_CHARS);
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 > MAX_OUTPUT_CHARS * 0.8 ? truncated.substring(0, lastNewline) : truncated;
126
- return { content: [{ type: 'text', text: cleanCut + '\n\n... (output truncated — refine query or use file/in/exclude parameters to narrow scope)' }] };
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 toolResult(error); // soft error — won't kill sibling calls
308
- return toolResult(output.formatAbout(result, {
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 toolResult(error); // context uses soft error (not toolError)
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 toolResult(text);
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 toolResult(error); // soft error
331
- return toolResult(output.formatImpact(result));
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 toolResult(error); // soft error
338
- return toolResult(output.formatBlast(result, {
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 toolResult(error); // soft error
347
- return toolResult(output.formatSmart(result));
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 toolResult(error); // soft error
354
- return toolResult(output.formatTrace(result, {
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 toolResult(error);
364
- return toolResult(output.formatReverseTrace(result, {
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 toolResult(error);
373
- if (!result) return toolResult(`No usage examples found for "${ep.name}".`);
374
- return toolResult(output.formatExample(result, ep.name));
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 toolResult(error);
381
- if (!result) return toolResult(`Symbol "${ep.name}" not found.`);
382
- return toolResult(output.formatRelated(result, {
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 toolResult(error); // soft error
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 toolResult(text);
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 toolResult(error); // soft error
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 toolResult(text);
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 toolResult(error); // soft error
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 toolResult(text);
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 toolResult(error); // soft error
441
+ if (!ok) return tr(error); // soft error
423
442
  if (structural) {
424
- return toolResult(output.formatStructuralSearch(result));
443
+ return tr(output.formatStructuralSearch(result));
425
444
  }
426
- return toolResult(output.formatSearch(result, ep.term));
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 toolResult(error); // soft error
433
- return toolResult(output.formatTests(result, ep.name));
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 toolResult(error);
440
- return toolResult(output.formatAffectedTests(result));
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 toolResult(error); // soft error
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 toolResult(dcText);
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 toolResult(error);
461
- return toolResult(output.formatEntrypoints(result));
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 toolResult(error); // soft error
470
- return toolResult(output.formatImports(result, ep.file));
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 toolResult(error); // soft error
477
- return toolResult(output.formatExporters(result, ep.file));
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 toolResult(error); // soft error
484
- return toolResult(output.formatFileExports(result, ep.file));
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 toolResult(error); // soft error
491
- return toolResult(output.formatGraph(result, {
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 toolResult(error);
503
- return toolResult(output.formatCircularDeps(result));
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 toolResult(error); // soft error
512
- return toolResult(output.formatVerify(result));
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 toolResult(error); // soft error
519
- return toolResult(output.formatPlan(result));
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 toolResult(error); // soft error — e.g. "not a git repo"
526
- return toolResult(output.formatDiffImpact(result));
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 toolResult(error); // soft error
535
- return toolResult(output.formatTypedef(result, ep.name));
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 toolResult(error); // soft error
542
- return toolResult(output.formatStackTrace(result));
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 toolResult(error); // soft error
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 toolResult(apiText);
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 toolResult(error); // soft error
558
- return toolResult(output.formatStats(result, { top: ep.top || 0 }));
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 toolResult(error); // soft error
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 toolResult(notes + output.formatFnResult(result));
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 toolResult(error); // soft error (class not found)
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 toolResult(notes + output.formatClassResult(result));
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 toolResult(error); // soft error
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 toolResult(output.formatLines(result));
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 toolResult(error); // soft error
618
- return toolResult(result.text);
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.6",
3
+ "version": "3.8.7",
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",