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.
@@ -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 this.addChunksInternal(chunks);
248
+ await done;
189
249
  }
190
250
  catch (err) {
251
+ this.writeLock = Promise.resolve();
191
252
  if (isCorruptionError(err) && await this.tryRepair()) {
192
- await this.addChunksInternal(chunks);
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
- id: row.id,
345
- content: row.content,
346
- description: row.description ?? "",
347
- metadata: {
348
- filePath: row.filePath,
349
- startLine: row.startLine,
350
- endLine: row.endLine,
351
- language: row.language,
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
- if (whereClause)
398
- query = query.where(whereClause);
399
- const results = await query.limit(topK).toArray();
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-rag-plugin",
3
- "version": "1.17.2",
3
+ "version": "1.18.0",
4
4
  "description": "OpenCode plugin for local-first RAG-based semantic code search",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin-entry.js",