ucn 3.7.27 → 3.7.29

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/README.md CHANGED
@@ -20,13 +20,13 @@ Instead of reading full files, UCN gives precise, AST-verified answers.
20
20
  ```bash
21
21
  npm install -g ucn
22
22
 
23
- ucn about handleRequest # full picture: definition, callers, callees, tests
24
- ucn impact handleRequest # all call sites with arguments
25
- ucn trace main --depth=3 # call tree, no file reads
26
- ucn deadcode # unused functions, AST-verified
27
- ucn fn handleRequest # extract a function without reading the file
28
- ucn toc # project overview
29
- ucn --interactive # REPL mode, index stays in memory
23
+ ucn about handleRequest # full picture: definition, callers, callees, tests
24
+ ucn impact handleRequest # all call sites with arguments
25
+ ucn trace main --depth=3 # call tree, no file reads
26
+ ucn deadcode # unused functions, AST-verified
27
+ ucn fn handleRequest # extract a function without reading the file
28
+ ucn toc # project overview
29
+ ucn --interactive # REPL mode, index stays in memory
30
30
  ```
31
31
 
32
32
  Parses JS/TS, Python, Go, Rust, Java, and HTML with tree-sitter. Runs locally.
package/core/callers.js CHANGED
@@ -386,6 +386,13 @@ function findCallees(index, def, options = {}) {
386
386
  let selfAttrCalls = null; // collected for Python self.attr.method() resolution
387
387
  let selfMethodCalls = null; // collected for Python self.method() resolution
388
388
 
389
+ // Build local variable type map for receiver resolution
390
+ // Scans for patterns like: bt = Backtester(...) → bt maps to Backtester
391
+ let localTypes = null;
392
+ if (language === 'python' || language === 'javascript') {
393
+ localTypes = _buildLocalTypeMap(index, def, calls);
394
+ }
395
+
389
396
  for (const call of calls) {
390
397
  // Filter to calls within this function's scope
391
398
  // Method 1: Direct match via enclosingFunction (fast path for direct calls)
@@ -412,6 +419,22 @@ function findCallees(index, def, options = {}) {
412
419
  // self.method() / cls.method() / this.method() — resolve to same-class method below
413
420
  } else if (call.receiver === 'super') {
414
421
  // super().method() — resolve to parent class method below
422
+ } else if (localTypes && localTypes.has(call.receiver)) {
423
+ // Resolve method calls on locally-constructed objects:
424
+ // bt = Backtester(...); bt.run_backtest() → Backtester.run_backtest
425
+ const className = localTypes.get(call.receiver);
426
+ const symbols = index.symbols.get(call.name);
427
+ const match = symbols?.find(s => s.className === className);
428
+ if (match) {
429
+ const key = match.bindingId || `${className}.${call.name}`;
430
+ const existing = callees.get(key);
431
+ if (existing) {
432
+ existing.count += 1;
433
+ } else {
434
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1 });
435
+ }
436
+ }
437
+ continue;
415
438
  } else if (language !== 'go' && language !== 'java' && language !== 'rust' && !options.includeMethods) {
416
439
  continue;
417
440
  }
@@ -704,7 +727,18 @@ function findCallees(index, def, options = {}) {
704
727
  }
705
728
  }
706
729
  }
707
- // Priority 4: If default (symbols[0]) is a test file, prefer non-test
730
+ // Priority 4: If default is from a bundled/minified file, prefer non-bundled
731
+ if (!bindingId) {
732
+ const calleeFileEntry = index.files.get(callee.file);
733
+ if (calleeFileEntry && calleeFileEntry.isBundled) {
734
+ const nonBundled = symbols.find(s => {
735
+ const fe = index.files.get(s.file);
736
+ return fe && !fe.isBundled;
737
+ });
738
+ if (nonBundled) callee = nonBundled;
739
+ }
740
+ }
741
+ // Priority 5: If default is a test file, prefer non-test
708
742
  if (!bindingId) {
709
743
  const calleeFileEntry = index.files.get(callee.file);
710
744
  if (calleeFileEntry && isTestFile(calleeFileEntry.relativePath, calleeFileEntry.language)) {
@@ -778,6 +812,60 @@ function getInstanceAttributeTypes(index, filePath, className) {
778
812
  return fileCache.get(className) || null;
779
813
  }
780
814
 
815
+ /**
816
+ * Build a local variable type map for a function body.
817
+ * Scans for constructor-call assignments: var = ClassName(...)
818
+ * Returns Map<varName, className> or null if none found.
819
+ * @param {object} index - ProjectIndex instance
820
+ * @param {object} def - Function definition with file, startLine, endLine
821
+ * @param {Array} calls - Cached call sites for the file
822
+ */
823
+ function _buildLocalTypeMap(index, def, calls) {
824
+ let content;
825
+ try {
826
+ content = index._readFile(def.file);
827
+ } catch {
828
+ return null;
829
+ }
830
+ const lines = content.split('\n');
831
+ const localTypes = new Map();
832
+
833
+ for (const call of calls) {
834
+ // Only look at calls within this function's scope
835
+ if (call.line < def.startLine || call.line > def.endLine) continue;
836
+ // Only direct calls (not method calls) — these are potential constructors
837
+ if (call.isMethod || call.isPotentialCallback) continue;
838
+
839
+ // Check if this call's name corresponds to a class in the symbol table
840
+ const symbols = index.symbols.get(call.name);
841
+ if (!symbols) continue;
842
+ const isClass = symbols.some(s => NON_CALLABLE_TYPES.has(s.type));
843
+ if (!isClass) continue;
844
+
845
+ // Check the source line for assignment pattern: var = ClassName(...)
846
+ const sourceLine = lines[call.line - 1];
847
+ if (!sourceLine) continue;
848
+
849
+ // Match: identifier = ClassName(...) or identifier: Type = ClassName(...)
850
+ const escapedName = call.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
851
+ const assignMatch = sourceLine.match(
852
+ new RegExp(`(\\w+)\\s*(?::\\s*\\w+)?\\s*=\\s*${escapedName}\\s*\\(`)
853
+ );
854
+ if (assignMatch) {
855
+ localTypes.set(assignMatch[1], call.name);
856
+ }
857
+ // Match: with ClassName(...) as identifier:
858
+ const withMatch = sourceLine.match(
859
+ new RegExp(`with\\s+${escapedName}\\s*\\([^)]*\\)\\s+as\\s+(\\w+)`)
860
+ );
861
+ if (withMatch) {
862
+ localTypes.set(withMatch[1], call.name);
863
+ }
864
+ }
865
+
866
+ return localTypes.size > 0 ? localTypes : null;
867
+ }
868
+
781
869
  /**
782
870
  * Check if a function is used as a callback anywhere in the codebase
783
871
  * @param {object} index - ProjectIndex instance
package/core/discovery.js CHANGED
@@ -30,6 +30,7 @@ const DEFAULT_IGNORES = [
30
30
 
31
31
  // Build outputs
32
32
  'dist',
33
+ '*-dist',
33
34
  'build',
34
35
  'out',
35
36
  '.next',
@@ -38,6 +39,12 @@ const DEFAULT_IGNORES = [
38
39
  '.output',
39
40
  '.vercel',
40
41
  '.netlify',
42
+ '.turbo',
43
+ '.parcel-cache',
44
+ '.svelte-kit',
45
+ '.docusaurus',
46
+ 'storybook-static',
47
+ '_site',
41
48
 
42
49
  // Test/coverage
43
50
  'coverage',
package/core/execute.js CHANGED
@@ -184,7 +184,12 @@ const HANDLERS = {
184
184
  find: (index, p) => {
185
185
  const err = requireName(p.name);
186
186
  if (err) return { ok: false, error: err };
187
- const exclude = applyTestExclusions(p.exclude, p.includeTests);
187
+ // Auto-include tests when pattern clearly targets test functions
188
+ let includeTests = p.includeTests;
189
+ if (!includeTests && p.name && /^test[_*?]/i.test(p.name)) {
190
+ includeTests = true;
191
+ }
192
+ const exclude = applyTestExclusions(p.exclude, includeTests);
188
193
  const result = index.find(p.name, {
189
194
  file: p.file,
190
195
  exact: p.exact || false,
package/core/imports.js CHANGED
@@ -507,6 +507,32 @@ function loadTsConfig(tsconfigPath, visited) {
507
507
  }
508
508
  }
509
509
 
510
+ // Follow project references to collect paths from referenced configs
511
+ // (tsconfig.json with "references" pointing to tsconfig.app.json, tsconfig.node.json, etc.)
512
+ if (config.references && Array.isArray(config.references)) {
513
+ for (const ref of config.references) {
514
+ if (!ref.path) continue;
515
+ let refPath = path.resolve(configDir, ref.path);
516
+ // If reference points to a directory, look for tsconfig.json inside it
517
+ if (fs.existsSync(refPath) && fs.statSync(refPath).isDirectory()) {
518
+ refPath = path.join(refPath, 'tsconfig.json');
519
+ }
520
+ // Add .json extension if not present
521
+ if (!refPath.endsWith('.json')) refPath += '.json';
522
+ if (fs.existsSync(refPath)) {
523
+ try {
524
+ const refResult = loadTsConfig(refPath, visited);
525
+ if (refResult) {
526
+ basePaths = { ...basePaths, ...refResult.paths };
527
+ if (refResult.baseUrl && !baseUrl) baseUrl = refResult.baseUrl;
528
+ }
529
+ } catch {
530
+ // Skip malformed reference config
531
+ }
532
+ }
533
+ }
534
+ }
535
+
510
536
  // Child config values override base config
511
537
  const mergedPaths = { ...basePaths, ...(config.compilerOptions?.paths || {}) };
512
538
  const compiledPaths = Object.entries(mergedPaths).map(([pattern, targets]) => ({
package/core/output.js CHANGED
@@ -613,6 +613,10 @@ function formatApi(symbols, filePath) {
613
613
 
614
614
  if (symbols.length === 0) {
615
615
  lines.push(' (none found)');
616
+ if (filePath && filePath.endsWith('.py')) {
617
+ lines.push('');
618
+ lines.push('Note: Python requires __all__ for export detection. Use \'toc\' command to see all functions/classes.');
619
+ }
616
620
  } else {
617
621
  // Group by file
618
622
  const byFile = new Map();
package/core/project.js CHANGED
@@ -759,10 +759,17 @@ class ProjectIndex {
759
759
  // Glob pattern matching (e.g., _update*, handle*Request, get?ata)
760
760
  const isGlob = name.includes('*') || name.includes('?');
761
761
  if (isGlob && !options.exact) {
762
- // Guard against bare wildcards that would match everything
762
+ // Bare wildcard: return all symbols
763
763
  const stripped = name.replace(/[*?]/g, '');
764
764
  if (stripped.length === 0) {
765
- return [];
765
+ const all = [];
766
+ for (const [, symbols] of this.symbols) {
767
+ for (const sym of symbols) {
768
+ all.push({ ...sym, _fuzzyScore: 800 });
769
+ }
770
+ }
771
+ all.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
772
+ return this._applyFindFilters(all, options);
766
773
  }
767
774
  const globRegex = new RegExp('^' + name.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
768
775
  const matches = [];
@@ -1477,42 +1484,145 @@ class ProjectIndex {
1477
1484
  */
1478
1485
  isKeyword(name, language) {
1479
1486
  if (!LANGUAGE_KEYWORDS) {
1480
- // Initialize on first use
1487
+ // Initialize on first use — includes both keywords AND builtins
1488
+ // to prevent cross-language false positives (e.g. Python set() → JS bundle)
1481
1489
  LANGUAGE_KEYWORDS = {
1482
1490
  javascript: new Set([
1491
+ // Keywords
1483
1492
  'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
1484
1493
  'continue', 'return', 'function', 'class', 'const', 'let', 'var',
1485
1494
  'new', 'this', 'super', 'import', 'export', 'default', 'from',
1486
1495
  'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
1487
- 'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'with'
1496
+ 'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'with',
1497
+ // Global builtins
1498
+ 'undefined', 'NaN', 'Infinity', 'globalThis',
1499
+ 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
1500
+ 'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
1501
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
1502
+ 'console', 'JSON', 'Math', 'Date', 'RegExp',
1503
+ 'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt',
1504
+ 'Map', 'Set', 'WeakMap', 'WeakSet', 'WeakRef',
1505
+ 'Promise', 'Proxy', 'Reflect',
1506
+ 'Error', 'TypeError', 'RangeError', 'ReferenceError', 'SyntaxError',
1507
+ 'URIError', 'EvalError', 'AggregateError',
1508
+ 'ArrayBuffer', 'SharedArrayBuffer', 'DataView',
1509
+ 'Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
1510
+ 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array',
1511
+ 'BigInt64Array', 'BigUint64Array',
1512
+ 'TextEncoder', 'TextDecoder', 'URL', 'URLSearchParams',
1513
+ 'fetch', 'Request', 'Response', 'Headers',
1514
+ 'atob', 'btoa', 'structuredClone', 'queueMicrotask',
1515
+ 'require'
1488
1516
  ]),
1489
1517
  python: new Set([
1518
+ // Keywords
1490
1519
  'if', 'else', 'elif', 'for', 'while', 'def', 'class', 'return',
1491
1520
  'import', 'from', 'try', 'except', 'finally', 'raise', 'async',
1492
1521
  'await', 'yield', 'with', 'as', 'lambda', 'pass', 'break',
1493
1522
  'continue', 'del', 'global', 'nonlocal', 'assert', 'is', 'not',
1494
- 'and', 'or', 'in', 'True', 'False', 'None', 'self', 'cls'
1523
+ 'and', 'or', 'in', 'True', 'False', 'None', 'self', 'cls',
1524
+ // Type constructors
1525
+ 'int', 'float', 'str', 'bool', 'list', 'dict', 'set', 'tuple',
1526
+ 'bytes', 'bytearray', 'frozenset', 'complex', 'memoryview',
1527
+ 'object', 'type',
1528
+ // Common builtins
1529
+ 'print', 'len', 'range', 'abs', 'round', 'min', 'max', 'sum',
1530
+ 'sorted', 'reversed', 'enumerate', 'zip', 'map', 'filter',
1531
+ 'any', 'all', 'iter', 'next', 'hash', 'id', 'repr', 'format',
1532
+ 'chr', 'ord', 'hex', 'oct', 'bin', 'pow', 'divmod',
1533
+ 'input', 'open', 'super',
1534
+ // Introspection
1535
+ 'isinstance', 'issubclass', 'hasattr', 'getattr', 'setattr',
1536
+ 'delattr', 'callable', 'dir', 'vars', 'globals', 'locals', 'help',
1537
+ // Decorators / descriptors
1538
+ 'property', 'staticmethod', 'classmethod',
1539
+ // Exception types
1540
+ 'Exception', 'BaseException', 'ValueError', 'TypeError',
1541
+ 'KeyError', 'IndexError', 'AttributeError', 'RuntimeError',
1542
+ 'NotImplementedError', 'StopIteration', 'StopAsyncIteration',
1543
+ 'GeneratorExit', 'OSError', 'IOError', 'FileNotFoundError',
1544
+ 'FileExistsError', 'PermissionError', 'IsADirectoryError',
1545
+ 'ImportError', 'ModuleNotFoundError', 'NameError',
1546
+ 'UnboundLocalError', 'ZeroDivisionError', 'OverflowError',
1547
+ 'FloatingPointError', 'ArithmeticError', 'LookupError',
1548
+ 'RecursionError', 'MemoryError', 'SystemExit',
1549
+ 'KeyboardInterrupt', 'AssertionError',
1550
+ 'UnicodeError', 'UnicodeDecodeError', 'UnicodeEncodeError',
1551
+ 'Warning', 'DeprecationWarning', 'FutureWarning',
1552
+ 'UserWarning', 'SyntaxWarning', 'RuntimeWarning',
1553
+ 'ConnectionError', 'TimeoutError', 'BrokenPipeError',
1554
+ // Other builtins
1555
+ 'NotImplemented', 'Ellipsis', '__import__', '__name__',
1556
+ '__file__', '__doc__', '__all__', '__init__', '__new__',
1557
+ '__del__', '__repr__', '__str__', '__len__', '__iter__'
1495
1558
  ]),
1496
1559
  go: new Set([
1560
+ // Keywords
1497
1561
  'if', 'else', 'for', 'switch', 'case', 'break', 'continue',
1498
1562
  'return', 'func', 'type', 'struct', 'interface', 'package',
1499
1563
  'import', 'go', 'defer', 'select', 'chan', 'map', 'range',
1500
- 'fallthrough', 'goto', 'var', 'const', 'default'
1564
+ 'fallthrough', 'goto', 'var', 'const', 'default',
1565
+ // Builtins
1566
+ 'append', 'cap', 'close', 'copy', 'delete', 'len', 'make',
1567
+ 'new', 'panic', 'recover', 'print', 'println', 'complex',
1568
+ 'real', 'imag', 'clear', 'min', 'max',
1569
+ // Builtin types (prevent cross-language matches)
1570
+ 'error', 'string', 'bool', 'byte', 'rune',
1571
+ 'int', 'int8', 'int16', 'int32', 'int64',
1572
+ 'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr',
1573
+ 'float32', 'float64', 'complex64', 'complex128',
1574
+ 'nil', 'true', 'false', 'iota'
1501
1575
  ]),
1502
1576
  rust: new Set([
1577
+ // Keywords
1503
1578
  'if', 'else', 'for', 'while', 'loop', 'fn', 'impl', 'pub',
1504
1579
  'mod', 'use', 'crate', 'self', 'super', 'match', 'unsafe',
1505
1580
  'move', 'ref', 'mut', 'where', 'let', 'const', 'struct',
1506
1581
  'enum', 'trait', 'async', 'await', 'return', 'break',
1507
- 'continue', 'type', 'as', 'in', 'dyn', 'static'
1582
+ 'continue', 'type', 'as', 'in', 'dyn', 'static',
1583
+ // Macros (common calls that aren't project functions)
1584
+ 'println', 'print', 'eprintln', 'eprint', 'format',
1585
+ 'vec', 'panic', 'assert', 'assert_eq', 'assert_ne',
1586
+ 'debug_assert', 'debug_assert_eq', 'debug_assert_ne',
1587
+ 'todo', 'unimplemented', 'unreachable',
1588
+ 'cfg', 'derive', 'include', 'include_str', 'include_bytes',
1589
+ 'env', 'concat', 'stringify', 'file', 'line', 'column',
1590
+ // Std prelude types/traits
1591
+ 'Some', 'None', 'Ok', 'Err', 'Box', 'Vec', 'String',
1592
+ 'Option', 'Result', 'Clone', 'Copy', 'Drop',
1593
+ 'Default', 'Debug', 'Display', 'Iterator',
1594
+ 'From', 'Into', 'TryFrom', 'TryInto',
1595
+ 'AsRef', 'AsMut', 'Deref', 'DerefMut',
1596
+ 'Send', 'Sync', 'Sized', 'Unpin',
1597
+ 'Fn', 'FnMut', 'FnOnce',
1598
+ 'PartialEq', 'Eq', 'PartialOrd', 'Ord', 'Hash'
1508
1599
  ]),
1509
1600
  java: new Set([
1601
+ // Keywords
1510
1602
  'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
1511
1603
  'continue', 'return', 'class', 'interface', 'enum', 'extends',
1512
1604
  'implements', 'new', 'this', 'super', 'import', 'package',
1513
1605
  'try', 'catch', 'finally', 'throw', 'throws', 'abstract',
1514
1606
  'static', 'final', 'synchronized', 'volatile', 'transient',
1515
- 'native', 'void', 'instanceof', 'default'
1607
+ 'native', 'void', 'instanceof', 'default',
1608
+ // Primitive types
1609
+ 'boolean', 'byte', 'char', 'short', 'int', 'long',
1610
+ 'float', 'double', 'null', 'true', 'false',
1611
+ // java.lang builtins (auto-imported)
1612
+ 'System', 'String', 'Object', 'Class', 'Integer', 'Long',
1613
+ 'Double', 'Float', 'Boolean', 'Character', 'Byte', 'Short',
1614
+ 'Math', 'StringBuilder', 'StringBuffer',
1615
+ 'Thread', 'Runnable', 'Throwable',
1616
+ 'Exception', 'RuntimeException', 'Error',
1617
+ 'NullPointerException', 'IllegalArgumentException',
1618
+ 'IllegalStateException', 'IndexOutOfBoundsException',
1619
+ 'ClassCastException', 'UnsupportedOperationException',
1620
+ 'ArithmeticException', 'SecurityException',
1621
+ 'StackOverflowError', 'OutOfMemoryError',
1622
+ 'Override', 'Deprecated', 'SuppressWarnings',
1623
+ 'FunctionalInterface', 'SafeVarargs',
1624
+ 'Iterable', 'Comparable', 'AutoCloseable', 'Cloneable',
1625
+ 'Enum', 'Record', 'Void'
1516
1626
  ])
1517
1627
  };
1518
1628
  // TypeScript/TSX share JavaScript keywords
@@ -487,10 +487,25 @@ function findCallsInCode(code, parser) {
487
487
  }
488
488
  }
489
489
  }
490
- // Track non-callable assignments: count = 5, name = "hello"
490
+ // Track non-callable assignments.
491
+ // First: explicit literal check (handles dicts-with-lambdas correctly)
491
492
  if (right && isNonCallableInit(right)) {
492
493
  nonCallableNames.add(left.text);
493
494
  }
495
+ // Second: function call results are generally non-callable data
496
+ // (e.g., close = series.dropna(), result = db.query(...))
497
+ // Exception: partial() already handled above via alias tracking.
498
+ else if (right?.type === 'call' && !aliases.has(left.text)) {
499
+ nonCallableNames.add(left.text);
500
+ }
501
+ // Third: subscript/attribute access results are non-callable data
502
+ // (e.g., close = candles["close"].values, item = data[0])
503
+ else if (right && !aliases.has(left.text) &&
504
+ ['subscript', 'attribute', 'binary_operator', 'comparison_operator',
505
+ 'unary_operator', 'conditional_expression', 'await',
506
+ 'parenthesized_expression', 'not_operator', 'boolean_operator'].includes(right.type)) {
507
+ nonCallableNames.add(left.text);
508
+ }
494
509
  }
495
510
  }
496
511
 
package/mcp/server.js CHANGED
@@ -276,7 +276,7 @@ server.registerTool(
276
276
  const index = getIndex(project_dir);
277
277
  const ep = normalizeParams({ name, file, exclude, with_types, all, include_methods, include_uncertain, top });
278
278
  const { ok, result, error } = execute(index, 'about', ep);
279
- if (!ok) return toolError(error);
279
+ if (!ok) return toolResult(error); // soft error — won't kill sibling calls
280
280
  return toolResult(output.formatAbout(result, {
281
281
  allHint: 'Repeat with all=true to show all.',
282
282
  methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.'
@@ -299,7 +299,7 @@ server.registerTool(
299
299
  const index = getIndex(project_dir);
300
300
  const ep = normalizeParams({ name, file, exclude });
301
301
  const { ok, result, error } = execute(index, 'impact', ep);
302
- if (!ok) return toolError(error);
302
+ if (!ok) return toolResult(error); // soft error
303
303
  return toolResult(output.formatImpact(result));
304
304
  }
305
305
 
@@ -315,7 +315,7 @@ server.registerTool(
315
315
  const index = getIndex(project_dir);
316
316
  const ep = normalizeParams({ name, file, depth, all, include_methods, include_uncertain });
317
317
  const { ok, result, error } = execute(index, 'trace', ep);
318
- if (!ok) return toolError(error);
318
+ if (!ok) return toolResult(error); // soft error
319
319
  return toolResult(output.formatTrace(result, {
320
320
  allHint: 'Set depth to expand all children.',
321
321
  methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
@@ -347,7 +347,7 @@ server.registerTool(
347
347
  const index = getIndex(project_dir);
348
348
  const ep = normalizeParams({ name, file, exclude, include_tests, exact, in: inPath });
349
349
  const { ok, result, error } = execute(index, 'find', ep);
350
- if (!ok) return toolError(error);
350
+ if (!ok) return toolResult(error); // soft error
351
351
  return toolResult(output.formatFind(result, name, top));
352
352
  }
353
353
 
@@ -355,7 +355,7 @@ server.registerTool(
355
355
  const index = getIndex(project_dir);
356
356
  const ep = normalizeParams({ name, exclude, include_tests, code_only, context: ctxLines, in: inPath });
357
357
  const { ok, result, error } = execute(index, 'usages', ep);
358
- if (!ok) return toolError(error);
358
+ if (!ok) return toolResult(error); // soft error
359
359
  return toolResult(output.formatUsages(result, name));
360
360
  }
361
361
 
@@ -363,7 +363,7 @@ server.registerTool(
363
363
  const index = getIndex(project_dir);
364
364
  const ep = normalizeParams({ detailed, top_level, all, top });
365
365
  const { ok, result, error } = execute(index, 'toc', ep);
366
- if (!ok) return toolError(error);
366
+ if (!ok) return toolResult(error); // soft error
367
367
  return toolResult(output.formatToc(result, {
368
368
  topHint: 'Set top=N or use detailed=false for compact view.'
369
369
  }));
@@ -373,7 +373,7 @@ server.registerTool(
373
373
  const index = getIndex(project_dir);
374
374
  const ep = normalizeParams({ term, exclude, include_tests, code_only, context: ctxLines, case_sensitive, in: inPath, regex });
375
375
  const { ok, result, error } = execute(index, 'search', ep);
376
- if (!ok) return toolError(error);
376
+ if (!ok) return toolResult(error); // soft error
377
377
  return toolResult(output.formatSearch(result, term));
378
378
  }
379
379
 
@@ -381,7 +381,7 @@ server.registerTool(
381
381
  const index = getIndex(project_dir);
382
382
  const ep = normalizeParams({ name, calls_only });
383
383
  const { ok, result, error } = execute(index, 'tests', ep);
384
- if (!ok) return toolError(error);
384
+ if (!ok) return toolResult(error); // soft error
385
385
  return toolResult(output.formatTests(result, name));
386
386
  }
387
387
 
@@ -389,7 +389,7 @@ server.registerTool(
389
389
  const index = getIndex(project_dir);
390
390
  const ep = normalizeParams({ exclude, in: inPath, include_exported, include_decorated, include_tests });
391
391
  const { ok, result, error } = execute(index, 'deadcode', ep);
392
- if (!ok) return toolError(error);
392
+ if (!ok) return toolResult(error); // soft error
393
393
  return toolResult(output.formatDeadcode(result, {
394
394
  top: top || 0,
395
395
  decoratedHint: !include_decorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use include_decorated=true to include them.` : undefined,
@@ -402,28 +402,28 @@ server.registerTool(
402
402
  case 'imports': {
403
403
  const index = getIndex(project_dir);
404
404
  const { ok, result, error } = execute(index, 'imports', { file });
405
- if (!ok) return toolError(error);
405
+ if (!ok) return toolResult(error); // soft error
406
406
  return toolResult(output.formatImports(result, file));
407
407
  }
408
408
 
409
409
  case 'exporters': {
410
410
  const index = getIndex(project_dir);
411
411
  const { ok, result, error } = execute(index, 'exporters', { file });
412
- if (!ok) return toolError(error);
412
+ if (!ok) return toolResult(error); // soft error
413
413
  return toolResult(output.formatExporters(result, file));
414
414
  }
415
415
 
416
416
  case 'file_exports': {
417
417
  const index = getIndex(project_dir);
418
418
  const { ok, result, error } = execute(index, 'fileExports', { file });
419
- if (!ok) return toolError(error);
419
+ if (!ok) return toolResult(error); // soft error
420
420
  return toolResult(output.formatFileExports(result, file));
421
421
  }
422
422
 
423
423
  case 'graph': {
424
424
  const index = getIndex(project_dir);
425
425
  const { ok, result, error } = execute(index, 'graph', { file, direction, depth, all });
426
- if (!ok) return toolError(error);
426
+ if (!ok) return toolResult(error); // soft error
427
427
  return toolResult(output.formatGraph(result, {
428
428
  showAll: all || depth !== undefined,
429
429
  maxDepth: depth ?? 2, file,
@@ -437,7 +437,7 @@ server.registerTool(
437
437
  case 'verify': {
438
438
  const index = getIndex(project_dir);
439
439
  const { ok, result, error } = execute(index, 'verify', { name, file });
440
- if (!ok) return toolError(error);
440
+ if (!ok) return toolResult(error); // soft error
441
441
  return toolResult(output.formatVerify(result));
442
442
  }
443
443
 
@@ -445,14 +445,14 @@ server.registerTool(
445
445
  const index = getIndex(project_dir);
446
446
  const ep = normalizeParams({ name, add_param, remove_param, rename_to, default_value, file });
447
447
  const { ok, result, error } = execute(index, 'plan', ep);
448
- if (!ok) return toolError(error);
448
+ if (!ok) return toolResult(error); // soft error
449
449
  return toolResult(output.formatPlan(result));
450
450
  }
451
451
 
452
452
  case 'diff_impact': {
453
453
  const index = getIndex(project_dir);
454
454
  const { ok, result, error } = execute(index, 'diffImpact', { base, staged, file });
455
- if (!ok) return toolError(error);
455
+ if (!ok) return toolResult(error); // soft error — e.g. "not a git repo"
456
456
  return toolResult(output.formatDiffImpact(result));
457
457
  }
458
458
 
@@ -461,28 +461,28 @@ server.registerTool(
461
461
  case 'typedef': {
462
462
  const index = getIndex(project_dir);
463
463
  const { ok, result, error } = execute(index, 'typedef', { name, exact });
464
- if (!ok) return toolError(error);
464
+ if (!ok) return toolResult(error); // soft error
465
465
  return toolResult(output.formatTypedef(result, name));
466
466
  }
467
467
 
468
468
  case 'stacktrace': {
469
469
  const index = getIndex(project_dir);
470
470
  const { ok, result, error } = execute(index, 'stacktrace', { stack });
471
- if (!ok) return toolError(error);
471
+ if (!ok) return toolResult(error); // soft error
472
472
  return toolResult(output.formatStackTrace(result));
473
473
  }
474
474
 
475
475
  case 'api': {
476
476
  const index = getIndex(project_dir);
477
477
  const { ok, result, error } = execute(index, 'api', { file });
478
- if (!ok) return toolError(error);
478
+ if (!ok) return toolResult(error); // soft error
479
479
  return toolResult(output.formatApi(result, file || '.'));
480
480
  }
481
481
 
482
482
  case 'stats': {
483
483
  const index = getIndex(project_dir);
484
484
  const { ok, result, error } = execute(index, 'stats', { functions });
485
- if (!ok) return toolError(error);
485
+ if (!ok) return toolResult(error); // soft error
486
486
  return toolResult(output.formatStats(result, { top: top || 0 }));
487
487
  }
488
488
 
@@ -494,7 +494,7 @@ server.registerTool(
494
494
  const index = getIndex(project_dir);
495
495
  const ep = normalizeParams({ name, file, all });
496
496
  const { ok, result, error } = execute(index, 'fn', ep);
497
- if (!ok) return toolError(error);
497
+ if (!ok) return toolResult(error); // soft error
498
498
  // MCP path security: validate all result files are within project root
499
499
  for (const entry of result.entries) {
500
500
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
@@ -527,7 +527,7 @@ server.registerTool(
527
527
  const index = getIndex(project_dir);
528
528
  const ep = normalizeParams({ file, range });
529
529
  const { ok, result, error } = execute(index, 'lines', ep);
530
- if (!ok) return toolError(error);
530
+ if (!ok) return toolResult(error); // soft error
531
531
  // MCP path security: validate file is within project root
532
532
  const check = resolveAndValidatePath(index, result.relativePath);
533
533
  if (typeof check !== 'string') return check;
@@ -545,7 +545,7 @@ server.registerTool(
545
545
  itemCount: lookup.itemCount, symbolName: lookup.symbolName,
546
546
  validateRoot: true
547
547
  });
548
- if (!ok) return toolError(error);
548
+ if (!ok) return toolResult(error); // soft error
549
549
  return toolResult(result.text);
550
550
  }
551
551
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.27",
3
+ "version": "3.7.29",
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",
@@ -9,7 +9,8 @@
9
9
  "ucn-mcp": "mcp/server.js"
10
10
  },
11
11
  "scripts": {
12
- "test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/accuracy.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js"
12
+ "test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/accuracy.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js",
13
+ "benchmark:agent": "node test/agent-understanding-benchmark.js"
13
14
  },
14
15
  "keywords": [
15
16
  "mcp",