ucn 5.2.0 → 5.2.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 +17 -3
- package/.claude/skills/ucn/references/commands.md +4 -4
- package/README.md +17 -5
- package/cli/index.js +3 -0
- package/core/accessors.js +183 -0
- package/core/account.js +36 -8
- package/core/analysis.js +46 -0
- package/core/ast-analysis.js +104 -0
- package/core/cache.js +150 -9
- package/core/callers.js +1196 -122
- package/core/command-contracts.js +13 -13
- package/core/deadcode.js +41 -2
- package/core/execute.js +4 -1
- package/core/graph.js +72 -5
- package/core/index-ir.js +15 -3
- package/core/ir.js +58 -9
- package/core/output/analysis.js +30 -1
- package/core/output/graph.js +28 -8
- package/core/output/public.js +4 -0
- package/core/output/refactoring.js +31 -2
- package/core/output/reporting.js +7 -0
- package/core/project.js +88 -3
- package/core/verify.js +85 -3
- package/languages/c-family.js +27 -34
- package/languages/csharp.js +26 -1
- package/languages/go.js +170 -42
- package/languages/javascript.js +258 -10
- package/languages/python.js +591 -32
- package/languages/rust.js +1 -0
- package/package.json +1 -1
package/core/cache.js
CHANGED
|
@@ -26,8 +26,9 @@ const CACHE_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
|
26
26
|
const CACHE_MAX_PROJECTS = 128;
|
|
27
27
|
const CACHE_MAX_BYTES = 1024 * 1024 * 1024;
|
|
28
28
|
|
|
29
|
-
function discoveryRulesHash(root) {
|
|
30
|
-
|
|
29
|
+
function discoveryRulesHash(root, patterns = null) {
|
|
30
|
+
const rules = patterns || parseGitignore(root);
|
|
31
|
+
return crypto.createHash('md5').update(rules.join('\0')).digest('hex');
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -644,7 +645,131 @@ function clearAllCaches() {
|
|
|
644
645
|
// symbols retain AST-derived parameter qualification/forwarding effects, so
|
|
645
646
|
// replacement-list requalification cannot masquerade as lexical calls
|
|
646
647
|
// (fix #306).
|
|
647
|
-
|
|
648
|
+
// v189: Python import records retain function-local/deferred scope so cycle
|
|
649
|
+
// reporting can classify import-time vs lazy edges from fresh and cached
|
|
650
|
+
// indexes; C# properties retain property identity instead of masquerading as
|
|
651
|
+
// ordinary fields for accessor impact/refactoring.
|
|
652
|
+
// v190: members of Rust generic impl owners retain ownerGenerics so blanket
|
|
653
|
+
// impl parameters (`impl<I> Trait for I`) cannot be mistaken for concrete
|
|
654
|
+
// receiver types after a cache round-trip (fix #302).
|
|
655
|
+
// v191: Python call records retain untyped loop-element provenance so a
|
|
656
|
+
// same-spelled project method cannot gain confirmed identity after reload.
|
|
657
|
+
// v192: JS/TS callback records carry moduleLocalBinding so dynamically
|
|
658
|
+
// produced module values cannot borrow target identity from file imports.
|
|
659
|
+
// v193: overload-heavy JS/TS class-member aliases materialize when every
|
|
660
|
+
// declared return has one concrete runtime head, changing indexed symbols.
|
|
661
|
+
// v194: JS/TS static fields preserve direct same-file callable forwarding for
|
|
662
|
+
// immutable module/export aliases (fix #313).
|
|
663
|
+
// v195: JS/TS call records retain safe ordered namespace-spread receiver
|
|
664
|
+
// compositions for exact module export ownership (fix #314).
|
|
665
|
+
// v196: TS parameter type references no longer mark namespace receiver roots
|
|
666
|
+
// as locally shadowed, changing receiverLocalBinding evidence (fix #315).
|
|
667
|
+
// v197 (fix #317): JS/TS expression-bodied arrow symbols persist the exact
|
|
668
|
+
// returned call span so query-time flow can resolve compiler-inferred factory
|
|
669
|
+
// results without treating block bodies or arbitrary expressions as returns.
|
|
670
|
+
// v198 (fix #318): JS/TS one-return methods persist exact this-field paths for
|
|
671
|
+
// generic declared-field result flow.
|
|
672
|
+
// v199 (fix #321): Python call records persist receiver types derived from
|
|
673
|
+
// exact, scope-local class-value aliases such as `_Segment = Segment`.
|
|
674
|
+
// v200 (fix #323): Python dotted module calls persist their exact import
|
|
675
|
+
// specifier (`import rich.repr` → `rich.repr.auto()`).
|
|
676
|
+
// v201 (fix #324): Python method-call records persist simple subscript roots
|
|
677
|
+
// and their local type provenance for `items[key].method()` dispatch; local
|
|
678
|
+
// callable aliases/non-callable bindings no longer leak across functions.
|
|
679
|
+
// v202 (fix #325): Python call records preserve exact typed subscript sources
|
|
680
|
+
// across one local assignment (`item = items[key]; item.method()`).
|
|
681
|
+
// v203 (fix #326): Python call records type stable, direct module constructor
|
|
682
|
+
// globals even when their functions are declared before the assignment.
|
|
683
|
+
// v204 (fix #328): Python union-alias symbols retain their concrete members
|
|
684
|
+
// so imported annotations can participate in exact conditional narrowing.
|
|
685
|
+
// v205 (fix #332): Go method-call records retain capture-aware lexical scope
|
|
686
|
+
// chains so return-type flow reaches nested closures without crossing shadows.
|
|
687
|
+
// v206 (fix #333): Go calls assigned in `var` declarations retain their
|
|
688
|
+
// declaration targets for compiler-return-type receiver flow.
|
|
689
|
+
// v207 (fix #334): Go method-call records retain compiler-exact receiver
|
|
690
|
+
// types for values bound from declared map, slice, and array indexes.
|
|
691
|
+
// v208 (fix #335): Go indexed-value calls preserve receiver-root/field
|
|
692
|
+
// provenance so sibling-file container declarations resolve query-time.
|
|
693
|
+
const CACHE_FORMAT_VERSION = 208;
|
|
694
|
+
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Persist the small hot-name usage cache independently of index/call shards.
|
|
698
|
+
* This keeps one-shot CLI repeats fast without rewriting the whole project
|
|
699
|
+
* cache after every read-only account query.
|
|
700
|
+
*/
|
|
701
|
+
function saveUsageCache(index, cachePath) {
|
|
702
|
+
if (!index.usageCacheDirty) return null;
|
|
703
|
+
const cacheDir = cachePath
|
|
704
|
+
? path.dirname(cachePath)
|
|
705
|
+
: getProjectCacheDir(index.root);
|
|
706
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
707
|
+
const entries = [];
|
|
708
|
+
for (const [key, cached] of index._usageResultCache || []) {
|
|
709
|
+
const parts = key.split('\0');
|
|
710
|
+
if (parts.length < 3 || !Array.isArray(cached?.value)) continue;
|
|
711
|
+
const [filePath, fileHash, name, mode = ''] = parts;
|
|
712
|
+
const fileEntry = index.files.get(filePath);
|
|
713
|
+
// Drop removed/changed-file generations rather than carrying dead LRU
|
|
714
|
+
// weight across incremental builds.
|
|
715
|
+
if (!fileEntry || fileEntry.hash !== fileHash) continue;
|
|
716
|
+
entries.push([
|
|
717
|
+
path.relative(index.root, filePath), fileHash, name, mode,
|
|
718
|
+
cached.value,
|
|
719
|
+
]);
|
|
720
|
+
}
|
|
721
|
+
const usageFile = path.join(cacheDir, USAGE_CACHE_FILE);
|
|
722
|
+
const tmpFile = usageFile + '.tmp';
|
|
723
|
+
fs.writeFileSync(tmpFile, JSON.stringify({
|
|
724
|
+
version: CACHE_FORMAT_VERSION,
|
|
725
|
+
ucnVersion: UCN_VERSION,
|
|
726
|
+
entries,
|
|
727
|
+
}));
|
|
728
|
+
fs.renameSync(tmpFile, usageFile);
|
|
729
|
+
index.usageCacheDirty = false;
|
|
730
|
+
return usageFile;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function loadUsageCache(index, cacheFile) {
|
|
734
|
+
const usageFile = path.join(path.dirname(cacheFile), USAGE_CACHE_FILE);
|
|
735
|
+
if (!fs.existsSync(usageFile)) return false;
|
|
736
|
+
try {
|
|
737
|
+
const payload = JSON.parse(fs.readFileSync(usageFile, 'utf-8'));
|
|
738
|
+
if (payload.version !== CACHE_FORMAT_VERSION ||
|
|
739
|
+
payload.ucnVersion !== UCN_VERSION ||
|
|
740
|
+
!Array.isArray(payload.entries)) return false;
|
|
741
|
+
const restored = new Map();
|
|
742
|
+
let weightTotal = 0;
|
|
743
|
+
for (const entry of payload.entries) {
|
|
744
|
+
if (!Array.isArray(entry) || entry.length < 5) continue;
|
|
745
|
+
const [relativePath, fileHash, name, mode, value] = entry;
|
|
746
|
+
if (typeof relativePath !== 'string' ||
|
|
747
|
+
typeof fileHash !== 'string' ||
|
|
748
|
+
typeof name !== 'string' || typeof mode !== 'string' ||
|
|
749
|
+
!Array.isArray(value)) continue;
|
|
750
|
+
const filePath = path.resolve(index.root, relativePath);
|
|
751
|
+
const fileEntry = index.files.get(filePath);
|
|
752
|
+
if (!fileEntry || fileEntry.hash !== fileHash) continue;
|
|
753
|
+
const suffix = mode ? `\0${mode}` : '';
|
|
754
|
+
const key = `${filePath}\0${fileHash}\0${name}${suffix}`;
|
|
755
|
+
const weight = 64 + value.length * 40;
|
|
756
|
+
restored.set(key, { value, weight });
|
|
757
|
+
weightTotal += weight;
|
|
758
|
+
while (restored.size > 4096 || weightTotal > 16 * 1024 * 1024) {
|
|
759
|
+
const oldest = restored.entries().next().value;
|
|
760
|
+
if (!oldest) break;
|
|
761
|
+
restored.delete(oldest[0]);
|
|
762
|
+
weightTotal -= oldest[1].weight;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
index._usageResultCache = restored;
|
|
766
|
+
index._usageResultCacheWeight = weightTotal;
|
|
767
|
+
index.usageCacheDirty = false;
|
|
768
|
+
return true;
|
|
769
|
+
} catch (_) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
648
773
|
|
|
649
774
|
/**
|
|
650
775
|
* Save index to cache file
|
|
@@ -922,8 +1047,14 @@ function loadCache(index, cachePath) {
|
|
|
922
1047
|
: (relPath) => rootPrefix + relPath.replace(/\//g, path.sep);
|
|
923
1048
|
|
|
924
1049
|
// Loading into a previously-used ProjectIndex replaces its indexed
|
|
925
|
-
// contents, so no parsed tree
|
|
1050
|
+
// contents, so no parsed tree or cross-file derived query answer from
|
|
1051
|
+
// the old state may survive. Usage results are content-hash keyed and
|
|
1052
|
+
// remain safe; these caches are graph/text-universe keyed instead.
|
|
926
1053
|
index._clearParsedTreeCache?.();
|
|
1054
|
+
index._groundSetCache = new Map();
|
|
1055
|
+
index._groundSetCacheLines = 0;
|
|
1056
|
+
index._nameBindingReachCache = new Map();
|
|
1057
|
+
index._returnTypeFlowCache = new Map();
|
|
927
1058
|
|
|
928
1059
|
// Reconstruct files Map: relative key → absolute key, restore path and relativePath
|
|
929
1060
|
// Initialize symbols/bindings arrays (will be populated from top-level symbols)
|
|
@@ -1071,6 +1202,8 @@ function loadCache(index, cachePath) {
|
|
|
1071
1202
|
index.buildInheritanceGraph();
|
|
1072
1203
|
}
|
|
1073
1204
|
|
|
1205
|
+
loadUsageCache(index, cacheFile);
|
|
1206
|
+
|
|
1074
1207
|
return true;
|
|
1075
1208
|
} catch (e) {
|
|
1076
1209
|
return false;
|
|
@@ -1088,9 +1221,17 @@ function isCacheStale(index) {
|
|
|
1088
1221
|
if (index._loadedConfigHash && currentConfigHash !== index._loadedConfigHash) {
|
|
1089
1222
|
return true;
|
|
1090
1223
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1224
|
+
// Parse gitignore rules once for both the discovery fingerprint and the
|
|
1225
|
+
// new-file walk below. On a fresh cache these used to run two identical
|
|
1226
|
+
// `git ls-files` subprocesses per one-shot CLI invocation, accounting for
|
|
1227
|
+
// roughly a third of warm-start staleness time on measured repositories.
|
|
1228
|
+
let gitignorePatterns = null;
|
|
1229
|
+
if (index._loadedDiscoveryHash) {
|
|
1230
|
+
gitignorePatterns = parseGitignore(index.root);
|
|
1231
|
+
if (discoveryRulesHash(index.root, gitignorePatterns) !==
|
|
1232
|
+
index._loadedDiscoveryHash) {
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1094
1235
|
}
|
|
1095
1236
|
// Modified/deleted detection (stat sweep) runs UNCONDITIONALLY — agents
|
|
1096
1237
|
// edit a file and re-query through MCP within seconds, and a stale answer
|
|
@@ -1149,7 +1290,7 @@ function isCacheStale(index) {
|
|
|
1149
1290
|
});
|
|
1150
1291
|
},
|
|
1151
1292
|
};
|
|
1152
|
-
|
|
1293
|
+
if (!gitignorePatterns) gitignorePatterns = parseGitignore(index.root);
|
|
1153
1294
|
globOpts.gitignorePatterns = gitignorePatterns;
|
|
1154
1295
|
globOpts.trackedPaths = gitTrackedPaths(index.root);
|
|
1155
1296
|
const configExclude = index.config.exclude || [];
|
|
@@ -1343,7 +1484,7 @@ function _computeReachabilityFingerprint(index) {
|
|
|
1343
1484
|
}
|
|
1344
1485
|
|
|
1345
1486
|
module.exports = {
|
|
1346
|
-
saveCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
|
|
1487
|
+
saveCache, saveUsageCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
|
|
1347
1488
|
getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
|
|
1348
1489
|
getLegacyProjectCacheDir, migrateLegacyProjectCache, clearProjectCache,
|
|
1349
1490
|
clearAllCaches, pruneUserCache,
|