ucn 4.2.2 → 5.0.1
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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +445 -300
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -131
- package/core/cache.js +533 -11
- package/core/callers.js +5533 -494
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +421 -20
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +204 -42
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +216 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -177
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +371 -116
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +428 -16
- package/languages/javascript.js +452 -49
- package/languages/python.js +1041 -32
- package/languages/rust.js +1415 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +41 -24
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/cache.js
CHANGED
|
@@ -7,12 +7,302 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
10
11
|
const crypto = require('crypto');
|
|
11
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, DEFAULT_IGNORES,
|
|
14
|
+
classifyUnsupportedSourceFile,
|
|
15
|
+
} = require('./discovery');
|
|
16
|
+
const { codeUnitCompare } = require('./shared');
|
|
12
17
|
|
|
13
18
|
// Read UCN version for cache invalidation
|
|
14
19
|
const UCN_VERSION = require('../package.json').version;
|
|
15
20
|
|
|
21
|
+
const CACHE_DIRECTORY_NAME = 'ucn';
|
|
22
|
+
const CACHE_PROJECTS_DIRECTORY = 'projects';
|
|
23
|
+
const LEGACY_CACHE_DIRECTORY = '.ucn-cache';
|
|
24
|
+
const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
25
|
+
const CACHE_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
26
|
+
const CACHE_MAX_PROJECTS = 128;
|
|
27
|
+
const CACHE_MAX_BYTES = 1024 * 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
function discoveryRulesHash(root) {
|
|
30
|
+
return crypto.createHash('md5').update(parseGitignore(root).join('\0')).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the per-user cache root without writing anything.
|
|
35
|
+
*
|
|
36
|
+
* UCN_CACHE_DIR is the explicit override. Otherwise follow the host cache
|
|
37
|
+
* convention: XDG_CACHE_HOME on Unix, Library/Caches on macOS, and
|
|
38
|
+
* LOCALAPPDATA on Windows. Every fallback remains below the user's home.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} [options]
|
|
41
|
+
* @param {NodeJS.ProcessEnv} [options.env]
|
|
42
|
+
* @param {string} [options.platform]
|
|
43
|
+
* @param {string} [options.homeDir]
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function getUserCacheRoot({
|
|
47
|
+
env = process.env,
|
|
48
|
+
platform = process.platform,
|
|
49
|
+
homeDir = os.homedir(),
|
|
50
|
+
} = {}) {
|
|
51
|
+
const resolveConfiguredPath = (configured) => {
|
|
52
|
+
const value = String(configured || '').trim();
|
|
53
|
+
if (!value) return null;
|
|
54
|
+
if (value === '~') return homeDir;
|
|
55
|
+
if (value.startsWith('~/') || value.startsWith('~\\')) {
|
|
56
|
+
return path.resolve(homeDir, value.slice(2));
|
|
57
|
+
}
|
|
58
|
+
return path.resolve(value);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const explicit = resolveConfiguredPath(env.UCN_CACHE_DIR);
|
|
62
|
+
if (explicit) return explicit;
|
|
63
|
+
|
|
64
|
+
const xdg = resolveConfiguredPath(env.XDG_CACHE_HOME);
|
|
65
|
+
if (xdg) return path.join(xdg, CACHE_DIRECTORY_NAME);
|
|
66
|
+
|
|
67
|
+
if (platform === 'darwin') {
|
|
68
|
+
return path.join(homeDir, 'Library', 'Caches', CACHE_DIRECTORY_NAME);
|
|
69
|
+
}
|
|
70
|
+
if (platform === 'win32') {
|
|
71
|
+
const localAppData = resolveConfiguredPath(env.LOCALAPPDATA);
|
|
72
|
+
return localAppData
|
|
73
|
+
? path.join(localAppData, CACHE_DIRECTORY_NAME, 'cache')
|
|
74
|
+
: path.join(homeDir, 'AppData', 'Local', CACHE_DIRECTORY_NAME, 'cache');
|
|
75
|
+
}
|
|
76
|
+
return path.join(homeDir, '.cache', CACHE_DIRECTORY_NAME);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Canonicalize a project root for cache identity. Symlinked paths to the same
|
|
81
|
+
* checkout share one cache, while separate copies remain isolated.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} projectRoot
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function canonicalProjectRoot(projectRoot) {
|
|
87
|
+
const resolved = path.resolve(projectRoot);
|
|
88
|
+
try {
|
|
89
|
+
const realpath = fs.realpathSync.native || fs.realpathSync;
|
|
90
|
+
return realpath(resolved);
|
|
91
|
+
} catch (_) {
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Return the default cache directory for one project.
|
|
98
|
+
*
|
|
99
|
+
* The readable basename aids manual inspection; the canonical-path hash
|
|
100
|
+
* prevents collisions between unrelated projects with the same directory
|
|
101
|
+
* name without exposing the full checkout path.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} projectRoot
|
|
104
|
+
* @param {object} [options] - Forwarded to getUserCacheRoot()
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function getProjectCacheDir(projectRoot, options) {
|
|
108
|
+
const canonicalRoot = canonicalProjectRoot(projectRoot);
|
|
109
|
+
const slug = (path.basename(canonicalRoot) || 'project')
|
|
110
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
111
|
+
.replace(/^-+|-+$/g, '')
|
|
112
|
+
.slice(0, 48) || 'project';
|
|
113
|
+
const identity = process.platform === 'win32'
|
|
114
|
+
? canonicalRoot.toLowerCase()
|
|
115
|
+
: canonicalRoot;
|
|
116
|
+
const hash = crypto.createHash('sha256').update(identity).digest('hex').slice(0, 20);
|
|
117
|
+
return path.join(getUserCacheRoot(options), CACHE_PROJECTS_DIRECTORY, `${slug}-${hash}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} projectRoot
|
|
122
|
+
* @param {object} [options]
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
function getProjectCachePath(projectRoot, options) {
|
|
126
|
+
return path.join(getProjectCacheDir(projectRoot, options), 'index.json');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} projectRoot
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
function getLegacyProjectCacheDir(projectRoot) {
|
|
134
|
+
return path.join(path.resolve(projectRoot), LEGACY_CACHE_DIRECTORY);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Move a legacy project-local cache to the per-user location. Migration is
|
|
139
|
+
* best-effort because cache availability must never block an analysis query.
|
|
140
|
+
* Symlinks are left untouched to avoid following or deleting user-managed
|
|
141
|
+
* paths outside the project.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} projectRoot
|
|
144
|
+
* @param {string} [targetDir]
|
|
145
|
+
* @returns {boolean} True when a legacy directory was removed or migrated.
|
|
146
|
+
*/
|
|
147
|
+
function migrateLegacyProjectCache(projectRoot, targetDir = getProjectCacheDir(projectRoot)) {
|
|
148
|
+
const legacyDir = getLegacyProjectCacheDir(projectRoot);
|
|
149
|
+
let legacyStat;
|
|
150
|
+
try {
|
|
151
|
+
legacyStat = fs.lstatSync(legacyDir);
|
|
152
|
+
} catch (_) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (!legacyStat.isDirectory() || legacyStat.isSymbolicLink()) return false;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
if (fs.existsSync(targetDir)) {
|
|
159
|
+
fs.rmSync(legacyDir, { recursive: true, force: true });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
164
|
+
try {
|
|
165
|
+
fs.renameSync(legacyDir, targetDir);
|
|
166
|
+
return true;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.code !== 'EXDEV') throw error;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tempDir = `${targetDir}.migrating-${process.pid}-${Date.now()}`;
|
|
172
|
+
try {
|
|
173
|
+
fs.cpSync(legacyDir, tempDir, { recursive: true, errorOnExist: true });
|
|
174
|
+
fs.renameSync(tempDir, targetDir);
|
|
175
|
+
fs.rmSync(legacyDir, { recursive: true, force: true });
|
|
176
|
+
return true;
|
|
177
|
+
} catch (_) {
|
|
178
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
} catch (_) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Remove both the current per-user cache and the obsolete project-local cache
|
|
188
|
+
* for one project.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} projectRoot
|
|
191
|
+
* @returns {string[]} Removed directories.
|
|
192
|
+
*/
|
|
193
|
+
function clearProjectCache(projectRoot) {
|
|
194
|
+
const removed = [];
|
|
195
|
+
for (const cacheDir of [
|
|
196
|
+
getProjectCacheDir(projectRoot),
|
|
197
|
+
getLegacyProjectCacheDir(projectRoot),
|
|
198
|
+
]) {
|
|
199
|
+
try {
|
|
200
|
+
if (!fs.existsSync(cacheDir)) continue;
|
|
201
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
202
|
+
removed.push(cacheDir);
|
|
203
|
+
} catch (_) {
|
|
204
|
+
// Cache cleanup is best-effort; callers can rebuild regardless.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return removed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function directorySize(root) {
|
|
211
|
+
let total = 0;
|
|
212
|
+
const pending = [root];
|
|
213
|
+
while (pending.length > 0) {
|
|
214
|
+
const current = pending.pop();
|
|
215
|
+
let entries;
|
|
216
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); }
|
|
217
|
+
catch (_) { continue; }
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const target = path.join(current, entry.name);
|
|
220
|
+
if (entry.isSymbolicLink()) continue;
|
|
221
|
+
if (entry.isDirectory()) pending.push(target);
|
|
222
|
+
else {
|
|
223
|
+
try { total += fs.statSync(target).size; } catch (_) { /* raced */ }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return total;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Bound the shared per-user cache by age, project count, and total bytes. */
|
|
231
|
+
function pruneUserCache({ force = false, now = Date.now() } = {}) {
|
|
232
|
+
const cacheRoot = getUserCacheRoot();
|
|
233
|
+
const projectsRoot = path.join(cacheRoot, CACHE_PROJECTS_DIRECTORY);
|
|
234
|
+
const marker = path.join(cacheRoot, '.last-pruned');
|
|
235
|
+
try {
|
|
236
|
+
if (!force && fs.existsSync(marker) &&
|
|
237
|
+
now - fs.statSync(marker).mtimeMs < CACHE_PRUNE_INTERVAL_MS) {
|
|
238
|
+
return { removed: [], skipped: true };
|
|
239
|
+
}
|
|
240
|
+
} catch (_) { /* run maintenance */ }
|
|
241
|
+
if (!fs.existsSync(projectsRoot)) return { removed: [], skipped: false };
|
|
242
|
+
|
|
243
|
+
const removed = [];
|
|
244
|
+
const entries = [];
|
|
245
|
+
for (const dirent of fs.readdirSync(projectsRoot, { withFileTypes: true })) {
|
|
246
|
+
if (!dirent.isDirectory() || dirent.isSymbolicLink()) continue;
|
|
247
|
+
const dir = path.join(projectsRoot, dirent.name);
|
|
248
|
+
const indexFile = path.join(dir, 'index.json');
|
|
249
|
+
let root = null;
|
|
250
|
+
let touched = 0;
|
|
251
|
+
try {
|
|
252
|
+
const stat = fs.statSync(indexFile);
|
|
253
|
+
touched = stat.mtimeMs;
|
|
254
|
+
const data = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
255
|
+
root = data.root || null;
|
|
256
|
+
touched = Math.max(touched, Number(data.timestamp) || 0);
|
|
257
|
+
} catch (_) { /* malformed/incomplete cache expires below */ }
|
|
258
|
+
if (!root || !fs.existsSync(root) || now - touched > CACHE_TTL_MS) {
|
|
259
|
+
try {
|
|
260
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
261
|
+
removed.push(dir);
|
|
262
|
+
} catch (_) { /* best effort */ }
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
entries.push({ dir, touched, bytes: null });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
entries.sort((a, b) => a.touched - b.touched || codeUnitCompare(a.dir, b.dir));
|
|
269
|
+
while (entries.length > CACHE_MAX_PROJECTS) {
|
|
270
|
+
const victim = entries.shift();
|
|
271
|
+
try { fs.rmSync(victim.dir, { recursive: true, force: true }); removed.push(victim.dir); }
|
|
272
|
+
catch (_) { /* best effort */ }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let totalBytes = 0;
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
entry.bytes = directorySize(entry.dir);
|
|
278
|
+
totalBytes += entry.bytes;
|
|
279
|
+
}
|
|
280
|
+
while (entries.length > 1 && totalBytes > CACHE_MAX_BYTES) {
|
|
281
|
+
const victim = entries.shift();
|
|
282
|
+
try {
|
|
283
|
+
fs.rmSync(victim.dir, { recursive: true, force: true });
|
|
284
|
+
removed.push(victim.dir);
|
|
285
|
+
totalBytes -= victim.bytes;
|
|
286
|
+
} catch (_) { /* best effort */ }
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
fs.mkdirSync(cacheRoot, { recursive: true });
|
|
290
|
+
fs.writeFileSync(marker, String(now));
|
|
291
|
+
} catch (_) { /* maintenance must never block analysis */ }
|
|
292
|
+
return { removed, skipped: false, totalBytes, projects: entries.length };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function clearAllCaches() {
|
|
296
|
+
const cacheRoot = getUserCacheRoot();
|
|
297
|
+
try {
|
|
298
|
+
if (!fs.existsSync(cacheRoot)) return [];
|
|
299
|
+
fs.rmSync(cacheRoot, { recursive: true, force: true });
|
|
300
|
+
return [cacheRoot];
|
|
301
|
+
} catch (_) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
16
306
|
// Index/calls cache format version — bump when the persisted call-record or
|
|
17
307
|
// symbol shape changes (saveCache writes it; loadCache rejects anything else).
|
|
18
308
|
// v14: Go qualified composite literals (pkg.Foo{...}) record the package
|
|
@@ -164,7 +454,152 @@ const UCN_VERSION = require('../package.json').version;
|
|
|
164
454
|
// (`m.get_many::<T>()`); v64 caches misclassified them as references.
|
|
165
455
|
// v66: Rust call records preserve receiverFlowInvalidated after a non-call
|
|
166
456
|
// lexical rebinding, preventing stale return types from excluding true calls.
|
|
167
|
-
|
|
457
|
+
// v67: JS/TS constructor calls preserve lexical-shadow evidence, and method
|
|
458
|
+
// calls on fresh constructions preserve the exact constructed receiver type.
|
|
459
|
+
// Both fields affect caller identity and must not be read from older shards.
|
|
460
|
+
// v68: unaliased dotted Python imports persist separate package and submodule
|
|
461
|
+
// ownership edges (`import pkg.sub` binds pkg and loads pkg.sub).
|
|
462
|
+
// v69: CommonJS simple-require bindings preserve defaultLike so callable
|
|
463
|
+
// module.exports values resolve exactly across caller and callee queries.
|
|
464
|
+
// v70: Python receiver records preserve exact constructor provenance and
|
|
465
|
+
// context-manager result bindings for conservative dispatch tiering.
|
|
466
|
+
// v71: structural method records preserve untyped local-receiver ownership;
|
|
467
|
+
// single-owner spelling alone cannot confirm a parameter/assigned value.
|
|
468
|
+
// v72: Rust type symbols preserve the compiler-declared Deref Target so
|
|
469
|
+
// wrapper receiver method lookup reaches the target type without guessing.
|
|
470
|
+
// v73: JS/TS call records preserve qualified-constructor provenance and bound
|
|
471
|
+
// local receivers; conditional reassignments no longer persist a definite
|
|
472
|
+
// inferred constructor type past a branch.
|
|
473
|
+
// v74: JS/TS CommonJS object-spread barrels persist `module.exports = {
|
|
474
|
+
// ...require('./source') }` as source-bearing re-export-all records. Namespace
|
|
475
|
+
// calls can therefore establish exact name ownership through the facade.
|
|
476
|
+
// Java nested type symbols preserve their enclosing type for exact constructor
|
|
477
|
+
// ownership across same-named top-level and inner classes, and cast receivers
|
|
478
|
+
// retain their compiler-declared type. Rust tuple fields are indexed by numeric
|
|
479
|
+
// position so `self.0.method()` participates in declared-field resolution.
|
|
480
|
+
// v75: v5's shared IR path persists lexical-owner ranges and C/C++/C# symbol
|
|
481
|
+
// metadata; JS/TS call records preserve inline CommonJS module ownership,
|
|
482
|
+
// explicit builtin-member reassignment, and the richer C-family receiver/flow
|
|
483
|
+
// evidence used by caller resolution.
|
|
484
|
+
// v76: Java member symbols retain their nested enclosing type so overload and
|
|
485
|
+
// owner identity cannot conflate same-named inner classes.
|
|
486
|
+
// v77: Java call records retain explicit type-qualified receivers and nested
|
|
487
|
+
// type qualifiers for receiver-owner routing before overload selection.
|
|
488
|
+
// v78: Java argument kinds retain qualified producer ownership, same-class
|
|
489
|
+
// helper calls, and generic collection value types for exact overload routing.
|
|
490
|
+
// v79: Java chained-call records retain their full producer type path for
|
|
491
|
+
// platform collection/receiver ownership.
|
|
492
|
+
// v80: Rust chained-call records retain exact producer byte offsets so
|
|
493
|
+
// same-line nested constructors cannot collide in the fold.
|
|
494
|
+
// v81: Rust producer identities include the complete byte span, separating
|
|
495
|
+
// repeated same-name hops whose nested call expressions share a start offset.
|
|
496
|
+
// v82: Rust macro symbols persist conservative transcriber return contracts;
|
|
497
|
+
// macro invocations and token-tree calls retain complete producer spans and
|
|
498
|
+
// path ownership for exact builder-chain folding.
|
|
499
|
+
// v83: Rust callable symbols persist callback parameter type contracts and
|
|
500
|
+
// calls inside closures retain their enclosing call/argument identity.
|
|
501
|
+
// v84: Rust token-tree call records retain their containing macro identity so
|
|
502
|
+
// code-generation templates (`quote!`) can use their own trust policy.
|
|
503
|
+
// v85: Rust callable symbols persist declared iterator Item types for exact
|
|
504
|
+
// closure and loop receiver inference.
|
|
505
|
+
// v86: JS/TS package self-references can resolve a source entry when exported
|
|
506
|
+
// build artifacts are absent, changing persisted module ownership graphs.
|
|
507
|
+
// v87: JS/TS and Rust call records retain their lexical function-scope chain
|
|
508
|
+
// so return-flow assignments remain visible inside nested closures.
|
|
509
|
+
// v88: JS/TS callable symbols persist a unanimous concrete constructor return
|
|
510
|
+
// for runtime dispatch through interface-typed factory annotations.
|
|
511
|
+
// v89: JS/TS call records retain exact producer byte spans for same-line
|
|
512
|
+
// return-flow assignments and chained-call identity.
|
|
513
|
+
// v90: TypeScript object type aliases persist their declared field/method
|
|
514
|
+
// members so structural field-hop resolution can use their type contracts;
|
|
515
|
+
// explicit Type.prototype method bindings retain their type owner.
|
|
516
|
+
// v91: Python call records retain complete attribute receiver paths and their
|
|
517
|
+
// annotated root type for multi-hop declared-field resolution.
|
|
518
|
+
// v92: Python calls retain isinstance-refined receiver types.
|
|
519
|
+
// v93: Python calls retain receiver types derived from annotated iterable
|
|
520
|
+
// tuple destructuring in loops and comprehensions.
|
|
521
|
+
// v94: Python variadic parameter receivers retain their container type
|
|
522
|
+
// (**kwargs is dict, *args is tuple).
|
|
523
|
+
// v95: Python static generic aliases normalize to their runtime identity;
|
|
524
|
+
// calls retain receiver types from same-file callable iterable returns and
|
|
525
|
+
// collection-protocol conditional normalization.
|
|
526
|
+
// v96: Python explicit instance-field type comments type receivers yielded
|
|
527
|
+
// by loops and comprehensions over those fields.
|
|
528
|
+
// v97: Python receiver records preserve compiler-declared variable types
|
|
529
|
+
// through later assignments and unwrap parenthesized chained receivers.
|
|
530
|
+
// v98: Python calls retain declared attribute paths that supply loop and
|
|
531
|
+
// comprehension receiver values.
|
|
532
|
+
// v99: Python context-manager producer calls retain their `as` target so
|
|
533
|
+
// Iterator/ContextManager return contracts type the bound receiver.
|
|
534
|
+
// v100: builtin open() context bindings retain their IO receiver type.
|
|
535
|
+
// v101: Python generator yield assignments retain the declared send type.
|
|
536
|
+
// v102: Python subscript receivers retain exact value types from local
|
|
537
|
+
// string-keyed dictionary literals.
|
|
538
|
+
// v103: Python pickle round-trip receivers retain the serialized value's
|
|
539
|
+
// type plus the stdlib provenance needed to validate that contract.
|
|
540
|
+
// v104: Python self-referential assignment calls retain the receiver's
|
|
541
|
+
// pre-assignment type.
|
|
542
|
+
// v105: Python calls persist nested lexical ownership, returned-constructor
|
|
543
|
+
// contracts, and runtime instance-field assignments used by exact receiver
|
|
544
|
+
// flow.
|
|
545
|
+
// v106: Go calls persist package-scope typed receivers and exact wrapper-call
|
|
546
|
+
// ownership used by the nominal dispatch path.
|
|
547
|
+
// v107: Rust imports persist fully flattened grouped-use leaves; call records
|
|
548
|
+
// retain iterator/collection contracts, match-payload provenance, multi-hop
|
|
549
|
+
// field paths, tuple/match assignment producers, and primitive slice roots.
|
|
550
|
+
// v108: JavaScript/TypeScript call records retain unresolved deep-member
|
|
551
|
+
// receiver provenance so name bindings cannot impersonate receiver identity.
|
|
552
|
+
// v109: Python call records retain positive receiver-exact hasattr capability
|
|
553
|
+
// guards so uncertainty can name the runtime dispatch boundary.
|
|
554
|
+
// v110: Rust untyped closure parameters block same-named outer receiver
|
|
555
|
+
// annotations instead of persisting a stale, unrelated receiver type.
|
|
556
|
+
// v111: Java call IR preserves lexical nested-type owners and uses declared
|
|
557
|
+
// field types for overload argument shapes.
|
|
558
|
+
// v112: Java argument kinds retain class-qualified field identity so the
|
|
559
|
+
// field declaration's static value type participates in overload selection.
|
|
560
|
+
// v113: persist common unsupported source files so cached repo/health output
|
|
561
|
+
// cannot silently lose the grep handoff discovered during a full scan.
|
|
562
|
+
// v114: Rust impl owners normalize nested generics and reference impls to the
|
|
563
|
+
// same concrete identity used by receiver-flow analysis.
|
|
564
|
+
// v115: C# callable symbols persist extension-method identity and their
|
|
565
|
+
// receiver parameter marker; string/char literal receiver typing changes the
|
|
566
|
+
// call evidence consumed from cached indexes.
|
|
567
|
+
// v116: C# call records persist overload argument kinds and callable-scoped
|
|
568
|
+
// receiver types; enum members are indexed as typed fields for exact overload
|
|
569
|
+
// selection.
|
|
570
|
+
// v117: nullable GetValueOrDefault argument shapes retain their underlying
|
|
571
|
+
// value type for C# inherited-overload selection.
|
|
572
|
+
// v118: C# declarations nested under preprocessor nodes and recovered
|
|
573
|
+
// namespace-level method siblings remain attached to their lexical class.
|
|
574
|
+
// v125: C/C++ multiline typedef/class symbols persist the declaration
|
|
575
|
+
// identifier's nameLine so compiler-selected handles survive caching.
|
|
576
|
+
// v126: C/C++ parser recovery recognizes calling-convention/export macros
|
|
577
|
+
// between a builtin return type and a function name.
|
|
578
|
+
// v127: ambiguous .h files use the repository translation-unit convention
|
|
579
|
+
// when no compilation database or same-directory source sibling is present.
|
|
580
|
+
// v128: C++ using-alias symbols are indexed and template-qualified
|
|
581
|
+
// out-of-line methods close with their in-class declarations.
|
|
582
|
+
// v150: C/C++ call records persist source spans and chained-receiver producer
|
|
583
|
+
// links so the nominal return-type fold can resolve call().member() identity.
|
|
584
|
+
// v151: inheritance graphs normalize compiler-owned qualified nominal bases
|
|
585
|
+
// (for example `detail::buffer<T>`) to their indexed type identity.
|
|
586
|
+
// v152: C/C++ callable signatures persist anonymous C-style variadic tails.
|
|
587
|
+
// v153: C++ callable symbols persist explicit C-language linkage identity.
|
|
588
|
+
// v154: C/C++ calls parsed from preprocessor replacement lists persist their
|
|
589
|
+
// macro-body and macro-parameter provenance.
|
|
590
|
+
// v155: C# method symbols persist explicit-interface ownership so ordinary
|
|
591
|
+
// member overload resolution cannot select an interface-only implementation.
|
|
592
|
+
// v156: C# calls persist cast receiver types and multi-hop/null-forgiving
|
|
593
|
+
// declared-field paths used for compiler-grade member ownership.
|
|
594
|
+
// v157: C# cast receivers distinguish `((IFace)this).M()` from an arbitrary
|
|
595
|
+
// interface-typed variable so explicit implementations are never overclaimed.
|
|
596
|
+
// v159: importBindings persist source lines so rename plans can edit aliased
|
|
597
|
+
// CJS/Python imports without relying on usage-kind heuristics.
|
|
598
|
+
// v160: C# file entries persist project-wide `global using` modules.
|
|
599
|
+
// v166: C++ nested aliases persist their lexical owner ranges, and `auto`
|
|
600
|
+
// return functions persist a unanimously inferred local concrete type. v165
|
|
601
|
+
// was used during prerelease development before both fields were complete.
|
|
602
|
+
const CACHE_FORMAT_VERSION = 166;
|
|
168
603
|
|
|
169
604
|
/**
|
|
170
605
|
* Save index to cache file
|
|
@@ -173,9 +608,12 @@ const CACHE_FORMAT_VERSION = 66;
|
|
|
173
608
|
* @returns {string} - Path to cache file
|
|
174
609
|
*/
|
|
175
610
|
function saveCache(index, cachePath) {
|
|
611
|
+
if (!cachePath) {
|
|
612
|
+
migrateLegacyProjectCache(index.root);
|
|
613
|
+
}
|
|
176
614
|
const cacheDir = cachePath
|
|
177
615
|
? path.dirname(cachePath)
|
|
178
|
-
:
|
|
616
|
+
: getProjectCacheDir(index.root);
|
|
179
617
|
|
|
180
618
|
if (!fs.existsSync(cacheDir)) {
|
|
181
619
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
@@ -197,6 +635,7 @@ function saveCache(index, cachePath) {
|
|
|
197
635
|
// Hash config to detect when graph rebuild is needed on load
|
|
198
636
|
const configHash = crypto.createHash('md5')
|
|
199
637
|
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
638
|
+
const discoveryHash = discoveryRulesHash(index.root);
|
|
200
639
|
|
|
201
640
|
// Strip redundant fields from symbols and file entries to reduce cache size.
|
|
202
641
|
// v6: All paths stored as relative paths (saves ~60% on large codebases).
|
|
@@ -274,6 +713,7 @@ function saveCache(index, cachePath) {
|
|
|
274
713
|
version: CACHE_FORMAT_VERSION,
|
|
275
714
|
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
276
715
|
configHash,
|
|
716
|
+
discoveryHash,
|
|
277
717
|
root,
|
|
278
718
|
// PERF-2: refresh buildTime on each save so partial rebuilds report
|
|
279
719
|
// accurate stats. Falls back to original on first save.
|
|
@@ -289,10 +729,21 @@ function saveCache(index, cachePath) {
|
|
|
289
729
|
failedFiles: index.failedFiles
|
|
290
730
|
? Array.from(index.failedFiles).map(f => path.relative(root, f))
|
|
291
731
|
: [],
|
|
732
|
+
unsupportedFiles: Array.isArray(index.unsupportedFiles)
|
|
733
|
+
? index.unsupportedFiles
|
|
734
|
+
: [],
|
|
735
|
+
discoveryIssues: Array.isArray(index.discoveryIssues)
|
|
736
|
+
? index.discoveryIssues
|
|
737
|
+
: [],
|
|
738
|
+
truncated: index.truncated || null,
|
|
292
739
|
...(reachableSymbolsRel !== undefined && {
|
|
293
740
|
reachableSymbols: reachableSymbolsRel,
|
|
294
741
|
reachableFingerprint,
|
|
295
742
|
}),
|
|
743
|
+
...(index._computedDispatchBlindspots instanceof Map && {
|
|
744
|
+
computedDispatchBlindspots: [...index._computedDispatchBlindspots]
|
|
745
|
+
.map(([filePath, sites]) => [path.relative(root, filePath), sites]),
|
|
746
|
+
}),
|
|
296
747
|
};
|
|
297
748
|
|
|
298
749
|
// PERF-3: atomic write — tmp file + rename so concurrent readers/writers
|
|
@@ -307,6 +758,7 @@ function saveCache(index, cachePath) {
|
|
|
307
758
|
if (index.reachabilityDirty) {
|
|
308
759
|
index.reachabilityDirty = false;
|
|
309
760
|
}
|
|
761
|
+
index.computedDispatchDirty = false;
|
|
310
762
|
|
|
311
763
|
// Save callsCache sharded by directory for lazy loading.
|
|
312
764
|
// Write to a temp directory first, then atomic swap to avoid data loss on crash.
|
|
@@ -354,6 +806,7 @@ function saveCache(index, cachePath) {
|
|
|
354
806
|
}
|
|
355
807
|
}
|
|
356
808
|
|
|
809
|
+
if (!cachePath) pruneUserCache();
|
|
357
810
|
return cacheFile;
|
|
358
811
|
}
|
|
359
812
|
|
|
@@ -364,7 +817,10 @@ function saveCache(index, cachePath) {
|
|
|
364
817
|
* @returns {boolean} - True if loaded successfully
|
|
365
818
|
*/
|
|
366
819
|
function loadCache(index, cachePath) {
|
|
367
|
-
|
|
820
|
+
if (!cachePath) {
|
|
821
|
+
migrateLegacyProjectCache(index.root);
|
|
822
|
+
}
|
|
823
|
+
const cacheFile = cachePath || getProjectCachePath(index.root);
|
|
368
824
|
|
|
369
825
|
if (!fs.existsSync(cacheFile)) {
|
|
370
826
|
return false;
|
|
@@ -412,6 +868,10 @@ function loadCache(index, cachePath) {
|
|
|
412
868
|
? (relPath) => rootPrefix + relPath
|
|
413
869
|
: (relPath) => rootPrefix + relPath.replace(/\//g, path.sep);
|
|
414
870
|
|
|
871
|
+
// Loading into a previously-used ProjectIndex replaces its indexed
|
|
872
|
+
// contents, so no parsed tree from the old state may survive.
|
|
873
|
+
index._clearParsedTreeCache?.();
|
|
874
|
+
|
|
415
875
|
// Reconstruct files Map: relative key → absolute key, restore path and relativePath
|
|
416
876
|
// Initialize symbols/bindings arrays (will be populated from top-level symbols)
|
|
417
877
|
index.files = new Map();
|
|
@@ -494,6 +954,24 @@ function loadCache(index, cachePath) {
|
|
|
494
954
|
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f : toAbs(f))
|
|
495
955
|
);
|
|
496
956
|
}
|
|
957
|
+
index.unsupportedFiles = Array.isArray(cacheData.unsupportedFiles)
|
|
958
|
+
? cacheData.unsupportedFiles
|
|
959
|
+
: [];
|
|
960
|
+
index.discoveryIssues = Array.isArray(cacheData.discoveryIssues)
|
|
961
|
+
? cacheData.discoveryIssues
|
|
962
|
+
: [];
|
|
963
|
+
index.truncated = cacheData.truncated || null;
|
|
964
|
+
index._loadedConfigHash = cacheData.configHash || null;
|
|
965
|
+
index._loadedDiscoveryHash = cacheData.discoveryHash || null;
|
|
966
|
+
if (Array.isArray(cacheData.computedDispatchBlindspots)) {
|
|
967
|
+
index._computedDispatchBlindspots = new Map(
|
|
968
|
+
cacheData.computedDispatchBlindspots.map(([relPath, sites]) => [
|
|
969
|
+
path.isAbsolute(relPath) ? relPath : toAbs(relPath),
|
|
970
|
+
Array.isArray(sites) ? sites : [],
|
|
971
|
+
]),
|
|
972
|
+
);
|
|
973
|
+
index.computedDispatchDirty = false;
|
|
974
|
+
}
|
|
497
975
|
|
|
498
976
|
// Restore calleeIndex if persisted (v7 caches only; v8+ rebuilds lazily)
|
|
499
977
|
if (Array.isArray(cacheData.calleeIndex)) {
|
|
@@ -549,6 +1027,15 @@ function loadCache(index, cachePath) {
|
|
|
549
1027
|
* @returns {boolean} - True if cache needs rebuilding
|
|
550
1028
|
*/
|
|
551
1029
|
function isCacheStale(index) {
|
|
1030
|
+
const currentConfigHash = crypto.createHash('md5')
|
|
1031
|
+
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
1032
|
+
if (index._loadedConfigHash && currentConfigHash !== index._loadedConfigHash) {
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
if (index._loadedDiscoveryHash &&
|
|
1036
|
+
discoveryRulesHash(index.root) !== index._loadedDiscoveryHash) {
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
552
1039
|
// Modified/deleted detection (stat sweep) runs UNCONDITIONALLY — agents
|
|
553
1040
|
// edit a file and re-query through MCP within seconds, and a stale answer
|
|
554
1041
|
// presented as fresh is the worst trust failure the tool can produce.
|
|
@@ -594,20 +1081,52 @@ function isCacheStale(index) {
|
|
|
594
1081
|
// Slow path: glob the project to detect new files added since last build.
|
|
595
1082
|
// Only reached when all cached files are unchanged.
|
|
596
1083
|
const pattern = detectProjectPattern(index.root);
|
|
597
|
-
const
|
|
1084
|
+
const currentUnsupported = [];
|
|
1085
|
+
const globOpts = {
|
|
1086
|
+
root: index.root,
|
|
1087
|
+
onSkippedFile: (filePath) => {
|
|
1088
|
+
const kind = classifyUnsupportedSourceFile(filePath);
|
|
1089
|
+
if (!kind) return;
|
|
1090
|
+
currentUnsupported.push({
|
|
1091
|
+
relativePath: path.relative(index.root, filePath),
|
|
1092
|
+
...kind,
|
|
1093
|
+
});
|
|
1094
|
+
},
|
|
1095
|
+
};
|
|
598
1096
|
const gitignorePatterns = parseGitignore(index.root);
|
|
1097
|
+
globOpts.gitignorePatterns = gitignorePatterns;
|
|
1098
|
+
globOpts.trackedPaths = gitTrackedPaths(index.root);
|
|
599
1099
|
const configExclude = index.config.exclude || [];
|
|
600
|
-
if (
|
|
601
|
-
globOpts.ignores = [...DEFAULT_IGNORES, ...
|
|
1100
|
+
if (configExclude.length > 0) {
|
|
1101
|
+
globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
|
|
602
1102
|
}
|
|
603
1103
|
const currentFiles = expandGlob(pattern, globOpts);
|
|
604
1104
|
const cachedPaths = new Set(index.files.keys());
|
|
1105
|
+
const currentPaths = new Set(currentFiles);
|
|
1106
|
+
|
|
1107
|
+
// A cached file can still exist on disk while becoming excluded by a new
|
|
1108
|
+
// .gitignore/.ucn.json rule. Treat that set contraction as stale too;
|
|
1109
|
+
// otherwise ignored code remains queryable until an unrelated edit forces
|
|
1110
|
+
// a rebuild.
|
|
1111
|
+
for (const file of cachedPaths) {
|
|
1112
|
+
if (!currentPaths.has(file)) return true;
|
|
1113
|
+
}
|
|
605
1114
|
|
|
606
1115
|
for (const file of currentFiles) {
|
|
607
1116
|
if (!cachedPaths.has(file) && !(index.failedFiles && index.failedFiles.has(file))) {
|
|
608
1117
|
return true; // New file found
|
|
609
1118
|
}
|
|
610
1119
|
}
|
|
1120
|
+
const cachedUnsupported = (index.unsupportedFiles || [])
|
|
1121
|
+
.map(f => `${f.relativePath}\0${f.language}`)
|
|
1122
|
+
.sort();
|
|
1123
|
+
const discoveredUnsupported = currentUnsupported
|
|
1124
|
+
.map(f => `${f.relativePath}\0${f.language}`)
|
|
1125
|
+
.sort();
|
|
1126
|
+
if (cachedUnsupported.length !== discoveredUnsupported.length ||
|
|
1127
|
+
cachedUnsupported.some((value, i) => value !== discoveredUnsupported[i])) {
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
611
1130
|
|
|
612
1131
|
// Record when we last confirmed the cache is fresh (enables 2s skip on burst calls)
|
|
613
1132
|
index._lastFreshAt = Date.now();
|
|
@@ -624,11 +1143,11 @@ function _prepareCallsCache(index, cacheFile) {
|
|
|
624
1143
|
if (index._callsCacheLoaded) return;
|
|
625
1144
|
// Shards live beside the selected index.json. A custom cachePath must be
|
|
626
1145
|
// a complete portable cache, not an index file that silently looks for
|
|
627
|
-
// call shards under
|
|
1146
|
+
// call shards under the default project cache. The latter caused cache-loaded
|
|
628
1147
|
// semantic queries to reparse source (or consume unrelated stale shards).
|
|
629
1148
|
const cacheDir = cacheFile
|
|
630
1149
|
? path.dirname(cacheFile)
|
|
631
|
-
:
|
|
1150
|
+
: getProjectCacheDir(index.root);
|
|
632
1151
|
index._callsCacheDir = cacheDir;
|
|
633
1152
|
const manifestFile = path.join(cacheDir, 'calls', 'manifest.json');
|
|
634
1153
|
if (fs.existsSync(manifestFile)) {
|
|
@@ -674,7 +1193,7 @@ function loadCallsCache(index) {
|
|
|
674
1193
|
|
|
675
1194
|
// Legacy format: single calls-cache.json
|
|
676
1195
|
const callsCacheFile = index._callsCacheLegacyFile ||
|
|
677
|
-
path.join(index._callsCacheDir ||
|
|
1196
|
+
path.join(index._callsCacheDir || getProjectCacheDir(index.root), 'calls-cache.json');
|
|
678
1197
|
if (!fs.existsSync(callsCacheFile)) return index.callsCache.size > 0;
|
|
679
1198
|
|
|
680
1199
|
try {
|
|
@@ -713,7 +1232,7 @@ function ensureCallsCacheLoaded(index) {
|
|
|
713
1232
|
*/
|
|
714
1233
|
function _loadCallsShard(index, hash) {
|
|
715
1234
|
const shardFile = path.join(
|
|
716
|
-
index._callsCacheDir ||
|
|
1235
|
+
index._callsCacheDir || getProjectCacheDir(index.root),
|
|
717
1236
|
'calls', `${hash}.json`);
|
|
718
1237
|
try {
|
|
719
1238
|
const data = JSON.parse(fs.readFileSync(shardFile, 'utf-8'));
|
|
@@ -769,5 +1288,8 @@ function _computeReachabilityFingerprint(index) {
|
|
|
769
1288
|
|
|
770
1289
|
module.exports = {
|
|
771
1290
|
saveCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
|
|
1291
|
+
getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
|
|
1292
|
+
getLegacyProjectCacheDir, migrateLegacyProjectCache, clearProjectCache,
|
|
1293
|
+
clearAllCaches, pruneUserCache,
|
|
772
1294
|
_computeReachabilityFingerprint, CACHE_FORMAT_VERSION,
|
|
773
1295
|
};
|