ucn 3.7.45 → 3.7.47
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 +6 -4
- package/README.md +23 -24
- package/cli/index.js +14 -6
- package/core/cache.js +176 -51
- package/core/callers.js +315 -51
- package/core/deadcode.js +42 -16
- package/core/discovery.js +1 -1
- package/core/execute.js +148 -11
- package/core/output.js +26 -4
- package/core/project.js +290 -52
- package/core/registry.js +1 -0
- package/core/shared.js +1 -1
- package/core/stacktrace.js +31 -2
- package/core/verify.js +11 -0
- package/languages/go.js +331 -23
- package/languages/index.js +20 -1
- package/languages/java.js +109 -4
- package/languages/rust.js +93 -4
- package/mcp/server.js +33 -16
- package/package.json +11 -10
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ucn
|
|
3
|
-
description: "Code
|
|
3
|
+
description: "Code intelligence toolkit — extract functions, trace callers, analyze impact, detect dead code without reading whole files. PREFER over grep+read when you need: who calls a function, what breaks if you change it, or the full call chain of a pipeline. One `ucn about` replaces 3-4 grep+read cycles. One `ucn trace` maps an entire execution flow without reading any files. Works on JS/TS, Python, Go, Rust, Java, HTML. Skip for plain text search or codebases under 500 LOC."
|
|
4
4
|
allowed-tools: Bash(ucn *), Bash(npx ucn *)
|
|
5
5
|
argument-hint: "[command] [symbol-name] [--flags]"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# UCN — Universal Code Navigator
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Extract functions, trace call chains, find callers, and detect dead code — without reading entire files. Works on JS/TS, Python, Go, Rust, Java, and HTML (inline scripts and event handlers).
|
|
11
11
|
|
|
12
12
|
## When to Reach for UCN Instead of Grep/Read
|
|
13
13
|
|
|
14
|
-
**Use UCN when
|
|
14
|
+
**Use UCN when the next action would be:**
|
|
15
15
|
|
|
16
16
|
- "Let me grep for all callers of this function" → `ucn impact <name>` — finds every call site, grouped by file, with args shown
|
|
17
17
|
- "Let me read this 800-line file to find one function" → `ucn fn <name> --file=<hint>` — extracts just that function
|
|
@@ -145,6 +145,8 @@ ucn [target] <command> [name] [--flags]
|
|
|
145
145
|
| `--detailed` | Show full symbol listing per file in `toc` |
|
|
146
146
|
| `--top-level` | Show only top-level functions in `toc` (exclude nested/indented) |
|
|
147
147
|
| `--top=N` | Limit result count (default: 10 for most commands) |
|
|
148
|
+
| `--limit=N` | Limit result count for `find`, `usages`, `search`, `deadcode`, `api`, `toc` |
|
|
149
|
+
| `--max-files=N` | Max files to index (for large projects with 10K+ files) |
|
|
148
150
|
| `--max-lines=N` | Max source lines for `class` (large classes show summary by default) |
|
|
149
151
|
| `--case-sensitive` | Case-sensitive text search (default: case-insensitive) |
|
|
150
152
|
| `--exact` | Exact name match only in `find`/`typedef` (no substring) |
|
|
@@ -164,7 +166,7 @@ ucn trace problematic_function --depth=2 # See what it calls
|
|
|
164
166
|
```bash
|
|
165
167
|
ucn impact the_function # Who will break?
|
|
166
168
|
ucn smart the_function # See it + its helpers
|
|
167
|
-
# ... make
|
|
169
|
+
# ... make changes ...
|
|
168
170
|
ucn verify the_function # Did all call sites survive?
|
|
169
171
|
```
|
|
170
172
|
|
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# UCN
|
|
1
|
+
# UCN - Universal Code Navigator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Code intelligence for AI agents and developers - understand, extract, and navigate code without reading whole files.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Who calls this function?
|
|
7
|
-
- What breaks if I change this
|
|
8
|
-
- What
|
|
9
|
-
- What code is safe to delete?
|
|
5
|
+
Precise answers to structural code questions:
|
|
6
|
+
- Who calls this function? → without grepping the whole project
|
|
7
|
+
- What breaks if I change this? → every call site, with arguments
|
|
8
|
+
- What does this function do? → extracted with dependencies inline
|
|
9
|
+
- What code is safe to delete? → verified unused symbols
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
One command replaces 3-4 grep+read cycles. Powered by tree-sitter.
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/ucn)
|
|
14
14
|
[](LICENSE)
|
|
@@ -20,22 +20,21 @@ Instead of reading full files, UCN gives precise, AST-verified answers.
|
|
|
20
20
|
```bash
|
|
21
21
|
npm install -g ucn
|
|
22
22
|
|
|
23
|
+
ucn toc # project overview
|
|
24
|
+
ucn fn handleRequest # extract a function without reading the file
|
|
23
25
|
ucn about handleRequest # full picture: definition, callers, callees, tests
|
|
24
26
|
ucn impact handleRequest # all call sites with arguments
|
|
25
27
|
ucn trace main --depth=3 # call tree, no file reads
|
|
26
28
|
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
29
|
```
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
Supports JS/TS, Python, Go, Rust, Java, and HTML. Runs locally.
|
|
33
32
|
|
|
34
33
|
```
|
|
35
|
-
Terminal AI Agents
|
|
36
|
-
│ │
|
|
37
|
-
CLI MCP
|
|
38
|
-
|
|
34
|
+
Terminal AI Agents Agent Skills
|
|
35
|
+
│ │ │
|
|
36
|
+
CLI MCP Skill
|
|
37
|
+
└────────────────────┼────────────────────┘
|
|
39
38
|
│
|
|
40
39
|
┌──────┴──────┐
|
|
41
40
|
│ UCN Engine │
|
|
@@ -48,7 +47,7 @@ Parses JS/TS, Python, Go, Rust, Java, and HTML with tree-sitter. Runs locally.
|
|
|
48
47
|
|
|
49
48
|
## Why UCN
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
AI agents waste tokens reading entire files to find one function, or grep for callers and miss half of them. UCN builds a structural index of the codebase - it knows which functions call which, what depends on what, and what's unused. One command gives what would take 3-4 file reads and greps.
|
|
52
51
|
|
|
53
52
|
"What happens when `build()` runs?"
|
|
54
53
|
|
|
@@ -139,7 +138,7 @@ VS Code uses `.vscode/mcp.json`:
|
|
|
139
138
|
|
|
140
139
|
</details>
|
|
141
140
|
|
|
142
|
-
All 28 commands ship as a single MCP tool
|
|
141
|
+
All 28 commands ship as a single MCP tool - under 2KB of context.
|
|
143
142
|
|
|
144
143
|
### Agent Skill (no server needed)
|
|
145
144
|
|
|
@@ -294,13 +293,13 @@ ucn deadcode --exclude=test # what can be deleted?
|
|
|
294
293
|
|
|
295
294
|
## Limitations
|
|
296
295
|
|
|
297
|
-
UCN
|
|
296
|
+
UCN analyzes code structure statically - it doesn't run code.
|
|
298
297
|
|
|
299
|
-
- **5 languages + HTML**
|
|
300
|
-
- **Static analysis only**
|
|
301
|
-
- **Duck-typed methods**
|
|
302
|
-
- **Single project scope**
|
|
303
|
-
- **First-query index time**
|
|
298
|
+
- **5 languages + HTML** - JS/TS, Python, Go, Rust, Java. Falls back to text search for others.
|
|
299
|
+
- **Static analysis only** - Can't follow `eval()`, `getattr()`, reflection, or other dynamic dispatch.
|
|
300
|
+
- **Duck-typed methods** - `obj.method()` in JS/TS/Python is marked "uncertain" when the receiver type is ambiguous. Go/Rust/Java resolve with high confidence.
|
|
301
|
+
- **Single project scope** - Follows imports within the project but not into `node_modules` or `site-packages`.
|
|
302
|
+
- **First-query index time** - A few seconds on large projects. Cached incrementally after that.
|
|
304
303
|
|
|
305
304
|
---
|
|
306
305
|
|
package/cli/index.js
CHANGED
|
@@ -102,6 +102,8 @@ function parseFlags(tokens) {
|
|
|
102
102
|
regex: tokens.includes('--no-regex') ? false : undefined,
|
|
103
103
|
functions: tokens.includes('--functions'),
|
|
104
104
|
className: getValueFlag('--class-name'),
|
|
105
|
+
limit: parseInt(getValueFlag('--limit') || '0') || undefined,
|
|
106
|
+
maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
|
|
105
107
|
};
|
|
106
108
|
}
|
|
107
109
|
|
|
@@ -126,7 +128,7 @@ const knownFlags = new Set([
|
|
|
126
128
|
'--default', '--top', '--no-follow-symlinks',
|
|
127
129
|
'--base', '--staged', '--stack',
|
|
128
130
|
'--regex', '--no-regex', '--functions',
|
|
129
|
-
'--max-lines', '--class-name'
|
|
131
|
+
'--max-lines', '--class-name', '--limit', '--max-files'
|
|
130
132
|
]);
|
|
131
133
|
|
|
132
134
|
// Handle help flag
|
|
@@ -396,7 +398,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
396
398
|
// If cache was loaded but stale, force rebuild to avoid duplicates
|
|
397
399
|
let needsCacheSave = false;
|
|
398
400
|
if (!usedCache) {
|
|
399
|
-
index.build(null, { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks });
|
|
401
|
+
index.build(null, { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, maxFiles: flags.maxFiles });
|
|
400
402
|
needsCacheSave = flags.cache;
|
|
401
403
|
}
|
|
402
404
|
|
|
@@ -408,8 +410,9 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
408
410
|
// ── Commands using shared executor ───────────────────────────────
|
|
409
411
|
|
|
410
412
|
case 'toc': {
|
|
411
|
-
const { ok, result, error } = execute(index, 'toc', flags);
|
|
413
|
+
const { ok, result, error, note } = execute(index, 'toc', flags);
|
|
412
414
|
if (!ok) fail(error);
|
|
415
|
+
if (note) console.error(note);
|
|
413
416
|
printOutput(result, output.formatTocJson, r => output.formatToc(r, {
|
|
414
417
|
detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
|
|
415
418
|
uncertainHint: 'use --include-uncertain to include all'
|
|
@@ -429,8 +432,9 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
429
432
|
}
|
|
430
433
|
|
|
431
434
|
case 'usages': {
|
|
432
|
-
const { ok, result, error } = execute(index, 'usages', { name: arg, ...flags });
|
|
435
|
+
const { ok, result, error, note } = execute(index, 'usages', { name: arg, ...flags });
|
|
433
436
|
if (!ok) fail(error);
|
|
437
|
+
if (note) console.error(note);
|
|
434
438
|
printOutput(result,
|
|
435
439
|
r => output.formatUsagesJson(r, arg),
|
|
436
440
|
r => output.formatUsages(r, arg)
|
|
@@ -665,8 +669,9 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
665
669
|
}
|
|
666
670
|
|
|
667
671
|
case 'api': {
|
|
668
|
-
const { ok, result, error } = execute(index, 'api', { file: arg });
|
|
672
|
+
const { ok, result, error, note } = execute(index, 'api', { file: arg || flags.file, limit: flags.limit });
|
|
669
673
|
if (!ok) fail(error);
|
|
674
|
+
if (note) console.error(note);
|
|
670
675
|
printOutput(result,
|
|
671
676
|
r => output.formatApiJson(r, arg),
|
|
672
677
|
r => output.formatApi(r, arg)
|
|
@@ -685,8 +690,9 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
685
690
|
}
|
|
686
691
|
|
|
687
692
|
case 'deadcode': {
|
|
688
|
-
const { ok, result, error } = execute(index, 'deadcode', { ...flags, in: flags.in || subdirScope });
|
|
693
|
+
const { ok, result, error, note } = execute(index, 'deadcode', { ...flags, in: flags.in || subdirScope });
|
|
689
694
|
if (!ok) fail(error);
|
|
695
|
+
if (note) console.error(note);
|
|
690
696
|
printOutput(result,
|
|
691
697
|
output.formatDeadcodeJson,
|
|
692
698
|
r => output.formatDeadcode(r, {
|
|
@@ -1040,6 +1046,8 @@ Common Flags:
|
|
|
1040
1046
|
--direction=X Graph direction: imports, importers, or both (default: both)
|
|
1041
1047
|
--all Expand truncated sections (about, trace, graph, related)
|
|
1042
1048
|
--top=N Limit results (find, deadcode)
|
|
1049
|
+
--limit=N Limit result count (find, usages, search, deadcode, api, toc)
|
|
1050
|
+
--max-files=N Max files to index (large projects)
|
|
1043
1051
|
--context=N Lines of context around matches
|
|
1044
1052
|
--json Machine-readable output
|
|
1045
1053
|
--code-only Filter out comments and strings
|
package/core/cache.js
CHANGED
|
@@ -30,10 +30,10 @@ function saveCache(index, cachePath) {
|
|
|
30
30
|
|
|
31
31
|
const cacheFile = cachePath || path.join(cacheDir, 'index.json');
|
|
32
32
|
|
|
33
|
-
// Prepare callsCache for serialization (exclude content
|
|
33
|
+
// Prepare callsCache for serialization (exclude content, use relative paths)
|
|
34
34
|
const callsCacheData = [];
|
|
35
35
|
for (const [filePath, entry] of index.callsCache) {
|
|
36
|
-
callsCacheData.push([filePath, {
|
|
36
|
+
callsCacheData.push([path.relative(index.root, filePath), {
|
|
37
37
|
mtime: entry.mtime,
|
|
38
38
|
hash: entry.hash,
|
|
39
39
|
calls: entry.calls
|
|
@@ -41,23 +41,71 @@ function saveCache(index, cachePath) {
|
|
|
41
41
|
}]);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Hash config to detect when graph rebuild is needed on load
|
|
45
|
+
const configHash = crypto.createHash('md5')
|
|
46
|
+
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
47
|
+
|
|
48
|
+
// Strip redundant fields from symbols and file entries to reduce cache size.
|
|
49
|
+
// v6: All paths stored as relative paths (saves ~60% on large codebases).
|
|
50
|
+
// symbol.file = path.join(root, symbol.relativePath) — reconstructable
|
|
51
|
+
// symbol.bindingId = relativePath:type:startLine — reconstructable
|
|
52
|
+
// fileEntry.path = Map key — redundant
|
|
53
|
+
// fileEntry.relativePath = now the Map key — redundant
|
|
54
|
+
const root = index.root;
|
|
55
|
+
const strippedSymbols = [];
|
|
56
|
+
for (const [name, defs] of index.symbols) {
|
|
57
|
+
const stripped = defs.map(s => {
|
|
58
|
+
const { file, bindingId, ...rest } = s;
|
|
59
|
+
return rest;
|
|
60
|
+
});
|
|
61
|
+
strippedSymbols.push([name, stripped]);
|
|
62
|
+
}
|
|
63
|
+
// Files: use relativePath as key, strip path, relativePath, symbols, and bindings from entries.
|
|
64
|
+
// symbols/bindings are already stored in the top-level symbols map — no need to duplicate.
|
|
65
|
+
const strippedFiles = [];
|
|
66
|
+
for (const [, entry] of index.files) {
|
|
67
|
+
const { path: _p, relativePath: rp, symbols: _s, bindings: _b, ...rest } = entry;
|
|
68
|
+
strippedFiles.push([rp, rest]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Convert graph paths from absolute to relative
|
|
72
|
+
const relGraph = (graph) => {
|
|
73
|
+
const result = [];
|
|
74
|
+
for (const [absKey, absValues] of graph) {
|
|
75
|
+
const relKey = path.relative(root, absKey);
|
|
76
|
+
const relValues = absValues.map(v => path.relative(root, v));
|
|
77
|
+
result.push([relKey, relValues]);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
};
|
|
81
|
+
|
|
44
82
|
const cacheData = {
|
|
45
|
-
version:
|
|
83
|
+
version: 7, // v7: strip symbols/bindings from file entries (dedup ~45% cache reduction)
|
|
46
84
|
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
47
|
-
|
|
85
|
+
configHash,
|
|
86
|
+
root,
|
|
48
87
|
buildTime: index.buildTime,
|
|
49
88
|
timestamp: Date.now(),
|
|
50
|
-
files:
|
|
51
|
-
symbols:
|
|
52
|
-
importGraph:
|
|
53
|
-
exportGraph:
|
|
89
|
+
files: strippedFiles,
|
|
90
|
+
symbols: strippedSymbols,
|
|
91
|
+
importGraph: relGraph(index.importGraph),
|
|
92
|
+
exportGraph: relGraph(index.exportGraph),
|
|
93
|
+
// extendsGraph/extendedByGraph use class names as keys (not file paths)
|
|
54
94
|
extendsGraph: Array.from(index.extendsGraph.entries()),
|
|
55
95
|
extendedByGraph: Array.from(index.extendedByGraph.entries()),
|
|
56
|
-
|
|
57
|
-
|
|
96
|
+
failedFiles: index.failedFiles
|
|
97
|
+
? Array.from(index.failedFiles).map(f => path.relative(root, f))
|
|
98
|
+
: []
|
|
58
99
|
};
|
|
59
100
|
|
|
60
101
|
fs.writeFileSync(cacheFile, JSON.stringify(cacheData));
|
|
102
|
+
|
|
103
|
+
// Save callsCache to a separate file (lazy-loaded on demand)
|
|
104
|
+
if (callsCacheData.length > 0) {
|
|
105
|
+
const callsCacheFile = path.join(path.dirname(cacheFile), 'calls-cache.json');
|
|
106
|
+
fs.writeFileSync(callsCacheFile, JSON.stringify(callsCacheData));
|
|
107
|
+
}
|
|
108
|
+
|
|
61
109
|
return cacheFile;
|
|
62
110
|
}
|
|
63
111
|
|
|
@@ -78,9 +126,8 @@ function loadCache(index, cachePath) {
|
|
|
78
126
|
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
|
79
127
|
|
|
80
128
|
// Check version compatibility
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
if (cacheData.version !== 4) {
|
|
129
|
+
// v7: symbols/bindings stripped from file entries (dedup)
|
|
130
|
+
if (cacheData.version !== 7) {
|
|
84
131
|
return false;
|
|
85
132
|
}
|
|
86
133
|
|
|
@@ -97,13 +144,57 @@ function loadCache(index, cachePath) {
|
|
|
97
144
|
return false;
|
|
98
145
|
}
|
|
99
146
|
|
|
100
|
-
|
|
147
|
+
const root = cacheData.root || index.root;
|
|
148
|
+
|
|
149
|
+
// Reconstruct files Map: relative key → absolute key, restore path and relativePath
|
|
150
|
+
// Initialize symbols/bindings arrays (will be populated from top-level symbols)
|
|
151
|
+
index.files = new Map();
|
|
152
|
+
for (const [relPath, entry] of cacheData.files) {
|
|
153
|
+
const absPath = path.join(root, relPath);
|
|
154
|
+
entry.path = absPath;
|
|
155
|
+
entry.relativePath = relPath;
|
|
156
|
+
if (!entry.symbols) entry.symbols = [];
|
|
157
|
+
if (!entry.bindings) entry.bindings = [];
|
|
158
|
+
index.files.set(absPath, entry);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Reconstruct symbols: restore file and bindingId from relativePath
|
|
162
|
+
// Also rebuild fileEntry.symbols and fileEntry.bindings from top-level data
|
|
101
163
|
index.symbols = new Map(cacheData.symbols);
|
|
102
|
-
|
|
103
|
-
|
|
164
|
+
for (const [, defs] of index.symbols) {
|
|
165
|
+
for (const s of defs) {
|
|
166
|
+
if (!s.file && s.relativePath) s.file = path.join(root, s.relativePath);
|
|
167
|
+
if (!s.bindingId && s.relativePath && s.type && s.startLine) {
|
|
168
|
+
s.bindingId = `${s.relativePath}:${s.type}:${s.startLine}`;
|
|
169
|
+
}
|
|
170
|
+
// Rebuild fileEntry.symbols and bindings from top-level symbols
|
|
171
|
+
const fileEntry = index.files.get(s.file);
|
|
172
|
+
if (fileEntry) {
|
|
173
|
+
fileEntry.symbols.push(s);
|
|
174
|
+
fileEntry.bindings.push({
|
|
175
|
+
id: s.bindingId,
|
|
176
|
+
name: s.name,
|
|
177
|
+
type: s.type,
|
|
178
|
+
startLine: s.startLine
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Reconstruct graphs: relative paths → absolute paths
|
|
185
|
+
const absGraph = (data) => {
|
|
186
|
+
const m = new Map();
|
|
187
|
+
for (const [relKey, relValues] of data) {
|
|
188
|
+
m.set(path.join(root, relKey), relValues.map(v => path.join(root, v)));
|
|
189
|
+
}
|
|
190
|
+
return m;
|
|
191
|
+
};
|
|
192
|
+
index.importGraph = absGraph(cacheData.importGraph);
|
|
193
|
+
index.exportGraph = absGraph(cacheData.exportGraph);
|
|
104
194
|
index.buildTime = cacheData.buildTime;
|
|
105
195
|
|
|
106
196
|
// Restore optional graphs if present
|
|
197
|
+
// extendsGraph/extendedByGraph use class names as keys (not file paths)
|
|
107
198
|
if (Array.isArray(cacheData.extendsGraph)) {
|
|
108
199
|
index.extendsGraph = new Map(cacheData.extendsGraph);
|
|
109
200
|
}
|
|
@@ -111,19 +202,26 @@ function loadCache(index, cachePath) {
|
|
|
111
202
|
index.extendedByGraph = new Map(cacheData.extendedByGraph);
|
|
112
203
|
}
|
|
113
204
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
205
|
+
// Eagerly load callsCache from separate file.
|
|
206
|
+
// Prevents 10K cold tree-sitter re-parses (2GB+ peak) when findCallers runs.
|
|
207
|
+
if (index.callsCache.size === 0) {
|
|
208
|
+
loadCallsCache(index);
|
|
117
209
|
}
|
|
118
210
|
|
|
119
|
-
// Restore failedFiles if present
|
|
211
|
+
// Restore failedFiles if present (convert relative paths back to absolute)
|
|
120
212
|
if (Array.isArray(cacheData.failedFiles)) {
|
|
121
|
-
index.failedFiles = new Set(
|
|
213
|
+
index.failedFiles = new Set(
|
|
214
|
+
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f : path.join(root, f))
|
|
215
|
+
);
|
|
122
216
|
}
|
|
123
217
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
218
|
+
// Only rebuild graphs if config changed (e.g., aliases modified)
|
|
219
|
+
const currentConfigHash = crypto.createHash('md5')
|
|
220
|
+
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
221
|
+
if (currentConfigHash !== cacheData.configHash) {
|
|
222
|
+
index.buildImportGraph();
|
|
223
|
+
index.buildInheritanceGraph();
|
|
224
|
+
}
|
|
127
225
|
|
|
128
226
|
return true;
|
|
129
227
|
} catch (e) {
|
|
@@ -137,32 +235,9 @@ function loadCache(index, cachePath) {
|
|
|
137
235
|
* @returns {boolean} - True if cache needs rebuilding
|
|
138
236
|
*/
|
|
139
237
|
function isCacheStale(index) {
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
const pattern = detectProjectPattern(index.root);
|
|
143
|
-
const globOpts = { root: index.root };
|
|
144
|
-
const gitignorePatterns = parseGitignore(index.root);
|
|
145
|
-
const configExclude = index.config.exclude || [];
|
|
146
|
-
if (gitignorePatterns.length > 0 || configExclude.length > 0) {
|
|
147
|
-
globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
|
|
148
|
-
}
|
|
149
|
-
const currentFiles = expandGlob(pattern, globOpts);
|
|
150
|
-
const cachedPaths = new Set(index.files.keys());
|
|
151
|
-
|
|
152
|
-
for (const file of currentFiles) {
|
|
153
|
-
if (!cachedPaths.has(file) && !(index.failedFiles && index.failedFiles.has(file))) {
|
|
154
|
-
return true; // New file found
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Check existing cached files for modifications/deletions
|
|
238
|
+
// Fast path: check cached files for modifications/deletions first (stat-only).
|
|
239
|
+
// This returns early without the expensive directory walk when any file changed.
|
|
159
240
|
for (const [filePath, fileEntry] of index.files) {
|
|
160
|
-
// File deleted
|
|
161
|
-
if (!fs.existsSync(filePath)) {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// File modified - check size first, then mtime, then hash
|
|
166
241
|
try {
|
|
167
242
|
const stat = fs.statSync(filePath);
|
|
168
243
|
|
|
@@ -183,11 +258,61 @@ function isCacheStale(index) {
|
|
|
183
258
|
return true;
|
|
184
259
|
}
|
|
185
260
|
} catch (e) {
|
|
186
|
-
return true;
|
|
261
|
+
return true; // File deleted or inaccessible
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Slow path: glob the project to detect new files added since last build.
|
|
266
|
+
// Only reached when all cached files are unchanged.
|
|
267
|
+
const pattern = detectProjectPattern(index.root);
|
|
268
|
+
const globOpts = { root: index.root };
|
|
269
|
+
const gitignorePatterns = parseGitignore(index.root);
|
|
270
|
+
const configExclude = index.config.exclude || [];
|
|
271
|
+
if (gitignorePatterns.length > 0 || configExclude.length > 0) {
|
|
272
|
+
globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
|
|
273
|
+
}
|
|
274
|
+
const currentFiles = expandGlob(pattern, globOpts);
|
|
275
|
+
const cachedPaths = new Set(index.files.keys());
|
|
276
|
+
|
|
277
|
+
for (const file of currentFiles) {
|
|
278
|
+
if (!cachedPaths.has(file) && !(index.failedFiles && index.failedFiles.has(file))) {
|
|
279
|
+
return true; // New file found
|
|
187
280
|
}
|
|
188
281
|
}
|
|
189
282
|
|
|
190
283
|
return false;
|
|
191
284
|
}
|
|
192
285
|
|
|
193
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Load callsCache from separate file on demand.
|
|
288
|
+
* Only loads if callsCache is empty (not already populated from inline or prior load).
|
|
289
|
+
* @param {object} index - ProjectIndex instance
|
|
290
|
+
* @returns {boolean} - True if loaded successfully
|
|
291
|
+
*/
|
|
292
|
+
function loadCallsCache(index) {
|
|
293
|
+
if (index.callsCache.size > 0) return true; // Already populated
|
|
294
|
+
if (index._callsCacheLoaded) return false; // Already attempted, file didn't exist
|
|
295
|
+
index._callsCacheLoaded = true;
|
|
296
|
+
|
|
297
|
+
const callsCacheFile = path.join(index.root, '.ucn-cache', 'calls-cache.json');
|
|
298
|
+
if (!fs.existsSync(callsCacheFile)) return false;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const data = JSON.parse(fs.readFileSync(callsCacheFile, 'utf-8'));
|
|
302
|
+
if (Array.isArray(data)) {
|
|
303
|
+
// Convert relative paths back to absolute
|
|
304
|
+
const absData = data.map(([relPath, entry]) => {
|
|
305
|
+
// Handle both relative (v6+) and absolute (legacy) paths
|
|
306
|
+
const absPath = path.isAbsolute(relPath) ? relPath : path.join(index.root, relPath);
|
|
307
|
+
return [absPath, entry];
|
|
308
|
+
});
|
|
309
|
+
index.callsCache = new Map(absData);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// Corrupted file — ignore
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = { saveCache, loadCache, loadCallsCache, isCacheStale };
|