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.
- package/.claude/skills/ucn/SKILL.md +3 -3
- package/README.md +82 -115
- package/core/project.js +6 -7
- package/mcp/server.js +8 -8
- package/package.json +3 -3
- package/test/mcp-edge-cases.js +39 -2
- package/test/parser.test.js +239 -0
- package/test/reliability-test-prompt.md +0 -58
|
@@ -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
|
|
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
|
-
|
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
|
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: '
|
|
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: '
|
|
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.
|
|
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.
|
|
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
|
|
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: '
|
|
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
|
|
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.
|
|
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": "
|
|
8
|
-
"ucn-mcp": "
|
|
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"
|
package/test/mcp-edge-cases.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* MCP Server Edge Case Test Suite
|
|
5
5
|
*
|
|
6
|
-
* Tests
|
|
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 = '
|
|
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
|
// ============================================================================
|
package/test/parser.test.js
CHANGED
|
@@ -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.
|