opencode-rag-plugin 1.17.3 → 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.
@@ -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.
@@ -109,12 +109,14 @@ export class LanceDbStore {
109
109
  if (tableNames.includes(TABLE_NAME)) {
110
110
  this.table = await db.openTable(TABLE_NAME);
111
111
  if (await this.tableHasDescriptionColumn()) {
112
+ await this.migrateNewColumns();
112
113
  return this.table;
113
114
  }
114
115
  // Schema missing 'description' column -- try to add it gracefully first.
115
116
  try {
116
117
  await this.table.addColumns([{ name: "description", valueSql: "''" }]);
117
118
  console.warn("[lancedb] Added missing 'description' column to existing table.");
119
+ await this.migrateNewColumns();
118
120
  return this.table;
119
121
  }
120
122
  catch {
@@ -150,6 +152,9 @@ export class LanceDbStore {
150
152
  startLine: 0,
151
153
  endLine: 0,
152
154
  language: "",
155
+ kind: "",
156
+ quirkType: "",
157
+ tags: "",
153
158
  };
154
159
  this.table = await db.createTable({
155
160
  name: TABLE_NAME,
@@ -175,6 +180,58 @@ export class LanceDbStore {
175
180
  return false;
176
181
  }
177
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
+ }
178
235
  /**
179
236
  * Store chunks in the LanceDB table. New rows are inserted first, then
180
237
  * any old rows at the same (filePath, startLine) with different IDs are
@@ -214,6 +271,9 @@ export class LanceDbStore {
214
271
  startLine: c.metadata.startLine,
215
272
  endLine: c.metadata.endLine,
216
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) : "",
217
277
  }));
218
278
  if (rows.length === 0)
219
279
  return;
@@ -292,6 +352,15 @@ export class LanceDbStore {
292
352
  }
293
353
  }
294
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
+ }
295
364
  return {
296
365
  score: Math.min(1, Math.max(0, 1 - (row._distance ?? 0) / 2)),
297
366
  chunk: {
@@ -303,6 +372,9 @@ export class LanceDbStore {
303
372
  startLine: row.startLine,
304
373
  endLine: row.endLine,
305
374
  language: row.language,
375
+ kind: row.kind || undefined,
376
+ quirkType: row.quirkType || undefined,
377
+ tags,
306
378
  },
307
379
  },
308
380
  };
@@ -346,17 +418,31 @@ export class LanceDbStore {
346
418
  .where(`filePath = '${normalizedPath}'`)
347
419
  .toArray();
348
420
  return rows
349
- .map((row) => ({
350
- id: row.id,
351
- content: row.content,
352
- description: row.description ?? "",
353
- metadata: {
354
- filePath: row.filePath,
355
- startLine: row.startLine,
356
- endLine: row.endLine,
357
- language: row.language,
358
- },
359
- }))
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
+ })
360
446
  .sort((a, b) => a.metadata.startLine - b.metadata.startLine);
361
447
  }
362
448
  /**
@@ -380,6 +466,9 @@ export class LanceDbStore {
380
466
  endLine: row.endLine,
381
467
  content: row.content,
382
468
  description: row.description ?? "",
469
+ kind: row.kind ?? "",
470
+ quirkType: row.quirkType ?? "",
471
+ tags: row.tags ?? "",
383
472
  }));
384
473
  }
385
474
  /**
@@ -398,11 +487,26 @@ export class LanceDbStore {
398
487
  const count = await table.countRows();
399
488
  if (count === 0)
400
489
  return [];
401
- let query = table.vectorSearch(l2Normalize(embedding)).distanceType("cosine");
402
490
  const whereClause = buildWhereClause(filter);
403
- if (whereClause)
404
- query = query.where(whereClause);
405
- 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
+ }
406
510
  return results
407
511
  .map((row) => this.rowToSearchResult(row))
408
512
  .filter((r) => r.chunk.id !== "__seed__");
@@ -641,6 +745,10 @@ function buildWhereClause(filter) {
641
745
  const langs = filter.languages.map((l) => `'${l.replace(/'/g, "''")}'`).join(",");
642
746
  parts.push(`language IN (${langs})`);
643
747
  }
748
+ if (filter.kinds?.length) {
749
+ const kinds = filter.kinds.map((k) => `'${k.replace(/'/g, "''")}'`).join(",");
750
+ parts.push(`kind IN (${kinds})`);
751
+ }
644
752
  if (filter.pathPatterns?.length) {
645
753
  const likes = filter.pathPatterns.map((p) => {
646
754
  const escaped = p.replace(/'/g, "''");
@@ -654,4 +762,26 @@ function buildWhereClause(filter) {
654
762
  }
655
763
  return parts.length ? parts.join(" AND ") : undefined;
656
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
+ }
657
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.3",
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",