opencode-rag-plugin 1.17.2 → 1.18.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.
- package/ReadMe.md +4 -2
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/init-helpers.js +4 -0
- package/dist/cli/commands/quirk.d.ts +10 -0
- package/dist/cli/commands/quirk.js +141 -0
- package/dist/cli/index.js +2 -1
- package/dist/core/auto-update-state.d.ts +33 -0
- package/dist/core/auto-update-state.js +69 -0
- package/dist/core/config.d.ts +28 -2
- package/dist/core/config.js +24 -2
- package/dist/core/interfaces.d.ts +16 -1
- package/dist/opencode/tools.d.ts +38 -0
- package/dist/opencode/tools.js +127 -0
- package/dist/plugin.js +220 -7
- package/dist/quirks/monitor.d.ts +5 -0
- package/dist/quirks/monitor.js +23 -0
- package/dist/quirks/quirk-store.d.ts +25 -0
- package/dist/quirks/quirk-store.js +217 -0
- package/dist/quirks/types.d.ts +20 -0
- package/dist/quirks/types.js +2 -0
- package/dist/retriever/keyword-index.js +16 -0
- package/dist/vectorstore/lancedb.d.ts +12 -10
- package/dist/vectorstore/lancedb.js +154 -18
- package/dist/vectorstore/memory.js +3 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VectorStore, Chunk, SearchResult, MetadataFilter } from "../core/interfaces.js";
|
|
1
|
+
import type { VectorStore, Chunk, ChunkSummary, SearchResult, MetadataFilter } from "../core/interfaces.js";
|
|
2
2
|
/**
|
|
3
3
|
* L2-normalize a vector to unit length. Cosine models require unit vectors
|
|
4
4
|
* for the dot product to equal cosine similarity.
|
|
@@ -30,6 +30,7 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
30
30
|
private db;
|
|
31
31
|
private table;
|
|
32
32
|
private tableInit;
|
|
33
|
+
private writeLock;
|
|
33
34
|
/**
|
|
34
35
|
* @param dbPath - Filesystem path to the LanceDB database directory.
|
|
35
36
|
* @param vectorDimension - Dimension of the embedding vectors. Default: 384.
|
|
@@ -39,6 +40,15 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
39
40
|
private getTable;
|
|
40
41
|
private initTable;
|
|
41
42
|
private tableHasDescriptionColumn;
|
|
43
|
+
private hasColumn;
|
|
44
|
+
/** Add kind/quirkType/tags columns if missing from an existing table. */
|
|
45
|
+
private migrateNewColumns;
|
|
46
|
+
/**
|
|
47
|
+
* Ensure that kind/quirkType/tags columns are nullable so that queries
|
|
48
|
+
* with WHERE clauses on these columns do not panic on old fragments
|
|
49
|
+
* written before the columns existed.
|
|
50
|
+
*/
|
|
51
|
+
private ensureColumnsNullable;
|
|
42
52
|
/**
|
|
43
53
|
* Store chunks in the LanceDB table. New rows are inserted first, then
|
|
44
54
|
* any old rows at the same (filePath, startLine) with different IDs are
|
|
@@ -79,15 +89,7 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
79
89
|
* @param limit - Maximum number of rows to return.
|
|
80
90
|
* @returns An array of chunk summaries.
|
|
81
91
|
*/
|
|
82
|
-
getChunks(offset: number, limit: number): Promise<
|
|
83
|
-
id: string;
|
|
84
|
-
filePath: string;
|
|
85
|
-
language: string;
|
|
86
|
-
startLine: number;
|
|
87
|
-
endLine: number;
|
|
88
|
-
content: string;
|
|
89
|
-
description: string;
|
|
90
|
-
}[]>;
|
|
92
|
+
getChunks(offset: number, limit: number): Promise<ChunkSummary[]>;
|
|
91
93
|
/**
|
|
92
94
|
* Perform ANN search internally, returning results scored as cosine similarity (0-1).
|
|
93
95
|
* This method is called by `search()` and handles the actual query logic.
|
|
@@ -6,7 +6,7 @@ import fs from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { normalizeFilePath, manifestPathFor } from "../core/manifest.js";
|
|
8
8
|
const TABLE_NAME = "chunks";
|
|
9
|
-
const QUERY_COLUMNS = ["id", "content", "description", "filePath", "startLine", "endLine", "language"];
|
|
9
|
+
const QUERY_COLUMNS = ["id", "content", "description", "filePath", "startLine", "endLine", "language", "kind", "quirkType", "tags"];
|
|
10
10
|
/**
|
|
11
11
|
* L2-normalize a vector to unit length. Cosine models require unit vectors
|
|
12
12
|
* for the dot product to equal cosine similarity.
|
|
@@ -75,6 +75,7 @@ export class LanceDbStore {
|
|
|
75
75
|
db = null;
|
|
76
76
|
table = null;
|
|
77
77
|
tableInit = null;
|
|
78
|
+
writeLock = Promise.resolve(void 0);
|
|
78
79
|
/**
|
|
79
80
|
* @param dbPath - Filesystem path to the LanceDB database directory.
|
|
80
81
|
* @param vectorDimension - Dimension of the embedding vectors. Default: 384.
|
|
@@ -108,12 +109,14 @@ export class LanceDbStore {
|
|
|
108
109
|
if (tableNames.includes(TABLE_NAME)) {
|
|
109
110
|
this.table = await db.openTable(TABLE_NAME);
|
|
110
111
|
if (await this.tableHasDescriptionColumn()) {
|
|
112
|
+
await this.migrateNewColumns();
|
|
111
113
|
return this.table;
|
|
112
114
|
}
|
|
113
115
|
// Schema missing 'description' column -- try to add it gracefully first.
|
|
114
116
|
try {
|
|
115
117
|
await this.table.addColumns([{ name: "description", valueSql: "''" }]);
|
|
116
118
|
console.warn("[lancedb] Added missing 'description' column to existing table.");
|
|
119
|
+
await this.migrateNewColumns();
|
|
117
120
|
return this.table;
|
|
118
121
|
}
|
|
119
122
|
catch {
|
|
@@ -149,6 +152,9 @@ export class LanceDbStore {
|
|
|
149
152
|
startLine: 0,
|
|
150
153
|
endLine: 0,
|
|
151
154
|
language: "",
|
|
155
|
+
kind: "",
|
|
156
|
+
quirkType: "",
|
|
157
|
+
tags: "",
|
|
152
158
|
};
|
|
153
159
|
this.table = await db.createTable({
|
|
154
160
|
name: TABLE_NAME,
|
|
@@ -174,6 +180,58 @@ export class LanceDbStore {
|
|
|
174
180
|
return false;
|
|
175
181
|
}
|
|
176
182
|
}
|
|
183
|
+
async hasColumn(name) {
|
|
184
|
+
try {
|
|
185
|
+
const schema = await this.table.schema();
|
|
186
|
+
return schema.fields.some((f) => f.name === name);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Add kind/quirkType/tags columns if missing from an existing table. */
|
|
193
|
+
async migrateNewColumns() {
|
|
194
|
+
const missing = [];
|
|
195
|
+
for (const col of ["kind", "quirkType", "tags"]) {
|
|
196
|
+
if (!(await this.hasColumn(col))) {
|
|
197
|
+
missing.push({ name: col, valueSql: "''" });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (missing.length > 0) {
|
|
201
|
+
try {
|
|
202
|
+
await this.table.addColumns(missing);
|
|
203
|
+
console.warn(`[lancedb] Added missing columns: ${missing.map((c) => c.name).join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
console.warn("[lancedb] Could not auto-add missing columns. " +
|
|
207
|
+
"Run 'opencode-rag index --force' to rebuild the index with the correct schema.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
await this.ensureColumnsNullable(["kind", "quirkType", "tags"]);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Ensure that kind/quirkType/tags columns are nullable so that queries
|
|
215
|
+
* with WHERE clauses on these columns do not panic on old fragments
|
|
216
|
+
* written before the columns existed.
|
|
217
|
+
*/
|
|
218
|
+
async ensureColumnsNullable(columns) {
|
|
219
|
+
for (const col of columns) {
|
|
220
|
+
if (!(await this.hasColumn(col)))
|
|
221
|
+
continue;
|
|
222
|
+
try {
|
|
223
|
+
const schema = await this.table.schema();
|
|
224
|
+
const field = schema.fields.find((f) => f.name === col);
|
|
225
|
+
if (field && field.nullable === false) {
|
|
226
|
+
await this.table.alterColumns([{ path: col, nullable: true }]);
|
|
227
|
+
console.warn(`[lancedb] Made column '${col}' nullable to avoid null-fragment panics.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
console.warn(`[lancedb] Could not alter nullability of column '${col}'.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
177
235
|
/**
|
|
178
236
|
* Store chunks in the LanceDB table. New rows are inserted first, then
|
|
179
237
|
* any old rows at the same (filePath, startLine) with different IDs are
|
|
@@ -184,12 +242,17 @@ export class LanceDbStore {
|
|
|
184
242
|
async addChunks(chunks) {
|
|
185
243
|
if (chunks.length === 0)
|
|
186
244
|
return;
|
|
245
|
+
const done = this.writeLock.then(() => this.addChunksInternal(chunks));
|
|
246
|
+
this.writeLock = done.catch(() => { });
|
|
187
247
|
try {
|
|
188
|
-
await
|
|
248
|
+
await done;
|
|
189
249
|
}
|
|
190
250
|
catch (err) {
|
|
251
|
+
this.writeLock = Promise.resolve();
|
|
191
252
|
if (isCorruptionError(err) && await this.tryRepair()) {
|
|
192
|
-
|
|
253
|
+
const retry = this.addChunksInternal(chunks);
|
|
254
|
+
this.writeLock = retry.catch(() => { });
|
|
255
|
+
await retry;
|
|
193
256
|
return;
|
|
194
257
|
}
|
|
195
258
|
throw err;
|
|
@@ -208,6 +271,9 @@ export class LanceDbStore {
|
|
|
208
271
|
startLine: c.metadata.startLine,
|
|
209
272
|
endLine: c.metadata.endLine,
|
|
210
273
|
language: c.metadata.language,
|
|
274
|
+
kind: c.metadata.kind ?? "",
|
|
275
|
+
quirkType: c.metadata.quirkType ?? "",
|
|
276
|
+
tags: c.metadata.tags ? JSON.stringify(c.metadata.tags) : "",
|
|
211
277
|
}));
|
|
212
278
|
if (rows.length === 0)
|
|
213
279
|
return;
|
|
@@ -286,6 +352,15 @@ export class LanceDbStore {
|
|
|
286
352
|
}
|
|
287
353
|
}
|
|
288
354
|
rowToSearchResult(row) {
|
|
355
|
+
let tags;
|
|
356
|
+
try {
|
|
357
|
+
const raw = row.tags;
|
|
358
|
+
if (raw)
|
|
359
|
+
tags = JSON.parse(raw);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
tags = undefined;
|
|
363
|
+
}
|
|
289
364
|
return {
|
|
290
365
|
score: Math.min(1, Math.max(0, 1 - (row._distance ?? 0) / 2)),
|
|
291
366
|
chunk: {
|
|
@@ -297,6 +372,9 @@ export class LanceDbStore {
|
|
|
297
372
|
startLine: row.startLine,
|
|
298
373
|
endLine: row.endLine,
|
|
299
374
|
language: row.language,
|
|
375
|
+
kind: row.kind || undefined,
|
|
376
|
+
quirkType: row.quirkType || undefined,
|
|
377
|
+
tags,
|
|
300
378
|
},
|
|
301
379
|
},
|
|
302
380
|
};
|
|
@@ -340,17 +418,31 @@ export class LanceDbStore {
|
|
|
340
418
|
.where(`filePath = '${normalizedPath}'`)
|
|
341
419
|
.toArray();
|
|
342
420
|
return rows
|
|
343
|
-
.map((row) =>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
421
|
+
.map((row) => {
|
|
422
|
+
let tags;
|
|
423
|
+
try {
|
|
424
|
+
const raw = row.tags;
|
|
425
|
+
if (raw)
|
|
426
|
+
tags = JSON.parse(raw);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
tags = undefined;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
id: row.id,
|
|
433
|
+
content: row.content,
|
|
434
|
+
description: row.description ?? "",
|
|
435
|
+
metadata: {
|
|
436
|
+
filePath: row.filePath,
|
|
437
|
+
startLine: row.startLine,
|
|
438
|
+
endLine: row.endLine,
|
|
439
|
+
language: row.language,
|
|
440
|
+
kind: row.kind || undefined,
|
|
441
|
+
quirkType: row.quirkType || undefined,
|
|
442
|
+
tags,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
})
|
|
354
446
|
.sort((a, b) => a.metadata.startLine - b.metadata.startLine);
|
|
355
447
|
}
|
|
356
448
|
/**
|
|
@@ -374,6 +466,9 @@ export class LanceDbStore {
|
|
|
374
466
|
endLine: row.endLine,
|
|
375
467
|
content: row.content,
|
|
376
468
|
description: row.description ?? "",
|
|
469
|
+
kind: row.kind ?? "",
|
|
470
|
+
quirkType: row.quirkType ?? "",
|
|
471
|
+
tags: row.tags ?? "",
|
|
377
472
|
}));
|
|
378
473
|
}
|
|
379
474
|
/**
|
|
@@ -392,11 +487,26 @@ export class LanceDbStore {
|
|
|
392
487
|
const count = await table.countRows();
|
|
393
488
|
if (count === 0)
|
|
394
489
|
return [];
|
|
395
|
-
let query = table.vectorSearch(l2Normalize(embedding)).distanceType("cosine");
|
|
396
490
|
const whereClause = buildWhereClause(filter);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
491
|
+
let results;
|
|
492
|
+
try {
|
|
493
|
+
let query = table.vectorSearch(l2Normalize(embedding)).distanceType("cosine");
|
|
494
|
+
if (whereClause)
|
|
495
|
+
query = query.where(whereClause);
|
|
496
|
+
results = await query.limit(topK).toArray();
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
if (err instanceof Error && err.message.includes("non-nullable") && filter) {
|
|
500
|
+
const fallbackQuery = table.vectorSearch(l2Normalize(embedding)).distanceType("cosine");
|
|
501
|
+
const rawResults = await fallbackQuery.limit(topK * 5).toArray();
|
|
502
|
+
return rawResults
|
|
503
|
+
.map((row) => this.rowToSearchResult(row))
|
|
504
|
+
.filter((r) => r.chunk.id !== "__seed__")
|
|
505
|
+
.filter((r) => matchesFilterLocal(r.chunk, filter))
|
|
506
|
+
.slice(0, topK);
|
|
507
|
+
}
|
|
508
|
+
throw err;
|
|
509
|
+
}
|
|
400
510
|
return results
|
|
401
511
|
.map((row) => this.rowToSearchResult(row))
|
|
402
512
|
.filter((r) => r.chunk.id !== "__seed__");
|
|
@@ -635,6 +745,10 @@ function buildWhereClause(filter) {
|
|
|
635
745
|
const langs = filter.languages.map((l) => `'${l.replace(/'/g, "''")}'`).join(",");
|
|
636
746
|
parts.push(`language IN (${langs})`);
|
|
637
747
|
}
|
|
748
|
+
if (filter.kinds?.length) {
|
|
749
|
+
const kinds = filter.kinds.map((k) => `'${k.replace(/'/g, "''")}'`).join(",");
|
|
750
|
+
parts.push(`kind IN (${kinds})`);
|
|
751
|
+
}
|
|
638
752
|
if (filter.pathPatterns?.length) {
|
|
639
753
|
const likes = filter.pathPatterns.map((p) => {
|
|
640
754
|
const escaped = p.replace(/'/g, "''");
|
|
@@ -648,4 +762,26 @@ function buildWhereClause(filter) {
|
|
|
648
762
|
}
|
|
649
763
|
return parts.length ? parts.join(" AND ") : undefined;
|
|
650
764
|
}
|
|
765
|
+
/** Client-side metadata filter for use as a fallback when LanceDB WHERE panics. */
|
|
766
|
+
function matchesFilterLocal(chunk, filter) {
|
|
767
|
+
if (!filter)
|
|
768
|
+
return true;
|
|
769
|
+
if (filter.languages?.length && !filter.languages.includes(chunk.metadata.language))
|
|
770
|
+
return false;
|
|
771
|
+
if (filter.kinds?.length && !filter.kinds.includes(chunk.metadata.kind ?? ""))
|
|
772
|
+
return false;
|
|
773
|
+
if (filter.pathPatterns?.length) {
|
|
774
|
+
return filter.pathPatterns.some((p) => globMatchLocal(p, chunk.metadata.filePath));
|
|
775
|
+
}
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
function globMatchLocal(pattern, filePath) {
|
|
779
|
+
const GLOBSTAR = "\x00GS\x00";
|
|
780
|
+
let re = pattern.replace(/\*\*/g, GLOBSTAR);
|
|
781
|
+
re = re.replace(/([.+^${}()|[\]\\])/g, "\\$1");
|
|
782
|
+
re = re.replace(new RegExp(GLOBSTAR, "g"), ".*");
|
|
783
|
+
re = re.replace(/\*/g, "[^/]*");
|
|
784
|
+
re = re.replace(/\?/g, ".");
|
|
785
|
+
return new RegExp("^" + re + "$").test(filePath);
|
|
786
|
+
}
|
|
651
787
|
//# sourceMappingURL=lancedb.js.map
|
|
@@ -45,6 +45,9 @@ export class InMemoryVectorStore {
|
|
|
45
45
|
endLine: c.metadata.endLine,
|
|
46
46
|
content: c.content,
|
|
47
47
|
description: c.description ?? "",
|
|
48
|
+
kind: c.metadata.kind ?? "",
|
|
49
|
+
quirkType: c.metadata.quirkType ?? "",
|
|
50
|
+
tags: c.metadata.tags ? JSON.stringify(c.metadata.tags) : "",
|
|
48
51
|
}));
|
|
49
52
|
}
|
|
50
53
|
async listFiles() {
|