ucn 3.7.24 → 3.7.26
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/README.md +192 -463
- package/cli/index.js +285 -1054
- package/core/cache.js +193 -0
- package/core/callers.js +817 -0
- package/core/deadcode.js +320 -0
- package/core/discovery.js +1 -1
- package/core/execute.js +207 -10
- package/core/expand-cache.js +16 -5
- package/core/imports.js +21 -15
- package/core/output.js +370 -35
- package/core/project.js +365 -2272
- package/core/shared.js +11 -1
- package/core/stacktrace.js +313 -0
- package/core/verify.js +533 -0
- package/languages/go.js +57 -21
- package/languages/html.js +14 -3
- package/languages/java.js +4 -2
- package/languages/javascript.js +36 -9
- package/languages/rust.js +49 -17
- package/mcp/server.js +39 -172
- package/package.json +1 -1
package/core/cache.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/cache.js - Index persistence (save/load/staleness detection)
|
|
3
|
+
*
|
|
4
|
+
* Extracted from project.js. All functions take an `index` (ProjectIndex)
|
|
5
|
+
* as the first argument instead of using `this`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { expandGlob, detectProjectPattern, parseGitignore, DEFAULT_IGNORES } = require('./discovery');
|
|
12
|
+
|
|
13
|
+
// Read UCN version for cache invalidation
|
|
14
|
+
const UCN_VERSION = require('../package.json').version;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Save index to cache file
|
|
18
|
+
* @param {object} index - ProjectIndex instance
|
|
19
|
+
* @param {string} [cachePath] - Optional custom cache path
|
|
20
|
+
* @returns {string} - Path to cache file
|
|
21
|
+
*/
|
|
22
|
+
function saveCache(index, cachePath) {
|
|
23
|
+
const cacheDir = cachePath
|
|
24
|
+
? path.dirname(cachePath)
|
|
25
|
+
: path.join(index.root, '.ucn-cache');
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(cacheDir)) {
|
|
28
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const cacheFile = cachePath || path.join(cacheDir, 'index.json');
|
|
32
|
+
|
|
33
|
+
// Prepare callsCache for serialization (exclude content to save space)
|
|
34
|
+
const callsCacheData = [];
|
|
35
|
+
for (const [filePath, entry] of index.callsCache) {
|
|
36
|
+
callsCacheData.push([filePath, {
|
|
37
|
+
mtime: entry.mtime,
|
|
38
|
+
hash: entry.hash,
|
|
39
|
+
calls: entry.calls
|
|
40
|
+
// content is not persisted - will be read on demand
|
|
41
|
+
}]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cacheData = {
|
|
45
|
+
version: 4, // v4: className, memberType, isMethod for all languages
|
|
46
|
+
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
47
|
+
root: index.root,
|
|
48
|
+
buildTime: index.buildTime,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
files: Array.from(index.files.entries()),
|
|
51
|
+
symbols: Array.from(index.symbols.entries()),
|
|
52
|
+
importGraph: Array.from(index.importGraph.entries()),
|
|
53
|
+
exportGraph: Array.from(index.exportGraph.entries()),
|
|
54
|
+
extendsGraph: Array.from(index.extendsGraph.entries()),
|
|
55
|
+
extendedByGraph: Array.from(index.extendedByGraph.entries()),
|
|
56
|
+
callsCache: callsCacheData,
|
|
57
|
+
failedFiles: index.failedFiles ? Array.from(index.failedFiles) : []
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cacheData));
|
|
61
|
+
return cacheFile;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load index from cache file
|
|
66
|
+
* @param {object} index - ProjectIndex instance
|
|
67
|
+
* @param {string} [cachePath] - Optional custom cache path
|
|
68
|
+
* @returns {boolean} - True if loaded successfully
|
|
69
|
+
*/
|
|
70
|
+
function loadCache(index, cachePath) {
|
|
71
|
+
const cacheFile = cachePath || path.join(index.root, '.ucn-cache', 'index.json');
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(cacheFile)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
|
79
|
+
|
|
80
|
+
// Check version compatibility
|
|
81
|
+
// v4 adds className, memberType, isMethod for all languages
|
|
82
|
+
// Only accept exactly version 4 (or future versions handled explicitly)
|
|
83
|
+
if (cacheData.version !== 4) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Invalidate cache when UCN version changes (logic may have changed)
|
|
88
|
+
if (cacheData.ucnVersion !== UCN_VERSION) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Validate cache structure has required fields
|
|
93
|
+
if (!Array.isArray(cacheData.files) ||
|
|
94
|
+
!Array.isArray(cacheData.symbols) ||
|
|
95
|
+
!Array.isArray(cacheData.importGraph) ||
|
|
96
|
+
!Array.isArray(cacheData.exportGraph)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
index.files = new Map(cacheData.files);
|
|
101
|
+
index.symbols = new Map(cacheData.symbols);
|
|
102
|
+
index.importGraph = new Map(cacheData.importGraph);
|
|
103
|
+
index.exportGraph = new Map(cacheData.exportGraph);
|
|
104
|
+
index.buildTime = cacheData.buildTime;
|
|
105
|
+
|
|
106
|
+
// Restore optional graphs if present
|
|
107
|
+
if (Array.isArray(cacheData.extendsGraph)) {
|
|
108
|
+
index.extendsGraph = new Map(cacheData.extendsGraph);
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(cacheData.extendedByGraph)) {
|
|
111
|
+
index.extendedByGraph = new Map(cacheData.extendedByGraph);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Restore callsCache if present (v2+)
|
|
115
|
+
if (Array.isArray(cacheData.callsCache)) {
|
|
116
|
+
index.callsCache = new Map(cacheData.callsCache);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Restore failedFiles if present
|
|
120
|
+
if (Array.isArray(cacheData.failedFiles)) {
|
|
121
|
+
index.failedFiles = new Set(cacheData.failedFiles);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Rebuild derived graphs to ensure consistency with current config
|
|
125
|
+
index.buildImportGraph();
|
|
126
|
+
index.buildInheritanceGraph();
|
|
127
|
+
|
|
128
|
+
return true;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if cache is stale (any files changed or new files added)
|
|
136
|
+
* @param {object} index - ProjectIndex instance
|
|
137
|
+
* @returns {boolean} - True if cache needs rebuilding
|
|
138
|
+
*/
|
|
139
|
+
function isCacheStale(index) {
|
|
140
|
+
// Check for new files added to project
|
|
141
|
+
// Use same ignores as build() — .gitignore + .ucn.json exclude
|
|
142
|
+
const pattern = detectProjectPattern(index.root);
|
|
143
|
+
const globOpts = { root: index.root };
|
|
144
|
+
const gitignorePatterns = parseGitignore(index.root);
|
|
145
|
+
const configExclude = index.config.exclude || [];
|
|
146
|
+
if (gitignorePatterns.length > 0 || configExclude.length > 0) {
|
|
147
|
+
globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
|
|
148
|
+
}
|
|
149
|
+
const currentFiles = expandGlob(pattern, globOpts);
|
|
150
|
+
const cachedPaths = new Set(index.files.keys());
|
|
151
|
+
|
|
152
|
+
for (const file of currentFiles) {
|
|
153
|
+
if (!cachedPaths.has(file) && !(index.failedFiles && index.failedFiles.has(file))) {
|
|
154
|
+
return true; // New file found
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check existing cached files for modifications/deletions
|
|
159
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
160
|
+
// File deleted
|
|
161
|
+
if (!fs.existsSync(filePath)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// File modified - check size first, then mtime, then hash
|
|
166
|
+
try {
|
|
167
|
+
const stat = fs.statSync(filePath);
|
|
168
|
+
|
|
169
|
+
// If size changed, file changed
|
|
170
|
+
if (fileEntry.size !== undefined && stat.size !== fileEntry.size) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If mtime matches, file hasn't changed
|
|
175
|
+
if (fileEntry.mtime && stat.mtimeMs === fileEntry.mtime) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// mtime changed or not stored - verify with hash
|
|
180
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
181
|
+
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
182
|
+
if (hash !== fileEntry.hash) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { saveCache, loadCache, isCacheStale };
|