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.
package/dist/plugin.js CHANGED
@@ -15,7 +15,7 @@ import { appendDebugLog } from "./core/fileLogger.js";
15
15
  import { loadRuntimeOverrides, applyRuntimeOverrides } from "./core/runtime-overrides.js";
16
16
  import { createBackgroundIndexer } from "./watcher.js";
17
17
  import { createRagReadTool } from "./opencode/create-read-tool.js";
18
- import { createFileSkeletonTool, createFindUsagesTool, createDescribeImageTool, } from "./opencode/tools.js";
18
+ import { createFileSkeletonTool, createFindUsagesTool, createDescribeImageTool, createRecallQuirksTool, createAddQuirkTool, } from "./opencode/tools.js";
19
19
  import { resolveApiKey } from "./core/resolve-api-key.js";
20
20
  import { consumePendingRagInjection } from "./core/rag-injection-flag.js";
21
21
  import { loadDocProgress, markSubdirectoryDocumented } from "./core/doc-progress.js";
@@ -25,6 +25,7 @@ import { countTokens } from "./eval/token-counter.js";
25
25
  import { checkForUpdate, getCurrentVersion, installLatestUpdate } from "./core/version-check.js";
26
26
  import { loadAutoUpdateState, saveAutoUpdateState, shouldAttemptInstall } from "./core/auto-update-state.js";
27
27
  import { destroyAllPooledConnections } from "./embedder/http.js";
28
+ import { listQuirks, lintQuirks, recallQuirks } from "./quirks/quirk-store.js";
28
29
  import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
29
30
  import path from "node:path";
30
31
  import { fileURLToPath } from "node:url";
@@ -593,6 +594,40 @@ export function createRagHooks(options) {
593
594
  error: err,
594
595
  });
595
596
  }
597
+ try {
598
+ const recallQuirksTool = createRecallQuirksTool({
599
+ store,
600
+ embedder,
601
+ cfg: effectiveCfg,
602
+ keywordIndex: keywordIndex,
603
+ storePath: options.storePath,
604
+ });
605
+ tools["recall_quirks"] = recallQuirksTool;
606
+ }
607
+ catch (err) {
608
+ appendDebugLog(options.logFilePath, {
609
+ scope: "plugin",
610
+ message: "Failed to register recall_quirks tool",
611
+ error: err,
612
+ });
613
+ }
614
+ try {
615
+ const addQuirkTool = createAddQuirkTool({
616
+ store,
617
+ embedder,
618
+ cfg: effectiveCfg,
619
+ keywordIndex: keywordIndex,
620
+ storePath: options.storePath,
621
+ });
622
+ tools["add_quirk"] = addQuirkTool;
623
+ }
624
+ catch (err) {
625
+ appendDebugLog(options.logFilePath, {
626
+ scope: "plugin",
627
+ message: "Failed to register add_quirk tool",
628
+ error: err,
629
+ });
630
+ }
596
631
  if (readOverride) {
597
632
  const readTool = createRagReadTool({
598
633
  worktree: options.worktree,
@@ -666,6 +701,8 @@ export function createRagHooks(options) {
666
701
  "- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
667
702
  "- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
668
703
  "- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
704
+ "- `recall_quirks(query)`: query experiential quirk memory (gotchas, preferences, decisions). Call when you hit an error or need to recall known pitfalls.",
705
+ "- `add_quirk(content)`: store a new experiential memory. Call when you discover a non-obvious fact, gotcha, or coding convention.",
669
706
  "",
670
707
  "Decision tree — ALWAYS follow this order:",
671
708
  "1. User mentions code behavior/architecture → `search_semantic(query)`",
@@ -673,6 +710,8 @@ export function createRagHooks(options) {
673
710
  "3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
674
711
  "4. User asks a code question → `search_semantic` to gather context before answering",
675
712
  "5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
713
+ "6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
714
+ "7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
676
715
  "",
677
716
  "Proactive triggers — you MUST call these tools when:",
678
717
  "- User asks about code behavior, architecture, or implementation details",
@@ -882,6 +921,67 @@ export function createRagHooks(options) {
882
921
  }
883
922
  return;
884
923
  }
924
+ // Handle /quirk slash command
925
+ if (text.startsWith("/quirk")) {
926
+ const memoryCfg = getEffectiveCfg().memory;
927
+ if (!memoryCfg?.enabled) {
928
+ const parts = output?.parts ?? output?.message?.parts;
929
+ if (Array.isArray(parts) && parts.length > 0) {
930
+ const first = parts[0];
931
+ if (typeof first.text === "string") {
932
+ parts[0] = { ...first, text: "Quirk memory is not enabled. Set `memory.enabled` to `true` in opencode-rag.json." };
933
+ }
934
+ }
935
+ return;
936
+ }
937
+ const arg = text.slice(6).trim().toLowerCase();
938
+ const lines = [];
939
+ if (arg === "lint") {
940
+ const deps = { embedder, store, keywordIndex: keywordIndex, cfg: getEffectiveCfg(), storePath: options.storePath };
941
+ const issues = await lintQuirks(deps);
942
+ lines.push("## Quirk Lint", "");
943
+ if (issues.length === 0) {
944
+ lines.push("No issues found. All quirks look healthy.");
945
+ }
946
+ else {
947
+ lines.push(`Found ${issues.length} issue(s):\n`);
948
+ for (const issue of issues) {
949
+ lines.push(`- ${issue}`);
950
+ }
951
+ lines.push("", "Fix the issues and consider updating or removing the flagged quirks.");
952
+ }
953
+ }
954
+ else if (arg.startsWith("add ")) {
955
+ lines.push("## Add Quirk", "", "Use the `add_quirk` tool to store a new experiential memory entry. Example:", "", '`content`: "npm install needs --legacy-peer-deps due to LanceDB peer dep conflicts"', '`quirkType`: "gotcha"', "`tags`: [\"npm\", \"lancedb\", \"installation\"]");
956
+ }
957
+ else {
958
+ // Status / list
959
+ const deps = { embedder, store, keywordIndex: keywordIndex, cfg: getEffectiveCfg(), storePath: options.storePath };
960
+ const quirks = await listQuirks(deps);
961
+ lines.push("## Quirk Memory", "");
962
+ if (quirks.length === 0) {
963
+ lines.push("No quirks stored yet.");
964
+ }
965
+ else {
966
+ lines.push(`${quirks.length} quirk(s) stored:\n`);
967
+ for (const q of quirks) {
968
+ const badge = q.quirkType ? `[\`${q.quirkType}\`] ` : "";
969
+ const tags = q.tags.length > 0 ? ` _(${q.tags.join(", ")})_` : "";
970
+ lines.push(`- ${badge}${q.content.slice(0, 100)}${tags} `);
971
+ }
972
+ lines.push("", "Commands:", "- `/quirk` — show this status", "- `/quirk lint` — health-check quirks", "- `/quirk add <text>` — instructions to use the add_quirk tool");
973
+ }
974
+ }
975
+ const quirkMsg = lines.join("\n");
976
+ const parts = output?.parts ?? output?.message?.parts;
977
+ if (Array.isArray(parts) && parts.length > 0) {
978
+ const first = parts[0];
979
+ if (typeof first.text === "string") {
980
+ parts[0] = { ...first, text: quirkMsg };
981
+ }
982
+ }
983
+ return;
984
+ }
885
985
  // Handle hotkey-triggered RAG injection (Ctrl+Enter / Ctrl+Alt+Enter)
886
986
  const pendingInjection = consumePendingRagInjection(options.storePath);
887
987
  if (pendingInjection) {
@@ -938,6 +1038,40 @@ export function createRagHooks(options) {
938
1038
  });
939
1039
  }
940
1040
  }
1041
+ // Auto-inject quirks when memory.autoInject is enabled
1042
+ const memoryCfg = effectiveCfg.memory;
1043
+ if (memoryCfg?.autoInject) {
1044
+ try {
1045
+ const quirkDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effectiveCfg, storePath: options.storePath };
1046
+ const quirkResults = await recallQuirks(quirkDeps, searchQuery, { topK: 3 });
1047
+ if (quirkResults.length > 0) {
1048
+ const quirkLines = ["", "---", "⚠ **Quirk memory**", ""];
1049
+ for (const qr of quirkResults) {
1050
+ const m = qr.chunk.metadata;
1051
+ const badge = m.quirkType ? `[\`${m.quirkType}\`] ` : "";
1052
+ quirkLines.push(`- ${badge}${qr.chunk.content.slice(0, 200)}`);
1053
+ }
1054
+ const quirkBlock = quirkLines.join("\n");
1055
+ const parts = output?.parts ?? output?.message?.parts;
1056
+ if (Array.isArray(parts) && parts.length > 0) {
1057
+ const first = parts[0];
1058
+ if (typeof first.text === "string") {
1059
+ parts[0] = { ...first, text: first.text + "\n\n" + quirkBlock };
1060
+ }
1061
+ const msgParts = output?.message?.parts;
1062
+ if (Array.isArray(msgParts) && msgParts !== parts) {
1063
+ const mfirst = msgParts[0];
1064
+ if (typeof mfirst.text === "string") {
1065
+ msgParts[0] = { ...mfirst, text: mfirst.text + "\n\n" + quirkBlock };
1066
+ }
1067
+ }
1068
+ }
1069
+ }
1070
+ }
1071
+ catch {
1072
+ // Non-critical — must never throw
1073
+ }
1074
+ }
941
1075
  }
942
1076
  appendDebugLog(options.logFilePath, {
943
1077
  scope: "chat.message",
@@ -0,0 +1,5 @@
1
+ /** Check whether quirk content is allowed within the immutable environment boundary. */
2
+ export declare function isQuirkAllowed(content: string): {
3
+ ok: boolean;
4
+ reason?: string;
5
+ };
@@ -0,0 +1,23 @@
1
+ /** Patterns that are never allowed in quirk memory — the immutable trust boundary. */
2
+ const BLOCKED_PATTERNS = [
3
+ /\bskip\s+(?:\S+\s+)*?tests?\b/i,
4
+ /\bdisable\s+(lint|typecheck|strict)/i,
5
+ /\bignore\s+(all\s+)?errors/i,
6
+ /\brm\s+-rf\b/,
7
+ /\bforce\s+push\b/,
8
+ /\bgit\s+push\s+--force\b/,
9
+ /\bdelete\s+.+\.git/i,
10
+ /\btouch\s+(?:\.env|withheld)/i,
11
+ /\bbypass\s+(?:security|review|check)/i,
12
+ ];
13
+ /** Check whether quirk content is allowed within the immutable environment boundary. */
14
+ export function isQuirkAllowed(content) {
15
+ for (const pattern of BLOCKED_PATTERNS) {
16
+ const match = content.match(pattern);
17
+ if (match) {
18
+ return { ok: false, reason: `Content matches blocked pattern: "${match[0]}"` };
19
+ }
20
+ }
21
+ return { ok: true };
22
+ }
23
+ //# sourceMappingURL=monitor.js.map
@@ -0,0 +1,25 @@
1
+ import type { EmbeddingProvider, VectorStore, KeywordIndex, SearchResult } from "../core/interfaces.js";
2
+ import type { RagConfig } from "../core/config.js";
3
+ import type { Quirk, QuirkInput } from "./types.js";
4
+ /** Dependencies required by all quirk-store operations. */
5
+ export interface QuirkStoreDeps {
6
+ embedder: EmbeddingProvider;
7
+ store: VectorStore;
8
+ keywordIndex: KeywordIndex;
9
+ cfg: RagConfig;
10
+ storePath: string;
11
+ }
12
+ /** Add a new quirk to the vector store, keyword index, and audit log. */
13
+ export declare function addQuirk(deps: QuirkStoreDeps, input: QuirkInput): Promise<Quirk>;
14
+ /** Remove a quirk by its ID. */
15
+ export declare function removeQuirk(deps: QuirkStoreDeps, id: string): Promise<void>;
16
+ /** List all quirks sorted by lastObserved descending. */
17
+ export declare function listQuirks(deps: QuirkStoreDeps): Promise<Quirk[]>;
18
+ /** Recall quirks matching a query, with confidence re-weighting. */
19
+ export declare function recallQuirks(deps: QuirkStoreDeps, query: string, options?: {
20
+ topK?: number;
21
+ quirkType?: string;
22
+ tags?: string[];
23
+ }): Promise<SearchResult[]>;
24
+ /** Lint quirks for low confidence, staleness, duplicates, and orphan source refs. */
25
+ export declare function lintQuirks(deps: QuirkStoreDeps): Promise<string[]>;
@@ -0,0 +1,217 @@
1
+ import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import path from "node:path";
4
+ import { retrieve } from "../retriever/retriever.js";
5
+ import { isQuirkAllowed } from "./monitor.js";
6
+ const QUIRK_FILE_PREFIX = "quirk:";
7
+ const QUIKK_JSONL = "quirks.jsonl";
8
+ function jsonlPath(storePath) {
9
+ return path.join(storePath, QUIKK_JSONL);
10
+ }
11
+ function isMemoryStore(storePath) {
12
+ return storePath.startsWith("memory:");
13
+ }
14
+ /** In-memory backup for memory:// stores. */
15
+ const memQuirks = new Map();
16
+ function readJsonl(filePath) {
17
+ if (!existsSync(filePath))
18
+ return [];
19
+ const raw = readFileSync(filePath, "utf-8").trim();
20
+ if (!raw)
21
+ return [];
22
+ return raw.split("\n").map((l) => JSON.parse(l));
23
+ }
24
+ function appendJsonl(filePath, q) {
25
+ const dir = path.dirname(filePath);
26
+ if (!existsSync(dir)) {
27
+ mkdirSync(dir, { recursive: true });
28
+ }
29
+ appendFileSync(filePath, JSON.stringify(q) + "\n", "utf-8");
30
+ }
31
+ function rewriteJsonl(filePath, quirks) {
32
+ const dir = path.dirname(filePath);
33
+ if (!existsSync(dir)) {
34
+ mkdirSync(dir, { recursive: true });
35
+ }
36
+ writeFileSync(filePath, quirks.map((q) => JSON.stringify(q)).join("\n") + "\n", "utf-8");
37
+ }
38
+ function nowISO() {
39
+ return new Date().toISOString();
40
+ }
41
+ /** Add a new quirk to the vector store, keyword index, and audit log. */
42
+ export async function addQuirk(deps, input) {
43
+ const allowed = isQuirkAllowed(input.content);
44
+ if (!allowed.ok) {
45
+ throw new Error(`Quirk rejected by trust monitor: ${allowed.reason}`);
46
+ }
47
+ const id = `quirk:${randomUUID()}`;
48
+ const lastObserved = nowISO();
49
+ const confidence = input.confidence ?? 1;
50
+ const quirk = {
51
+ id,
52
+ content: input.content,
53
+ quirkType: input.quirkType,
54
+ tags: input.tags ?? [],
55
+ confidence,
56
+ lastObserved,
57
+ sourceRef: input.sourceRef,
58
+ };
59
+ const prefix = deps.cfg.embedding.documentPrefix ?? "";
60
+ const chunkContent = prefix + input.content;
61
+ const embeddings = await deps.embedder.embed([chunkContent], "document");
62
+ const embedding = embeddings[0];
63
+ if (!embedding || embedding.length === 0) {
64
+ throw new Error("Embedding returned empty vector for quirk content");
65
+ }
66
+ const chunk = {
67
+ id,
68
+ content: input.content,
69
+ description: "",
70
+ embedding,
71
+ metadata: {
72
+ filePath: QUIRK_FILE_PREFIX + id,
73
+ startLine: 0,
74
+ endLine: 0,
75
+ language: "quirk",
76
+ kind: "quirk",
77
+ quirkType: input.quirkType,
78
+ tags: input.tags ?? [],
79
+ confidence,
80
+ lastObserved,
81
+ },
82
+ };
83
+ await deps.store.addChunks([chunk]);
84
+ deps.keywordIndex.addChunks([chunk]);
85
+ if (!isMemoryStore(deps.storePath)) {
86
+ appendJsonl(jsonlPath(deps.storePath), quirk);
87
+ }
88
+ else {
89
+ memQuirks.set(id, quirk);
90
+ }
91
+ return quirk;
92
+ }
93
+ /** Remove a quirk by its ID. */
94
+ export async function removeQuirk(deps, id) {
95
+ const filePath = QUIRK_FILE_PREFIX + id;
96
+ await deps.store.deleteByFilePath(filePath);
97
+ deps.keywordIndex.removeByFilePath(filePath);
98
+ if (!isMemoryStore(deps.storePath)) {
99
+ const jp = jsonlPath(deps.storePath);
100
+ const all = readJsonl(jp).filter((q) => q.id !== id);
101
+ rewriteJsonl(jp, all);
102
+ }
103
+ else {
104
+ memQuirks.delete(id);
105
+ }
106
+ }
107
+ /** List all quirks sorted by lastObserved descending. */
108
+ export async function listQuirks(deps) {
109
+ if (!isMemoryStore(deps.storePath)) {
110
+ const jp = jsonlPath(deps.storePath);
111
+ if (existsSync(jp)) {
112
+ const all = readJsonl(jp);
113
+ all.sort((a, b) => b.lastObserved.localeCompare(a.lastObserved));
114
+ return all;
115
+ }
116
+ // Fallback: scan the store
117
+ const filePaths = await deps.store.getFilePaths();
118
+ const quirkPaths = filePaths.filter((fp) => fp.startsWith(QUIRK_FILE_PREFIX));
119
+ const result = [];
120
+ for (const fp of quirkPaths) {
121
+ const chunks = await deps.store.getChunksByFilePath(fp);
122
+ for (const c of chunks) {
123
+ result.push({
124
+ id: c.id,
125
+ content: c.content,
126
+ quirkType: c.metadata.quirkType,
127
+ tags: c.metadata.tags ?? [],
128
+ confidence: c.metadata.confidence ?? 1,
129
+ lastObserved: c.metadata.lastObserved ?? "",
130
+ sourceRef: undefined,
131
+ });
132
+ }
133
+ }
134
+ result.sort((a, b) => b.lastObserved.localeCompare(a.lastObserved));
135
+ return result;
136
+ }
137
+ const all = [...memQuirks.values()];
138
+ all.sort((a, b) => b.lastObserved.localeCompare(a.lastObserved));
139
+ return all;
140
+ }
141
+ /** Recall quirks matching a query, with confidence re-weighting. */
142
+ export async function recallQuirks(deps, query, options) {
143
+ const topK = options?.topK ?? 10;
144
+ const minConfidence = deps.cfg.memory?.minConfidence ?? 0.5;
145
+ const filter = { kinds: ["quirk"] };
146
+ if (options?.quirkType) {
147
+ filter.languages = [options.quirkType];
148
+ }
149
+ const recallMinScore = deps.cfg.memory?.recallMinScore ?? 0.72;
150
+ const raw = await retrieve(query, deps.embedder, deps.store, {
151
+ topK: topK * 3,
152
+ minScore: recallMinScore,
153
+ keywordIndex: deps.keywordIndex,
154
+ keywordWeight: deps.cfg.retrieval.hybridSearch?.keywordWeight,
155
+ hybridEnabled: false,
156
+ queryPrefix: deps.cfg.embedding.queryPrefix,
157
+ filter,
158
+ });
159
+ // Confidence re-weighting + type/tag filtering
160
+ const filtered = raw.filter((r) => {
161
+ const conf = r.chunk.metadata.confidence ?? 1;
162
+ if (conf < minConfidence)
163
+ return false;
164
+ if (options?.quirkType && r.chunk.metadata.quirkType !== options.quirkType)
165
+ return false;
166
+ if (options?.tags?.length) {
167
+ const chunkTags = r.chunk.metadata.tags ?? [];
168
+ if (!options.tags.some((t) => chunkTags.includes(t)))
169
+ return false;
170
+ }
171
+ return true;
172
+ });
173
+ // Re-weight score by confidence, re-sort
174
+ for (const r of filtered) {
175
+ const conf = r.chunk.metadata.confidence ?? 1;
176
+ r.score = r.score * Math.min(1, Math.max(0.01, conf));
177
+ }
178
+ filtered.sort((a, b) => b.score - a.score);
179
+ return filtered.slice(0, topK);
180
+ }
181
+ /** Lint quirks for low confidence, staleness, duplicates, and orphan source refs. */
182
+ export async function lintQuirks(deps) {
183
+ const issues = [];
184
+ const quirks = await listQuirks(deps);
185
+ const cfg = deps.cfg.memory;
186
+ for (const q of quirks) {
187
+ if (q.confidence < (cfg?.minConfidence ?? 0.5)) {
188
+ issues.push(`Low confidence (${q.confidence}): "${q.content.slice(0, 60)}..." [${q.id}]`);
189
+ }
190
+ if (cfg?.decay?.enabled && q.lastObserved) {
191
+ const ageDays = (Date.now() - new Date(q.lastObserved).getTime()) / 86_400_000;
192
+ const halfLife = cfg.decay.halfLifeDays;
193
+ if (halfLife > 0 && ageDays > halfLife * 2) {
194
+ issues.push(`Stale (${Math.round(ageDays)}d old): "${q.content.slice(0, 60)}..." [${q.id}]`);
195
+ }
196
+ }
197
+ }
198
+ // Duplicate detection (lexically similar content)
199
+ for (let i = 0; i < quirks.length; i++) {
200
+ for (let j = i + 1; j < quirks.length; j++) {
201
+ const sim = lexicalSimilarity(quirks[i].content, quirks[j].content);
202
+ if (sim > 0.85) {
203
+ issues.push(`Near-duplicate (${(sim * 100).toFixed(0)}% similar): "${quirks[i].content.slice(0, 50)}..." ↔ "${quirks[j].content.slice(0, 50)}..."`);
204
+ }
205
+ }
206
+ }
207
+ return issues;
208
+ }
209
+ /** Simple Jaccard-based lexical similarity (word overlap). */
210
+ function lexicalSimilarity(a, b) {
211
+ const wordsA = new Set(a.toLowerCase().split(/\W+/).filter(Boolean));
212
+ const wordsB = new Set(b.toLowerCase().split(/\W+/).filter(Boolean));
213
+ const intersection = new Set([...wordsA].filter((w) => wordsB.has(w)));
214
+ const union = new Set([...wordsA, ...wordsB]);
215
+ return union.size === 0 ? 0 : intersection.size / union.size;
216
+ }
217
+ //# sourceMappingURL=quirk-store.js.map
@@ -0,0 +1,20 @@
1
+ /** Quirk sub-type labels. */
2
+ export type QuirkType = "gotcha" | "preference" | "decision" | "environment-constraint";
3
+ /** Input for adding a new quirk. */
4
+ export interface QuirkInput {
5
+ content: string;
6
+ quirkType?: string;
7
+ tags?: string[];
8
+ confidence?: number;
9
+ sourceRef?: string;
10
+ }
11
+ /** A quirk entry stored in the vectordb and audit log. */
12
+ export interface Quirk {
13
+ id: string;
14
+ content: string;
15
+ quirkType?: string;
16
+ tags: string[];
17
+ confidence: number;
18
+ lastObserved: string;
19
+ sourceRef?: string;
20
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -184,6 +184,9 @@ export class KeywordIndex {
184
184
  startLine: chunk.metadata.startLine,
185
185
  endLine: chunk.metadata.endLine,
186
186
  language: chunk.metadata.language,
187
+ kind: chunk.metadata.kind ?? "",
188
+ quirkType: chunk.metadata.quirkType ?? "",
189
+ tags: chunk.metadata.tags ? JSON.stringify(chunk.metadata.tags) : "",
187
190
  },
188
191
  ])),
189
192
  };
@@ -215,6 +218,14 @@ export class KeywordIndex {
215
218
  index.invertedIndex.set(token, docMap);
216
219
  }
217
220
  for (const [id, data] of Object.entries(parsed.chunkMap)) {
221
+ let tags;
222
+ try {
223
+ if (data.tags)
224
+ tags = JSON.parse(data.tags);
225
+ }
226
+ catch {
227
+ tags = undefined;
228
+ }
218
229
  index.chunkMap.set(id, {
219
230
  id: data.id,
220
231
  content: data.content,
@@ -224,6 +235,9 @@ export class KeywordIndex {
224
235
  startLine: data.startLine,
225
236
  endLine: data.endLine,
226
237
  language: data.language,
238
+ kind: data.kind || undefined,
239
+ quirkType: data.quirkType || undefined,
240
+ tags,
227
241
  },
228
242
  });
229
243
  }
@@ -251,6 +265,8 @@ function matchesFilter(chunk, filter) {
251
265
  return true;
252
266
  if (filter.languages?.length && !filter.languages.includes(chunk.metadata.language))
253
267
  return false;
268
+ if (filter.kinds?.length && !filter.kinds.includes(chunk.metadata.kind ?? ""))
269
+ return false;
254
270
  if (filter.pathPatterns?.length) {
255
271
  return filter.pathPatterns.some((p) => globMatch(p, chunk.metadata.filePath));
256
272
  }
@@ -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.
@@ -40,6 +40,15 @@ export declare class LanceDbStore implements VectorStore {
40
40
  private getTable;
41
41
  private initTable;
42
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;
43
52
  /**
44
53
  * Store chunks in the LanceDB table. New rows are inserted first, then
45
54
  * any old rows at the same (filePath, startLine) with different IDs are
@@ -80,15 +89,7 @@ export declare class LanceDbStore implements VectorStore {
80
89
  * @param limit - Maximum number of rows to return.
81
90
  * @returns An array of chunk summaries.
82
91
  */
83
- getChunks(offset: number, limit: number): Promise<{
84
- id: string;
85
- filePath: string;
86
- language: string;
87
- startLine: number;
88
- endLine: number;
89
- content: string;
90
- description: string;
91
- }[]>;
92
+ getChunks(offset: number, limit: number): Promise<ChunkSummary[]>;
92
93
  /**
93
94
  * Perform ANN search internally, returning results scored as cosine similarity (0-1).
94
95
  * This method is called by `search()` and handles the actual query logic.