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/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 };