ucn 4.2.3 → 5.0.2
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 +438 -305
- 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 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- 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 +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- 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 +212 -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 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- 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 +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- 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
|
|
@@ -180,11 +470,136 @@ const UCN_VERSION = require('../package.json').version;
|
|
|
180
470
|
// v73: JS/TS call records preserve qualified-constructor provenance and bound
|
|
181
471
|
// local receivers; conditional reassignments no longer persist a definite
|
|
182
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.
|
|
183
476
|
// Java nested type symbols preserve their enclosing type for exact constructor
|
|
184
477
|
// ownership across same-named top-level and inner classes, and cast receivers
|
|
185
478
|
// retain their compiler-declared type. Rust tuple fields are indexed by numeric
|
|
186
479
|
// position so `self.0.method()` participates in declared-field resolution.
|
|
187
|
-
|
|
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;
|
|
188
603
|
|
|
189
604
|
/**
|
|
190
605
|
* Save index to cache file
|
|
@@ -193,9 +608,12 @@ const CACHE_FORMAT_VERSION = 73;
|
|
|
193
608
|
* @returns {string} - Path to cache file
|
|
194
609
|
*/
|
|
195
610
|
function saveCache(index, cachePath) {
|
|
611
|
+
if (!cachePath) {
|
|
612
|
+
migrateLegacyProjectCache(index.root);
|
|
613
|
+
}
|
|
196
614
|
const cacheDir = cachePath
|
|
197
615
|
? path.dirname(cachePath)
|
|
198
|
-
:
|
|
616
|
+
: getProjectCacheDir(index.root);
|
|
199
617
|
|
|
200
618
|
if (!fs.existsSync(cacheDir)) {
|
|
201
619
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
@@ -217,6 +635,7 @@ function saveCache(index, cachePath) {
|
|
|
217
635
|
// Hash config to detect when graph rebuild is needed on load
|
|
218
636
|
const configHash = crypto.createHash('md5')
|
|
219
637
|
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
638
|
+
const discoveryHash = discoveryRulesHash(index.root);
|
|
220
639
|
|
|
221
640
|
// Strip redundant fields from symbols and file entries to reduce cache size.
|
|
222
641
|
// v6: All paths stored as relative paths (saves ~60% on large codebases).
|
|
@@ -294,6 +713,7 @@ function saveCache(index, cachePath) {
|
|
|
294
713
|
version: CACHE_FORMAT_VERSION,
|
|
295
714
|
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
296
715
|
configHash,
|
|
716
|
+
discoveryHash,
|
|
297
717
|
root,
|
|
298
718
|
// PERF-2: refresh buildTime on each save so partial rebuilds report
|
|
299
719
|
// accurate stats. Falls back to original on first save.
|
|
@@ -309,10 +729,21 @@ function saveCache(index, cachePath) {
|
|
|
309
729
|
failedFiles: index.failedFiles
|
|
310
730
|
? Array.from(index.failedFiles).map(f => path.relative(root, f))
|
|
311
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,
|
|
312
739
|
...(reachableSymbolsRel !== undefined && {
|
|
313
740
|
reachableSymbols: reachableSymbolsRel,
|
|
314
741
|
reachableFingerprint,
|
|
315
742
|
}),
|
|
743
|
+
...(index._computedDispatchBlindspots instanceof Map && {
|
|
744
|
+
computedDispatchBlindspots: [...index._computedDispatchBlindspots]
|
|
745
|
+
.map(([filePath, sites]) => [path.relative(root, filePath), sites]),
|
|
746
|
+
}),
|
|
316
747
|
};
|
|
317
748
|
|
|
318
749
|
// PERF-3: atomic write — tmp file + rename so concurrent readers/writers
|
|
@@ -327,6 +758,7 @@ function saveCache(index, cachePath) {
|
|
|
327
758
|
if (index.reachabilityDirty) {
|
|
328
759
|
index.reachabilityDirty = false;
|
|
329
760
|
}
|
|
761
|
+
index.computedDispatchDirty = false;
|
|
330
762
|
|
|
331
763
|
// Save callsCache sharded by directory for lazy loading.
|
|
332
764
|
// Write to a temp directory first, then atomic swap to avoid data loss on crash.
|
|
@@ -374,6 +806,7 @@ function saveCache(index, cachePath) {
|
|
|
374
806
|
}
|
|
375
807
|
}
|
|
376
808
|
|
|
809
|
+
if (!cachePath) pruneUserCache();
|
|
377
810
|
return cacheFile;
|
|
378
811
|
}
|
|
379
812
|
|
|
@@ -384,7 +817,10 @@ function saveCache(index, cachePath) {
|
|
|
384
817
|
* @returns {boolean} - True if loaded successfully
|
|
385
818
|
*/
|
|
386
819
|
function loadCache(index, cachePath) {
|
|
387
|
-
|
|
820
|
+
if (!cachePath) {
|
|
821
|
+
migrateLegacyProjectCache(index.root);
|
|
822
|
+
}
|
|
823
|
+
const cacheFile = cachePath || getProjectCachePath(index.root);
|
|
388
824
|
|
|
389
825
|
if (!fs.existsSync(cacheFile)) {
|
|
390
826
|
return false;
|
|
@@ -432,6 +868,10 @@ function loadCache(index, cachePath) {
|
|
|
432
868
|
? (relPath) => rootPrefix + relPath
|
|
433
869
|
: (relPath) => rootPrefix + relPath.replace(/\//g, path.sep);
|
|
434
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
|
+
|
|
435
875
|
// Reconstruct files Map: relative key → absolute key, restore path and relativePath
|
|
436
876
|
// Initialize symbols/bindings arrays (will be populated from top-level symbols)
|
|
437
877
|
index.files = new Map();
|
|
@@ -514,6 +954,24 @@ function loadCache(index, cachePath) {
|
|
|
514
954
|
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f : toAbs(f))
|
|
515
955
|
);
|
|
516
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
|
+
}
|
|
517
975
|
|
|
518
976
|
// Restore calleeIndex if persisted (v7 caches only; v8+ rebuilds lazily)
|
|
519
977
|
if (Array.isArray(cacheData.calleeIndex)) {
|
|
@@ -569,6 +1027,15 @@ function loadCache(index, cachePath) {
|
|
|
569
1027
|
* @returns {boolean} - True if cache needs rebuilding
|
|
570
1028
|
*/
|
|
571
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
|
+
}
|
|
572
1039
|
// Modified/deleted detection (stat sweep) runs UNCONDITIONALLY — agents
|
|
573
1040
|
// edit a file and re-query through MCP within seconds, and a stale answer
|
|
574
1041
|
// presented as fresh is the worst trust failure the tool can produce.
|
|
@@ -614,20 +1081,52 @@ function isCacheStale(index) {
|
|
|
614
1081
|
// Slow path: glob the project to detect new files added since last build.
|
|
615
1082
|
// Only reached when all cached files are unchanged.
|
|
616
1083
|
const pattern = detectProjectPattern(index.root);
|
|
617
|
-
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
|
+
};
|
|
618
1096
|
const gitignorePatterns = parseGitignore(index.root);
|
|
1097
|
+
globOpts.gitignorePatterns = gitignorePatterns;
|
|
1098
|
+
globOpts.trackedPaths = gitTrackedPaths(index.root);
|
|
619
1099
|
const configExclude = index.config.exclude || [];
|
|
620
|
-
if (
|
|
621
|
-
globOpts.ignores = [...DEFAULT_IGNORES, ...
|
|
1100
|
+
if (configExclude.length > 0) {
|
|
1101
|
+
globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
|
|
622
1102
|
}
|
|
623
1103
|
const currentFiles = expandGlob(pattern, globOpts);
|
|
624
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
|
+
}
|
|
625
1114
|
|
|
626
1115
|
for (const file of currentFiles) {
|
|
627
1116
|
if (!cachedPaths.has(file) && !(index.failedFiles && index.failedFiles.has(file))) {
|
|
628
1117
|
return true; // New file found
|
|
629
1118
|
}
|
|
630
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
|
+
}
|
|
631
1130
|
|
|
632
1131
|
// Record when we last confirmed the cache is fresh (enables 2s skip on burst calls)
|
|
633
1132
|
index._lastFreshAt = Date.now();
|
|
@@ -644,11 +1143,11 @@ function _prepareCallsCache(index, cacheFile) {
|
|
|
644
1143
|
if (index._callsCacheLoaded) return;
|
|
645
1144
|
// Shards live beside the selected index.json. A custom cachePath must be
|
|
646
1145
|
// a complete portable cache, not an index file that silently looks for
|
|
647
|
-
// call shards under
|
|
1146
|
+
// call shards under the default project cache. The latter caused cache-loaded
|
|
648
1147
|
// semantic queries to reparse source (or consume unrelated stale shards).
|
|
649
1148
|
const cacheDir = cacheFile
|
|
650
1149
|
? path.dirname(cacheFile)
|
|
651
|
-
:
|
|
1150
|
+
: getProjectCacheDir(index.root);
|
|
652
1151
|
index._callsCacheDir = cacheDir;
|
|
653
1152
|
const manifestFile = path.join(cacheDir, 'calls', 'manifest.json');
|
|
654
1153
|
if (fs.existsSync(manifestFile)) {
|
|
@@ -694,7 +1193,7 @@ function loadCallsCache(index) {
|
|
|
694
1193
|
|
|
695
1194
|
// Legacy format: single calls-cache.json
|
|
696
1195
|
const callsCacheFile = index._callsCacheLegacyFile ||
|
|
697
|
-
path.join(index._callsCacheDir ||
|
|
1196
|
+
path.join(index._callsCacheDir || getProjectCacheDir(index.root), 'calls-cache.json');
|
|
698
1197
|
if (!fs.existsSync(callsCacheFile)) return index.callsCache.size > 0;
|
|
699
1198
|
|
|
700
1199
|
try {
|
|
@@ -733,7 +1232,7 @@ function ensureCallsCacheLoaded(index) {
|
|
|
733
1232
|
*/
|
|
734
1233
|
function _loadCallsShard(index, hash) {
|
|
735
1234
|
const shardFile = path.join(
|
|
736
|
-
index._callsCacheDir ||
|
|
1235
|
+
index._callsCacheDir || getProjectCacheDir(index.root),
|
|
737
1236
|
'calls', `${hash}.json`);
|
|
738
1237
|
try {
|
|
739
1238
|
const data = JSON.parse(fs.readFileSync(shardFile, 'utf-8'));
|
|
@@ -789,5 +1288,8 @@ function _computeReachabilityFingerprint(index) {
|
|
|
789
1288
|
|
|
790
1289
|
module.exports = {
|
|
791
1290
|
saveCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
|
|
1291
|
+
getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
|
|
1292
|
+
getLegacyProjectCacheDir, migrateLegacyProjectCache, clearProjectCache,
|
|
1293
|
+
clearAllCaches, pruneUserCache,
|
|
792
1294
|
_computeReachabilityFingerprint, CACHE_FORMAT_VERSION,
|
|
793
1295
|
};
|