ucn 3.8.22 → 3.8.25
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 +114 -11
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +960 -37
- package/core/bridge.js +1111 -0
- package/core/brief.js +408 -0
- package/core/cache.js +213 -59
- package/core/callers.js +117 -41
- package/core/check.js +200 -0
- package/core/deadcode.js +31 -2
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph-build.js +4 -4
- package/core/graph.js +31 -12
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parallel-build.js +10 -7
- package/core/parser.js +8 -2
- package/core/project.js +147 -41
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +139 -15
- package/core/shared.js +101 -5
- package/core/tracing.js +31 -12
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +10 -2
package/core/cache.js
CHANGED
|
@@ -68,34 +68,53 @@ function saveCache(index, cachePath) {
|
|
|
68
68
|
strippedFiles.push([rp, rest]);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Convert graph paths from absolute to relative
|
|
71
|
+
// Convert graph paths from absolute to relative (Sets serialized as arrays)
|
|
72
72
|
const relGraph = (graph) => {
|
|
73
73
|
const result = [];
|
|
74
74
|
for (const [absKey, absValues] of graph) {
|
|
75
75
|
const relKey = path.relative(root, absKey);
|
|
76
|
-
const relValues = absValues.map(v => path.relative(root, v));
|
|
76
|
+
const relValues = [...absValues].map(v => path.relative(root, v));
|
|
77
77
|
result.push([relKey, relValues]);
|
|
78
78
|
}
|
|
79
79
|
return result;
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
// calleeIndex is NOT persisted in index.json — it's rebuilt lazily from callsCache
|
|
83
|
+
// on first findCallers/buildCalleeIndex call. Removing it saves ~22MB (14%) on large projects.
|
|
84
|
+
|
|
85
|
+
// PERF-1: persist _reachableSymbols if computed. Set keys are
|
|
86
|
+
// "absolutePath:line"; we strip the root prefix on save and re-attach on
|
|
87
|
+
// load so paths stay portable. Sorted for stable output ordering.
|
|
88
|
+
//
|
|
89
|
+
// Also save a fingerprint so we can detect index drift on load: if the
|
|
90
|
+
// saved fingerprint matches the loaded index state, the cached set is
|
|
91
|
+
// still valid. If the index was rebuilt after load (stale cache → build),
|
|
92
|
+
// the fingerprint won't match and computeReachability will recompute.
|
|
93
|
+
let reachableSymbolsRel = undefined;
|
|
94
|
+
let reachableFingerprint = undefined;
|
|
95
|
+
if (index._reachableSymbols && index._reachableSymbols.size > 0) {
|
|
96
|
+
const rels = [];
|
|
97
|
+
for (const k of index._reachableSymbols) {
|
|
98
|
+
const colon = k.lastIndexOf(':');
|
|
99
|
+
if (colon < 0) continue;
|
|
100
|
+
const absFile = k.slice(0, colon);
|
|
101
|
+
const lineStr = k.slice(colon + 1);
|
|
102
|
+
const relFile = path.relative(root, absFile);
|
|
103
|
+
rels.push(`${relFile}:${lineStr}`);
|
|
91
104
|
}
|
|
105
|
+
rels.sort(); // stable ordering — output contract
|
|
106
|
+
reachableSymbolsRel = rels;
|
|
107
|
+
reachableFingerprint = _computeReachabilityFingerprint(index);
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
const cacheData = {
|
|
95
|
-
|
|
111
|
+
// v10: persist _reachableSymbols set (computed by entrypoints.computeReachability)
|
|
112
|
+
version: 10,
|
|
96
113
|
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
97
114
|
configHash,
|
|
98
115
|
root,
|
|
116
|
+
// PERF-2: refresh buildTime on each save so partial rebuilds report
|
|
117
|
+
// accurate stats. Falls back to original on first save.
|
|
99
118
|
buildTime: index.buildTime,
|
|
100
119
|
timestamp: Date.now(),
|
|
101
120
|
files: strippedFiles,
|
|
@@ -108,23 +127,37 @@ function saveCache(index, cachePath) {
|
|
|
108
127
|
failedFiles: index.failedFiles
|
|
109
128
|
? Array.from(index.failedFiles).map(f => path.relative(root, f))
|
|
110
129
|
: [],
|
|
111
|
-
...(
|
|
130
|
+
...(reachableSymbolsRel !== undefined && {
|
|
131
|
+
reachableSymbols: reachableSymbolsRel,
|
|
132
|
+
reachableFingerprint,
|
|
133
|
+
}),
|
|
112
134
|
};
|
|
113
135
|
|
|
114
|
-
|
|
136
|
+
// PERF-3: atomic write — tmp file + rename so concurrent readers/writers
|
|
137
|
+
// never see a torn JSON. The calls/ shard write below already does this.
|
|
138
|
+
const tmpFile = cacheFile + '.tmp';
|
|
139
|
+
fs.writeFileSync(tmpFile, JSON.stringify(cacheData));
|
|
140
|
+
fs.renameSync(tmpFile, cacheFile);
|
|
141
|
+
|
|
142
|
+
// MED-1 (Round 5): clear the reachabilityDirty flag now that the set is
|
|
143
|
+
// safely persisted. The cli/index.js cache-save guard checks this flag
|
|
144
|
+
// along with needsCacheSave/callsCacheDirty.
|
|
145
|
+
if (index.reachabilityDirty) {
|
|
146
|
+
index.reachabilityDirty = false;
|
|
147
|
+
}
|
|
115
148
|
|
|
116
|
-
// Save callsCache sharded by directory for lazy loading
|
|
149
|
+
// Save callsCache sharded by directory for lazy loading.
|
|
150
|
+
// Write to a temp directory first, then atomic swap to avoid data loss on crash.
|
|
117
151
|
if (callsCacheData.length > 0) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
fs.rmSync(legacyFile, { force: true });
|
|
152
|
+
const cacheDir = path.dirname(cacheFile);
|
|
153
|
+
const callsDir = path.join(cacheDir, 'calls');
|
|
154
|
+
const callsTmpDir = path.join(cacheDir, 'calls.tmp');
|
|
155
|
+
|
|
156
|
+
// Clean up any leftover temp dir from a previous crashed save
|
|
157
|
+
if (fs.existsSync(callsTmpDir)) {
|
|
158
|
+
fs.rmSync(callsTmpDir, { recursive: true, force: true });
|
|
126
159
|
}
|
|
127
|
-
fs.mkdirSync(
|
|
160
|
+
fs.mkdirSync(callsTmpDir, { recursive: true });
|
|
128
161
|
|
|
129
162
|
// Group by directory
|
|
130
163
|
const shards = new Map();
|
|
@@ -134,17 +167,29 @@ function saveCache(index, cachePath) {
|
|
|
134
167
|
shards.get(dir).push([relPath, entry]);
|
|
135
168
|
}
|
|
136
169
|
|
|
137
|
-
// Write
|
|
170
|
+
// Write all shards to temp directory
|
|
138
171
|
const shardManifest = [];
|
|
139
172
|
for (const [dir, entries] of shards) {
|
|
140
173
|
const hash = crypto.createHash('md5').update(dir).digest('hex').slice(0, 10);
|
|
141
|
-
const shardFile = path.join(
|
|
174
|
+
const shardFile = path.join(callsTmpDir, `${hash}.json`);
|
|
142
175
|
fs.writeFileSync(shardFile, JSON.stringify(entries));
|
|
143
176
|
shardManifest.push([dir, hash, entries.length]);
|
|
144
177
|
}
|
|
145
178
|
|
|
146
|
-
// Write manifest
|
|
147
|
-
fs.writeFileSync(path.join(
|
|
179
|
+
// Write manifest to temp directory
|
|
180
|
+
fs.writeFileSync(path.join(callsTmpDir, 'manifest.json'), JSON.stringify(shardManifest));
|
|
181
|
+
|
|
182
|
+
// Atomic swap: remove old, rename temp to final
|
|
183
|
+
if (fs.existsSync(callsDir)) {
|
|
184
|
+
fs.rmSync(callsDir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
fs.renameSync(callsTmpDir, callsDir);
|
|
187
|
+
|
|
188
|
+
// Clean up legacy monolithic file
|
|
189
|
+
const legacyFile = path.join(cacheDir, 'calls-cache.json');
|
|
190
|
+
if (fs.existsSync(legacyFile)) {
|
|
191
|
+
fs.rmSync(legacyFile, { force: true });
|
|
192
|
+
}
|
|
148
193
|
}
|
|
149
194
|
|
|
150
195
|
return cacheFile;
|
|
@@ -168,7 +213,9 @@ function loadCache(index, cachePath) {
|
|
|
168
213
|
|
|
169
214
|
// Check version compatibility
|
|
170
215
|
// v7: symbols/bindings stripped from file entries (dedup)
|
|
171
|
-
|
|
216
|
+
// v9: addSymbol propagates isAsync/isGenerator/paramTypes (force rebuild for old)
|
|
217
|
+
// v10: persists _reachableSymbols set
|
|
218
|
+
if (cacheData.version !== 10) {
|
|
172
219
|
return false;
|
|
173
220
|
}
|
|
174
221
|
|
|
@@ -186,12 +233,19 @@ function loadCache(index, cachePath) {
|
|
|
186
233
|
}
|
|
187
234
|
|
|
188
235
|
const root = cacheData.root || index.root;
|
|
236
|
+
// Fast path conversion: string concat is ~70x faster than path.join for
|
|
237
|
+
// cache-stored relative paths (no '..' segments). On Windows, path.relative
|
|
238
|
+
// produces backslash paths, so rootPrefix uses the native separator.
|
|
239
|
+
const rootPrefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
240
|
+
const toAbs = path.sep === '/'
|
|
241
|
+
? (relPath) => rootPrefix + relPath
|
|
242
|
+
: (relPath) => rootPrefix + relPath.replace(/\//g, path.sep);
|
|
189
243
|
|
|
190
244
|
// Reconstruct files Map: relative key → absolute key, restore path and relativePath
|
|
191
245
|
// Initialize symbols/bindings arrays (will be populated from top-level symbols)
|
|
192
246
|
index.files = new Map();
|
|
193
247
|
for (const [relPath, entry] of cacheData.files) {
|
|
194
|
-
const absPath =
|
|
248
|
+
const absPath = toAbs(relPath);
|
|
195
249
|
entry.path = absPath;
|
|
196
250
|
entry.relativePath = relPath;
|
|
197
251
|
if (!entry.symbols) entry.symbols = [];
|
|
@@ -204,7 +258,7 @@ function loadCache(index, cachePath) {
|
|
|
204
258
|
index.symbols = new Map(cacheData.symbols);
|
|
205
259
|
for (const [, defs] of index.symbols) {
|
|
206
260
|
for (const s of defs) {
|
|
207
|
-
if (!s.file && s.relativePath) s.file =
|
|
261
|
+
if (!s.file && s.relativePath) s.file = toAbs(s.relativePath);
|
|
208
262
|
if (!s.bindingId && s.relativePath && s.type && s.startLine) {
|
|
209
263
|
s.bindingId = `${s.relativePath}:${s.type}:${s.startLine}`;
|
|
210
264
|
}
|
|
@@ -222,11 +276,14 @@ function loadCache(index, cachePath) {
|
|
|
222
276
|
}
|
|
223
277
|
}
|
|
224
278
|
|
|
225
|
-
// Reconstruct graphs: relative paths → absolute paths
|
|
279
|
+
// Reconstruct graphs: relative paths → absolute paths (as Sets)
|
|
280
|
+
// Uses string concat (toAbs) instead of path.join — 70x faster on 464K edges
|
|
226
281
|
const absGraph = (data) => {
|
|
227
282
|
const m = new Map();
|
|
228
283
|
for (const [relKey, relValues] of data) {
|
|
229
|
-
|
|
284
|
+
const absValues = new Set();
|
|
285
|
+
for (const v of relValues) absValues.add(toAbs(v));
|
|
286
|
+
m.set(toAbs(relKey), absValues);
|
|
230
287
|
}
|
|
231
288
|
return m;
|
|
232
289
|
};
|
|
@@ -243,10 +300,10 @@ function loadCache(index, cachePath) {
|
|
|
243
300
|
index.extendedByGraph = new Map(cacheData.extendedByGraph);
|
|
244
301
|
}
|
|
245
302
|
|
|
246
|
-
//
|
|
247
|
-
//
|
|
303
|
+
// Prepare lazy calls cache loading — load manifest but defer shard parsing.
|
|
304
|
+
// Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
|
|
248
305
|
if (index.callsCache.size === 0) {
|
|
249
|
-
|
|
306
|
+
_prepareCallsCache(index);
|
|
250
307
|
}
|
|
251
308
|
|
|
252
309
|
// Build directory→files index from loaded data
|
|
@@ -257,21 +314,44 @@ function loadCache(index, cachePath) {
|
|
|
257
314
|
// Restore failedFiles if present (convert relative paths back to absolute)
|
|
258
315
|
if (Array.isArray(cacheData.failedFiles)) {
|
|
259
316
|
index.failedFiles = new Set(
|
|
260
|
-
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f :
|
|
317
|
+
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f : toAbs(f))
|
|
261
318
|
);
|
|
262
319
|
}
|
|
263
320
|
|
|
264
|
-
// Restore calleeIndex if persisted
|
|
321
|
+
// Restore calleeIndex if persisted (v7 caches only; v8+ rebuilds lazily)
|
|
265
322
|
if (Array.isArray(cacheData.calleeIndex)) {
|
|
266
323
|
index.calleeIndex = new Map();
|
|
267
324
|
for (const [name, files] of cacheData.calleeIndex) {
|
|
268
325
|
if (!Array.isArray(files)) continue;
|
|
269
326
|
index.calleeIndex.set(name, new Set(
|
|
270
|
-
files.map(f => path.isAbsolute(f) ? f :
|
|
327
|
+
files.map(f => path.isAbsolute(f) ? f : toAbs(f))
|
|
271
328
|
));
|
|
272
329
|
}
|
|
273
330
|
}
|
|
274
331
|
|
|
332
|
+
// PERF-1: restore _reachableSymbols if persisted (v10+).
|
|
333
|
+
// Saved as relative-path keys; rehydrate to absolute keys here so the
|
|
334
|
+
// in-memory set matches what computeReachability would produce fresh.
|
|
335
|
+
// The fingerprint is checked by computeReachability before reuse — if
|
|
336
|
+
// the index drifts (e.g. a rebuild after stale cache), the cached set
|
|
337
|
+
// is dropped and recomputed.
|
|
338
|
+
if (Array.isArray(cacheData.reachableSymbols)) {
|
|
339
|
+
const reachable = new Set();
|
|
340
|
+
for (const k of cacheData.reachableSymbols) {
|
|
341
|
+
if (typeof k !== 'string') continue;
|
|
342
|
+
const colon = k.lastIndexOf(':');
|
|
343
|
+
if (colon < 0) continue;
|
|
344
|
+
const relFile = k.slice(0, colon);
|
|
345
|
+
const lineStr = k.slice(colon + 1);
|
|
346
|
+
const absFile = path.isAbsolute(relFile) ? relFile : toAbs(relFile);
|
|
347
|
+
reachable.add(`${absFile}:${lineStr}`);
|
|
348
|
+
}
|
|
349
|
+
index._reachableSymbols = reachable;
|
|
350
|
+
if (cacheData.reachableFingerprint) {
|
|
351
|
+
index._reachableFingerprint = cacheData.reachableFingerprint;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
275
355
|
// Only rebuild graphs if config changed (e.g., aliases modified)
|
|
276
356
|
const currentConfigHash = crypto.createHash('md5')
|
|
277
357
|
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
@@ -292,6 +372,12 @@ function loadCache(index, cachePath) {
|
|
|
292
372
|
* @returns {boolean} - True if cache needs rebuilding
|
|
293
373
|
*/
|
|
294
374
|
function isCacheStale(index) {
|
|
375
|
+
// Ultra-fast path: skip full check if last confirmed-fresh < 2s ago (covers MCP burst calls).
|
|
376
|
+
// Only uses _lastFreshAt (set at the end of a successful full check), not cache save timestamp.
|
|
377
|
+
if (index._lastFreshAt && Date.now() - index._lastFreshAt < 2000) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
295
381
|
// Fast path: check cached files for modifications/deletions first (stat-only).
|
|
296
382
|
// This returns early without the expensive directory walk when any file changed.
|
|
297
383
|
for (const [filePath, fileEntry] of index.files) {
|
|
@@ -337,44 +423,64 @@ function isCacheStale(index) {
|
|
|
337
423
|
}
|
|
338
424
|
}
|
|
339
425
|
|
|
426
|
+
// Record when we last confirmed the cache is fresh (enables 2s skip on burst calls)
|
|
427
|
+
index._lastFreshAt = Date.now();
|
|
340
428
|
return false;
|
|
341
429
|
}
|
|
342
430
|
|
|
343
431
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
432
|
+
* Prepare calls cache for lazy loading — reads manifest but defers shard parsing.
|
|
433
|
+
* Called during loadCache() to set up the manifest without the ~1s shard parse cost.
|
|
434
|
+
* Actual shards are loaded on first ensureCallsCacheLoaded() call.
|
|
346
435
|
* @param {object} index - ProjectIndex instance
|
|
347
|
-
* @returns {boolean} - True if loaded successfully
|
|
348
436
|
*/
|
|
349
|
-
function
|
|
350
|
-
if (index.
|
|
351
|
-
if (index._callsCacheLoaded) return false; // Already attempted, file didn't exist
|
|
352
|
-
index._callsCacheLoaded = true;
|
|
353
|
-
|
|
437
|
+
function _prepareCallsCache(index) {
|
|
438
|
+
if (index._callsCacheLoaded) return;
|
|
354
439
|
const cacheDir = path.join(index.root, '.ucn-cache');
|
|
355
|
-
|
|
356
|
-
// Try sharded format first (calls/manifest.json)
|
|
357
440
|
const manifestFile = path.join(cacheDir, 'calls', 'manifest.json');
|
|
358
441
|
if (fs.existsSync(manifestFile)) {
|
|
359
442
|
try {
|
|
360
443
|
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
|
|
361
|
-
// Store manifest for lazy loading
|
|
362
444
|
index._callsManifest = new Map();
|
|
363
445
|
for (const [dir, hash, count] of manifest) {
|
|
364
446
|
index._callsManifest.set(dir, { hash, count, loaded: false });
|
|
365
447
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
_loadCallsShard(index, hash);
|
|
369
|
-
}
|
|
370
|
-
return true;
|
|
448
|
+
index._callsCachePrepared = true;
|
|
449
|
+
return;
|
|
371
450
|
} catch (e) {
|
|
372
|
-
// Corrupted manifest — fall through
|
|
451
|
+
// Corrupted manifest — fall through
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// Check legacy format
|
|
455
|
+
const legacyFile = path.join(cacheDir, 'calls-cache.json');
|
|
456
|
+
if (fs.existsSync(legacyFile)) {
|
|
457
|
+
index._callsCacheLegacyFile = legacyFile;
|
|
458
|
+
index._callsCachePrepared = true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Load callsCache from separate file on demand.
|
|
464
|
+
* Only loads if callsCache is empty (not already populated from inline or prior load).
|
|
465
|
+
* @param {object} index - ProjectIndex instance
|
|
466
|
+
* @returns {boolean} - True if loaded successfully
|
|
467
|
+
*/
|
|
468
|
+
function loadCallsCache(index) {
|
|
469
|
+
if (index.callsCache.size > 0) return true; // Already populated
|
|
470
|
+
if (index._callsCacheLoaded) return false; // Already attempted, file didn't exist
|
|
471
|
+
index._callsCacheLoaded = true;
|
|
472
|
+
|
|
473
|
+
// If manifest was prepared lazily, load all shards now
|
|
474
|
+
if (index._callsManifest) {
|
|
475
|
+
for (const [, { hash }] of index._callsManifest) {
|
|
476
|
+
_loadCallsShard(index, hash);
|
|
373
477
|
}
|
|
478
|
+
return index.callsCache.size > 0;
|
|
374
479
|
}
|
|
375
480
|
|
|
376
481
|
// Legacy format: single calls-cache.json
|
|
377
|
-
const callsCacheFile =
|
|
482
|
+
const callsCacheFile = index._callsCacheLegacyFile ||
|
|
483
|
+
path.join(index.root, '.ucn-cache', 'calls-cache.json');
|
|
378
484
|
if (!fs.existsSync(callsCacheFile)) return false;
|
|
379
485
|
|
|
380
486
|
try {
|
|
@@ -393,6 +499,17 @@ function loadCallsCache(index) {
|
|
|
393
499
|
return false;
|
|
394
500
|
}
|
|
395
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Ensure calls cache is fully loaded (trigger lazy load if prepared but not loaded).
|
|
504
|
+
* Call this before any operation that needs callsCache (findCallers, buildCalleeIndex, etc.)
|
|
505
|
+
* @param {object} index - ProjectIndex instance
|
|
506
|
+
*/
|
|
507
|
+
function ensureCallsCacheLoaded(index) {
|
|
508
|
+
if (index._callsCachePrepared && !index._callsCacheLoaded) {
|
|
509
|
+
loadCallsCache(index);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
396
513
|
/**
|
|
397
514
|
* Load a single calls shard by hash.
|
|
398
515
|
* @param {object} index - ProjectIndex instance
|
|
@@ -403,9 +520,13 @@ function _loadCallsShard(index, hash) {
|
|
|
403
520
|
try {
|
|
404
521
|
const data = JSON.parse(fs.readFileSync(shardFile, 'utf-8'));
|
|
405
522
|
if (!Array.isArray(data)) return;
|
|
523
|
+
const rootPrefix = index.root.endsWith(path.sep) ? index.root : index.root + path.sep;
|
|
524
|
+
const toAbsShard = path.sep === '/'
|
|
525
|
+
? (rp) => rootPrefix + rp
|
|
526
|
+
: (rp) => rootPrefix + rp.replace(/\//g, path.sep);
|
|
406
527
|
for (const [relPath, entry] of data) {
|
|
407
528
|
if (!relPath || !entry) continue;
|
|
408
|
-
const absPath = path.isAbsolute(relPath) ? relPath :
|
|
529
|
+
const absPath = path.isAbsolute(relPath) ? relPath : toAbsShard(relPath);
|
|
409
530
|
index.callsCache.set(absPath, entry);
|
|
410
531
|
}
|
|
411
532
|
} catch (e) {
|
|
@@ -413,4 +534,37 @@ function _loadCallsShard(index, hash) {
|
|
|
413
534
|
}
|
|
414
535
|
}
|
|
415
536
|
|
|
416
|
-
|
|
537
|
+
/**
|
|
538
|
+
* Compute a cheap fingerprint of the index used to detect drift since the
|
|
539
|
+
* last reachability computation. Two states with the same fingerprint are
|
|
540
|
+
* indistinguishable for reachability purposes (file count + symbol count are
|
|
541
|
+
* monotonic with structural changes; an extra `entries[0]` byte detects most
|
|
542
|
+
* incremental rebuilds even when counts happen to match).
|
|
543
|
+
*
|
|
544
|
+
* Used by entrypoints.computeReachability to decide whether the persisted
|
|
545
|
+
* `_reachableSymbols` set is still valid.
|
|
546
|
+
*
|
|
547
|
+
* @param {object} index - ProjectIndex instance
|
|
548
|
+
* @returns {string} compact fingerprint
|
|
549
|
+
*/
|
|
550
|
+
function _computeReachabilityFingerprint(index) {
|
|
551
|
+
const fileCount = index.files ? index.files.size : 0;
|
|
552
|
+
const symbolCount = index.symbols ? index.symbols.size : 0;
|
|
553
|
+
// Sample a tiny prefix of the symbol map for a cheap structural check.
|
|
554
|
+
// Map iteration order is insertion order, which is stable across an
|
|
555
|
+
// unmodified load (built from cacheData.symbols in the same order).
|
|
556
|
+
let sample = '';
|
|
557
|
+
if (index.symbols && index.symbols.size > 0) {
|
|
558
|
+
let count = 0;
|
|
559
|
+
for (const [name, defs] of index.symbols) {
|
|
560
|
+
sample += name + ':' + (Array.isArray(defs) ? defs.length : 0) + '|';
|
|
561
|
+
if (++count >= 8) break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return `${fileCount}:${symbolCount}:${sample}`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
module.exports = {
|
|
568
|
+
saveCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
|
|
569
|
+
_computeReachabilityFingerprint,
|
|
570
|
+
};
|