ucn 3.7.29 → 3.7.31

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
@@ -306,8 +306,9 @@ function runFileCommand(filePath, command, arg) {
306
306
  api: { file: relativePath },
307
307
  };
308
308
 
309
- const { ok, result, error } = execute(index, canonical, paramsByCommand[canonical]);
309
+ const { ok, result, error, note } = execute(index, canonical, paramsByCommand[canonical]);
310
310
  if (!ok) fail(error);
311
+ if (note) console.error(note);
311
312
 
312
313
  // Format output using same formatters as project mode
313
314
  switch (canonical) {
@@ -416,8 +417,9 @@ function runProjectCommand(rootDir, command, arg) {
416
417
  }
417
418
 
418
419
  case 'find': {
419
- const { ok, result, error } = execute(index, 'find', { name: arg, ...flags });
420
+ const { ok, result, error, note } = execute(index, 'find', { name: arg, ...flags });
420
421
  if (!ok) fail(error);
422
+ if (note) console.error(note);
421
423
  printOutput(result,
422
424
  r => output.formatSymbolJson(r, arg),
423
425
  r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
@@ -1257,8 +1259,9 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1257
1259
  }
1258
1260
 
1259
1261
  case 'find': {
1260
- const { ok, result, error } = execute(index, 'find', { name: arg, ...iflags });
1262
+ const { ok, result, error, note } = execute(index, 'find', { name: arg, ...iflags });
1261
1263
  if (!ok) { console.log(error); return; }
1264
+ if (note) console.log(note);
1262
1265
  console.log(output.formatFindDetailed(result, arg, { depth: iflags.depth, top: iflags.top, all: iflags.all }));
1263
1266
  break;
1264
1267
  }
package/core/callers.js CHANGED
@@ -719,11 +719,20 @@ function findCallees(index, def, options = {}) {
719
719
  const sameDir = symbols.find(s => path.dirname(s.file) === defDir);
720
720
  if (sameDir) {
721
721
  callee = sameDir;
722
- } else if (defReceiver) {
723
- // Priority 3: Same receiver type (for methods)
724
- const sameReceiver = symbols.find(s => s.receiver === defReceiver);
725
- if (sameReceiver) {
726
- callee = sameReceiver;
722
+ } else {
723
+ // Priority 2.5: Imported file check if the caller's file imports
724
+ // from any of the candidate callee files
725
+ const callerImports = fileEntry?.imports || [];
726
+ const importedFiles = new Set(callerImports.map(imp => imp.resolvedPath).filter(Boolean));
727
+ const importedCallee = symbols.find(s => importedFiles.has(s.file));
728
+ if (importedCallee) {
729
+ callee = importedCallee;
730
+ } else if (defReceiver) {
731
+ // Priority 3: Same receiver type (for methods)
732
+ const sameReceiver = symbols.find(s => s.receiver === defReceiver);
733
+ if (sameReceiver) {
734
+ callee = sameReceiver;
735
+ }
727
736
  }
728
737
  }
729
738
  }
@@ -749,6 +758,33 @@ function findCallees(index, def, options = {}) {
749
758
  if (nonTest) callee = nonTest;
750
759
  }
751
760
  }
761
+ // Priority 6: Usage-based tiebreaker for cross-language/cross-directory ambiguity
762
+ // Matches resolveSymbol() scoring logic in project.js
763
+ if (!bindingId && callee === symbols[0] && symbols.length > 1) {
764
+ const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
765
+ const scored = symbols.map(s => {
766
+ let score = 0;
767
+ const fe = index.files.get(s.file);
768
+ const rp = fe ? fe.relativePath : (s.relativePath || '');
769
+ if (typeOrder.has(s.type)) score += 1000;
770
+ if (isTestFile(rp, detectLanguage(s.file))) score -= 500;
771
+ if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
772
+ if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
773
+ return { symbol: s, score };
774
+ });
775
+ scored.sort((a, b) => b.score - a.score);
776
+ if (scored.length > 1 && scored[0].score === scored[1].score) {
777
+ const tiedScore = scored[0].score;
778
+ const tiedCandidates = scored.filter(s => s.score === tiedScore);
779
+ for (const c of tiedCandidates) {
780
+ c.usageCount = index.countSymbolUsages(c.symbol).total;
781
+ }
782
+ tiedCandidates.sort((a, b) => b.usageCount - a.usageCount);
783
+ callee = tiedCandidates[0].symbol;
784
+ } else {
785
+ callee = scored[0].symbol;
786
+ }
787
+ }
752
788
  }
753
789
 
754
790
  // Skip test-file callees when caller is production code and
package/core/execute.js CHANGED
@@ -41,6 +41,38 @@ function requireTerm(term) {
41
41
  return null;
42
42
  }
43
43
 
44
+ /**
45
+ * Split Class.method syntax into className and methodName.
46
+ * Returns { className, methodName } or null if not applicable.
47
+ * Handles: "Class.method" → { className: "Class", methodName: "method" }
48
+ * Skips: ".method", "a.b.c" (multi-dot), names without dots
49
+ */
50
+ function splitClassMethod(name) {
51
+ if (!name || typeof name !== 'string') return null;
52
+ const dotIndex = name.indexOf('.');
53
+ if (dotIndex <= 0 || dotIndex === name.length - 1) return null;
54
+ // Only split on first dot, and only if there's exactly one dot
55
+ if (name.indexOf('.', dotIndex + 1) !== -1) return null;
56
+ return {
57
+ className: name.substring(0, dotIndex),
58
+ methodName: name.substring(dotIndex + 1)
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Apply Class.method syntax to params object.
64
+ * If name contains ".", splits it and sets p.name and p.className.
65
+ * Only applies if p.className is not already set.
66
+ */
67
+ function applyClassMethodSyntax(p) {
68
+ if (p.className) return; // already set explicitly
69
+ const split = splitClassMethod(p.name);
70
+ if (split) {
71
+ p.name = split.methodName;
72
+ p.className = split.className;
73
+ }
74
+ }
75
+
44
76
  /** Normalize exclude to an array (accepts string CSV, array, or falsy). */
45
77
  function toExcludeArray(exclude) {
46
78
  if (!exclude) return [];
@@ -93,9 +125,11 @@ const HANDLERS = {
93
125
  about: (index, p) => {
94
126
  const err = requireName(p.name);
95
127
  if (err) return { ok: false, error: err };
128
+ applyClassMethodSyntax(p);
96
129
  const result = index.about(p.name, {
97
130
  withTypes: p.withTypes || false,
98
131
  file: p.file,
132
+ className: p.className,
99
133
  all: p.all,
100
134
  includeMethods: p.includeMethods,
101
135
  includeUncertain: p.includeUncertain || false,
@@ -103,17 +137,36 @@ const HANDLERS = {
103
137
  maxCallers: num(p.top, undefined),
104
138
  maxCallees: num(p.top, undefined),
105
139
  });
106
- if (!result) return { ok: false, error: `Symbol "${p.name}" not found.` };
140
+ if (!result) {
141
+ // Give better error if file/className filter is the problem
142
+ if (p.file || p.className) {
143
+ const unfiltered = index.about(p.name, {
144
+ withTypes: p.withTypes || false,
145
+ all: false,
146
+ includeMethods: p.includeMethods,
147
+ includeUncertain: p.includeUncertain || false,
148
+ exclude: toExcludeArray(p.exclude),
149
+ });
150
+ if (unfiltered && unfiltered.found !== false && unfiltered.symbol) {
151
+ const loc = `${unfiltered.symbol.file}:${unfiltered.symbol.startLine}`;
152
+ const filterDesc = p.className ? `class "${p.className}"` : `file "${p.file}"`;
153
+ return { ok: false, error: `Symbol "${p.name}" not found in ${filterDesc}. Found in: ${loc}. Use the correct --file or Class.method syntax.` };
154
+ }
155
+ }
156
+ return { ok: false, error: `Symbol "${p.name}" not found.` };
157
+ }
107
158
  return { ok: true, result };
108
159
  },
109
160
 
110
161
  context: (index, p) => {
111
162
  const err = requireName(p.name);
112
163
  if (err) return { ok: false, error: err };
164
+ applyClassMethodSyntax(p);
113
165
  const result = index.context(p.name, {
114
166
  includeMethods: p.includeMethods,
115
167
  includeUncertain: p.includeUncertain || false,
116
168
  file: p.file,
169
+ className: p.className,
117
170
  exclude: toExcludeArray(p.exclude),
118
171
  });
119
172
  if (!result) return { ok: false, error: `Symbol "${p.name}" not found.` };
@@ -123,9 +176,12 @@ const HANDLERS = {
123
176
  impact: (index, p) => {
124
177
  const err = requireName(p.name);
125
178
  if (err) return { ok: false, error: err };
179
+ applyClassMethodSyntax(p);
126
180
  const result = index.impact(p.name, {
127
181
  file: p.file,
182
+ className: p.className,
128
183
  exclude: toExcludeArray(p.exclude),
184
+ top: num(p.top, undefined),
129
185
  });
130
186
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
131
187
  return { ok: true, result };
@@ -134,8 +190,10 @@ const HANDLERS = {
134
190
  smart: (index, p) => {
135
191
  const err = requireName(p.name);
136
192
  if (err) return { ok: false, error: err };
193
+ applyClassMethodSyntax(p);
137
194
  const result = index.smart(p.name, {
138
195
  file: p.file,
196
+ className: p.className,
139
197
  withTypes: p.withTypes || false,
140
198
  includeMethods: p.includeMethods,
141
199
  includeUncertain: p.includeUncertain || false,
@@ -147,10 +205,12 @@ const HANDLERS = {
147
205
  trace: (index, p) => {
148
206
  const err = requireName(p.name);
149
207
  if (err) return { ok: false, error: err };
208
+ applyClassMethodSyntax(p);
150
209
  const depthVal = num(p.depth, undefined);
151
210
  const result = index.trace(p.name, {
152
211
  depth: depthVal ?? 3,
153
212
  file: p.file,
213
+ className: p.className,
154
214
  all: p.all || depthVal !== undefined,
155
215
  includeMethods: p.includeMethods,
156
216
  includeUncertain: p.includeUncertain || false,
@@ -162,7 +222,8 @@ const HANDLERS = {
162
222
  example: (index, p) => {
163
223
  const err = requireName(p.name);
164
224
  if (err) return { ok: false, error: err };
165
- const result = index.example(p.name);
225
+ applyClassMethodSyntax(p);
226
+ const result = index.example(p.name, { file: p.file, className: p.className });
166
227
  if (!result) return { ok: false, error: `No examples found for "${p.name}".` };
167
228
  return { ok: true, result };
168
229
  },
@@ -170,8 +231,10 @@ const HANDLERS = {
170
231
  related: (index, p) => {
171
232
  const err = requireName(p.name);
172
233
  if (err) return { ok: false, error: err };
234
+ applyClassMethodSyntax(p);
173
235
  const result = index.related(p.name, {
174
236
  file: p.file,
237
+ className: p.className,
175
238
  top: num(p.top, undefined),
176
239
  all: p.all,
177
240
  });
@@ -184,28 +247,38 @@ const HANDLERS = {
184
247
  find: (index, p) => {
185
248
  const err = requireName(p.name);
186
249
  if (err) return { ok: false, error: err };
250
+ applyClassMethodSyntax(p);
187
251
  // Auto-include tests when pattern clearly targets test functions
252
+ // But only if the user didn't explicitly set include_tests=false
188
253
  let includeTests = p.includeTests;
189
- if (!includeTests && p.name && /^test[_*?]/i.test(p.name)) {
254
+ if (includeTests === undefined && p.name && /^test[_*?]/i.test(p.name)) {
190
255
  includeTests = true;
191
256
  }
192
257
  const exclude = applyTestExclusions(p.exclude, includeTests);
193
258
  const result = index.find(p.name, {
194
259
  file: p.file,
260
+ className: p.className,
195
261
  exact: p.exact || false,
196
262
  exclude,
197
263
  in: p.in,
198
264
  });
199
- return { ok: true, result };
265
+ // Warn if exact mode silently disables glob expansion
266
+ let note;
267
+ if (p.exact && p.name && (p.name.includes('*') || p.name.includes('?'))) {
268
+ note = `Note: exact=true treats "${p.name}" as a literal name (glob expansion disabled).`;
269
+ }
270
+ return { ok: true, result, note };
200
271
  },
201
272
 
202
273
  usages: (index, p) => {
203
274
  const err = requireName(p.name);
204
275
  if (err) return { ok: false, error: err };
276
+ applyClassMethodSyntax(p);
205
277
  const exclude = applyTestExclusions(p.exclude, p.includeTests);
206
278
  const result = index.usages(p.name, {
207
279
  codeOnly: p.codeOnly || false,
208
280
  context: num(p.context, 0),
281
+ className: p.className,
209
282
  exclude,
210
283
  in: p.in,
211
284
  });
@@ -240,8 +313,10 @@ const HANDLERS = {
240
313
  tests: (index, p) => {
241
314
  const err = requireName(p.name);
242
315
  if (err) return { ok: false, error: err };
316
+ applyClassMethodSyntax(p);
243
317
  const result = index.tests(p.name, {
244
318
  callsOnly: p.callsOnly || false,
319
+ className: p.className,
245
320
  });
246
321
  return { ok: true, result };
247
322
  },
@@ -262,6 +337,7 @@ const HANDLERS = {
262
337
  fn: (index, p) => {
263
338
  const err = requireName(p.name);
264
339
  if (err) return { ok: false, error: err };
340
+ applyClassMethodSyntax(p);
265
341
 
266
342
  const fnNames = p.name.includes(',')
267
343
  ? p.name.split(',').map(n => n.trim()).filter(Boolean)
@@ -271,11 +347,23 @@ const HANDLERS = {
271
347
  const notes = [];
272
348
 
273
349
  for (const fnName of fnNames) {
274
- const matches = index.find(fnName, { file: p.file, skipCounts: true })
350
+ // For comma-separated names, each may have Class.method syntax
351
+ const fnSplit = splitClassMethod(fnName);
352
+ const actualName = fnSplit ? fnSplit.methodName : fnName;
353
+ const fnClassName = fnSplit ? fnSplit.className : p.className;
354
+ const matches = index.find(actualName, { file: p.file, className: fnClassName, skipCounts: true })
275
355
  .filter(m => m.type === 'function' || m.params !== undefined);
276
356
 
277
357
  if (matches.length === 0) {
278
- notes.push(`Function "${fnName}" not found.`);
358
+ // Check if it's a class — suggest `class` command instead
359
+ const CLASS_TYPES = ['class', 'interface', 'type', 'enum', 'struct', 'trait'];
360
+ const classMatches = index.find(actualName, { file: p.file, className: fnClassName, skipCounts: true })
361
+ .filter(m => CLASS_TYPES.includes(m.type));
362
+ if (classMatches.length > 0) {
363
+ notes.push(`"${fnName}" is a ${classMatches[0].type}, not a function. Use \`class ${fnName}\` instead.`);
364
+ } else {
365
+ notes.push(`Function "${fnName}" not found.`);
366
+ }
279
367
  continue;
280
368
  }
281
369
 
@@ -470,13 +558,15 @@ const HANDLERS = {
470
558
  verify: (index, p) => {
471
559
  const err = requireName(p.name);
472
560
  if (err) return { ok: false, error: err };
473
- const result = index.verify(p.name, { file: p.file });
561
+ applyClassMethodSyntax(p);
562
+ const result = index.verify(p.name, { file: p.file, className: p.className });
474
563
  return { ok: true, result };
475
564
  },
476
565
 
477
566
  plan: (index, p) => {
478
567
  const err = requireName(p.name);
479
568
  if (err) return { ok: false, error: err };
569
+ applyClassMethodSyntax(p);
480
570
  if (!p.addParam && !p.removeParam && !p.renameTo) {
481
571
  return { ok: false, error: 'Plan requires an operation: add_param, remove_param, or rename_to.' };
482
572
  }
@@ -486,6 +576,7 @@ const HANDLERS = {
486
576
  renameTo: p.renameTo,
487
577
  defaultValue: p.defaultValue,
488
578
  file: p.file,
579
+ className: p.className,
489
580
  });
490
581
  return { ok: true, result };
491
582
  },
@@ -504,7 +595,8 @@ const HANDLERS = {
504
595
  typedef: (index, p) => {
505
596
  const err = requireName(p.name);
506
597
  if (err) return { ok: false, error: err };
507
- const result = index.typedef(p.name, { exact: p.exact || false });
598
+ applyClassMethodSyntax(p);
599
+ const result = index.typedef(p.name, { exact: p.exact || false, className: p.className });
508
600
  return { ok: true, result };
509
601
  },
510
602
 
@@ -125,6 +125,50 @@ class ExpandCache {
125
125
  }
126
126
  }
127
127
 
128
+ /**
129
+ * Detect the end of a function/method body starting from startLine.
130
+ * Uses brace/indent counting to find the closing boundary.
131
+ * Falls back to startLine + 30 if detection fails.
132
+ */
133
+ function _detectFunctionEnd(fileLines, startLine) {
134
+ const maxScan = 500; // Avoid scanning huge files
135
+ const idx = startLine - 1;
136
+ if (idx >= fileLines.length) return startLine;
137
+
138
+ const firstLine = fileLines[idx];
139
+
140
+ // Python: indentation-based — find the first non-empty line at same or lesser indent
141
+ if (/^\s*def\s|^\s*class\s|^\s*async\s+def\s/.test(firstLine)) {
142
+ const baseIndent = firstLine.match(/^(\s*)/)[1].length;
143
+ let end = startLine;
144
+ for (let i = idx + 1; i < Math.min(idx + maxScan, fileLines.length); i++) {
145
+ const line = fileLines[i];
146
+ if (line.trim() === '') { end = i + 1; continue; } // blank lines are part of body
147
+ const indent = line.match(/^(\s*)/)[1].length;
148
+ if (indent <= baseIndent) break;
149
+ end = i + 1;
150
+ }
151
+ return end;
152
+ }
153
+
154
+ // Brace-based languages (JS/TS/Go/Java/Rust): count braces
155
+ let braceCount = 0;
156
+ let foundBrace = false;
157
+ for (let i = idx; i < Math.min(idx + maxScan, fileLines.length); i++) {
158
+ const line = fileLines[i];
159
+ for (const ch of line) {
160
+ if (ch === '{') { braceCount++; foundBrace = true; }
161
+ else if (ch === '}') { braceCount--; }
162
+ }
163
+ if (foundBrace && braceCount <= 0) {
164
+ return i + 1;
165
+ }
166
+ }
167
+
168
+ // Fallback: show 30 lines from start
169
+ return Math.min(startLine + 30, fileLines.length);
170
+ }
171
+
128
172
  /**
129
173
  * Render an expand match to text lines.
130
174
  * Shared by MCP and interactive mode to avoid duplicated rendering logic.
@@ -156,7 +200,13 @@ function renderExpandItem(match, root, { validateRoot = false } = {}) {
156
200
  const content = fs.readFileSync(filePath, 'utf-8');
157
201
  const fileLines = content.split('\n');
158
202
  const startLine = match.startLine || match.line || 1;
159
- const endLine = match.endLine || startLine + 20;
203
+ let endLine = match.endLine;
204
+
205
+ // When endLine is missing or equals startLine, the expand would show only 1 line.
206
+ // Scan forward from startLine to find the actual function/method body end.
207
+ if (!endLine || endLine <= startLine) {
208
+ endLine = _detectFunctionEnd(fileLines, startLine);
209
+ }
160
210
 
161
211
  const lines = [];
162
212
  lines.push(`[${match.num}] ${match.name} (${match.type})`);
package/core/output.js CHANGED
@@ -938,7 +938,11 @@ function formatImpact(impact, options = {}) {
938
938
  lines.push('');
939
939
 
940
940
  // Summary
941
- lines.push(`CALL SITES: ${impact.totalCallSites}`);
941
+ if (impact.shownCallSites !== undefined && impact.shownCallSites < impact.totalCallSites) {
942
+ lines.push(`CALL SITES: ${impact.shownCallSites} shown of ${impact.totalCallSites} total`);
943
+ } else {
944
+ lines.push(`CALL SITES: ${impact.totalCallSites}`);
945
+ }
942
946
  lines.push(` Files affected: ${impact.byFile.length}`);
943
947
 
944
948
  // Patterns
package/core/project.js CHANGED
@@ -700,6 +700,14 @@ class ProjectIndex {
700
700
  return { def: null, definitions: [], warnings: [] };
701
701
  }
702
702
 
703
+ // Filter by class name (Class.method syntax)
704
+ if (options.className) {
705
+ const filtered = definitions.filter(d => d.className === options.className);
706
+ if (filtered.length > 0) {
707
+ definitions = filtered;
708
+ }
709
+ }
710
+
703
711
  // Filter by file if specified
704
712
  if (options.file) {
705
713
  const filtered = definitions.filter(d =>
@@ -735,6 +743,20 @@ class ProjectIndex {
735
743
  // Sort by score descending, then by index order for stability
736
744
  scored.sort((a, b) => b.score - a.score);
737
745
 
746
+ // Tiebreaker: when top candidates have equal score, prefer by usage count
747
+ if (scored.length > 1 && scored[0].score === scored[1].score) {
748
+ const tiedScore = scored[0].score;
749
+ const tiedCandidates = scored.filter(s => s.score === tiedScore);
750
+ for (const candidate of tiedCandidates) {
751
+ candidate.usageCount = this.countSymbolUsages(candidate.def).total;
752
+ }
753
+ tiedCandidates.sort((a, b) => b.usageCount - a.usageCount);
754
+ // Rebuild scored array: sorted tied candidates first, then rest
755
+ const rest = scored.filter(s => s.score !== tiedScore);
756
+ scored.length = 0;
757
+ scored.push(...tiedCandidates, ...rest);
758
+ }
759
+
738
760
  const def = scored[0].def;
739
761
 
740
762
  // Build warnings
@@ -812,6 +834,11 @@ class ProjectIndex {
812
834
  _applyFindFilters(matches, options) {
813
835
  let filtered = matches;
814
836
 
837
+ // Filter by class name (Class.method syntax)
838
+ if (options.className) {
839
+ filtered = filtered.filter(m => m.className === options.className);
840
+ }
841
+
815
842
  // Filter by file pattern
816
843
  if (options.file) {
817
844
  filtered = filtered.filter(m =>
@@ -944,7 +971,10 @@ class ProjectIndex {
944
971
  const usages = [];
945
972
 
946
973
  // Get definitions (filtered)
947
- const allDefinitions = this.symbols.get(name) || [];
974
+ let allDefinitions = this.symbols.get(name) || [];
975
+ if (options.className) {
976
+ allDefinitions = allDefinitions.filter(d => d.className === options.className);
977
+ }
948
978
  const definitions = options.exclude || options.in
949
979
  ? allDefinitions.filter(d => this.matchesFilters(d.relativePath, options))
950
980
  : allDefinitions;
@@ -1136,7 +1166,7 @@ class ProjectIndex {
1136
1166
  context(name, options = {}) {
1137
1167
  this._beginOp();
1138
1168
  try {
1139
- const resolved = this.resolveSymbol(name, { file: options.file });
1169
+ const resolved = this.resolveSymbol(name, { file: options.file, className: options.className });
1140
1170
  let { def, definitions, warnings } = resolved;
1141
1171
  if (!def) {
1142
1172
  return null;
@@ -1296,7 +1326,7 @@ class ProjectIndex {
1296
1326
  smart(name, options = {}) {
1297
1327
  this._beginOp();
1298
1328
  try {
1299
- const { def } = this.resolveSymbol(name, { file: options.file });
1329
+ const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
1300
1330
  if (!def) {
1301
1331
  return null;
1302
1332
  }
@@ -2348,7 +2378,7 @@ class ProjectIndex {
2348
2378
  related(name, options = {}) {
2349
2379
  this._beginOp();
2350
2380
  try {
2351
- const { def } = this.resolveSymbol(name, { file: options.file });
2381
+ const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
2352
2382
  if (!def) {
2353
2383
  return null;
2354
2384
  }
@@ -2499,7 +2529,7 @@ class ProjectIndex {
2499
2529
  // trace defaults to includeMethods=true (execution flow should show method calls)
2500
2530
  const includeMethods = options.includeMethods ?? true;
2501
2531
 
2502
- const { def, definitions, warnings } = this.resolveSymbol(name, { file: options.file });
2532
+ const { def, definitions, warnings } = this.resolveSymbol(name, { file: options.file, className: options.className });
2503
2533
  if (!def) {
2504
2534
  return null;
2505
2535
  }
@@ -2605,7 +2635,7 @@ class ProjectIndex {
2605
2635
  impact(name, options = {}) {
2606
2636
  this._beginOp();
2607
2637
  try {
2608
- const { def } = this.resolveSymbol(name, { file: options.file });
2638
+ const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
2609
2639
  if (!def) {
2610
2640
  return null;
2611
2641
  }
@@ -2668,6 +2698,12 @@ class ProjectIndex {
2668
2698
  filteredSites = callSites.filter(s => this.matchesFilters(s.file, { exclude: options.exclude }));
2669
2699
  }
2670
2700
 
2701
+ // Apply top limit if specified (limits total call sites shown)
2702
+ const totalBeforeLimit = filteredSites.length;
2703
+ if (options.top && options.top > 0 && filteredSites.length > options.top) {
2704
+ filteredSites = filteredSites.slice(0, options.top);
2705
+ }
2706
+
2671
2707
  // Group by file
2672
2708
  const byFile = new Map();
2673
2709
  for (const site of filteredSites) {
@@ -2687,7 +2723,8 @@ class ProjectIndex {
2687
2723
  signature: this.formatSignature(def),
2688
2724
  params: def.params,
2689
2725
  paramsStructured: def.paramsStructured,
2690
- totalCallSites: filteredSites.length,
2726
+ totalCallSites: totalBeforeLimit,
2727
+ shownCallSites: filteredSites.length,
2691
2728
  byFile: Array.from(byFile.entries()).map(([file, sites]) => ({
2692
2729
  file,
2693
2730
  count: sites.length,
@@ -2752,10 +2789,10 @@ class ProjectIndex {
2752
2789
  const includeMethods = options.includeMethods ?? true;
2753
2790
 
2754
2791
  // Find symbol definition(s) — skip counts since about() computes its own via usages()
2755
- const definitions = this.find(name, { exact: true, file: options.file, skipCounts: true });
2792
+ const definitions = this.find(name, { exact: true, file: options.file, className: options.className, skipCounts: true });
2756
2793
  if (definitions.length === 0) {
2757
2794
  // Try fuzzy match (needs counts for suggestion ranking)
2758
- const fuzzy = this.find(name, { file: options.file });
2795
+ const fuzzy = this.find(name, { file: options.file, className: options.className });
2759
2796
  if (fuzzy.length === 0) {
2760
2797
  return null;
2761
2798
  }
@@ -2773,7 +2810,7 @@ class ProjectIndex {
2773
2810
  }
2774
2811
 
2775
2812
  // Use resolveSymbol for consistent primary selection (prefers non-test files)
2776
- const { def: resolved } = this.resolveSymbol(name, { file: options.file });
2813
+ const { def: resolved } = this.resolveSymbol(name, { file: options.file, className: options.className });
2777
2814
  const primary = resolved || definitions[0];
2778
2815
  const others = definitions.filter(d =>
2779
2816
  d.relativePath !== primary.relativePath || d.startLine !== primary.startLine
@@ -3237,11 +3274,12 @@ class ProjectIndex {
3237
3274
  * @param {string} name - Symbol name
3238
3275
  * @returns {{ best: object, totalCalls: number } | null}
3239
3276
  */
3240
- example(name) {
3277
+ example(name, options = {}) {
3241
3278
  this._beginOp();
3242
3279
  try {
3243
3280
  const usages = this.usages(name, {
3244
3281
  codeOnly: true,
3282
+ className: options.className,
3245
3283
  exclude: ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'],
3246
3284
  context: 5
3247
3285
  });
package/core/verify.js CHANGED
@@ -177,7 +177,7 @@ function identifyCallPatterns(callSites, funcName) {
177
177
  function verify(index, name, options = {}) {
178
178
  index._beginOp();
179
179
  try {
180
- const { def } = index.resolveSymbol(name, { file: options.file });
180
+ const { def } = index.resolveSymbol(name, { file: options.file, className: options.className });
181
181
  if (!def) {
182
182
  return { found: false, function: name };
183
183
  }
@@ -315,9 +315,9 @@ function plan(index, name, options = {}) {
315
315
  return { found: false, function: name };
316
316
  }
317
317
 
318
- const resolved = index.resolveSymbol(name, { file: options.file });
318
+ const resolved = index.resolveSymbol(name, { file: options.file, className: options.className });
319
319
  const def = resolved.def || definitions[0];
320
- const impact = index.impact(name, { file: options.file });
320
+ const impact = index.impact(name, { file: options.file, className: options.className });
321
321
  const currentParams = def.paramsStructured || [];
322
322
  const currentSignature = index.formatSignature(def);
323
323
 
@@ -433,6 +433,29 @@ function plan(index, name, options = {}) {
433
433
  });
434
434
  }
435
435
  }
436
+
437
+ // Also include import statements that reference the renamed function
438
+ const usages = index.usages(name, { codeOnly: true });
439
+ const importUsages = usages.filter(u => u.usageType === 'import' && !u.isDefinition);
440
+ for (const imp of importUsages) {
441
+ // Skip if already covered by a call site change in the same file:line
442
+ const alreadyCovered = changes.some(c =>
443
+ c.file === (imp.relativePath || imp.file) && c.line === imp.line
444
+ );
445
+ if (alreadyCovered) continue;
446
+ const newImport = imp.content.trim().replace(
447
+ new RegExp('\\b' + escapeRegExp(name) + '\\b'),
448
+ options.renameTo
449
+ );
450
+ changes.push({
451
+ file: imp.relativePath || imp.file,
452
+ line: imp.line,
453
+ expression: imp.content.trim(),
454
+ suggestion: `Update import: ${newImport}`,
455
+ newExpression: newImport,
456
+ isImport: true
457
+ });
458
+ }
436
459
  }
437
460
 
438
461
  return {
@@ -330,6 +330,14 @@ function findStateObjects(code, parser) {
330
330
  const objects = [];
331
331
 
332
332
  const statePattern = /^(CONFIG|SETTINGS|[A-Z][A-Z0-9_]+|[A-Z][a-zA-Z]*(?:Config|Settings|Options|State|Store|Context))$/;
333
+ // Pattern for UPPER_CASE constants that may have scalar values (string, number, bool, etc.)
334
+ const constantPattern = /^[A-Z][A-Z0-9_]{1,}$/;
335
+ // RHS types that are scalar/simple values (not dict/list which are handled separately)
336
+ const scalarTypes = new Set([
337
+ 'string', 'concatenated_string', 'integer', 'float', 'true', 'false', 'none',
338
+ 'unary_operator', 'binary_operator', 'tuple', 'set', 'parenthesized_expression',
339
+ 'call', 'attribute', 'identifier', 'subscript',
340
+ ]);
333
341
 
334
342
  traverseTree(tree.rootNode, (node) => {
335
343
  if (node.type === 'expression_statement' && node.parent === tree.rootNode) {
@@ -346,6 +354,10 @@ function findStateObjects(code, parser) {
346
354
  if ((isObject || isArray) && statePattern.test(name)) {
347
355
  const { startLine, endLine } = nodeToLocation(node, code);
348
356
  objects.push({ name, startLine, endLine });
357
+ } else if (constantPattern.test(name) && scalarTypes.has(rightNode.type)) {
358
+ // Module-level UPPER_CASE constants with scalar values
359
+ const { startLine, endLine } = nodeToLocation(node, code);
360
+ objects.push({ name, startLine, endLine, isConstant: true });
349
361
  }
350
362
  }
351
363
  }
package/mcp/server.js CHANGED
@@ -297,7 +297,7 @@ server.registerTool(
297
297
 
298
298
  case 'impact': {
299
299
  const index = getIndex(project_dir);
300
- const ep = normalizeParams({ name, file, exclude });
300
+ const ep = normalizeParams({ name, file, exclude, top });
301
301
  const { ok, result, error } = execute(index, 'impact', ep);
302
302
  if (!ok) return toolResult(error); // soft error
303
303
  return toolResult(output.formatImpact(result));
@@ -346,9 +346,11 @@ server.registerTool(
346
346
  case 'find': {
347
347
  const index = getIndex(project_dir);
348
348
  const ep = normalizeParams({ name, file, exclude, include_tests, exact, in: inPath });
349
- const { ok, result, error } = execute(index, 'find', ep);
349
+ const { ok, result, error, note } = execute(index, 'find', ep);
350
350
  if (!ok) return toolResult(error); // soft error
351
- return toolResult(output.formatFind(result, name, top));
351
+ let text = output.formatFind(result, name, top);
352
+ if (note) text += '\n\n' + note;
353
+ return toolResult(text);
352
354
  }
353
355
 
354
356
  case 'usages': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.29",
3
+ "version": "3.7.31",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Universal Code Navigator — AST-based call graph analysis for AI agents. Find callers, trace impact, detect dead code across JS/TS, Python, Go, Rust, Java, and HTML. CLI, MCP server, and agent skill.",
6
6
  "main": "index.js",