grepmax 0.26.37 → 0.26.40
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-plugin/marketplace.json +1 -1
- package/dist/lib/index/batch-processor.js +43 -5
- package/dist/lib/index/embedding-reuse.js +47 -0
- package/dist/lib/store/vector-db.js +50 -2
- package/dist/lib/workers/orchestrator.js +28 -4
- package/package.json +2 -2
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -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.26.
|
|
13
|
+
"version": "0.26.40",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Robert Owens",
|
|
16
16
|
"email": "robowens@me.com"
|
|
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.ProjectBatchProcessor = void 0;
|
|
46
|
+
exports.applyReusedEmbeddings = applyReusedEmbeddings;
|
|
46
47
|
const path = __importStar(require("node:path"));
|
|
47
48
|
const config_1 = require("../../config");
|
|
48
49
|
const vector_db_1 = require("../store/vector-db");
|
|
@@ -172,6 +173,18 @@ class ProjectBatchProcessor {
|
|
|
172
173
|
recentReindexed,
|
|
173
174
|
};
|
|
174
175
|
}
|
|
176
|
+
loadReusableEmbeddings(absPath) {
|
|
177
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
178
|
+
// An optimisation only: a failed lookup costs a full re-embed, nothing more.
|
|
179
|
+
try {
|
|
180
|
+
return yield this.vectorDb.getReusableEmbeddings(absPath);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
(0, logger_1.debug)(this.wtag, `embedding reuse lookup failed for ${absPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
175
188
|
close() {
|
|
176
189
|
return __awaiter(this, void 0, void 0, function* () {
|
|
177
190
|
var _a;
|
|
@@ -360,11 +373,14 @@ class ProjectBatchProcessor {
|
|
|
360
373
|
return;
|
|
361
374
|
}
|
|
362
375
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
},
|
|
376
|
+
// A forced reprocess is a repair; it must not trust what is stored.
|
|
377
|
+
const reusable = !forceReprocess && (cached === null || cached === void 0 ? void 0 : cached.hasVectors) !== false && (cached === null || cached === void 0 ? void 0 : cached.hash)
|
|
378
|
+
? yield this.loadReusableEmbeddings(absPath)
|
|
379
|
+
: undefined;
|
|
380
|
+
const result = yield pool.processFile(Object.assign({ path: absPath, absolutePath: absPath, projectRoot: this.projectRoot }, ((reusable === null || reusable === void 0 ? void 0 : reusable.size)
|
|
381
|
+
? { reusableKeys: Array.from(reusable.keys()) }
|
|
382
|
+
: {})), batchAc.signal);
|
|
383
|
+
applyReusedEmbeddings(result, reusable);
|
|
368
384
|
// Policy and filesystem state can change while embedding is in
|
|
369
385
|
// flight. Never commit a result that no longer describes the path.
|
|
370
386
|
const current = yield this.filePolicy.classifyFile(absPath);
|
|
@@ -652,3 +668,25 @@ class ProjectBatchProcessor {
|
|
|
652
668
|
}
|
|
653
669
|
exports.ProjectBatchProcessor = ProjectBatchProcessor;
|
|
654
670
|
ProjectBatchProcessor.RECENT_BATCH_WINDOW = 5;
|
|
671
|
+
/**
|
|
672
|
+
* Fill the records a worker left unembedded from the embeddings offered to it.
|
|
673
|
+
* Throws when one cannot be filled, so the file is retried rather than stored
|
|
674
|
+
* with an empty vector.
|
|
675
|
+
*/
|
|
676
|
+
function applyReusedEmbeddings(result, reusable) {
|
|
677
|
+
var _a;
|
|
678
|
+
if (!((_a = result.reused) === null || _a === void 0 ? void 0 : _a.length))
|
|
679
|
+
return;
|
|
680
|
+
for (const { index, key } of result.reused) {
|
|
681
|
+
const record = result.vectors[index];
|
|
682
|
+
const embedding = reusable === null || reusable === void 0 ? void 0 : reusable.get(key);
|
|
683
|
+
if (!record || !embedding) {
|
|
684
|
+
throw new Error(`worker reused an embedding that was not offered (chunk ${index})`);
|
|
685
|
+
}
|
|
686
|
+
record.vector = embedding.vector;
|
|
687
|
+
record.colbert = embedding.colbert;
|
|
688
|
+
record.colbert_scale = embedding.colbert_scale;
|
|
689
|
+
record.pooled_colbert_48d = embedding.pooled_colbert_48d;
|
|
690
|
+
record.doc_token_ids = embedding.doc_token_ids;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.embeddingReuseKey = embeddingReuseKey;
|
|
4
|
+
exports.toFloat32 = toFloat32;
|
|
5
|
+
exports.toInt32 = toInt32;
|
|
6
|
+
exports.toBytes = toBytes;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
/** Keyed by the exact text passed to the embedder (`PreparedChunk.content`). */
|
|
9
|
+
function embeddingReuseKey(content) {
|
|
10
|
+
return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
function unwrap(val) {
|
|
13
|
+
if (val &&
|
|
14
|
+
!ArrayBuffer.isView(val) &&
|
|
15
|
+
typeof val.toArray === "function") {
|
|
16
|
+
return val.toArray();
|
|
17
|
+
}
|
|
18
|
+
return val;
|
|
19
|
+
}
|
|
20
|
+
// Copies rather than views: an Arrow view pins its whole record batch buffer.
|
|
21
|
+
function toFloat32(val) {
|
|
22
|
+
const v = unwrap(val);
|
|
23
|
+
if (v instanceof Float32Array)
|
|
24
|
+
return v.slice();
|
|
25
|
+
if (ArrayBuffer.isView(v) || Array.isArray(v)) {
|
|
26
|
+
return Float32Array.from(v);
|
|
27
|
+
}
|
|
28
|
+
return new Float32Array(0);
|
|
29
|
+
}
|
|
30
|
+
function toInt32(val) {
|
|
31
|
+
const v = unwrap(val);
|
|
32
|
+
if (v instanceof Int32Array)
|
|
33
|
+
return v.slice();
|
|
34
|
+
if (ArrayBuffer.isView(v) || Array.isArray(v)) {
|
|
35
|
+
return Int32Array.from(v);
|
|
36
|
+
}
|
|
37
|
+
return new Int32Array(0);
|
|
38
|
+
}
|
|
39
|
+
function toBytes(val) {
|
|
40
|
+
const v = unwrap(val);
|
|
41
|
+
if (ArrayBuffer.isView(v)) {
|
|
42
|
+
return Buffer.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength).slice());
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(v))
|
|
45
|
+
return Buffer.from(v);
|
|
46
|
+
return Buffer.alloc(0);
|
|
47
|
+
}
|
|
@@ -51,6 +51,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
51
51
|
const lancedb = __importStar(require("@lancedb/lancedb"));
|
|
52
52
|
const apache_arrow_1 = require("apache-arrow");
|
|
53
53
|
const config_1 = require("../../config");
|
|
54
|
+
const embedding_reuse_1 = require("../index/embedding-reuse");
|
|
54
55
|
const index_config_1 = require("../index/index-config");
|
|
55
56
|
const cleanup_1 = require("../utils/cleanup");
|
|
56
57
|
const filter_builder_1 = require("../utils/filter-builder");
|
|
@@ -304,10 +305,15 @@ class VectorDB {
|
|
|
304
305
|
(0, logger_1.log)("vectordb", `Periodic maintenance failed: ${err}`);
|
|
305
306
|
}
|
|
306
307
|
}));
|
|
307
|
-
|
|
308
|
-
|
|
308
|
+
// Compare against the promise actually stored, not `run`: `.finally()`
|
|
309
|
+
// returns a new promise, so checking `run` never matched and the field
|
|
310
|
+
// latched non-null forever — isMaintenanceActive() then read true for the
|
|
311
|
+
// daemon's whole life, silently blocking every recycle and idle shutdown.
|
|
312
|
+
const tracked = run.finally(() => {
|
|
313
|
+
if (this.maintenancePromise === tracked)
|
|
309
314
|
this.maintenancePromise = null;
|
|
310
315
|
});
|
|
316
|
+
this.maintenancePromise = tracked;
|
|
311
317
|
}, MAINTENANCE_INTERVAL_MS);
|
|
312
318
|
this.maintenanceTimer.unref();
|
|
313
319
|
}
|
|
@@ -1430,6 +1436,48 @@ class VectorDB {
|
|
|
1430
1436
|
return unique;
|
|
1431
1437
|
});
|
|
1432
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* The stored embeddings of one file, keyed by `embeddingReuseKey(content)`.
|
|
1441
|
+
* Rows whose dense width disagrees with this table are left out rather than
|
|
1442
|
+
* offered — reuse must never be the way a wrong-width vector gets copied.
|
|
1443
|
+
*/
|
|
1444
|
+
getReusableEmbeddings(filePath) {
|
|
1445
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1446
|
+
var _a;
|
|
1447
|
+
const table = yield this.ensureTable();
|
|
1448
|
+
const rows = yield table
|
|
1449
|
+
.query()
|
|
1450
|
+
.select([
|
|
1451
|
+
"content",
|
|
1452
|
+
"vector",
|
|
1453
|
+
"colbert",
|
|
1454
|
+
"colbert_scale",
|
|
1455
|
+
"pooled_colbert_48d",
|
|
1456
|
+
"doc_token_ids",
|
|
1457
|
+
])
|
|
1458
|
+
.where(`path = '${(0, filter_builder_1.escapeSqlString)(filePath)}'`)
|
|
1459
|
+
.toArray();
|
|
1460
|
+
const reusable = new Map();
|
|
1461
|
+
for (const row of rows) {
|
|
1462
|
+
const vector = (0, embedding_reuse_1.toFloat32)(row.vector);
|
|
1463
|
+
const colbert = (0, embedding_reuse_1.toBytes)(row.colbert);
|
|
1464
|
+
if (vector.length !== this.vectorDim || colbert.length === 0)
|
|
1465
|
+
continue;
|
|
1466
|
+
const pooled = row.pooled_colbert_48d == null
|
|
1467
|
+
? undefined
|
|
1468
|
+
: (0, embedding_reuse_1.toFloat32)(row.pooled_colbert_48d);
|
|
1469
|
+
const tokenIds = row.doc_token_ids == null ? undefined : (0, embedding_reuse_1.toInt32)(row.doc_token_ids);
|
|
1470
|
+
reusable.set((0, embedding_reuse_1.embeddingReuseKey)(String((_a = row.content) !== null && _a !== void 0 ? _a : "")), {
|
|
1471
|
+
vector,
|
|
1472
|
+
colbert,
|
|
1473
|
+
colbert_scale: typeof row.colbert_scale === "number" ? row.colbert_scale : 1,
|
|
1474
|
+
pooled_colbert_48d: (pooled === null || pooled === void 0 ? void 0 : pooled.length) ? pooled : undefined,
|
|
1475
|
+
doc_token_ids: tokenIds,
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
return reusable;
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1433
1481
|
getStats() {
|
|
1434
1482
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1435
1483
|
const table = yield this.ensureTable();
|
|
@@ -52,6 +52,7 @@ const uuid_1 = require("uuid");
|
|
|
52
52
|
const config_1 = require("../../config");
|
|
53
53
|
const chunker_1 = require("../index/chunker");
|
|
54
54
|
const embedding_generation_1 = require("../index/embedding-generation");
|
|
55
|
+
const embedding_reuse_1 = require("../index/embedding-reuse");
|
|
55
56
|
const skeleton_1 = require("../skeleton");
|
|
56
57
|
const file_utils_1 = require("../utils/file-utils");
|
|
57
58
|
const logger_1 = require("../utils/logger");
|
|
@@ -234,6 +235,7 @@ class WorkerOrchestrator {
|
|
|
234
235
|
}
|
|
235
236
|
processFile(input, onProgress) {
|
|
236
237
|
return __awaiter(this, void 0, void 0, function* () {
|
|
238
|
+
var _a;
|
|
237
239
|
const fileStart = performance.now();
|
|
238
240
|
const absolutePath = path.isAbsolute(input.path)
|
|
239
241
|
? input.path
|
|
@@ -281,12 +283,32 @@ class WorkerOrchestrator {
|
|
|
281
283
|
return { vectors: [], hash, mtimeMs, size };
|
|
282
284
|
}
|
|
283
285
|
const preparedChunks = this.toPreparedChunks(input.path, hash, chunks, skeletonResult.success ? skeletonResult.skeleton : undefined);
|
|
286
|
+
const reusableKeys = ((_a = input.reusableKeys) === null || _a === void 0 ? void 0 : _a.length)
|
|
287
|
+
? new Set(input.reusableKeys)
|
|
288
|
+
: null;
|
|
289
|
+
const reused = [];
|
|
290
|
+
const toEmbed = [];
|
|
291
|
+
preparedChunks.forEach((chunk, idx) => {
|
|
292
|
+
if (reusableKeys) {
|
|
293
|
+
const key = (0, embedding_reuse_1.embeddingReuseKey)(chunk.content);
|
|
294
|
+
if (reusableKeys.has(key)) {
|
|
295
|
+
reused.push({ index: idx, key });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
toEmbed.push(idx);
|
|
300
|
+
});
|
|
284
301
|
const embedStart = performance.now();
|
|
285
|
-
const
|
|
286
|
-
|
|
302
|
+
const embedded = yield this.computeHybrid(toEmbed.map((idx) => preparedChunks[idx].content), onProgress);
|
|
303
|
+
const hybrids = new Map();
|
|
304
|
+
toEmbed.forEach((chunkIdx, i) => {
|
|
305
|
+
if (embedded[i])
|
|
306
|
+
hybrids.set(chunkIdx, embedded[i]);
|
|
307
|
+
});
|
|
308
|
+
(0, logger_1.debug)("orch", `embedded ${input.path} → ${embedded.length} vectors (${reused.length} reused) ${(performance.now() - embedStart).toFixed(0)}ms`);
|
|
287
309
|
const vectors = preparedChunks.map((chunk, idx) => {
|
|
288
310
|
var _a;
|
|
289
|
-
const hybrid = (_a = hybrids
|
|
311
|
+
const hybrid = (_a = hybrids.get(idx)) !== null && _a !== void 0 ? _a : {
|
|
290
312
|
dense: new Float32Array(),
|
|
291
313
|
colbert: new Int8Array(),
|
|
292
314
|
scale: 1,
|
|
@@ -295,7 +317,9 @@ class WorkerOrchestrator {
|
|
|
295
317
|
});
|
|
296
318
|
onProgress === null || onProgress === void 0 ? void 0 : onProgress();
|
|
297
319
|
(0, logger_1.debug)("orch", `processFile done: ${input.path} ${vectors.length} vectors ${(performance.now() - fileStart).toFixed(0)}ms`);
|
|
298
|
-
return
|
|
320
|
+
return reused.length
|
|
321
|
+
? { vectors, hash, mtimeMs, size, reused }
|
|
322
|
+
: { vectors, hash, mtimeMs, size };
|
|
299
323
|
});
|
|
300
324
|
}
|
|
301
325
|
encodeQuery(text) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.40",
|
|
4
4
|
"author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
41
|
"test:typecheck": "tsc -p tsconfig.tests.json",
|
|
42
42
|
"prepublishOnly": "pnpm run test:typecheck && pnpm build",
|
|
43
|
-
"preversion": "pnpm test && pnpm typecheck && pnpm run test:typecheck && pnpm run format:check",
|
|
43
|
+
"preversion": "pnpm audit --prod && pnpm test && pnpm typecheck && pnpm run test:typecheck && pnpm run format:check",
|
|
44
44
|
"version": "bash scripts/sync-versions.sh && git add -A",
|
|
45
45
|
"postversion": "bash scripts/postrelease.sh"
|
|
46
46
|
},
|