ucn 3.7.46 → 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 +2 -0
- 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 +32 -15
- package/package.json +1 -1
|
@@ -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) |
|
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 };
|