ucn 3.4.5 → 3.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/README.md CHANGED
@@ -155,125 +155,11 @@ ucn verify the_function # Did all call sites survive?
155
155
  ucn deadcode --exclude=test # What can be deleted?
156
156
  ucn toc # Project overview
157
157
  ```
158
+
158
159
  ## Supported Languages
159
160
 
160
161
  JavaScript, TypeScript, Python, Go, Rust, Java
161
162
 
162
- ## Install
163
-
164
- ```bash
165
- npm install -g ucn
166
- ```
167
-
168
- ### MCP Server
169
-
170
- UCN includes a built-in [MCP](https://modelcontextprotocol.io) server, so any MCP-compatible AI client can use it as a tool.
171
-
172
- **Claude Code** (`~/.claude/mcp-config.json`):
173
- ```json
174
- {
175
- "mcpServers": {
176
- "ucn": {
177
- "command": "npx",
178
- "args": ["-y", "ucn", "--mcp"]
179
- }
180
- }
181
- }
182
- ```
183
-
184
- **Claude Desktop** (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
185
- ```json
186
- {
187
- "mcpServers": {
188
- "ucn": {
189
- "command": "npx",
190
- "args": ["-y", "ucn", "--mcp"]
191
- }
192
- }
193
- }
194
- ```
195
-
196
- **Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json` in project):
197
- ```json
198
- {
199
- "mcpServers": {
200
- "ucn": {
201
- "command": "npx",
202
- "args": ["-y", "ucn", "--mcp"]
203
- }
204
- }
205
- }
206
- ```
207
-
208
- **Windsurf** (`~/.codeium/windsurf/mcp_config.json`):
209
- ```json
210
- {
211
- "mcpServers": {
212
- "ucn": {
213
- "command": "npx",
214
- "args": ["-y", "ucn", "--mcp"]
215
- }
216
- }
217
- }
218
- ```
219
-
220
- **VS Code Copilot** (`.vscode/mcp.json`):
221
- ```json
222
- {
223
- "servers": {
224
- "ucn": {
225
- "type": "stdio",
226
- "command": "npx",
227
- "args": ["-y", "ucn", "--mcp"]
228
- }
229
- }
230
- }
231
- ```
232
-
233
- **Zed** (Settings > `settings.json`):
234
- ```json
235
- {
236
- "context_servers": {
237
- "ucn": {
238
- "command": "npx",
239
- "args": ["-y", "ucn", "--mcp"]
240
- }
241
- }
242
- }
243
- ```
244
-
245
- The MCP server exposes 27 tools: `ucn_about`, `ucn_context`, `ucn_impact`, `ucn_smart`, `ucn_trace`, `ucn_find`, `ucn_usages`, `ucn_toc`, `ucn_deadcode`, `ucn_fn`, `ucn_class`, `ucn_verify`, `ucn_imports`, `ucn_exporters`, `ucn_tests`, `ucn_related`, `ucn_graph`, `ucn_file_exports`, `ucn_search`, `ucn_plan`, `ucn_typedef`, `ucn_stacktrace`, `ucn_example`, `ucn_expand`, `ucn_lines`, `ucn_api`, `ucn_stats`.
246
-
247
- ### Claude Code Skill (alternative)
248
-
249
- To use UCN as a skill in Claude Code (alternative to MCP):
250
-
251
- ```bash
252
- mkdir -p ~/.claude/skills
253
-
254
- # If installed via npm:
255
- cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.claude/skills/
256
-
257
- # If cloned from git:
258
- git clone https://github.com/mleoca/ucn.git
259
- cp -r ucn/.claude/skills/ucn ~/.claude/skills/
260
- ```
261
-
262
- ### Codex (optional)
263
-
264
- To use UCN as a skill in OpenAI Codex:
265
-
266
- ```bash
267
- mkdir -p ~/.agents/skills
268
-
269
- # If installed via npm:
270
- cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.agents/skills/
271
-
272
- # If cloned from git:
273
- git clone https://github.com/mleoca/ucn.git
274
- cp -r ucn/.claude/skills/ucn ~/.agents/skills/
275
- ```
276
-
277
163
  ## Usage
278
164
 
279
165
  ```
@@ -368,6 +254,87 @@ Quick Start:
368
254
  ucn --interactive # Multiple queries
369
255
  ```
370
256
 
257
+ ## Install
258
+
259
+ ```bash
260
+ npm install -g ucn
261
+ ```
262
+
263
+ ### MCP Server
264
+
265
+ UCN includes a built-in [MCP](https://modelcontextprotocol.io) server, so any MCP-compatible AI client can use it as a tool. It exposes 27 tools (`ucn_about`, `ucn_context`, `ucn_impact`, `ucn_smart`, `ucn_trace`, `ucn_find`, `ucn_usages`, `ucn_toc`, `ucn_deadcode`, `ucn_fn`, `ucn_class`, `ucn_verify`, `ucn_imports`, `ucn_exporters`, `ucn_tests`, `ucn_related`, `ucn_graph`, `ucn_file_exports`, `ucn_search`, `ucn_plan`, `ucn_typedef`, `ucn_stacktrace`, `ucn_example`, `ucn_expand`, `ucn_lines`, `ucn_api`, `ucn_stats`).
266
+
267
+ **One-line setup** (for clients that support it):
268
+
269
+ | Client | Command |
270
+ |--------|---------|
271
+ | Claude Code | `claude mcp add ucn -- npx -y ucn --mcp` |
272
+ | OpenAI Codex CLI | `codex mcp add ucn -- npx -y ucn --mcp` |
273
+ | VS Code Copilot | `code --add-mcp '{"name":"ucn","command":"npx","args":["-y","ucn","--mcp"]}'` |
274
+
275
+ **Manual config** — add to the appropriate config file for your client:
276
+
277
+ | Client | Config file |
278
+ |--------|-------------|
279
+ | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) |
280
+ | Cursor | `~/.cursor/mcp.json` or `.cursor/mcp.json` in project |
281
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
282
+ | Cline | VS Code sidebar > MCP Servers > Configure |
283
+ | Claude Code | `~/.claude/mcp-config.json` |
284
+
285
+ ```json
286
+ {
287
+ "mcpServers": {
288
+ "ucn": {
289
+ "command": "npx",
290
+ "args": ["-y", "ucn", "--mcp"]
291
+ }
292
+ }
293
+ }
294
+ ```
295
+
296
+ <details>
297
+ <summary>VS Code Copilot uses a slightly different format (<code>.vscode/mcp.json</code>)</summary>
298
+
299
+ ```json
300
+ {
301
+ "servers": {
302
+ "ucn": {
303
+ "type": "stdio",
304
+ "command": "npx",
305
+ "args": ["-y", "ucn", "--mcp"]
306
+ }
307
+ }
308
+ }
309
+ ```
310
+ </details>
311
+
312
+ ### Claude Code Skill (alternative to MCP)
313
+
314
+ ```bash
315
+ mkdir -p ~/.claude/skills
316
+
317
+ # If installed via npm:
318
+ cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.claude/skills/
319
+
320
+ # If cloned from git:
321
+ git clone https://github.com/mleoca/ucn.git
322
+ cp -r ucn/.claude/skills/ucn ~/.claude/skills/
323
+ ```
324
+
325
+ ### Codex Skill (alternative to MCP)
326
+
327
+ ```bash
328
+ mkdir -p ~/.agents/skills
329
+
330
+ # If installed via npm:
331
+ cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.agents/skills/
332
+
333
+ # If cloned from git:
334
+ git clone https://github.com/mleoca/ucn.git
335
+ cp -r ucn/.claude/skills/ucn ~/.agents/skills/
336
+ ```
337
+
371
338
  ## License
372
339
 
373
340
  MIT
package/core/project.js CHANGED
@@ -1803,7 +1803,7 @@ class ProjectIndex {
1803
1803
  // Find all test files
1804
1804
  const testFiles = [];
1805
1805
  for (const [filePath, fileEntry] of this.files) {
1806
- if (isTestFile(filePath, fileEntry.language)) {
1806
+ if (isTestFile(fileEntry.relativePath, fileEntry.language)) {
1807
1807
  testFiles.push({ path: filePath, entry: fileEntry });
1808
1808
  }
1809
1809
  }
@@ -2108,14 +2108,13 @@ class ProjectIndex {
2108
2108
  continue;
2109
2109
  }
2110
2110
 
2111
+ const fileEntry = this.files.get(symbol.file);
2112
+ const lang = fileEntry?.language;
2113
+
2111
2114
  // Skip test files unless requested
2112
- if (!options.includeTests && isTestFile(symbol.file, symbol.language)) {
2115
+ if (!options.includeTests && isTestFile(symbol.relativePath, lang)) {
2113
2116
  continue;
2114
2117
  }
2115
-
2116
- // Check if exported
2117
- const fileEntry = this.files.get(symbol.file);
2118
- const lang = fileEntry?.language;
2119
2118
  const mods = symbol.modifiers || [];
2120
2119
 
2121
2120
  // Language-specific entry points (called by runtime, no AST-visible callers)
@@ -3653,7 +3652,7 @@ class ProjectIndex {
3653
3652
  totalState += state.length;
3654
3653
  totalLines += fileEntry.lines;
3655
3654
  totalDynamic += fileEntry.dynamicImports || 0;
3656
- if (isTestFile(filePath)) totalTests += 1;
3655
+ if (isTestFile(fileEntry.relativePath, fileEntry.language)) totalTests += 1;
3657
3656
 
3658
3657
  const entry = {
3659
3658
  file: fileEntry.relativePath,
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.5",
3
+ "version": "3.4.7",
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"
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * MCP Server Edge Case Test Suite
5
5
  *
6
- * Tests all 23 UCN MCP tools with null/crash safety, input validation,
6
+ * Tests UCN MCP tools with null/crash safety, input validation,
7
7
  * and normal operation edge cases.
8
8
  *
9
9
  * Communicates with the MCP server over stdio using newline-delimited JSON-RPC.
@@ -13,7 +13,7 @@ const { spawn } = require('child_process');
13
13
  const path = require('path');
14
14
 
15
15
  const SERVER_PATH = path.join(__dirname, '..', 'mcp', 'server.js');
16
- const PROJECT_DIR = '/Users/mihail/ucn';
16
+ const PROJECT_DIR = path.resolve(__dirname, '..');
17
17
  const TIMEOUT_MS = 30000;
18
18
 
19
19
  // ============================================================================
@@ -242,6 +242,25 @@ const tests = [
242
242
  args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
243
243
  },
244
244
 
245
+ {
246
+ category: 'Null/Crash Safety',
247
+ tool: 'ucn_api',
248
+ desc: 'nonexistent file',
249
+ args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js' }
250
+ },
251
+ {
252
+ category: 'Null/Crash Safety',
253
+ tool: 'ucn_lines',
254
+ desc: 'nonexistent file',
255
+ args: { project_dir: PROJECT_DIR, file: 'nonexistent/path/to/file.js', range: '1-10' }
256
+ },
257
+ {
258
+ category: 'Null/Crash Safety',
259
+ tool: 'ucn_expand',
260
+ desc: 'no prior context call',
261
+ args: { project_dir: PROJECT_DIR, number: 1 }
262
+ },
263
+
245
264
  // ========================================================================
246
265
  // CATEGORY 2: Input Validation
247
266
  // ========================================================================
@@ -321,6 +340,24 @@ const tests = [
321
340
  desc: 'search for "TODO"',
322
341
  args: { project_dir: PROJECT_DIR, term: 'TODO' }
323
342
  },
343
+ {
344
+ category: 'Normal Operations',
345
+ tool: 'ucn_api',
346
+ desc: 'project API',
347
+ args: { project_dir: PROJECT_DIR }
348
+ },
349
+ {
350
+ category: 'Normal Operations',
351
+ tool: 'ucn_stats',
352
+ desc: 'project stats',
353
+ args: { project_dir: PROJECT_DIR }
354
+ },
355
+ {
356
+ category: 'Normal Operations',
357
+ tool: 'ucn_lines',
358
+ desc: 'extract lines 1-5 from discovery.js',
359
+ args: { project_dir: PROJECT_DIR, file: 'core/discovery.js', range: '1-5' }
360
+ },
324
361
  ];
325
362
 
326
363
  // ============================================================================
@@ -6392,5 +6392,244 @@ class DataService:
6392
6392
  });
6393
6393
  });
6394
6394
 
6395
+ // Regression: isTestFile should use relative paths, not absolute paths
6396
+ // Bug: When project lived at /Users/x/test/project/, the /test/ in the parent
6397
+ // path matched the Python test pattern /\/tests?\//, marking ALL files as test files.
6398
+ // This caused deadcode to either miss real dead code or produce false positives.
6399
+ describe('Regression: deadcode uses relative paths for isTestFile', () => {
6400
+ it('should not treat non-test files as test files when project is inside a /test/ directory', () => {
6401
+ // Simulate a project inside a directory named "test"
6402
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-relpath-${Date.now()}`, 'test', 'myproject');
6403
+ const toolsDir = path.join(tmpDir, 'tools');
6404
+ fs.mkdirSync(toolsDir, { recursive: true });
6405
+
6406
+ try {
6407
+ fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
6408
+ fs.writeFileSync(path.join(toolsDir, '__init__.py'), '');
6409
+ fs.writeFileSync(path.join(toolsDir, 'helper.py'), `
6410
+ def unused_helper():
6411
+ return 42
6412
+
6413
+ def used_helper():
6414
+ return 1
6415
+ `);
6416
+ fs.writeFileSync(path.join(tmpDir, 'main.py'), `
6417
+ from tools.helper import used_helper
6418
+
6419
+ def main():
6420
+ print(used_helper())
6421
+ `);
6422
+
6423
+ const index = new ProjectIndex(tmpDir);
6424
+ index.build(null, { quiet: true });
6425
+
6426
+ const dead = index.deadcode();
6427
+ const deadNames = dead.map(d => d.name);
6428
+
6429
+ // unused_helper should be flagged as dead code
6430
+ assert.ok(deadNames.includes('unused_helper'),
6431
+ `unused_helper should be flagged as dead code, got: ${deadNames.join(', ')}`);
6432
+
6433
+ // used_helper should NOT be flagged
6434
+ assert.ok(!deadNames.includes('used_helper'),
6435
+ `used_helper should not be flagged as dead code`);
6436
+ } finally {
6437
+ fs.rmSync(path.join(os.tmpdir(), `ucn-test-relpath-${Date.now()}`), { recursive: true, force: true });
6438
+ // Clean up the created dir tree
6439
+ const topDir = tmpDir.split('/test/myproject')[0];
6440
+ if (topDir.includes('ucn-test-relpath')) {
6441
+ fs.rmSync(topDir, { recursive: true, force: true });
6442
+ }
6443
+ }
6444
+ });
6445
+
6446
+ it('should correctly filter test files even when project is inside /test/ directory', () => {
6447
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-relpath2-${Date.now()}`, 'test', 'myproject');
6448
+ const testsDir = path.join(tmpDir, 'tests');
6449
+ fs.mkdirSync(testsDir, { recursive: true });
6450
+
6451
+ try {
6452
+ fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
6453
+ fs.writeFileSync(path.join(tmpDir, 'app.py'), `
6454
+ def exported_func():
6455
+ return 42
6456
+
6457
+ def unused_func():
6458
+ return 0
6459
+ `);
6460
+ fs.writeFileSync(path.join(testsDir, 'test_app.py'), `
6461
+ from app import exported_func
6462
+
6463
+ def test_exported():
6464
+ assert exported_func() == 42
6465
+
6466
+ def _helper_in_test():
6467
+ return 'setup'
6468
+ `);
6469
+
6470
+ const index = new ProjectIndex(tmpDir);
6471
+ index.build(null, { quiet: true });
6472
+
6473
+ // Default: test files excluded
6474
+ const deadDefault = index.deadcode();
6475
+ const deadDefaultNames = deadDefault.map(d => d.name);
6476
+
6477
+ // unused_func from app.py should appear
6478
+ assert.ok(deadDefaultNames.includes('unused_func'),
6479
+ `unused_func should be in deadcode results`);
6480
+
6481
+ // _helper_in_test from test file should NOT appear (test files excluded by default)
6482
+ assert.ok(!deadDefaultNames.includes('_helper_in_test'),
6483
+ `_helper_in_test should not appear without --include-tests`);
6484
+
6485
+ // With --include-tests: test file symbols should appear
6486
+ const deadWithTests = index.deadcode({ includeTests: true });
6487
+ const deadWithTestsNames = deadWithTests.map(d => d.name);
6488
+
6489
+ assert.ok(deadWithTestsNames.includes('_helper_in_test'),
6490
+ `_helper_in_test should appear with --include-tests`);
6491
+
6492
+ // test_* functions should still be excluded (they're entry points)
6493
+ assert.ok(!deadWithTestsNames.includes('test_exported'),
6494
+ `test_exported should not be flagged (entry point)`);
6495
+ } finally {
6496
+ const topDir = tmpDir.split('/test/myproject')[0];
6497
+ if (topDir.includes('ucn-test-relpath2')) {
6498
+ fs.rmSync(topDir, { recursive: true, force: true });
6499
+ }
6500
+ }
6501
+ });
6502
+ });
6503
+
6504
+ // Regression: deadcode --include-exported should not include test file symbols
6505
+ // unless --include-tests is also specified
6506
+ describe('Regression: deadcode --include-exported respects test file filtering', () => {
6507
+ it('should not show test methods when only --include-exported is set', () => {
6508
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-exported-${Date.now()}`);
6509
+ const testsDir = path.join(tmpDir, 'tests');
6510
+ fs.mkdirSync(testsDir, { recursive: true });
6511
+
6512
+ try {
6513
+ fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
6514
+ fs.writeFileSync(path.join(tmpDir, 'lib.py'), `
6515
+ def public_func():
6516
+ return 42
6517
+ `);
6518
+ fs.writeFileSync(path.join(testsDir, 'test_lib.py'), `
6519
+ from lib import public_func
6520
+
6521
+ class TestLib:
6522
+ def test_public_func(self):
6523
+ assert public_func() == 42
6524
+
6525
+ def test_another(self):
6526
+ assert True
6527
+ `);
6528
+
6529
+ const index = new ProjectIndex(tmpDir);
6530
+ index.build(null, { quiet: true });
6531
+
6532
+ // --include-exported but NOT --include-tests
6533
+ const dead = index.deadcode({ includeExported: true, includeTests: false });
6534
+ const deadNames = dead.map(d => d.name);
6535
+
6536
+ // Test methods should NOT appear
6537
+ assert.ok(!deadNames.includes('test_public_func'),
6538
+ `test methods should not appear with only --include-exported`);
6539
+ assert.ok(!deadNames.includes('test_another'),
6540
+ `test methods should not appear with only --include-exported`);
6541
+ } finally {
6542
+ fs.rmSync(tmpDir, { recursive: true, force: true });
6543
+ }
6544
+ });
6545
+ });
6546
+
6547
+ // Regression: isTestFile relative path fix applies to all languages (not just Python)
6548
+ // Rust has /\/tests\// pattern that could match parent directories
6549
+ describe('Regression: deadcode relative path fix works for Rust projects', () => {
6550
+ it('should not treat non-test Rust files as test files when project is inside /tests/ directory', () => {
6551
+ // Project lives inside a directory called "tests"
6552
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-rust-relpath-${Date.now()}`, 'tests', 'myproject');
6553
+ const srcDir = path.join(tmpDir, 'src');
6554
+ fs.mkdirSync(srcDir, { recursive: true });
6555
+
6556
+ try {
6557
+ fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"');
6558
+ fs.writeFileSync(path.join(srcDir, 'lib.rs'), `
6559
+ fn unused_helper() -> i32 {
6560
+ 42
6561
+ }
6562
+
6563
+ pub fn used_func() -> i32 {
6564
+ unused_helper()
6565
+ }
6566
+ `);
6567
+
6568
+ const index = new ProjectIndex(tmpDir);
6569
+ index.build(null, { quiet: true });
6570
+
6571
+ const dead = index.deadcode();
6572
+ const deadNames = dead.map(d => d.name);
6573
+
6574
+ // unused_helper should be flagged (it has a caller but let's check it's not filtered)
6575
+ // The key assertion: src/lib.rs should NOT be treated as a test file
6576
+ const { isTestFile } = require('../core/discovery');
6577
+ assert.ok(!isTestFile('src/lib.rs', 'rust'),
6578
+ 'src/lib.rs should not be a test file');
6579
+
6580
+ // Verify the old bug: absolute path WOULD falsely match
6581
+ const absPath = path.join(tmpDir, 'src', 'lib.rs');
6582
+ // The absolute path contains /tests/ from parent dir
6583
+ assert.ok(absPath.includes('/tests/'),
6584
+ 'Absolute path should contain /tests/ from parent directory');
6585
+ } finally {
6586
+ const topDir = tmpDir.split('/tests/myproject')[0];
6587
+ if (topDir.includes('ucn-test-rust-relpath')) {
6588
+ fs.rmSync(topDir, { recursive: true, force: true });
6589
+ }
6590
+ }
6591
+ });
6592
+ });
6593
+
6594
+ // Regression: deadcode relative path fix works for JS projects with __tests__ in parent
6595
+ describe('Regression: deadcode relative path fix works for JS projects', () => {
6596
+ it('should not treat non-test JS files as test files when project is inside /__tests__/ directory', () => {
6597
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-js-relpath-${Date.now()}`, '__tests__', 'myproject');
6598
+ const srcDir = path.join(tmpDir, 'src');
6599
+ fs.mkdirSync(srcDir, { recursive: true });
6600
+
6601
+ try {
6602
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
6603
+ fs.writeFileSync(path.join(srcDir, 'utils.js'), `
6604
+ function unusedUtil() {
6605
+ return 42;
6606
+ }
6607
+
6608
+ function usedUtil() {
6609
+ return unusedUtil();
6610
+ }
6611
+
6612
+ module.exports = { usedUtil };
6613
+ `);
6614
+
6615
+ const index = new ProjectIndex(tmpDir);
6616
+ index.build(null, { quiet: true });
6617
+
6618
+ const dead = index.deadcode();
6619
+ const deadNames = dead.map(d => d.name);
6620
+
6621
+ // src/utils.js should NOT be treated as a test file
6622
+ const { isTestFile } = require('../core/discovery');
6623
+ assert.ok(!isTestFile('src/utils.js', 'javascript'),
6624
+ 'src/utils.js should not be a test file');
6625
+ } finally {
6626
+ const topDir = tmpDir.split('/__tests__/myproject')[0];
6627
+ if (topDir.includes('ucn-test-js-relpath')) {
6628
+ fs.rmSync(topDir, { recursive: true, force: true });
6629
+ }
6630
+ }
6631
+ });
6632
+ });
6633
+
6395
6634
  console.log('UCN v3 Test Suite');
6396
6635
  console.log('Run with: node --test test/parser.test.js');
@@ -1,58 +0,0 @@
1
- Run UCN reliability tests on 5 real-world projects. IMPORTANT: use node /Users/mihail/ucn/cli/index.js (NOT the global ucn) to test the current dev version. Use --clear-cache on every command.
2
-
3
- First run the unit tests: node --test test/parser.test.js — should be 212 pass, 0 fail.
4
-
5
- 1. Clone these repos to /tmp/ucn-reliability-test-2/ (shallow clone --depth 1):
6
- - fastify: https://github.com/fastify/fastify.git (JavaScript)
7
- - httpx: https://github.com/encode/httpx.git (Python)
8
- - hugo: https://github.com/gohugoio/hugo.git (Go)
9
- - alacritty: https://github.com/alacritty/alacritty.git (Rust)
10
- - spring-boot: https://github.com/spring-projects/spring-boot.git (Java — use spring-boot-project/spring-boot/src/main/java/org/springframework/boot only, pass that as the target path)
11
-
12
- 2. Launch 5 parallel agents (one per project). Each agent should run ALL of these commands with --clear-cache and 30s timeout:
13
-
14
- toc, toc --detailed, find <sym1>, find <sym2>, about <sym1>,
15
- context <class>, context <function>, impact <sym1>, smart <sym1>,
16
- trace <sym1> --depth=2, usages <sym2>, deadcode, fn <method>,
17
- imports <file>, exporters <file>, search "<term>", verify <sym1>,
18
- find nonexistentXYZ (edge case), context "" (edge case)
19
-
20
- 3. Key symbols per project:
21
- - fastify: route, FastifyInstance, inject, Reply, register
22
- - httpx: get, Client, request, Response, __init__
23
- - hugo: Build, Site, Execute, Page, New
24
- - alacritty: main, update, Display, build, new
25
- - spring-boot: run, SpringApplication, main, ApplicationContext, refresh
26
-
27
- For `fn` command, specifically test class methods (Python __init__, Java overloaded methods) — these were broken before and fixed in this version.
28
-
29
- 4. Focus areas for EVERY command — check ALL of the following (AST tree-sitter reliability is the main focus):
30
- - toc shows correct file/function/class counts, no crashes on large projects
31
- - toc --detailed lists all symbols with line ranges, no truncation errors
32
- - find returns ranked results by usage count, no duplicates
33
- - about prefers lib/src/core definitions over test/example files, shows usages/callers/callees/code
34
- - context <class> shows class name (not "undefined"), lists methods with signatures
35
- - context <function> shows callers and callees with correct counts and weights
36
- - context for non-existent symbols returns "Symbol not found" (NOT empty callers/callees)
37
- - impact groups call sites by file with code context, shows argument patterns
38
- - smart shows function code + inlined dependency code, overloads show ALL overload callees
39
- - trace builds correct call tree with depth levels, weights, and multiplicity
40
- - usages categorizes by type (definition/call/import/reference), no false positives
41
- - deadcode completes within timeout, correctly excludes entry points (main, init, __init__, #[test])
42
- - fn extracts correct code for BOTH top-level functions AND class methods (previously broken, now fixed)
43
- - fn auto-resolves best definition (prefers lib/src over test)
44
- - imports shows internal + external imports with resolved paths
45
- - exporters finds importers for the file (Python relative imports work, Java package imports resolve)
46
- - search returns matches with file grouping, respects project ignores
47
- - verify checks call signatures, totalCalls equals valid+mismatches+uncertain (no inflated counts from filtered method calls)
48
- - Edge cases: find nonexistentXYZ returns clean "not found", context "" returns usage error (no crash)
49
-
50
- 5. Each agent reports: command, status (OK/ERROR/CRASH/UNEXPECTED), notes.
51
- At the end: total pass/fail, issues by severity, patterns.
52
-
53
- 6. After all agents finish, compile a cross-project summary comparing results to the previous run (which had 85/95 = 89.5% before fixes, target is 95%+ with 0 crashes). The fixes applied were:
54
- - fn command now uses symbol index startLine/endLine directly (fixes class method extraction)
55
- - verify totalCalls now computed as valid+mismatches+uncertain (fixes inflated counts)
56
- - context returns null for undefined symbols (CLI shows "Symbol not found")
57
-
58
- All code uses tree-sitter AST parsing — reliability and correctness of AST-based analysis is the main focus. Any crash, incorrect result, or hang is a bug.