gitnexus 1.6.5-rc.45 → 1.6.5-rc.46

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.
@@ -55,8 +55,14 @@ export declare const computeChunkHash: (entries: Array<{
55
55
  */
56
56
  export declare const loadParseCache: (storagePath: string) => Promise<ParseCache>;
57
57
  /**
58
- * Persist the cache to disk atomically (write-and-rename) so a crash
59
- * mid-write doesn't leave a corrupt file.
58
+ * Persist the cache to disk using a temp directory + rename.
59
+ *
60
+ * Writes shards under `${cacheDir}.tmp`, then removes the old `cacheDir` and
61
+ * renames the temp directory into place. There is a crash window after
62
+ * `rm(cacheDir)` and before `rename(tmpDir, cacheDir)` where no cache exists;
63
+ * that is acceptable — `loadParseCache` yields empty and the next run
64
+ * reparses. This is not a single atomic swap of the whole tree, but avoids
65
+ * leaving a half-written shard set visible to readers.
60
66
  */
61
67
  export declare const saveParseCache: (storagePath: string, cache: ParseCache) => Promise<void>;
62
68
  /**
@@ -69,7 +69,12 @@ const GITNEXUS_PKG_VERSION = (() => {
69
69
  return '0.0.0-unknown';
70
70
  })();
71
71
  export const PARSE_CACHE_VERSION = `${SCHEMA_BUMP}+${GITNEXUS_PKG_VERSION}`;
72
- const CACHE_FILENAME = 'parse-cache.json';
72
+ const LEGACY_CACHE_FILENAME = 'parse-cache.json';
73
+ const CACHE_DIRNAME = 'parse-cache';
74
+ const CACHE_INDEX_FILENAME = 'index.json';
75
+ /** Keys on disk always come from `computeChunkHash` — 64-char lowercase hex. */
76
+ const CHUNK_CACHE_KEY_HEX_RE = /^[a-f0-9]{64}$/;
77
+ const isValidChunkCacheKey = (chunkHash) => CHUNK_CACHE_KEY_HEX_RE.test(chunkHash);
73
78
  /** SHA-256 hex of a single string or buffer. */
74
79
  const sha256Hex = (input) => createHash('sha256')
75
80
  .update(typeof input === 'string' ? Buffer.from(input) : input)
@@ -116,12 +121,12 @@ const mapReviver = (_key, value) => {
116
121
  }
117
122
  return value;
118
123
  };
119
- /**
120
- * Load the parse cache. Returns an empty cache on any failure (missing
121
- * file, corrupt JSON, version mismatch). Never throws on a normal load.
122
- */
123
- export const loadParseCache = async (storagePath) => {
124
- const cachePath = path.join(storagePath, CACHE_FILENAME);
124
+ const getLegacyCachePath = (storagePath) => path.join(storagePath, LEGACY_CACHE_FILENAME);
125
+ const getCacheDirPath = (storagePath) => path.join(storagePath, CACHE_DIRNAME);
126
+ const getCacheIndexPath = (storagePath) => path.join(getCacheDirPath(storagePath), CACHE_INDEX_FILENAME);
127
+ const getCacheChunkPath = (storagePath, chunkHash) => path.join(getCacheDirPath(storagePath), `${chunkHash}.json`);
128
+ const loadLegacyParseCache = async (storagePath) => {
129
+ const cachePath = getLegacyCachePath(storagePath);
125
130
  try {
126
131
  const raw = await fs.readFile(cachePath, 'utf-8');
127
132
  const data = JSON.parse(raw, mapReviver);
@@ -143,22 +148,88 @@ export const loadParseCache = async (storagePath) => {
143
148
  return emptyCache();
144
149
  }
145
150
  };
151
+ const loadShardedParseCache = async (storagePath) => {
152
+ const indexPath = getCacheIndexPath(storagePath);
153
+ try {
154
+ const raw = await fs.readFile(indexPath, 'utf-8');
155
+ const data = JSON.parse(raw);
156
+ if (typeof data !== 'object' ||
157
+ data === null ||
158
+ data.version !== PARSE_CACHE_VERSION ||
159
+ !Array.isArray(data.keys)) {
160
+ return emptyCache();
161
+ }
162
+ const entries = new Map();
163
+ for (const chunkHash of data.keys) {
164
+ if (typeof chunkHash !== 'string' || !isValidChunkCacheKey(chunkHash))
165
+ continue;
166
+ try {
167
+ const chunkRaw = await fs.readFile(getCacheChunkPath(storagePath, chunkHash), 'utf-8');
168
+ const chunkData = JSON.parse(chunkRaw, mapReviver);
169
+ if (Array.isArray(chunkData))
170
+ entries.set(chunkHash, chunkData);
171
+ }
172
+ catch {
173
+ /* skip corrupt or missing shard */
174
+ }
175
+ }
176
+ return { version: PARSE_CACHE_VERSION, entries, usedKeys: new Set() };
177
+ }
178
+ catch {
179
+ return null;
180
+ }
181
+ };
182
+ /**
183
+ * Load the parse cache. Returns an empty cache on any failure (missing
184
+ * file, corrupt JSON, version mismatch). Never throws on a normal load.
185
+ */
186
+ export const loadParseCache = async (storagePath) => {
187
+ const sharded = await loadShardedParseCache(storagePath);
188
+ if (sharded)
189
+ return sharded;
190
+ return loadLegacyParseCache(storagePath);
191
+ };
146
192
  /**
147
- * Persist the cache to disk atomically (write-and-rename) so a crash
148
- * mid-write doesn't leave a corrupt file.
193
+ * Persist the cache to disk using a temp directory + rename.
194
+ *
195
+ * Writes shards under `${cacheDir}.tmp`, then removes the old `cacheDir` and
196
+ * renames the temp directory into place. There is a crash window after
197
+ * `rm(cacheDir)` and before `rename(tmpDir, cacheDir)` where no cache exists;
198
+ * that is acceptable — `loadParseCache` yields empty and the next run
199
+ * reparses. This is not a single atomic swap of the whole tree, but avoids
200
+ * leaving a half-written shard set visible to readers.
149
201
  */
150
202
  export const saveParseCache = async (storagePath, cache) => {
151
203
  await fs.mkdir(storagePath, { recursive: true });
152
- const cachePath = path.join(storagePath, CACHE_FILENAME);
153
- const tmpPath = `${cachePath}.tmp`;
154
- const out = {
204
+ const cacheDir = getCacheDirPath(storagePath);
205
+ const tmpDir = `${cacheDir}.tmp`;
206
+ await fs.rm(tmpDir, { recursive: true, force: true });
207
+ await fs.mkdir(tmpDir, { recursive: true });
208
+ const keys = [];
209
+ for (const [chunkHash, chunkResults] of cache.entries) {
210
+ if (!isValidChunkCacheKey(chunkHash))
211
+ continue;
212
+ let payload;
213
+ try {
214
+ payload = JSON.stringify(chunkResults, mapReplacer);
215
+ }
216
+ catch {
217
+ // Extremely dense chunks could theoretically exceed string limits; skip
218
+ // rather than failing the entire save (orchestrator catches save errors).
219
+ continue;
220
+ }
221
+ keys.push(chunkHash);
222
+ const chunkPath = path.join(tmpDir, `${chunkHash}.json`);
223
+ await fs.writeFile(chunkPath, payload, 'utf-8');
224
+ }
225
+ const index = {
155
226
  version: cache.version,
156
- entries: Object.fromEntries(cache.entries),
227
+ keys,
157
228
  };
158
- // Compact JSON; this file can be tens of MB on a large repo and pretty-
159
- // printing roughly doubles size for no value.
160
- await fs.writeFile(tmpPath, JSON.stringify(out, mapReplacer), 'utf-8');
161
- await fs.rename(tmpPath, cachePath);
229
+ await fs.writeFile(path.join(tmpDir, CACHE_INDEX_FILENAME), JSON.stringify(index), 'utf-8');
230
+ await fs.rm(cacheDir, { recursive: true, force: true });
231
+ await fs.rename(tmpDir, cacheDir);
232
+ await fs.rm(getLegacyCachePath(storagePath), { force: true });
162
233
  };
163
234
  /**
164
235
  * Drop entries whose hashes are not in `usedHashes`. Called at the end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.5-rc.45",
3
+ "version": "1.6.5-rc.46",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",