ucn 4.0.2 → 4.1.0
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 +31 -7
- package/README.md +89 -50
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +4 -1
package/core/bridge.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
'use strict';
|
|
24
24
|
|
|
25
25
|
const fs = require('fs');
|
|
26
|
+
const { codeUnitCompare } = require('./shared');
|
|
26
27
|
const path = require('path');
|
|
27
28
|
const { getCachedCalls } = require('./callers');
|
|
28
29
|
const { langTraits } = require('../languages');
|
|
@@ -353,10 +354,10 @@ function extractServerRoutes(index) {
|
|
|
353
354
|
|
|
354
355
|
// Sort deterministically (file, line, method, path)
|
|
355
356
|
routes.sort((a, b) => {
|
|
356
|
-
if (a.file !== b.file) return a.file
|
|
357
|
+
if (a.file !== b.file) return codeUnitCompare(a.file, b.file);
|
|
357
358
|
if (a.line !== b.line) return a.line - b.line;
|
|
358
|
-
if (a.method !== b.method) return a.method
|
|
359
|
-
return a.path
|
|
359
|
+
if (a.method !== b.method) return codeUnitCompare(a.method, b.method);
|
|
360
|
+
return codeUnitCompare(a.path, b.path);
|
|
360
361
|
});
|
|
361
362
|
|
|
362
363
|
// Cache it
|
|
@@ -726,10 +727,10 @@ function extractClientRequests(index) {
|
|
|
726
727
|
|
|
727
728
|
// Stable sort
|
|
728
729
|
requests.sort((a, b) => {
|
|
729
|
-
if (a.file !== b.file) return a.file
|
|
730
|
+
if (a.file !== b.file) return codeUnitCompare(a.file, b.file);
|
|
730
731
|
if (a.line !== b.line) return a.line - b.line;
|
|
731
|
-
if (a.method !== b.method) return a.method
|
|
732
|
-
return a.path
|
|
732
|
+
if (a.method !== b.method) return codeUnitCompare(a.method, b.method);
|
|
733
|
+
return codeUnitCompare(a.path, b.path);
|
|
733
734
|
});
|
|
734
735
|
|
|
735
736
|
if (!index._endpointsCache) index._endpointsCache = {};
|
|
@@ -887,13 +888,13 @@ function bridgeEndpoints(index) {
|
|
|
887
888
|
// For each (request) keep all matches but sort with best first
|
|
888
889
|
bridges.sort((a, b) => {
|
|
889
890
|
// Group by request first
|
|
890
|
-
const reqCmpFile = a.request.file
|
|
891
|
+
const reqCmpFile = codeUnitCompare(a.request.file, b.request.file);
|
|
891
892
|
if (reqCmpFile !== 0) return reqCmpFile;
|
|
892
893
|
if (a.request.line !== b.request.line) return a.request.line - b.request.line;
|
|
893
894
|
// Then by confidence desc
|
|
894
895
|
if (a.confidence !== b.confidence) return b.confidence - a.confidence;
|
|
895
896
|
// Then by route file/line
|
|
896
|
-
if (a.route.file !== b.route.file) return a.route.file
|
|
897
|
+
if (a.route.file !== b.route.file) return codeUnitCompare(a.route.file, b.route.file);
|
|
897
898
|
return a.route.line - b.route.line;
|
|
898
899
|
});
|
|
899
900
|
|
|
@@ -1067,6 +1068,10 @@ function endpoints(index, options = {}) {
|
|
|
1067
1068
|
}
|
|
1068
1069
|
|
|
1069
1070
|
return {
|
|
1071
|
+
// Advisory when bridging (v4 two-tier surface): route↔request
|
|
1072
|
+
// matching is heuristic (per-match tiers EXACT/PARTIAL/UNCERTAIN),
|
|
1073
|
+
// not a verified claim. Route/request EXTRACTION is AST-based.
|
|
1074
|
+
...(opts.bridge && { advisory: 'heuristic-route-matching' }),
|
|
1070
1075
|
routes,
|
|
1071
1076
|
requests,
|
|
1072
1077
|
bridges,
|
package/core/cache.js
CHANGED
|
@@ -51,7 +51,65 @@ const UCN_VERSION = require('../package.json').version;
|
|
|
51
51
|
// node's line (the #201/RUST-2 name-node convention — multi-line receivers
|
|
52
52
|
// like `(&pkg.Name{...}).String()` reported the chain-start line; Go was the
|
|
53
53
|
// only parser still keying calls off the call node's start).
|
|
54
|
-
|
|
54
|
+
// v27 (fix #224): Python from-import submodules — `from . import jobs` binds
|
|
55
|
+
// jobs.py as a plain NAME; graph-build resolves the composed submodule
|
|
56
|
+
// specifier ('.jobs') into fileEntry.moduleResolved and adds the import edge,
|
|
57
|
+
// so submodule receivers behave like `import jobs` module receivers
|
|
58
|
+
// (persisted moduleResolved/importGraph shapes gain entries).
|
|
59
|
+
// v28 (fix #227): canonical index order — everything persisted is written from
|
|
60
|
+
// a canonicalized state (_canonicalizeOrder: files/callsCache by path, defs
|
|
61
|
+
// arrays by (relativePath, startLine, type, className), calleeIndex sorted),
|
|
62
|
+
// so fresh-build, cache-load, and incremental-rebuild states are
|
|
63
|
+
// byte-equivalent and command output no longer depends on cache history.
|
|
64
|
+
// v29 (fix #229): Rust impl members and Java class methods carry method-level
|
|
65
|
+
// `generics` — generic-param receiver types (t.wipe() on TStore: Wipe) resolve
|
|
66
|
+
// against the enclosing declaration instead of excluding as type mismatches.
|
|
67
|
+
// v30 (fix #230): TS parameter-property modifiers (protected/readonly/...)
|
|
68
|
+
// and parameter decorators are no longer recorded as parameter DEFAULTS in
|
|
69
|
+
// paramsStructured.
|
|
70
|
+
// v31 (fix #231): Java try-with-resources declarations type receivers —
|
|
71
|
+
// `try (Res r = new Res())` records receiverType on r.use() like a plain
|
|
72
|
+
// declared-type local (#220(7) typing-sources family).
|
|
73
|
+
// v32 (fix #238): super(...)/this(...) constructor-delegation call records
|
|
74
|
+
// (JS/TS 'constructor' with receiver 'super'; Java under the target class
|
|
75
|
+
// name), Java enum-constant constructor invocations (RED(1)), and Go/Rust
|
|
76
|
+
// zero-param signatures record '' instead of the '...' unknown sentinel.
|
|
77
|
+
// v33 (fix #240): persisted importGraph/exportGraph/moduleResolved content
|
|
78
|
+
// changed — Java wildcard imports link EVERY file directly in the package
|
|
79
|
+
// (non-recursively; subpackage false links dropped), Rust flat-layout crates
|
|
80
|
+
// (no src/) resolve crate:: paths, and super::/crate:: item imports fall back
|
|
81
|
+
// to the parent module FILE (mod.rs / <dir>.rs / lib.rs / main.rs).
|
|
82
|
+
// v34 (fix #241): Java/Python zero-param signatures record '' instead of the
|
|
83
|
+
// '...' unknown sentinel (completes #238's Go/Rust fix), and Rust struct field
|
|
84
|
+
// member symbols carry their own visibility in `modifiers` (pub/pub(crate)/...)
|
|
85
|
+
// so export listings judge fields per-symbol.
|
|
86
|
+
// v35 (fix #245): persisted exports/exportDetails/imports content changed —
|
|
87
|
+
// the JS/TS export scanner records `export abstract class`, `export declare`
|
|
88
|
+
// wrappers, and `export namespace` (the namespace is the importable name),
|
|
89
|
+
// and TS import-equals (`import x = require('./y')`) produces an import
|
|
90
|
+
// record (the dependency edge was invisible to all filedeps commands).
|
|
91
|
+
// v36 (fix #246): persisted importGraph/moduleResolved content changed —
|
|
92
|
+
// Rust own-package-name imports (`use my_crate::helper` in tests/, benches/,
|
|
93
|
+
// examples/) resolve into the package's source tree via the Cargo [package]
|
|
94
|
+
// name, so integration-test dependency edges exist.
|
|
95
|
+
// v37 (fix #247): JS/TS class members carry `private`/`protected`
|
|
96
|
+
// accessibility keywords in `modifiers` (deadcode's exported-member check
|
|
97
|
+
// treated every TS member as implicitly public).
|
|
98
|
+
// v38 (fix #248): Go generic receivers normalize to the type name
|
|
99
|
+
// (`Pair[K, V]` → `Pair` in receiver/className); Rust trait members carry
|
|
100
|
+
// the trait's own visibility instead of the non-Rust 'public', and Rust
|
|
101
|
+
// impl methods record async/unsafe/const/extern qualifiers in modifiers.
|
|
102
|
+
// v39 (fix #249): JS/TS modifiers come from AST tokens, not first-line
|
|
103
|
+
// text — cached symbols may carry export/async/default fabricated from
|
|
104
|
+
// string literals and comments.
|
|
105
|
+
// v40 (fix #251): Java field members carry visibility modifiers, enum
|
|
106
|
+
// constants carry their implicit public/static/final, and Python type
|
|
107
|
+
// aliases (PEP 695 + TypeAlias annotations) are indexed as 'type' symbols.
|
|
108
|
+
// v41 (fix #252): CJS export object maps index their shorthand/pair
|
|
109
|
+
// function properties (module.exports = { doThing(x){} }) and list them in
|
|
110
|
+
// exports; member-expression assignments record RHS identifier function
|
|
111
|
+
// references (window.onload = handler).
|
|
112
|
+
const CACHE_FORMAT_VERSION = 41;
|
|
55
113
|
|
|
56
114
|
/**
|
|
57
115
|
* Save index to cache file
|
|
@@ -281,7 +339,16 @@ function loadCache(index, cachePath) {
|
|
|
281
339
|
return false;
|
|
282
340
|
}
|
|
283
341
|
|
|
284
|
-
|
|
342
|
+
// Rehydrate against the CURRENT root, never cacheData.root — the
|
|
343
|
+
// cache stores relative paths precisely so a moved/copied project
|
|
344
|
+
// keeps its cache. Using the recorded root re-attached every path to
|
|
345
|
+
// the ORIGINAL directory: the staleness check then forced a rebuild
|
|
346
|
+
// of files/symbols, but _reachableSymbols survived with old-root
|
|
347
|
+
// keys (the fingerprint is path-blind), poisoning reachability notes
|
|
348
|
+
// and --unreachable-only permanently (fix #249, G9-parity P1 —
|
|
349
|
+
// repro: cp -r projA projB). The calls-shard loader already used
|
|
350
|
+
// index.root.
|
|
351
|
+
const root = index.root;
|
|
285
352
|
// Fast path conversion: string concat is ~70x faster than path.join for
|
|
286
353
|
// cache-stored relative paths (no '..' segments). On Windows, path.relative
|
|
287
354
|
// produces backslash paths, so rootPrefix uses the native separator.
|
|
@@ -325,6 +392,12 @@ function loadCache(index, cachePath) {
|
|
|
325
392
|
}
|
|
326
393
|
}
|
|
327
394
|
|
|
395
|
+
// Canonical order (see ProjectIndex._canonicalizeOrder): the loop above
|
|
396
|
+
// rebuilds fileEntry.symbols/bindings in NAME-MAP order, which differs
|
|
397
|
+
// from build's parse order — canonicalize so a loaded index is
|
|
398
|
+
// byte-equivalent to a freshly built one before anything derives from it.
|
|
399
|
+
index._canonicalizeOrder();
|
|
400
|
+
|
|
328
401
|
// Reconstruct graphs: relative paths → absolute paths (as Sets)
|
|
329
402
|
// Uses string concat (toAbs) instead of path.join — 70x faster on 464K edges
|
|
330
403
|
const absGraph = (data) => {
|
|
@@ -421,11 +494,13 @@ function loadCache(index, cachePath) {
|
|
|
421
494
|
* @returns {boolean} - True if cache needs rebuilding
|
|
422
495
|
*/
|
|
423
496
|
function isCacheStale(index) {
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
497
|
+
// Modified/deleted detection (stat sweep) runs UNCONDITIONALLY — agents
|
|
498
|
+
// edit a file and re-query through MCP within seconds, and a stale answer
|
|
499
|
+
// presented as fresh is the worst trust failure the tool can produce.
|
|
500
|
+
// The 2s freshness window below shields only the expensive directory
|
|
501
|
+
// walk (new-file detection): a brand-new file queried within 2s of the
|
|
502
|
+
// last full check is a far rarer race than an edit, and the walk is the
|
|
503
|
+
// part that costs real time on large repos.
|
|
429
504
|
|
|
430
505
|
// Fast path: check cached files for modifications/deletions first (stat-only).
|
|
431
506
|
// This returns early without the expensive directory walk when any file changed.
|
|
@@ -454,6 +529,13 @@ function isCacheStale(index) {
|
|
|
454
529
|
}
|
|
455
530
|
}
|
|
456
531
|
|
|
532
|
+
// Ultra-fast skip for the SLOW path only: last confirmed-fresh < 2s ago
|
|
533
|
+
// (covers MCP burst calls). Uses _lastFreshAt (set at the end of a
|
|
534
|
+
// successful full check), never the cache save timestamp.
|
|
535
|
+
if (index._lastFreshAt && Date.now() - index._lastFreshAt < 2000) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
|
|
457
539
|
// Slow path: glob the project to detect new files added since last build.
|
|
458
540
|
// Only reached when all cached files are unchanged.
|
|
459
541
|
const pattern = detectProjectPattern(index.root);
|
|
@@ -510,13 +592,14 @@ function _prepareCallsCache(index) {
|
|
|
510
592
|
|
|
511
593
|
/**
|
|
512
594
|
* Load callsCache from separate file on demand.
|
|
513
|
-
*
|
|
595
|
+
* Merges under existing entries (first writer wins) — anything already in
|
|
596
|
+
* memory came from a fresh parse of current disk content, so persisted data
|
|
597
|
+
* must never replace it (fix #227).
|
|
514
598
|
* @param {object} index - ProjectIndex instance
|
|
515
|
-
* @returns {boolean} - True if
|
|
599
|
+
* @returns {boolean} - True if entries are available after the load
|
|
516
600
|
*/
|
|
517
601
|
function loadCallsCache(index) {
|
|
518
|
-
if (index.callsCache.size > 0
|
|
519
|
-
if (index._callsCacheLoaded) return false; // Already attempted, file didn't exist
|
|
602
|
+
if (index._callsCacheLoaded) return index.callsCache.size > 0;
|
|
520
603
|
index._callsCacheLoaded = true;
|
|
521
604
|
|
|
522
605
|
// If manifest was prepared lazily, load all shards now
|
|
@@ -530,22 +613,24 @@ function loadCallsCache(index) {
|
|
|
530
613
|
// Legacy format: single calls-cache.json
|
|
531
614
|
const callsCacheFile = index._callsCacheLegacyFile ||
|
|
532
615
|
path.join(index.root, '.ucn-cache', 'calls-cache.json');
|
|
533
|
-
if (!fs.existsSync(callsCacheFile)) return
|
|
616
|
+
if (!fs.existsSync(callsCacheFile)) return index.callsCache.size > 0;
|
|
534
617
|
|
|
535
618
|
try {
|
|
536
619
|
const data = JSON.parse(fs.readFileSync(callsCacheFile, 'utf-8'));
|
|
537
620
|
if (Array.isArray(data)) {
|
|
538
|
-
const
|
|
621
|
+
for (const [relPath, entry] of data) {
|
|
622
|
+
if (!relPath || !entry) continue;
|
|
539
623
|
const absPath = path.isAbsolute(relPath) ? relPath : path.join(index.root, relPath);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
624
|
+
if (!index.callsCache.has(absPath)) {
|
|
625
|
+
index.callsCache.set(absPath, entry);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return index.callsCache.size > 0;
|
|
544
629
|
}
|
|
545
630
|
} catch (e) {
|
|
546
631
|
// Corrupted file — ignore
|
|
547
632
|
}
|
|
548
|
-
return
|
|
633
|
+
return index.callsCache.size > 0;
|
|
549
634
|
}
|
|
550
635
|
|
|
551
636
|
/**
|
|
@@ -576,7 +661,12 @@ function _loadCallsShard(index, hash) {
|
|
|
576
661
|
for (const [relPath, entry] of data) {
|
|
577
662
|
if (!relPath || !entry) continue;
|
|
578
663
|
const absPath = path.isAbsolute(relPath) ? relPath : toAbsShard(relPath);
|
|
579
|
-
|
|
664
|
+
// First writer wins: an entry already in memory came from a fresh
|
|
665
|
+
// parse of current disk content (or an earlier load) — never
|
|
666
|
+
// clobber it with persisted shard data (fix #227).
|
|
667
|
+
if (!index.callsCache.has(absPath)) {
|
|
668
|
+
index.callsCache.set(absPath, entry);
|
|
669
|
+
}
|
|
580
670
|
}
|
|
581
671
|
} catch (e) {
|
|
582
672
|
// Corrupted shard — skip
|