ucn 3.7.30 → 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
@@ -758,6 +758,33 @@ function findCallees(index, def, options = {}) {
758
758
  if (nonTest) callee = nonTest;
759
759
  }
760
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
+ }
761
788
  }
762
789
 
763
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,8 +176,10 @@ 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),
129
184
  top: num(p.top, undefined),
130
185
  });
@@ -135,8 +190,10 @@ const HANDLERS = {
135
190
  smart: (index, p) => {
136
191
  const err = requireName(p.name);
137
192
  if (err) return { ok: false, error: err };
193
+ applyClassMethodSyntax(p);
138
194
  const result = index.smart(p.name, {
139
195
  file: p.file,
196
+ className: p.className,
140
197
  withTypes: p.withTypes || false,
141
198
  includeMethods: p.includeMethods,
142
199
  includeUncertain: p.includeUncertain || false,
@@ -148,10 +205,12 @@ const HANDLERS = {
148
205
  trace: (index, p) => {
149
206
  const err = requireName(p.name);
150
207
  if (err) return { ok: false, error: err };
208
+ applyClassMethodSyntax(p);
151
209
  const depthVal = num(p.depth, undefined);
152
210
  const result = index.trace(p.name, {
153
211
  depth: depthVal ?? 3,
154
212
  file: p.file,
213
+ className: p.className,
155
214
  all: p.all || depthVal !== undefined,
156
215
  includeMethods: p.includeMethods,
157
216
  includeUncertain: p.includeUncertain || false,
@@ -163,7 +222,8 @@ const HANDLERS = {
163
222
  example: (index, p) => {
164
223
  const err = requireName(p.name);
165
224
  if (err) return { ok: false, error: err };
166
- const result = index.example(p.name);
225
+ applyClassMethodSyntax(p);
226
+ const result = index.example(p.name, { file: p.file, className: p.className });
167
227
  if (!result) return { ok: false, error: `No examples found for "${p.name}".` };
168
228
  return { ok: true, result };
169
229
  },
@@ -171,8 +231,10 @@ const HANDLERS = {
171
231
  related: (index, p) => {
172
232
  const err = requireName(p.name);
173
233
  if (err) return { ok: false, error: err };
234
+ applyClassMethodSyntax(p);
174
235
  const result = index.related(p.name, {
175
236
  file: p.file,
237
+ className: p.className,
176
238
  top: num(p.top, undefined),
177
239
  all: p.all,
178
240
  });
@@ -185,6 +247,7 @@ const HANDLERS = {
185
247
  find: (index, p) => {
186
248
  const err = requireName(p.name);
187
249
  if (err) return { ok: false, error: err };
250
+ applyClassMethodSyntax(p);
188
251
  // Auto-include tests when pattern clearly targets test functions
189
252
  // But only if the user didn't explicitly set include_tests=false
190
253
  let includeTests = p.includeTests;
@@ -194,20 +257,28 @@ const HANDLERS = {
194
257
  const exclude = applyTestExclusions(p.exclude, includeTests);
195
258
  const result = index.find(p.name, {
196
259
  file: p.file,
260
+ className: p.className,
197
261
  exact: p.exact || false,
198
262
  exclude,
199
263
  in: p.in,
200
264
  });
201
- 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 };
202
271
  },
203
272
 
204
273
  usages: (index, p) => {
205
274
  const err = requireName(p.name);
206
275
  if (err) return { ok: false, error: err };
276
+ applyClassMethodSyntax(p);
207
277
  const exclude = applyTestExclusions(p.exclude, p.includeTests);
208
278
  const result = index.usages(p.name, {
209
279
  codeOnly: p.codeOnly || false,
210
280
  context: num(p.context, 0),
281
+ className: p.className,
211
282
  exclude,
212
283
  in: p.in,
213
284
  });
@@ -242,8 +313,10 @@ const HANDLERS = {
242
313
  tests: (index, p) => {
243
314
  const err = requireName(p.name);
244
315
  if (err) return { ok: false, error: err };
316
+ applyClassMethodSyntax(p);
245
317
  const result = index.tests(p.name, {
246
318
  callsOnly: p.callsOnly || false,
319
+ className: p.className,
247
320
  });
248
321
  return { ok: true, result };
249
322
  },
@@ -264,6 +337,7 @@ const HANDLERS = {
264
337
  fn: (index, p) => {
265
338
  const err = requireName(p.name);
266
339
  if (err) return { ok: false, error: err };
340
+ applyClassMethodSyntax(p);
267
341
 
268
342
  const fnNames = p.name.includes(',')
269
343
  ? p.name.split(',').map(n => n.trim()).filter(Boolean)
@@ -273,11 +347,23 @@ const HANDLERS = {
273
347
  const notes = [];
274
348
 
275
349
  for (const fnName of fnNames) {
276
- 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 })
277
355
  .filter(m => m.type === 'function' || m.params !== undefined);
278
356
 
279
357
  if (matches.length === 0) {
280
- 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
+ }
281
367
  continue;
282
368
  }
283
369
 
@@ -472,13 +558,15 @@ const HANDLERS = {
472
558
  verify: (index, p) => {
473
559
  const err = requireName(p.name);
474
560
  if (err) return { ok: false, error: err };
475
- 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 });
476
563
  return { ok: true, result };
477
564
  },
478
565
 
479
566
  plan: (index, p) => {
480
567
  const err = requireName(p.name);
481
568
  if (err) return { ok: false, error: err };
569
+ applyClassMethodSyntax(p);
482
570
  if (!p.addParam && !p.removeParam && !p.renameTo) {
483
571
  return { ok: false, error: 'Plan requires an operation: add_param, remove_param, or rename_to.' };
484
572
  }
@@ -488,6 +576,7 @@ const HANDLERS = {
488
576
  renameTo: p.renameTo,
489
577
  defaultValue: p.defaultValue,
490
578
  file: p.file,
579
+ className: p.className,
491
580
  });
492
581
  return { ok: true, result };
493
582
  },
@@ -506,7 +595,8 @@ const HANDLERS = {
506
595
  typedef: (index, p) => {
507
596
  const err = requireName(p.name);
508
597
  if (err) return { ok: false, error: err };
509
- 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 });
510
600
  return { ok: true, result };
511
601
  },
512
602
 
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 =>
@@ -826,6 +834,11 @@ class ProjectIndex {
826
834
  _applyFindFilters(matches, options) {
827
835
  let filtered = matches;
828
836
 
837
+ // Filter by class name (Class.method syntax)
838
+ if (options.className) {
839
+ filtered = filtered.filter(m => m.className === options.className);
840
+ }
841
+
829
842
  // Filter by file pattern
830
843
  if (options.file) {
831
844
  filtered = filtered.filter(m =>
@@ -958,7 +971,10 @@ class ProjectIndex {
958
971
  const usages = [];
959
972
 
960
973
  // Get definitions (filtered)
961
- 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
+ }
962
978
  const definitions = options.exclude || options.in
963
979
  ? allDefinitions.filter(d => this.matchesFilters(d.relativePath, options))
964
980
  : allDefinitions;
@@ -1150,7 +1166,7 @@ class ProjectIndex {
1150
1166
  context(name, options = {}) {
1151
1167
  this._beginOp();
1152
1168
  try {
1153
- const resolved = this.resolveSymbol(name, { file: options.file });
1169
+ const resolved = this.resolveSymbol(name, { file: options.file, className: options.className });
1154
1170
  let { def, definitions, warnings } = resolved;
1155
1171
  if (!def) {
1156
1172
  return null;
@@ -1310,7 +1326,7 @@ class ProjectIndex {
1310
1326
  smart(name, options = {}) {
1311
1327
  this._beginOp();
1312
1328
  try {
1313
- const { def } = this.resolveSymbol(name, { file: options.file });
1329
+ const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
1314
1330
  if (!def) {
1315
1331
  return null;
1316
1332
  }
@@ -2362,7 +2378,7 @@ class ProjectIndex {
2362
2378
  related(name, options = {}) {
2363
2379
  this._beginOp();
2364
2380
  try {
2365
- const { def } = this.resolveSymbol(name, { file: options.file });
2381
+ const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
2366
2382
  if (!def) {
2367
2383
  return null;
2368
2384
  }
@@ -2513,7 +2529,7 @@ class ProjectIndex {
2513
2529
  // trace defaults to includeMethods=true (execution flow should show method calls)
2514
2530
  const includeMethods = options.includeMethods ?? true;
2515
2531
 
2516
- const { def, definitions, warnings } = this.resolveSymbol(name, { file: options.file });
2532
+ const { def, definitions, warnings } = this.resolveSymbol(name, { file: options.file, className: options.className });
2517
2533
  if (!def) {
2518
2534
  return null;
2519
2535
  }
@@ -2619,7 +2635,7 @@ class ProjectIndex {
2619
2635
  impact(name, options = {}) {
2620
2636
  this._beginOp();
2621
2637
  try {
2622
- const { def } = this.resolveSymbol(name, { file: options.file });
2638
+ const { def } = this.resolveSymbol(name, { file: options.file, className: options.className });
2623
2639
  if (!def) {
2624
2640
  return null;
2625
2641
  }
@@ -2773,10 +2789,10 @@ class ProjectIndex {
2773
2789
  const includeMethods = options.includeMethods ?? true;
2774
2790
 
2775
2791
  // Find symbol definition(s) — skip counts since about() computes its own via usages()
2776
- 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 });
2777
2793
  if (definitions.length === 0) {
2778
2794
  // Try fuzzy match (needs counts for suggestion ranking)
2779
- const fuzzy = this.find(name, { file: options.file });
2795
+ const fuzzy = this.find(name, { file: options.file, className: options.className });
2780
2796
  if (fuzzy.length === 0) {
2781
2797
  return null;
2782
2798
  }
@@ -2794,7 +2810,7 @@ class ProjectIndex {
2794
2810
  }
2795
2811
 
2796
2812
  // Use resolveSymbol for consistent primary selection (prefers non-test files)
2797
- const { def: resolved } = this.resolveSymbol(name, { file: options.file });
2813
+ const { def: resolved } = this.resolveSymbol(name, { file: options.file, className: options.className });
2798
2814
  const primary = resolved || definitions[0];
2799
2815
  const others = definitions.filter(d =>
2800
2816
  d.relativePath !== primary.relativePath || d.startLine !== primary.startLine
@@ -3258,11 +3274,12 @@ class ProjectIndex {
3258
3274
  * @param {string} name - Symbol name
3259
3275
  * @returns {{ best: object, totalCalls: number } | null}
3260
3276
  */
3261
- example(name) {
3277
+ example(name, options = {}) {
3262
3278
  this._beginOp();
3263
3279
  try {
3264
3280
  const usages = this.usages(name, {
3265
3281
  codeOnly: true,
3282
+ className: options.className,
3266
3283
  exclude: ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'],
3267
3284
  context: 5
3268
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
 
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.30",
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",