grepmax 0.25.0 → 0.26.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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.25.0",
13
+ "version": "0.26.0",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -343,6 +343,15 @@ gmax status # reports current / legacy / stale / unbuilt
343
343
  gmax repair --rebuild # guarded whole-corpus rebuild through the daemon
344
344
  ```
345
345
 
346
+ `legacy` means the existing index is compatible but its exact model fingerprint was inferred from
347
+ the previous registry shape. Run `gmax index` in that project to persist exact identity; no reset or
348
+ re-embedding is required when the cached files are unchanged.
349
+
350
+ Cache metadata migrations are lazy and do not require a reset. Catchup stamps compatible legacy
351
+ entries in place, hashes Markdown and MDX by exact bytes, and reprocesses only legacy Markdown or
352
+ paths whose declared vector state disagrees with LanceDB. Files that intentionally produce no
353
+ vectors remain valid cached entries.
354
+
346
355
  A model change cannot be applied by a per-project `gmax index --reset`, even when vector widths
347
356
  match: one query embedding cannot safely search rows from another embedding space. Stale-generation
348
357
  search and sync fail before mutation, preserving the existing index. Run `gmax repair --rebuild` to
@@ -48,7 +48,13 @@ Examples:
48
48
  const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
49
49
  console.log(` Configured: ${identity.configured.tier} ${identity.configured.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.configured.fingerprint)}]`);
50
50
  if (identity.built) {
51
- console.log(` Built: ${identity.built.tier} ${identity.built.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.built.fingerprint)}] (${identity.state})`);
51
+ const builtLabel = identity.state === "legacy" ? "Built (inferred):" : "Built:";
52
+ console.log(` ${builtLabel.padEnd(18)} ${identity.built.tier} ${identity.built.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.built.fingerprint)}] (${identity.state})`);
53
+ if (identity.state === "legacy") {
54
+ const notice = (0, embedding_status_1.formatLegacyEmbeddingNotice)(1);
55
+ if (notice)
56
+ console.log(` ${notice}`);
57
+ }
52
58
  }
53
59
  console.log(` Query log: ${globalConfig.queryLog ? "on" : "off"}`);
54
60
  if (project === null || project === void 0 ? void 0 : project.lastIndexed) {
@@ -281,6 +281,7 @@ exports.doctor = new commander_1.Command("doctor")
281
281
  });
282
282
  const staleEmbeddingProjects = projects.filter((p) => p.status === "indexed" &&
283
283
  (0, embedding_status_1.projectEmbeddingStatus)(p, globalConfig).state === "stale");
284
+ const legacyEmbeddingCount = (0, embedding_status_1.countLegacyEmbeddingProjects)(projects, globalConfig);
284
285
  if (opts.agent) {
285
286
  const fields = [
286
287
  "index_health",
@@ -297,6 +298,7 @@ exports.doctor = new commander_1.Command("doctor")
297
298
  `orphaned=${orphanedProjects.length}`,
298
299
  `stale_chunker=${staleChunkerProjects.length}`,
299
300
  `stale_embedding=${staleEmbeddingProjects.length}`,
301
+ `legacy_embedding=${legacyEmbeddingCount}`,
300
302
  `schema_dim=${physicalDim !== null && physicalDim !== void 0 ? physicalDim : "none"}`,
301
303
  `schema_dim_ok=${schemaGap ? "false" : "true"}`,
302
304
  ];
@@ -335,6 +337,11 @@ exports.doctor = new commander_1.Command("doctor")
335
337
  "fix=gmax repair --rebuild",
336
338
  ].join("\t"));
337
339
  }
340
+ const legacyNotice = (0, embedding_status_1.formatLegacyEmbeddingNotice)(legacyEmbeddingCount, {
341
+ agent: true,
342
+ });
343
+ if (legacyNotice)
344
+ console.log(legacyNotice);
338
345
  }
339
346
  else {
340
347
  console.log("\nIndex Health\n");
@@ -411,6 +418,9 @@ exports.doctor = new commander_1.Command("doctor")
411
418
  console.log(` - ${p.name || path.basename(p.root)} (${identity.built.tier} ${identity.built.vectorDim}d → ${identity.configured.tier} ${identity.configured.vectorDim}d, breaking)`);
412
419
  });
413
420
  }
421
+ const legacyNotice = (0, embedding_status_1.formatLegacyEmbeddingNotice)(legacyEmbeddingCount);
422
+ if (legacyNotice)
423
+ console.log(legacyNotice);
414
424
  // Projects
415
425
  if (orphanedProjects.length > 0) {
416
426
  console.log(`WARN Orphaned projects: ${orphanedProjects.length} (directories no longer exist)`);
@@ -425,21 +435,38 @@ exports.doctor = new commander_1.Command("doctor")
425
435
  if (projects.length > 0) {
426
436
  console.log("\nCache Coherence\n");
427
437
  try {
438
+ const { reconcileMetaEntry } = yield Promise.resolve().then(() => __importStar(require("../lib/index/cache-coherence")));
428
439
  const { MetaCache } = yield Promise.resolve().then(() => __importStar(require("../lib/store/meta-cache")));
429
440
  const mc = new MetaCache(config_1.PATHS.lmdbPath);
430
- for (const project of projects.filter((p) => p.status === "indexed")) {
431
- const prefix = project.root.endsWith("/")
432
- ? project.root
433
- : `${project.root}/`;
434
- const cachedCount = (yield mc.getKeysWithPrefix(prefix)).size;
435
- const vectorCount = yield db.countDistinctFilesForPath(prefix);
436
- if (cachedCount > 0) {
437
- const pct = Math.round((vectorCount / cachedCount) * 100);
438
- const status = pct >= 80 ? "ok" : "WARN";
439
- console.log(`${status} ${project.name || path.basename(project.root)}: ${vectorCount} indexed / ${cachedCount} cached (${pct}%)`);
441
+ try {
442
+ for (const project of projects.filter((p) => p.status === "indexed")) {
443
+ const prefix = project.root.endsWith("/")
444
+ ? project.root
445
+ : `${project.root}/`;
446
+ const cachedPaths = yield mc.getKeysWithPrefix(prefix);
447
+ const vectorPaths = yield db.getDistinctPathsForPrefix(prefix);
448
+ let pending = 0;
449
+ let legacy = 0;
450
+ for (const cachedPath of cachedPaths) {
451
+ const reconciliation = reconcileMetaEntry(cachedPath, mc.get(cachedPath), vectorPaths.has(cachedPath));
452
+ if (reconciliation.action === "reprocess")
453
+ pending++;
454
+ else if (reconciliation.action === "stamp")
455
+ legacy++;
456
+ }
457
+ for (const vectorPath of vectorPaths) {
458
+ if (!cachedPaths.has(vectorPath))
459
+ pending++;
460
+ }
461
+ if (cachedPaths.size > 0 || vectorPaths.size > 0) {
462
+ const status = pending === 0 ? "ok" : "WARN";
463
+ console.log(`${status} ${project.name || path.basename(project.root)}: ${vectorPaths.size} vector-backed / ${cachedPaths.size} cached; ${pending} pending reconciliation, ${legacy} legacy metadata`);
464
+ }
440
465
  }
441
466
  }
442
- yield mc.close();
467
+ finally {
468
+ yield mc.close();
469
+ }
443
470
  }
444
471
  catch (_k) { }
445
472
  }
@@ -141,6 +141,11 @@ function showCurrentProject() {
141
141
  if (identity.built) {
142
142
  console.log(`${style.dim("Built embedding")}: ${identity.built.tier} (${identity.built.vectorDim}d, ${identity.state})`);
143
143
  }
144
+ if (identity.state === "legacy") {
145
+ const notice = (0, embedding_status_1.formatLegacyEmbeddingNotice)(1);
146
+ if (notice)
147
+ console.log(style.yellow(notice));
148
+ }
144
149
  console.log(`${style.dim("Mode")}: ${globalConfig.embedMode}`);
145
150
  if (project.lastIndexed)
146
151
  console.log(`${style.dim("Indexed")}: ${formatDate(new Date(project.lastIndexed))}`);
@@ -184,6 +189,9 @@ function showAllProjects() {
184
189
  console.log(`${pad(project.name, nameWidth)} ${style.dim(pad(shortenPath(project.root), pathWidth))} ${pad(dimsStr, 4)} ${status}`);
185
190
  }
186
191
  console.log(`\n${style.dim("Global config")}: ${globalConfig.modelTier} (${globalConfig.vectorDim}d), ${globalConfig.embedMode}`);
192
+ const legacyNotice = (0, embedding_status_1.formatLegacyEmbeddingNotice)((0, embedding_status_1.countLegacyEmbeddingProjects)(projects, globalConfig));
193
+ if (legacyNotice)
194
+ console.log(style.yellow(`\n${legacyNotice}`));
187
195
  const stale = projects.filter((project) => (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig).state === "stale");
188
196
  if (stale.length > 0) {
189
197
  console.log(style.yellow(`\n${stale.length} project(s) use a stale embedding generation. Existing indexes are preserved; run 'gmax repair --rebuild' to rebuild the whole corpus.`));
@@ -161,6 +161,9 @@ Examples:
161
161
  const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
162
162
  console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}\tembedding=${identity.state}${isCurrent ? "\tcurrent" : ""}`);
163
163
  }
164
+ const legacyNotice = (0, embedding_status_1.formatLegacyEmbeddingNotice)((0, embedding_status_1.countLegacyEmbeddingProjects)(projects, globalConfig), { agent: true });
165
+ if (legacyNotice)
166
+ console.log(legacyNotice);
164
167
  yield (0, exit_1.gracefulExit)();
165
168
  return;
166
169
  }
@@ -199,6 +202,9 @@ Examples:
199
202
  const name = project.name.padEnd(nameWidth);
200
203
  console.log(` ${name} ${chunks.padEnd(12)} ${age.padEnd(10)} ${statusStr} embedding:${embedding}${marker}`);
201
204
  }
205
+ const legacyNotice = (0, embedding_status_1.formatLegacyEmbeddingNotice)((0, embedding_status_1.countLegacyEmbeddingProjects)(projects, globalConfig));
206
+ if (legacyNotice)
207
+ console.log(`\n${style.yellow(legacyNotice)}`);
202
208
  if (currentRoot) {
203
209
  console.log(`\n${style.dim("Current")}: ${shortenPath(currentRoot)}`);
204
210
  }
@@ -468,6 +468,13 @@ class Daemon {
468
468
  return this.withProjectLock(root, signal, () => this.runSharedOperation("watch", signal, () => this.watchProjectWithinOperation(root)));
469
469
  }
470
470
  watchProjectWithinOperation(root) {
471
+ const project = (0, project_registry_1.getProject)(root);
472
+ if ((project === null || project === void 0 ? void 0 : project.status) === "indexed" &&
473
+ this.activeGeneration &&
474
+ (0, embedding_generation_1.compareEmbeddingGeneration)(project, this.activeGeneration).state ===
475
+ "stale") {
476
+ throw new Error("project embedding generation is stale; run gmax repair --rebuild to rebuild the whole corpus");
477
+ }
471
478
  return this.watcherManager.watchProject(root);
472
479
  }
473
480
  runSharedOperation(name, signal, fn) {
@@ -738,7 +745,7 @@ class Daemon {
738
745
  .state === "stale") {
739
746
  (0, ipc_handler_1.writeDone)(conn, {
740
747
  ok: false,
741
- error: "project embedding generation is stale; rebuild the project",
748
+ error: "project embedding generation is stale; run gmax repair --rebuild to rebuild the whole corpus",
742
749
  });
743
750
  return;
744
751
  }
@@ -53,6 +53,7 @@ exports.WatcherManager = void 0;
53
53
  const path = __importStar(require("node:path"));
54
54
  const watcher = __importStar(require("@parcel/watcher"));
55
55
  const batch_processor_1 = require("../index/batch-processor");
56
+ const cache_coherence_1 = require("../index/cache-coherence");
56
57
  const file_policy_1 = require("../index/file-policy");
57
58
  const walker_1 = require("../index/walker");
58
59
  const watcher_1 = require("../index/watcher");
@@ -451,9 +452,12 @@ class WatcherManager {
451
452
  var _d, _e;
452
453
  const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
453
454
  const metaCache = this.deps.getMetaCache();
455
+ const vectorDb = this.deps.getVectorDb();
454
456
  processor.filePolicy.invalidateIgnoreCache();
455
457
  const rootPrefix = root.endsWith("/") ? root : `${root}/`;
456
458
  const cachedPaths = yield metaCache.getKeysWithPrefix(rootPrefix);
459
+ const vectorPaths = yield vectorDb.getDistinctPathsForPrefix(rootPrefix);
460
+ const knownPaths = new Set([...cachedPaths, ...vectorPaths]);
457
461
  if (signal.aborted)
458
462
  return false;
459
463
  const seenPaths = new Set();
@@ -486,7 +490,23 @@ class WatcherManager {
486
490
  continue;
487
491
  const stats = classification.stat;
488
492
  seenPaths.add(absPath);
489
- const cached = metaCache.get(absPath);
493
+ let cached = metaCache.get(absPath);
494
+ const reconciliation = (0, cache_coherence_1.reconcileMetaEntry)(absPath, cached, vectorPaths.has(absPath));
495
+ if (reconciliation.action === "stamp") {
496
+ cached = reconciliation.entry;
497
+ metaCache.put(absPath, cached);
498
+ }
499
+ else if (reconciliation.action === "reprocess") {
500
+ processor.handleFileEvent("change", absPath, {
501
+ forceReprocess: true,
502
+ });
503
+ queued++;
504
+ if (queued % 500 === 0) {
505
+ (0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
506
+ yield abortableDelay(5000, signal);
507
+ }
508
+ continue;
509
+ }
490
510
  if (!isFileCached(cached, stats)) {
491
511
  // Fast path: if only mtime changed but size is identical and we have a hash,
492
512
  // just verify the hash in-process instead of sending to a worker.
@@ -545,12 +565,12 @@ class WatcherManager {
545
565
  (0, logger_1.debug)("catchup", `${path.basename(root)}: ${queued} queued, ${skipped} skipped (cached ok), ${seenPaths.size} total`);
546
566
  // Purge files deleted while daemon was offline
547
567
  let purged = 0;
548
- for (const cachedPath of cachedPaths) {
568
+ for (const cachedPath of knownPaths) {
549
569
  if (signal.aborted)
550
570
  return false;
551
571
  if (!seenPaths.has(cachedPath) &&
552
572
  !(0, walker_1.isPathProtectedByWalkState)(cachedPath, walkState)) {
553
- processor.handleFileEvent("unlink", cachedPath);
573
+ processor.handleFileEvent("unlink", cachedPath, { forceDelete: true });
554
574
  purged++;
555
575
  }
556
576
  }
@@ -49,6 +49,7 @@ const cache_check_1 = require("../utils/cache-check");
49
49
  const file_utils_1 = require("../utils/file-utils");
50
50
  const logger_1 = require("../utils/logger");
51
51
  const pool_1 = require("../workers/pool");
52
+ const cache_coherence_1 = require("./cache-coherence");
52
53
  const file_policy_1 = require("./file-policy");
53
54
  const watcher_batch_1 = require("./watcher-batch");
54
55
  const DEBOUNCE_MS = 2000;
@@ -61,6 +62,8 @@ class ProjectBatchProcessor {
61
62
  this.retryCount = new Map();
62
63
  this.retryAt = new Map();
63
64
  this.terminalFailures = new Set();
65
+ this.forcedReprocess = new Map();
66
+ this.forceGeneration = 0;
64
67
  this.debounceTimer = null;
65
68
  this.debounceDueMs = 0;
66
69
  this.activeBatch = null;
@@ -89,7 +92,7 @@ class ProjectBatchProcessor {
89
92
  })();
90
93
  this.batchTimeoutMs = Math.max(Math.ceil(taskTimeoutMs * 1.5), 120000);
91
94
  }
92
- handleFileEvent(event, absPath) {
95
+ handleFileEvent(event, absPath, options) {
93
96
  var _a, _b;
94
97
  if (this.closed)
95
98
  return;
@@ -108,8 +111,13 @@ class ProjectBatchProcessor {
108
111
  (_a = this.onPolicyChange) === null || _a === void 0 ? void 0 : _a.call(this);
109
112
  return;
110
113
  }
111
- if (!(0, file_utils_1.isIndexableFile)(normalized, 1) && !this.metaCache.get(normalized))
114
+ if (!(0, file_utils_1.isIndexableFile)(normalized, 1) &&
115
+ !this.metaCache.get(normalized) &&
116
+ !(event === "unlink" && (options === null || options === void 0 ? void 0 : options.forceDelete)))
112
117
  return;
118
+ if (options === null || options === void 0 ? void 0 : options.forceReprocess) {
119
+ this.forcedReprocess.set(normalized, ++this.forceGeneration);
120
+ }
113
121
  this.retryAt.delete(normalized);
114
122
  this.pending.set(normalized, event);
115
123
  (_b = this.onActivity) === null || _b === void 0 ? void 0 : _b.call(this);
@@ -199,12 +207,17 @@ class ProjectBatchProcessor {
199
207
  return;
200
208
  }
201
209
  const batch = new Map();
210
+ const batchForceGenerations = new Map();
202
211
  const now = Date.now();
203
212
  let taken = 0;
204
213
  for (const [absPath, event] of this.pending) {
205
214
  if (((_a = this.retryAt.get(absPath)) !== null && _a !== void 0 ? _a : 0) > now)
206
215
  continue;
207
216
  batch.set(absPath, event);
217
+ const forceGeneration = this.forcedReprocess.get(absPath);
218
+ if (forceGeneration !== undefined) {
219
+ batchForceGenerations.set(absPath, forceGeneration);
220
+ }
208
221
  taken++;
209
222
  if (taken >= MAX_BATCH_SIZE)
210
223
  break;
@@ -292,13 +305,16 @@ class ProjectBatchProcessor {
292
305
  }
293
306
  const stats = classification.stat;
294
307
  const cached = this.metaCache.get(absPath);
295
- if ((0, cache_check_1.isFileCached)(cached, stats)) {
308
+ const forceReprocess = batchForceGenerations.has(absPath);
309
+ if (!forceReprocess &&
310
+ (0, cache_coherence_1.isMetaEntryCacheCurrent)(cached, absPath) &&
311
+ (0, cache_check_1.isFileCached)(cached, stats)) {
296
312
  completed.add(absPath);
297
313
  continue;
298
314
  }
299
315
  // Fast path: if only mtime changed but size matches and we have a hash,
300
316
  // verify in-process instead of dispatching to a worker (~220ms saved).
301
- if ((cached === null || cached === void 0 ? void 0 : cached.hash) && cached.size === stats.size) {
317
+ if (!forceReprocess && (cached === null || cached === void 0 ? void 0 : cached.hash) && cached.size === stats.size) {
302
318
  const snapshot = yield (0, file_utils_1.readFileSnapshot)(absPath, {
303
319
  projectRoot: this.projectRoot,
304
320
  });
@@ -341,8 +357,13 @@ class ProjectBatchProcessor {
341
357
  hash: result.hash,
342
358
  mtimeMs: result.mtimeMs,
343
359
  size: result.size,
360
+ hashVersion: cache_coherence_1.CURRENT_META_HASH_VERSION,
361
+ hasVectors: result.vectors.length > 0,
344
362
  };
345
- if (cached && cached.hash === result.hash) {
363
+ if (!forceReprocess &&
364
+ (0, cache_coherence_1.isMetaEntryCacheCurrent)(cached, absPath) &&
365
+ cached.hash === result.hash &&
366
+ cached.hasVectors === result.vectors.length > 0) {
346
367
  metaUpdates.set(absPath, metaEntry);
347
368
  completed.add(absPath);
348
369
  continue;
@@ -441,6 +462,11 @@ class ProjectBatchProcessor {
441
462
  const remaining = this.pending.size;
442
463
  (0, logger_1.log)(this.wtag, `Batch complete: ${batch.size} files, ${reindexed} reindexed (${(duration / 1000).toFixed(1)}s)${remaining > 0 ? ` — ${remaining} remaining` : ""}`);
443
464
  for (const absPath of completed) {
465
+ const processedForce = batchForceGenerations.get(absPath);
466
+ if (processedForce !== undefined &&
467
+ this.forcedReprocess.get(absPath) === processedForce) {
468
+ this.forcedReprocess.delete(absPath);
469
+ }
444
470
  this.retryCount.delete(absPath);
445
471
  this.retryAt.delete(absPath);
446
472
  this.terminalFailures.delete(absPath);
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CURRENT_META_HASH_VERSION = void 0;
37
+ exports.isMetaEntryCacheCurrent = isMetaEntryCacheCurrent;
38
+ exports.reconcileMetaEntry = reconcileMetaEntry;
39
+ const path = __importStar(require("node:path"));
40
+ exports.CURRENT_META_HASH_VERSION = 1;
41
+ function usesLegacyMarkdownHash(filePath, entry) {
42
+ if (entry.hashVersion === exports.CURRENT_META_HASH_VERSION)
43
+ return false;
44
+ const extension = path.extname(filePath).toLowerCase();
45
+ return extension === ".md" || extension === ".mdx";
46
+ }
47
+ function isMetaEntryCacheCurrent(entry, _filePath) {
48
+ return ((entry === null || entry === void 0 ? void 0 : entry.hashVersion) === exports.CURRENT_META_HASH_VERSION &&
49
+ typeof entry.hasVectors === "boolean");
50
+ }
51
+ function reconcileMetaEntry(filePath, entry, vectorPresent) {
52
+ if (!entry) {
53
+ return { action: "reprocess", mustRewriteVectors: vectorPresent };
54
+ }
55
+ const explicitVectorState = typeof entry.hasVectors === "boolean";
56
+ if ((entry.hasVectors === true && !vectorPresent) ||
57
+ (entry.hasVectors === false && vectorPresent) ||
58
+ (!explicitVectorState && !vectorPresent)) {
59
+ return { action: "reprocess", mustRewriteVectors: true };
60
+ }
61
+ if (entry.hashVersion !== undefined &&
62
+ entry.hashVersion !== exports.CURRENT_META_HASH_VERSION) {
63
+ return { action: "reprocess", mustRewriteVectors: false };
64
+ }
65
+ if (usesLegacyMarkdownHash(filePath, entry)) {
66
+ return { action: "reprocess", mustRewriteVectors: false };
67
+ }
68
+ if (entry.hashVersion !== exports.CURRENT_META_HASH_VERSION || !explicitVectorState) {
69
+ return {
70
+ action: "stamp",
71
+ entry: Object.assign(Object.assign({}, entry), { hashVersion: exports.CURRENT_META_HASH_VERSION, hasVectors: vectorPresent }),
72
+ };
73
+ }
74
+ return { action: "current" };
75
+ }
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.projectEmbeddingStatus = projectEmbeddingStatus;
4
4
  exports.embeddingFingerprintLabel = embeddingFingerprintLabel;
5
+ exports.countLegacyEmbeddingProjects = countLegacyEmbeddingProjects;
6
+ exports.formatLegacyEmbeddingNotice = formatLegacyEmbeddingNotice;
5
7
  exports.assertEmbeddingSearchCompatible = assertEmbeddingSearchCompatible;
6
8
  const embedding_generation_1 = require("./embedding-generation");
7
9
  function projectEmbeddingStatus(project, config) {
@@ -15,6 +17,18 @@ function projectEmbeddingStatus(project, config) {
15
17
  function embeddingFingerprintLabel(fingerprint) {
16
18
  return fingerprint.slice(0, 12);
17
19
  }
20
+ function countLegacyEmbeddingProjects(projects, config) {
21
+ return projects.filter((project) => projectEmbeddingStatus(project, config).state === "legacy").length;
22
+ }
23
+ function formatLegacyEmbeddingNotice(count, options) {
24
+ if (count === 0)
25
+ return null;
26
+ if (options === null || options === void 0 ? void 0 : options.agent) {
27
+ return `legacy_embedding\tcount=${count}\tstate=compatible_inferred\tfix=gmax index`;
28
+ }
29
+ const projects = count === 1 ? "project" : "projects";
30
+ return `INFO Legacy embedding: ${count} ${projects} compatible but inferred; run 'gmax index' in each project to persist exact identity.`;
31
+ }
18
32
  function assertEmbeddingSearchCompatible(projects, config) {
19
33
  const stale = projects.filter((project) => projectEmbeddingStatus(project, config).state === "stale");
20
34
  if (stale.length === 0)
@@ -56,12 +56,13 @@ const path = __importStar(require("node:path"));
56
56
  const config_1 = require("../../config");
57
57
  const meta_cache_1 = require("../store/meta-cache");
58
58
  const vector_db_1 = require("../store/vector-db");
59
- // isIndexableFile no longer used — extension check inlined for performance
59
+ const cache_check_1 = require("../utils/cache-check");
60
60
  const lock_1 = require("../utils/lock");
61
61
  const logger_1 = require("../utils/logger");
62
62
  const project_registry_1 = require("../utils/project-registry");
63
63
  const project_root_1 = require("../utils/project-root");
64
64
  const pool_1 = require("../workers/pool");
65
+ const cache_coherence_1 = require("./cache-coherence");
65
66
  const embedding_generation_1 = require("./embedding-generation");
66
67
  const file_policy_1 = require("./file-policy");
67
68
  const index_config_1 = require("./index-config");
@@ -185,33 +186,35 @@ function initialSync(options) {
185
186
  }
186
187
  // At this point metaCache is always initialized (injected, created, or noop)
187
188
  const mc = metaCache;
189
+ let vectorPaths = new Set();
190
+ const reprocessPaths = new Set();
191
+ const mustRewritePaths = new Set();
188
192
  if (!dryRun) {
189
193
  // Scope checks to this project's paths only
190
194
  const projectKeys = yield mc.getKeysWithPrefix(rootPrefix);
191
195
  (0, logger_1.log)("index", `Cached files: ${projectKeys.size}`);
192
- // Coherence check: if LMDB has substantially more entries than LanceDB
193
- // has distinct files, the vector store is out of sync (e.g. batch
194
- // timeouts wrote MetaCache but not vectors, compaction failure, etc.).
195
- // Clear the stale cache entries so those files get re-embedded.
196
- const vectorFileCount = yield vectorDb.countDistinctFilesForPath(rootPrefix);
197
- if (projectKeys.size > 0) {
198
- const pct = Math.round((vectorFileCount / projectKeys.size) * 100);
199
- (0, logger_1.log)("index", `Coherence: ${vectorFileCount} vectors / ${projectKeys.size} cached (${pct}%)`);
200
- }
201
- if (projectKeys.size > 0 && vectorFileCount === 0) {
202
- (0, logger_1.log)("index", `Stale cache detected: ${projectKeys.size} cached files but no vectors — clearing cache`);
203
- for (const key of projectKeys) {
204
- mc.delete(key);
196
+ vectorPaths = yield vectorDb.getDistinctPathsForPrefix(rootPrefix);
197
+ let stamped = 0;
198
+ for (const key of projectKeys) {
199
+ const reconciliation = (0, cache_coherence_1.reconcileMetaEntry)(key, mc.get(key), vectorPaths.has(key));
200
+ if (reconciliation.action === "stamp") {
201
+ mc.put(key, reconciliation.entry);
202
+ stamped++;
205
203
  }
206
- projectKeys.clear();
207
- }
208
- else if (projectKeys.size > 0 &&
209
- vectorFileCount < projectKeys.size * 0.8) {
210
- (0, logger_1.log)("index", `Partial cache detected: ${vectorFileCount} files in vectors vs ${projectKeys.size} in cache — clearing cache to re-embed missing files`);
211
- for (const key of projectKeys) {
212
- mc.delete(key);
204
+ else if (reconciliation.action === "reprocess") {
205
+ reprocessPaths.add(key);
206
+ if (reconciliation.mustRewriteVectors)
207
+ mustRewritePaths.add(key);
213
208
  }
214
- projectKeys.clear();
209
+ }
210
+ for (const vectorPath of vectorPaths) {
211
+ if (projectKeys.has(vectorPath))
212
+ continue;
213
+ reprocessPaths.add(vectorPath);
214
+ mustRewritePaths.add(vectorPath);
215
+ }
216
+ if (projectKeys.size > 0 || vectorPaths.size > 0) {
217
+ (0, logger_1.log)("index", `Coherence: ${vectorPaths.size} vector files / ${projectKeys.size} cached; ${stamped} metadata stamps, ${reprocessPaths.size} paths to reconcile`);
215
218
  }
216
219
  if (reset) {
217
220
  forceReprocess = true;
@@ -431,10 +434,9 @@ function initialSync(options) {
431
434
  return;
432
435
  const stats = classified.stat;
433
436
  // Use absolute path as the key for MetaCache
434
- const cached = forceReprocess ? undefined : mc.get(absPath);
435
- if (cached &&
436
- cached.mtimeMs === stats.mtimeMs &&
437
- cached.size === stats.size) {
437
+ const cached = mc.get(absPath);
438
+ const forcePath = forceReprocess || reprocessPaths.has(absPath);
439
+ if (!forcePath && (0, cache_check_1.isFileCached)(cached, stats)) {
438
440
  cacheHits++;
439
441
  (0, logger_1.debug)("index", `file ${relPath}: cached`);
440
442
  processed += 1;
@@ -475,6 +477,8 @@ function initialSync(options) {
475
477
  hash: result.hash,
476
478
  mtimeMs: result.mtimeMs,
477
479
  size: result.size,
480
+ hashVersion: cache_coherence_1.CURRENT_META_HASH_VERSION,
481
+ hasVectors: result.vectors.length > 0,
478
482
  };
479
483
  if (result.shouldDelete) {
480
484
  (0, logger_1.debug)("index", `file ${relPath}: non-indexable (delete)`);
@@ -489,7 +493,10 @@ function initialSync(options) {
489
493
  markProgress(relPath);
490
494
  return;
491
495
  }
492
- if (cached && cached.hash === result.hash) {
496
+ if (!forceReprocess &&
497
+ !mustRewritePaths.has(absPath) &&
498
+ (cached === null || cached === void 0 ? void 0 : cached.hash) === result.hash &&
499
+ vectorPaths.has(absPath) === result.vectors.length > 0) {
493
500
  (0, logger_1.debug)("index", `file ${relPath}: hash unchanged`);
494
501
  if (!dryRun) {
495
502
  mc.put(absPath, metaEntry);
@@ -568,14 +575,9 @@ function initialSync(options) {
568
575
  : new Error(String(flushError));
569
576
  }
570
577
  throwIfSyncAborted();
571
- // Stale cleanup: only remove paths scoped to this project's root.
572
- // If MetaCache was cleared (coherence check), fall back to LanceDB paths
573
- // so we can still detect orphaned vectors from absorbed/removed sub-projects.
574
- let staleSource = cachedPaths;
575
- if (staleSource.size === 0 && !dryRun && !shouldSkipCleanup) {
576
- (0, logger_1.log)("index", "MetaCache empty — querying LanceDB for stale path detection");
577
- staleSource = yield vectorDb.getDistinctPathsForPrefix(rootPrefix);
578
- }
578
+ // Stale cleanup uses both stores so vector-only orphans cannot survive
579
+ // merely because another path still has metadata.
580
+ const staleSource = new Set([...cachedPaths, ...vectorPaths]);
579
581
  throwIfSyncAborted();
580
582
  const staleCandidates = computeStaleFiles(staleSource, seenPaths).filter((candidate) => !(0, walker_1.isPathProtectedByWalkState)(candidate, walkState));
581
583
  const stale = [];
@@ -49,6 +49,7 @@ exports.computeRetryAction = computeRetryAction;
49
49
  const fs = __importStar(require("node:fs"));
50
50
  const cache_check_1 = require("../utils/cache-check");
51
51
  const file_utils_1 = require("../utils/file-utils");
52
+ const cache_coherence_1 = require("./cache-coherence");
52
53
  function processBatchCore(batch, metaCache, pool, signal) {
53
54
  return __awaiter(this, void 0, void 0, function* () {
54
55
  let reindexed = 0;
@@ -80,7 +81,8 @@ function processBatchCore(batch, metaCache, pool, signal) {
80
81
  continue;
81
82
  }
82
83
  const cached = metaCache.get(absPath);
83
- if ((0, cache_check_1.isFileCached)(cached, stats)) {
84
+ if ((0, cache_coherence_1.isMetaEntryCacheCurrent)(cached, absPath) &&
85
+ (0, cache_check_1.isFileCached)(cached, stats)) {
84
86
  continue;
85
87
  }
86
88
  const result = yield pool.processFile({
@@ -91,8 +93,12 @@ function processBatchCore(batch, metaCache, pool, signal) {
91
93
  hash: result.hash,
92
94
  mtimeMs: result.mtimeMs,
93
95
  size: result.size,
96
+ hashVersion: cache_coherence_1.CURRENT_META_HASH_VERSION,
97
+ hasVectors: result.vectors.length > 0,
94
98
  };
95
- if (cached && cached.hash === result.hash) {
99
+ if ((0, cache_coherence_1.isMetaEntryCacheCurrent)(cached, absPath) &&
100
+ cached.hash === result.hash &&
101
+ cached.hasVectors === result.vectors.length > 0) {
96
102
  metaUpdates.set(absPath, metaEntry);
97
103
  continue;
98
104
  }
@@ -54,6 +54,7 @@ exports.startWatcher = startWatcher;
54
54
  const path = __importStar(require("node:path"));
55
55
  const watcher = __importStar(require("@parcel/watcher"));
56
56
  const batch_processor_1 = require("./batch-processor");
57
+ const cache_coherence_1 = require("./cache-coherence");
57
58
  const file_policy_1 = require("./file-policy");
58
59
  const walker_1 = require("./walker");
59
60
  // Ignore patterns for @parcel/watcher (micromatch globs + directory names).
@@ -124,6 +125,8 @@ function startWatcher(opts) {
124
125
  ? projectRoot
125
126
  : `${projectRoot}/`;
126
127
  const cached = yield opts.metaCache.getKeysWithPrefix(rootPrefix);
128
+ const vectorPaths = yield opts.vectorDb.getDistinctPathsForPrefix(rootPrefix);
129
+ const knownPaths = new Set([...cached, ...vectorPaths]);
127
130
  const seen = new Set();
128
131
  const state = (0, walker_1.createWalkState)();
129
132
  try {
@@ -138,7 +141,13 @@ function startWatcher(opts) {
138
141
  return;
139
142
  const absolute = path.join(projectRoot, relative);
140
143
  seen.add(absolute);
141
- processor.handleFileEvent("change", absolute);
144
+ const reconciliation = (0, cache_coherence_1.reconcileMetaEntry)(absolute, opts.metaCache.get(absolute), vectorPaths.has(absolute));
145
+ if (reconciliation.action === "stamp") {
146
+ opts.metaCache.put(absolute, reconciliation.entry);
147
+ }
148
+ processor.handleFileEvent("change", absolute, {
149
+ forceReprocess: reconciliation.action === "reprocess",
150
+ });
142
151
  }
143
152
  }
144
153
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
@@ -148,12 +157,14 @@ function startWatcher(opts) {
148
157
  }
149
158
  finally { if (e_1) throw e_1.error; }
150
159
  }
151
- for (const cachedPath of cached) {
160
+ for (const cachedPath of knownPaths) {
152
161
  if (closing)
153
162
  return;
154
163
  if (!seen.has(cachedPath) &&
155
164
  !(0, walker_1.isPathProtectedByWalkState)(cachedPath, state)) {
156
- processor.handleFileEvent("unlink", cachedPath);
165
+ processor.handleFileEvent("unlink", cachedPath, {
166
+ forceDelete: true,
167
+ });
157
168
  }
158
169
  }
159
170
  if (!state.rootComplete || state.errors.length > 0) {
@@ -43,7 +43,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.computeBufferHash = computeBufferHash;
46
- exports.stripMarkdownFrontmatter = stripMarkdownFrontmatter;
47
46
  exports.computeContentHash = computeContentHash;
48
47
  exports.hasNullByte = hasNullByte;
49
48
  exports.readFileSnapshot = readFileSnapshot;
@@ -62,35 +61,8 @@ const path_containment_1 = require("./path-containment");
62
61
  function computeBufferHash(buffer) {
63
62
  return (0, node_crypto_1.createHash)("sha256").update(buffer).digest("hex");
64
63
  }
65
- // Files whose leading YAML frontmatter is metadata, not content. A status:/tags:
66
- // edit bumps mtime and changes the bytes, but the embedding ignores frontmatter —
67
- // so hashing it would force a needless GPU re-embed on every metadata touch.
68
- const FRONTMATTER_EXTENSIONS = new Set([".md", ".mdx"]);
69
- // Leading YAML frontmatter: a whole `---` line at the very top (NOT a bare
70
- // `startsWith("---")`, which would also match a `---` thematic break or `--- x`),
71
- // through the next whole `---`/`...` line. Anchored at string start (no `m` flag)
72
- // with an optional BOM, so only true file-leading frontmatter matches; a doc that
73
- // opens with a thematic break and no closing fence is left untouched.
74
- const FRONTMATTER_BLOCK = /^\uFEFF?---[ \t]*\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/;
75
- /** Strip leading YAML frontmatter from markdown bytes; returns input unchanged otherwise. */
76
- function stripMarkdownFrontmatter(buffer) {
77
- const text = buffer.toString("utf8");
78
- const match = text.match(FRONTMATTER_BLOCK);
79
- if (!match)
80
- return buffer;
81
- return Buffer.from(text.slice(match[0].length), "utf8");
82
- }
83
- /**
84
- * Hash file content for change detection. For markdown, YAML frontmatter is
85
- * stripped first so a metadata-only edit doesn't bust `isFileCached` and trigger
86
- * a re-embed; every other file hashes its exact bytes. Must be used identically
87
- * across the catchup, watcher, and worker hash paths or they will disagree and
88
- * re-index in a loop.
89
- */
90
- function computeContentHash(buffer, filePath) {
91
- if (FRONTMATTER_EXTENSIONS.has((0, node_path_1.extname)(filePath).toLowerCase())) {
92
- return computeBufferHash(stripMarkdownFrontmatter(buffer));
93
- }
64
+ /** Hash the exact bytes passed to chunking and embedding. */
65
+ function computeContentHash(buffer, _filePath) {
94
66
  return computeBufferHash(buffer);
95
67
  }
96
68
  function hasNullByte(buffer, sampleLength = 1024) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",