ucn 3.4.6 → 3.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -81,15 +81,15 @@ ucn deadcode --exclude=test # Skip test files (most useful)
81
81
 
82
82
  | Situation | Command | What it does |
83
83
  |-----------|---------|-------------|
84
- | Need function + all its helpers inline | `ucn smart <name>` | Returns function source with every helper it calls expanded below it |
84
+ | Need function + all its helpers inline | `ucn smart <name>` | Returns function source with every helper it calls expanded below it. Use instead of `about` when you need code, not metadata |
85
85
  | Checking if a refactor broke signatures | `ucn verify <name>` | Validates all call sites match the function's parameter count |
86
86
  | Understanding a file's role in the project | `ucn imports <file>` | What it depends on |
87
87
  | Understanding who depends on a file | `ucn exporters <file>` | Which files import it |
88
88
  | Quick project overview | `ucn toc` | Every file with function/class counts and line counts |
89
89
  | Finding all usages (not just calls) | `ucn usages <name>` | Groups into: definitions, calls, imports, type references |
90
- | Finding related code to refactor together | `ucn related <name>` | Functions sharing dependencies or in same file |
90
+ | Finding sibling/related functions | `ucn related <name>` | Name-based + structural matching (same file, shared deps). Not semantic — best for parse/format pairs |
91
91
  | Preview a rename or param change | `ucn plan <name> --rename-to=new_name` | Shows what would change without doing it |
92
- | Dependency tree for a file | `ucn graph <file> --depth=2` | Visual import tree |
92
+ | File-level dependency tree | `ucn graph <file> --depth=1` | Visual import tree. Can be noisy — use depth=1 for large/tightly-coupled projects. For function-level flow, use `trace` instead |
93
93
  | Find which tests cover a function | `ucn tests <name>` | Test files and test function names |
94
94
 
95
95
  ## Command Format
package/core/discovery.js CHANGED
@@ -95,7 +95,7 @@ const TEST_PATTERNS = {
95
95
  python: [
96
96
  /^test_.*\.py$/,
97
97
  /.*_test\.py$/,
98
- /\/tests?\//
98
+ /(^|\/)tests?\//
99
99
  ],
100
100
  go: [
101
101
  /.*_test\.go$/
@@ -107,7 +107,7 @@ const TEST_PATTERNS = {
107
107
  ],
108
108
  rust: [
109
109
  /.*_test\.rs$/,
110
- /\/tests\//
110
+ /(^|\/)tests\//
111
111
  ]
112
112
  };
113
113
 
package/core/project.js CHANGED
@@ -2127,12 +2127,22 @@ class ProjectIndex {
2127
2127
 
2128
2128
  // Python: Magic/dunder methods are called by the interpreter, not user code
2129
2129
  // test_* functions/methods are called by pytest/unittest via reflection
2130
+ // setUp/tearDown are unittest.TestCase framework methods called by test runner
2131
+ // pytest_* are pytest plugin hooks called by the framework
2130
2132
  const isPythonEntryPoint = lang === 'python' &&
2131
- (/^__\w+__$/.test(name) || /^test_/.test(name));
2133
+ (/^__\w+__$/.test(name) || /^test_/.test(name) ||
2134
+ /^(setUp|tearDown)(Class|Module)?$/.test(name) ||
2135
+ /^pytest_/.test(name));
2132
2136
 
2133
- // Rust: main() is entry point, #[test] functions are called by test runner
2137
+ // Rust: main() is entry point, #[test] and #[bench] functions are called by test/bench runner
2134
2138
  const isRustEntryPoint = lang === 'rust' &&
2135
- (name === 'main' || mods.includes('test'));
2139
+ (name === 'main' || mods.includes('test') || mods.includes('bench'));
2140
+
2141
+ // Rust: trait impl methods are invoked via trait dispatch, not direct calls
2142
+ // They can never be "dead" - the trait contract requires them to exist
2143
+ // className for trait impls contains " for " (e.g., "PartialEq for Glob")
2144
+ const isRustTraitImpl = lang === 'rust' && symbol.isMethod &&
2145
+ symbol.className && symbol.className.includes(' for ');
2136
2146
 
2137
2147
  // Go: Test*, Benchmark*, Example* functions are called by go test
2138
2148
  const isGoTestFunc = lang === 'go' &&
@@ -2141,6 +2151,15 @@ class ProjectIndex {
2141
2151
  // Java: @Test annotated methods are called by JUnit
2142
2152
  const isJavaTestMethod = lang === 'java' && mods.includes('test');
2143
2153
 
2154
+ // Java: @Override methods are invoked via polymorphic dispatch
2155
+ // They implement interface/superclass contracts and can't be dead
2156
+ const isJavaOverride = lang === 'java' && mods.includes('override');
2157
+
2158
+ // Skip trait impl / @Override methods entirely - they're required by the type system
2159
+ if (isRustTraitImpl || isJavaOverride) {
2160
+ continue;
2161
+ }
2162
+
2144
2163
  const isEntryPoint = isGoEntryPoint || isGoTestFunc ||
2145
2164
  isJavaEntryPoint || isJavaTestMethod ||
2146
2165
  isPythonEntryPoint || isRustEntryPoint;
package/mcp/server.js CHANGED
@@ -807,7 +807,7 @@ server.registerTool(
807
807
  server.registerTool(
808
808
  'ucn_about',
809
809
  {
810
- description: 'Everything about a code symbol: definition, source code, callers, callees, tests. First stop when investigating any function or class. Works on JS/TS, Python, Go, Rust, Java.',
810
+ description: 'Everything about a symbol in one call: definition, source code, callers, callees, tests. START HERE when investigating any function or class — replaces 3-4 grep+read cycles. For narrower views, use ucn_context (callers/callees only), ucn_smart (code + dependencies), or ucn_impact (call sites for refactoring).',
811
811
  inputSchema: z.object({
812
812
  project_dir: projectDirParam,
813
813
  name: nameParam,
@@ -832,7 +832,7 @@ server.registerTool(
832
832
  server.registerTool(
833
833
  'ucn_context',
834
834
  {
835
- description: 'Quick view of who calls a function and what it calls. Shows callers and callees with file locations and call weights.',
835
+ description: 'Lightweight caller/callee list with numbered items. Use when you just need "who calls X and what does X call" without full source code. Items are numbered use ucn_expand to drill into any item. For the full picture (code + tests + everything), use ucn_about instead.',
836
836
  inputSchema: z.object({
837
837
  project_dir: projectDirParam,
838
838
  name: nameParam,
@@ -866,7 +866,7 @@ server.registerTool(
866
866
  server.registerTool(
867
867
  'ucn_impact',
868
868
  {
869
- description: 'Before changing a function, see every call site grouped by file. Shows arguments used at each call site. Essential for signature changes.',
869
+ description: 'Every call site of a function, grouped by file, with the actual arguments used at each site. Use BEFORE changing a function signature — shows exactly what will break. For a lighter caller list without arguments, use ucn_context.',
870
870
  inputSchema: z.object({
871
871
  project_dir: projectDirParam,
872
872
  name: nameParam,
@@ -890,7 +890,7 @@ server.registerTool(
890
890
  server.registerTool(
891
891
  'ucn_smart',
892
892
  {
893
- description: 'Function source code with all its dependencies expanded inline. Everything you need to understand or modify a function in one response.',
893
+ description: 'Function source code with all its dependencies expanded inline. Use when you need to read or modify a function and want its helpers included — saves multiple file reads. For call relationships without source code, use ucn_context.',
894
894
  inputSchema: z.object({
895
895
  project_dir: projectDirParam,
896
896
  name: nameParam,
@@ -922,7 +922,7 @@ server.registerTool(
922
922
  server.registerTool(
923
923
  'ucn_trace',
924
924
  {
925
- description: 'Call tree visualization showing execution flow. Traces what a function calls, what those call, etc. Depth-limited.',
925
+ description: 'Call tree visualization showing execution flow from a function downward. Maps architecture — shows which modules a pipeline touches. For file-level dependency trees, use ucn_graph instead.',
926
926
  inputSchema: z.object({
927
927
  project_dir: projectDirParam,
928
928
  name: nameParam,
@@ -1180,7 +1180,7 @@ server.registerTool(
1180
1180
  server.registerTool(
1181
1181
  'ucn_related',
1182
1182
  {
1183
- description: 'Find functions related to a symbol: same file, similar names, shared callers/callees. Useful for discovering associated code.',
1183
+ description: 'Find structurally related functions: same file, similar names, shared callers/callees. Results are name-based and structural, not semantic — best for finding sibling functions (e.g. parse/format pairs) rather than conceptually related code.',
1184
1184
  inputSchema: z.object({
1185
1185
  project_dir: projectDirParam,
1186
1186
  name: nameParam,
@@ -1204,7 +1204,7 @@ server.registerTool(
1204
1204
  server.registerTool(
1205
1205
  'ucn_graph',
1206
1206
  {
1207
- description: 'Dependency graph for a file. Shows import/export tree as a visual hierarchy.',
1207
+ description: 'File-level dependency graph showing import/export relationships between files. Best for understanding module structure. Can be noisy in tightly-coupled projects — use depth=1 for large codebases. For function-level execution flow, use ucn_trace instead.',
1208
1208
  inputSchema: z.object({
1209
1209
  project_dir: projectDirParam,
1210
1210
  file: z.string().describe('File path (relative to project root or absolute) to graph dependencies for'),
@@ -1490,7 +1490,7 @@ server.registerTool(
1490
1490
  server.registerTool(
1491
1491
  'ucn_api',
1492
1492
  {
1493
- description: 'Show exported/public symbols in the project or a specific file. Lists the public API surface.',
1493
+ description: 'Show exported/public symbols in the project. Works best with JS/TS (export keyword), Go (capitalized names), Rust (pub), Java (public). For Python, requires __all__ — projects without it will return empty results. Use ucn_toc for a general overview instead.',
1494
1494
  inputSchema: z.object({
1495
1495
  project_dir: projectDirParam,
1496
1496
  file: z.string().optional().describe('Optional file path to show exports for (relative to project root)')
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.4.6",
3
+ "version": "3.4.8",
4
4
  "description": "Code navigation built by AI, for AI. Reduces context usage when working with large codebases.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "ucn": "./cli/index.js",
8
- "ucn-mcp": "./mcp/server.js"
7
+ "ucn": "cli/index.js",
8
+ "ucn-mcp": "mcp/server.js"
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test test/parser.test.js"
@@ -6631,5 +6631,252 @@ module.exports = { usedUtil };
6631
6631
  });
6632
6632
  });
6633
6633
 
6634
+ // Regression: Rust trait impl methods should not appear as deadcode
6635
+ describe('Regression: deadcode skips Rust trait impl methods', () => {
6636
+ it('should not report trait impl methods as dead code', () => {
6637
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-rust-trait-'));
6638
+ try {
6639
+ fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"\nversion = "0.1.0"');
6640
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
6641
+ // A struct with an inherent impl and a trait impl
6642
+ fs.writeFileSync(path.join(tmpDir, 'src', 'main.rs'), `
6643
+ struct Foo {
6644
+ val: i32,
6645
+ }
6646
+
6647
+ impl Foo {
6648
+ fn new(val: i32) -> Self {
6649
+ Foo { val }
6650
+ }
6651
+
6652
+ fn unused_method(&self) -> i32 {
6653
+ self.val
6654
+ }
6655
+ }
6656
+
6657
+ impl std::fmt::Display for Foo {
6658
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6659
+ write!(f, "{}", self.val)
6660
+ }
6661
+ }
6662
+
6663
+ impl PartialEq for Foo {
6664
+ fn eq(&self, other: &Self) -> bool {
6665
+ self.val == other.val
6666
+ }
6667
+ }
6668
+
6669
+ fn main() {
6670
+ let f = Foo::new(42);
6671
+ }
6672
+ `);
6673
+ const idx = new ProjectIndex(tmpDir);
6674
+ idx.build(null, { quiet: true });
6675
+ const dead = idx.deadcode();
6676
+ const deadNames = dead.map(d => d.name);
6677
+
6678
+ // Trait impl methods should NOT appear
6679
+ assert.ok(!deadNames.includes('fmt'), 'fmt (trait impl) should not be dead code');
6680
+ assert.ok(!deadNames.includes('eq'), 'eq (trait impl) should not be dead code');
6681
+
6682
+ // Genuinely unused inherent method SHOULD appear
6683
+ assert.ok(deadNames.includes('unused_method'), 'unused_method should be dead code');
6684
+ } finally {
6685
+ fs.rmSync(tmpDir, { recursive: true, force: true });
6686
+ }
6687
+ });
6688
+ });
6689
+
6690
+ // Regression: Rust #[bench] functions should be treated as entry points
6691
+ describe('Regression: deadcode treats Rust #[bench] as entry points', () => {
6692
+ it('should not report #[bench] functions as dead code', () => {
6693
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-rust-bench-'));
6694
+ try {
6695
+ fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"\nversion = "0.1.0"');
6696
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
6697
+ fs.mkdirSync(path.join(tmpDir, 'benches'), { recursive: true });
6698
+ fs.writeFileSync(path.join(tmpDir, 'src', 'main.rs'), `
6699
+ fn main() {}
6700
+
6701
+ fn helper() -> i32 { 42 }
6702
+ `);
6703
+ fs.writeFileSync(path.join(tmpDir, 'benches', 'my_bench.rs'), `
6704
+ #![feature(test)]
6705
+ extern crate test;
6706
+ use test::Bencher;
6707
+
6708
+ #[bench]
6709
+ fn bench_something(b: &mut Bencher) {
6710
+ b.iter(|| 1 + 1);
6711
+ }
6712
+
6713
+ fn unused_bench_helper() -> i32 {
6714
+ 42
6715
+ }
6716
+ `);
6717
+ const idx = new ProjectIndex(tmpDir);
6718
+ idx.build(null, { quiet: true });
6719
+ const dead = idx.deadcode();
6720
+ const deadNames = dead.map(d => d.name);
6721
+
6722
+ // #[bench] should NOT appear as dead code
6723
+ assert.ok(!deadNames.includes('bench_something'), 'bench_something should not be dead code');
6724
+
6725
+ // Genuinely unused function SHOULD appear
6726
+ assert.ok(deadNames.includes('unused_bench_helper'), 'unused_bench_helper should be dead code');
6727
+ } finally {
6728
+ fs.rmSync(tmpDir, { recursive: true, force: true });
6729
+ }
6730
+ });
6731
+ });
6732
+
6733
+ // Regression: test file patterns should match relative paths starting with tests/
6734
+ describe('Regression: test file patterns match relative paths', () => {
6735
+ it('should detect tests/ at start of relative path for Python', () => {
6736
+ const { isTestFile } = require('../core/discovery');
6737
+ // Relative paths starting with tests/ should match
6738
+ assert.ok(isTestFile('tests/test_app.py', 'python'),
6739
+ 'tests/test_app.py should be a test file');
6740
+ assert.ok(isTestFile('tests/helpers/factory.py', 'python'),
6741
+ 'tests/helpers/factory.py should be a test file');
6742
+ // Subdirectory should still work
6743
+ assert.ok(isTestFile('src/tests/test_util.py', 'python'),
6744
+ 'src/tests/test_util.py should be a test file');
6745
+ // Non-test paths should not match
6746
+ assert.ok(!isTestFile('src/utils.py', 'python'),
6747
+ 'src/utils.py should not be a test file');
6748
+ });
6749
+
6750
+ it('should detect tests/ at start of relative path for Rust', () => {
6751
+ const { isTestFile } = require('../core/discovery');
6752
+ assert.ok(isTestFile('tests/integration.rs', 'rust'),
6753
+ 'tests/integration.rs should be a test file');
6754
+ assert.ok(isTestFile('tests/examples/hello.rs', 'rust'),
6755
+ 'tests/examples/hello.rs should be a test file');
6756
+ // Non-test paths should not match
6757
+ assert.ok(!isTestFile('src/lib.rs', 'rust'),
6758
+ 'src/lib.rs should not be a test file');
6759
+ });
6760
+ });
6761
+
6762
+ // Regression: Java @Override methods should not appear as deadcode
6763
+ describe('Regression: deadcode skips Java @Override methods', () => {
6764
+ it('should not report @Override methods as dead code', () => {
6765
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-java-override-'));
6766
+ try {
6767
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
6768
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
6769
+ fs.writeFileSync(path.join(tmpDir, 'src', 'MyClass.java'), `
6770
+ public class MyClass implements Runnable {
6771
+ @Override
6772
+ public void run() {
6773
+ System.out.println("running");
6774
+ }
6775
+
6776
+ @Override
6777
+ public String toString() {
6778
+ return "MyClass";
6779
+ }
6780
+
6781
+ void unusedMethod() {
6782
+ System.out.println("unused");
6783
+ }
6784
+
6785
+ public static void main(String[] args) {
6786
+ new MyClass().run();
6787
+ }
6788
+ }
6789
+ `);
6790
+ const idx = new ProjectIndex(tmpDir);
6791
+ idx.build(null, { quiet: true });
6792
+ const dead = idx.deadcode();
6793
+ const deadNames = dead.map(d => d.name);
6794
+
6795
+ // @Override methods should NOT appear
6796
+ assert.ok(!deadNames.includes('run'), 'run (@Override) should not be dead code');
6797
+ assert.ok(!deadNames.includes('toString'), 'toString (@Override) should not be dead code');
6798
+
6799
+ // Genuinely unused method SHOULD appear
6800
+ assert.ok(deadNames.includes('unusedMethod'), 'unusedMethod should be dead code');
6801
+ } finally {
6802
+ fs.rmSync(tmpDir, { recursive: true, force: true });
6803
+ }
6804
+ });
6805
+ });
6806
+
6807
+ // Regression: Python setUp/tearDown and pytest_* should be entry points
6808
+ describe('Regression: deadcode treats Python framework methods as entry points', () => {
6809
+ it('should not report setUp/tearDown as dead code', () => {
6810
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-py-setup-'));
6811
+ try {
6812
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
6813
+ // setUp/tearDown in a non-test file (e.g., scripts/ directory)
6814
+ fs.writeFileSync(path.join(tmpDir, 'release_tests.py'), `
6815
+ import unittest
6816
+
6817
+ class TestFoo(unittest.TestCase):
6818
+ def setUp(self):
6819
+ self.x = 42
6820
+
6821
+ def tearDown(self):
6822
+ pass
6823
+
6824
+ def test_something(self):
6825
+ assert self.x == 42
6826
+ `);
6827
+ // Separate non-test file with genuinely unused code
6828
+ fs.writeFileSync(path.join(tmpDir, 'utils.py'), `
6829
+ def unused_helper():
6830
+ return 1
6831
+ `);
6832
+ const idx = new ProjectIndex(tmpDir);
6833
+ idx.build(null, { quiet: true });
6834
+ const dead = idx.deadcode();
6835
+ const deadNames = dead.map(d => d.name);
6836
+
6837
+ // Framework methods should NOT appear (even in non-test files)
6838
+ assert.ok(!deadNames.includes('setUp'), 'setUp should not be dead code');
6839
+ assert.ok(!deadNames.includes('tearDown'), 'tearDown should not be dead code');
6840
+ assert.ok(!deadNames.includes('test_something'), 'test_something should not be dead code');
6841
+
6842
+ // Genuinely unused function SHOULD appear
6843
+ assert.ok(deadNames.includes('unused_helper'), 'unused_helper should be dead code');
6844
+ } finally {
6845
+ fs.rmSync(tmpDir, { recursive: true, force: true });
6846
+ }
6847
+ });
6848
+
6849
+ it('should not report pytest_* hooks as dead code', () => {
6850
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-py-pytest-'));
6851
+ try {
6852
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
6853
+ fs.writeFileSync(path.join(tmpDir, 'conftest.py'), `
6854
+ def pytest_configure(config):
6855
+ config.addinivalue_line("markers", "slow: slow test")
6856
+
6857
+ def pytest_collection_modifyitems(config, items):
6858
+ pass
6859
+
6860
+ def unused_function():
6861
+ return 1
6862
+ `);
6863
+ const idx = new ProjectIndex(tmpDir);
6864
+ idx.build(null, { quiet: true });
6865
+ const dead = idx.deadcode();
6866
+ const deadNames = dead.map(d => d.name);
6867
+
6868
+ // pytest hooks should NOT appear
6869
+ assert.ok(!deadNames.includes('pytest_configure'), 'pytest_configure should not be dead code');
6870
+ assert.ok(!deadNames.includes('pytest_collection_modifyitems'),
6871
+ 'pytest_collection_modifyitems should not be dead code');
6872
+
6873
+ // Genuinely unused function SHOULD appear
6874
+ assert.ok(deadNames.includes('unused_function'), 'unused_function should be dead code');
6875
+ } finally {
6876
+ fs.rmSync(tmpDir, { recursive: true, force: true });
6877
+ }
6878
+ });
6879
+ });
6880
+
6634
6881
  console.log('UCN v3 Test Suite');
6635
6882
  console.log('Run with: node --test test/parser.test.js');