brainbank 0.7.0 → 0.8.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.
Files changed (156) hide show
  1. package/README.md +76 -1398
  2. package/bin/brainbank +5 -1
  3. package/dist/{chunk-N2OJRXSB.js → chunk-3HVCONGF.js} +1 -1
  4. package/dist/{chunk-N2OJRXSB.js.map → chunk-3HVCONGF.js.map} +1 -1
  5. package/dist/{chunk-CCXVL56V.js → chunk-3JZIM5AU.js} +6 -3
  6. package/dist/chunk-3JZIM5AU.js.map +1 -0
  7. package/dist/{chunk-6XOXM7MI.js → chunk-5KU2PP34.js} +2 -2
  8. package/dist/{chunk-6XOXM7MI.js.map → chunk-5KU2PP34.js.map} +1 -1
  9. package/dist/chunk-7JDCHUJV.js +89 -0
  10. package/dist/chunk-7JDCHUJV.js.map +1 -0
  11. package/dist/{chunk-B77KABWH.js → chunk-7T2ZCZQA.js} +17 -15
  12. package/dist/chunk-7T2ZCZQA.js.map +1 -0
  13. package/dist/chunk-E3J37GDA.js +74 -0
  14. package/dist/chunk-E3J37GDA.js.map +1 -0
  15. package/dist/chunk-JEFWMS5Z.js +217 -0
  16. package/dist/chunk-JEFWMS5Z.js.map +1 -0
  17. package/dist/chunk-JRVYSTMP.js +3256 -0
  18. package/dist/chunk-JRVYSTMP.js.map +1 -0
  19. package/dist/chunk-OLDHLOMT.js +69 -0
  20. package/dist/chunk-OLDHLOMT.js.map +1 -0
  21. package/dist/{chunk-424UFCY7.js → chunk-QTZNB6AK.js} +6 -2
  22. package/dist/chunk-QTZNB6AK.js.map +1 -0
  23. package/dist/chunk-RFF7HMP6.js +109 -0
  24. package/dist/chunk-RFF7HMP6.js.map +1 -0
  25. package/dist/{chunk-ZNLN2VWV.js → chunk-TD3TEFI3.js} +1 -1
  26. package/dist/chunk-TD3TEFI3.js.map +1 -0
  27. package/dist/cli.js +1036 -341
  28. package/dist/cli.js.map +1 -1
  29. package/dist/haiku-expander-WOVJIVXD.js +8 -0
  30. package/dist/haiku-pruner-DB77ZQLJ.js +8 -0
  31. package/dist/http-server-GIRELCCL.js +9 -0
  32. package/dist/index.d.ts +1774 -611
  33. package/dist/index.js +282 -70
  34. package/dist/index.js.map +1 -1
  35. package/dist/{local-embedding-ZIMTK6PU.js → local-embedding-2RNCC5EU.js} +2 -2
  36. package/dist/{openai-embedding-VQZCZQYT.js → openai-embedding-Z5I4K4CN.js} +2 -2
  37. package/dist/perplexity-context-embedding-V5YUMXDR.js +9 -0
  38. package/dist/{perplexity-embedding-227WQY4R.js → perplexity-embedding-X2S72OAC.js} +2 -2
  39. package/dist/plugin-FF4Q34TI.js +32 -0
  40. package/dist/{qwen3-reranker-3MHEENT5.js → qwen3-reranker-HVIQOLKS.js} +2 -2
  41. package/dist/{resolve-CUJWY6HP.js → resolve-Q5D6HECY.js} +2 -2
  42. package/package.json +25 -52
  43. package/src/brainbank.ts +620 -0
  44. package/src/cli/commands/collection.ts +77 -0
  45. package/src/cli/commands/context.ts +171 -0
  46. package/src/cli/commands/daemon.ts +100 -0
  47. package/src/cli/commands/docs.ts +71 -0
  48. package/src/cli/commands/files.ts +69 -0
  49. package/src/cli/commands/help.ts +72 -0
  50. package/src/cli/commands/index.ts +282 -0
  51. package/src/cli/commands/kv.ts +140 -0
  52. package/src/cli/commands/mcp.ts +13 -0
  53. package/src/cli/commands/reembed.ts +30 -0
  54. package/src/cli/commands/scan.ts +365 -0
  55. package/src/cli/commands/search.ts +130 -0
  56. package/src/cli/commands/stats.ts +44 -0
  57. package/src/cli/commands/status.ts +47 -0
  58. package/src/cli/commands/watch.ts +43 -0
  59. package/src/cli/factory/brain-context.ts +43 -0
  60. package/src/cli/factory/builtin-registration.ts +123 -0
  61. package/src/cli/factory/config-loader.ts +72 -0
  62. package/src/cli/factory/index.ts +65 -0
  63. package/src/cli/factory/plugin-loader.ts +146 -0
  64. package/src/cli/index.ts +63 -0
  65. package/src/cli/server-client.ts +135 -0
  66. package/src/cli/utils.ts +121 -0
  67. package/src/config.ts +50 -0
  68. package/src/constants.ts +13 -0
  69. package/src/db/adapter.ts +112 -0
  70. package/src/db/metadata.ts +130 -0
  71. package/src/db/migrations.ts +66 -0
  72. package/src/db/sqlite-adapter.ts +208 -0
  73. package/src/db/tracker.ts +91 -0
  74. package/src/engine/index-api.ts +85 -0
  75. package/src/engine/reembed.ts +206 -0
  76. package/src/engine/search-api.ts +222 -0
  77. package/src/index.ts +159 -0
  78. package/src/lib/fts.ts +57 -0
  79. package/src/lib/languages.ts +180 -0
  80. package/src/lib/logger.ts +125 -0
  81. package/src/lib/math.ts +87 -0
  82. package/src/lib/provider-key.ts +20 -0
  83. package/src/lib/prune.ts +71 -0
  84. package/src/lib/rerank.ts +33 -0
  85. package/src/lib/rrf.ts +133 -0
  86. package/src/lib/write-lock.ts +108 -0
  87. package/src/plugin.ts +323 -0
  88. package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
  89. package/src/providers/embeddings/embedding-worker.ts +141 -0
  90. package/src/providers/embeddings/local-embedding.ts +115 -0
  91. package/src/providers/embeddings/openai-embedding.ts +167 -0
  92. package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
  93. package/src/providers/embeddings/perplexity-embedding.ts +165 -0
  94. package/src/providers/embeddings/resolve.ts +34 -0
  95. package/src/providers/pruners/haiku-expander.ts +152 -0
  96. package/src/providers/pruners/haiku-pruner.ts +112 -0
  97. package/src/providers/rerankers/qwen3-reranker.ts +180 -0
  98. package/src/providers/vector/hnsw-index.ts +174 -0
  99. package/src/providers/vector/hnsw-loader.ts +129 -0
  100. package/src/search/bm25-boost.ts +61 -0
  101. package/src/search/context-builder.ts +298 -0
  102. package/src/search/keyword/composite-bm25-search.ts +62 -0
  103. package/src/search/types.ts +35 -0
  104. package/src/search/vector/composite-vector-search.ts +76 -0
  105. package/src/search/vector/mmr.ts +64 -0
  106. package/src/services/collection.ts +405 -0
  107. package/src/services/daemon.ts +87 -0
  108. package/src/services/http-server.ts +288 -0
  109. package/src/services/kv-service.ts +65 -0
  110. package/src/services/plugin-registry.ts +109 -0
  111. package/src/services/watch.ts +348 -0
  112. package/src/services/webhook-server.ts +100 -0
  113. package/src/types.ts +504 -0
  114. package/dist/base-3SNc_CeY.d.ts +0 -593
  115. package/dist/chunk-424UFCY7.js.map +0 -1
  116. package/dist/chunk-7EZR47JV.js +0 -232
  117. package/dist/chunk-7EZR47JV.js.map +0 -1
  118. package/dist/chunk-B77KABWH.js.map +0 -1
  119. package/dist/chunk-CCXVL56V.js.map +0 -1
  120. package/dist/chunk-DI3H6JVZ.js +0 -2432
  121. package/dist/chunk-DI3H6JVZ.js.map +0 -1
  122. package/dist/chunk-FGL32LUJ.js +0 -754
  123. package/dist/chunk-FGL32LUJ.js.map +0 -1
  124. package/dist/chunk-JRSKWF6K.js +0 -313
  125. package/dist/chunk-JRSKWF6K.js.map +0 -1
  126. package/dist/chunk-U2Q2XGPZ.js +0 -42
  127. package/dist/chunk-U2Q2XGPZ.js.map +0 -1
  128. package/dist/chunk-VQ27YUHH.js +0 -629
  129. package/dist/chunk-VQ27YUHH.js.map +0 -1
  130. package/dist/chunk-VVXYZIIB.js +0 -304
  131. package/dist/chunk-VVXYZIIB.js.map +0 -1
  132. package/dist/chunk-YOLKSYWK.js +0 -79
  133. package/dist/chunk-YOLKSYWK.js.map +0 -1
  134. package/dist/chunk-ZNLN2VWV.js.map +0 -1
  135. package/dist/code.d.ts +0 -33
  136. package/dist/code.js +0 -9
  137. package/dist/docs.d.ts +0 -21
  138. package/dist/docs.js +0 -9
  139. package/dist/git.d.ts +0 -33
  140. package/dist/git.js +0 -9
  141. package/dist/memory.d.ts +0 -17
  142. package/dist/memory.js +0 -9
  143. package/dist/notes.d.ts +0 -17
  144. package/dist/notes.js +0 -10
  145. package/dist/perplexity-context-embedding-KSVSZXMD.js +0 -9
  146. package/dist/resolve-CUJWY6HP.js.map +0 -1
  147. /package/dist/{code.js.map → haiku-expander-WOVJIVXD.js.map} +0 -0
  148. /package/dist/{docs.js.map → haiku-pruner-DB77ZQLJ.js.map} +0 -0
  149. /package/dist/{git.js.map → http-server-GIRELCCL.js.map} +0 -0
  150. /package/dist/{local-embedding-ZIMTK6PU.js.map → local-embedding-2RNCC5EU.js.map} +0 -0
  151. /package/dist/{memory.js.map → openai-embedding-Z5I4K4CN.js.map} +0 -0
  152. /package/dist/{notes.js.map → perplexity-context-embedding-V5YUMXDR.js.map} +0 -0
  153. /package/dist/{openai-embedding-VQZCZQYT.js.map → perplexity-embedding-X2S72OAC.js.map} +0 -0
  154. /package/dist/{perplexity-context-embedding-KSVSZXMD.js.map → plugin-FF4Q34TI.js.map} +0 -0
  155. /package/dist/{perplexity-embedding-227WQY4R.js.map → qwen3-reranker-HVIQOLKS.js.map} +0 -0
  156. /package/dist/{qwen3-reranker-3MHEENT5.js.map → resolve-Q5D6HECY.js.map} +0 -0
@@ -1,629 +0,0 @@
1
- import {
2
- normalizeBM25,
3
- reciprocalRankFusion
4
- } from "./chunk-YOLKSYWK.js";
5
- import {
6
- __name
7
- } from "./chunk-7QVYU63E.js";
8
-
9
- // src/indexers/docs/docs-indexer.ts
10
- import * as fs from "fs";
11
- import * as path from "path";
12
- import { createHash } from "crypto";
13
- var BREAK_SCORES = [
14
- [/^# /, 100],
15
- // H1
16
- [/^## /, 90],
17
- // H2
18
- [/^### /, 80],
19
- // H3
20
- [/^#### /, 70],
21
- // H4
22
- [/^##### /, 60],
23
- // H5
24
- [/^###### /, 50],
25
- // H6
26
- [/^```/, 80],
27
- // Code fence
28
- [/^---$/, 60],
29
- // Horizontal rule
30
- [/^\*\*\*$/, 60],
31
- // Horizontal rule alt
32
- [/^$/, 20],
33
- // Blank line (paragraph break)
34
- [/^[-*+] /, 5]
35
- // List item
36
- ];
37
- var TARGET_CHARS = 3e3;
38
- var WINDOW_CHARS = 600;
39
- var MIN_CHUNK_CHARS = 200;
40
- var IGNORED_DOC_DIRS = /* @__PURE__ */ new Set([
41
- "node_modules",
42
- ".git",
43
- ".hg",
44
- ".svn",
45
- "dist",
46
- "build",
47
- "out",
48
- "coverage",
49
- ".next",
50
- "__pycache__",
51
- ".tox",
52
- ".venv",
53
- "venv",
54
- "vendor",
55
- "target",
56
- ".cache",
57
- ".turbo"
58
- ]);
59
- var DocsIndexer = class {
60
- constructor(_db, _embedding, _hnsw, _vecCache) {
61
- this._db = _db;
62
- this._embedding = _embedding;
63
- this._hnsw = _hnsw;
64
- this._vecCache = _vecCache;
65
- }
66
- static {
67
- __name(this, "DocsIndexer");
68
- }
69
- /**
70
- * Index all documents in a collection.
71
- * Incremental — skips unchanged files (by content hash).
72
- */
73
- async indexCollection(collection, dirPath, pattern = "**/*.md", options = {}) {
74
- const absDir = path.resolve(dirPath);
75
- if (!fs.existsSync(absDir)) {
76
- throw new Error(`Collection path does not exist: ${absDir}`);
77
- }
78
- const files = this._walkFiles(absDir, pattern, options.ignore);
79
- let indexed = 0, skipped = 0, totalChunks = 0;
80
- for (let i = 0; i < files.length; i++) {
81
- const relPath = files[i];
82
- options.onProgress?.(relPath, i + 1, files.length);
83
- const absPath = path.join(absDir, relPath);
84
- const content = fs.readFileSync(absPath, "utf-8");
85
- const hash = createHash("sha256").update(content).digest("hex").slice(0, 16);
86
- if (this._isUnchanged(collection, relPath, hash)) {
87
- skipped++;
88
- continue;
89
- }
90
- this._removeOldChunks(collection, relPath);
91
- const chunkCount = await this._indexFile(collection, relPath, content, hash);
92
- indexed++;
93
- totalChunks += chunkCount;
94
- }
95
- return { indexed, skipped, chunks: totalChunks };
96
- }
97
- /** Walk directory tree and collect matching files. */
98
- _walkFiles(absDir, pattern, ignore) {
99
- const patternExt = pattern.match(/\.([\w]+)$/)?.[1];
100
- const files = [];
101
- const walk = /* @__PURE__ */ __name((dir, base) => {
102
- let entries;
103
- try {
104
- entries = fs.readdirSync(dir, { withFileTypes: true });
105
- } catch {
106
- return;
107
- }
108
- for (const e of entries) {
109
- const rel = base ? `${base}/${e.name}` : e.name;
110
- if (e.isDirectory()) {
111
- if (IGNORED_DOC_DIRS.has(e.name)) continue;
112
- walk(path.join(dir, e.name), rel);
113
- } else if (e.isFile()) {
114
- if (this._isIgnoredFile(rel, ignore)) continue;
115
- const ext = path.extname(e.name).slice(1);
116
- if (!patternExt || ext === patternExt) files.push(rel);
117
- }
118
- }
119
- }, "walk");
120
- walk(absDir, "");
121
- return files;
122
- }
123
- /** Check if a file matches any ignore patterns. */
124
- _isIgnoredFile(relPath, ignore) {
125
- if (!ignore) return false;
126
- return ignore.some((ig) => {
127
- const regex = ig.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "{{STAR}}").replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\{\{GLOBSTAR\}\}/g, ".*").replace(/\{\{STAR\}\}/g, "[^/]*");
128
- return new RegExp(regex).test(relPath);
129
- });
130
- }
131
- /** Check if all chunks for a file match the current hash and have vectors. */
132
- _isUnchanged(collection, relPath, hash) {
133
- const existing = this._db.prepare(
134
- `SELECT dc.id, dc.content_hash, dv.chunk_id AS has_vector
135
- FROM doc_chunks dc
136
- LEFT JOIN doc_vectors dv ON dv.chunk_id = dc.id
137
- WHERE dc.collection = ? AND dc.file_path = ?`
138
- ).all(collection, relPath);
139
- return existing.length > 0 && existing.every((c) => c.content_hash === hash && c.has_vector != null);
140
- }
141
- /** Remove old chunks and their HNSW vectors for a file. */
142
- _removeOldChunks(collection, relPath) {
143
- const oldChunks = this._db.prepare(
144
- "SELECT id FROM doc_chunks WHERE collection = ? AND file_path = ?"
145
- ).all(collection, relPath);
146
- for (const old of oldChunks) {
147
- this._hnsw.remove(old.id);
148
- this._vecCache.delete(old.id);
149
- }
150
- this._db.prepare(
151
- "DELETE FROM doc_chunks WHERE collection = ? AND file_path = ?"
152
- ).run(collection, relPath);
153
- }
154
- /** Index a single file: chunk, embed, store in DB + HNSW. */
155
- async _indexFile(collection, relPath, content, hash) {
156
- const title = this._extractTitle(content, relPath);
157
- const chunks = this._smartChunk(content);
158
- const insertChunk = this._db.prepare(`
159
- INSERT INTO doc_chunks (collection, file_path, title, content, seq, pos, content_hash)
160
- VALUES (?, ?, ?, ?, ?, ?, ?)
161
- `);
162
- const chunkIds = [];
163
- this._db.transaction(() => {
164
- for (let seq = 0; seq < chunks.length; seq++) {
165
- const result = insertChunk.run(
166
- collection,
167
- relPath,
168
- title,
169
- chunks[seq].text,
170
- seq,
171
- chunks[seq].pos,
172
- hash
173
- );
174
- chunkIds.push(Number(result.lastInsertRowid));
175
- }
176
- });
177
- const texts = chunks.map((c) => `title: ${title} | text: ${c.text}`);
178
- const embeddings = await this._embedding.embedBatch(texts);
179
- const insertVec = this._db.prepare(
180
- "INSERT OR REPLACE INTO doc_vectors (chunk_id, embedding) VALUES (?, ?)"
181
- );
182
- this._db.transaction(() => {
183
- for (let j = 0; j < chunkIds.length; j++) {
184
- insertVec.run(chunkIds[j], Buffer.from(embeddings[j].buffer));
185
- }
186
- });
187
- for (let j = 0; j < chunkIds.length; j++) {
188
- this._hnsw.add(embeddings[j], chunkIds[j]);
189
- this._vecCache.set(chunkIds[j], embeddings[j]);
190
- }
191
- return chunks.length;
192
- }
193
- /** Remove all indexed data for a collection. */
194
- removeCollection(collection) {
195
- const chunks = this._db.prepare(
196
- "SELECT id FROM doc_chunks WHERE collection = ?"
197
- ).all(collection);
198
- for (const chunk of chunks) {
199
- this._hnsw.remove(chunk.id);
200
- this._vecCache.delete(chunk.id);
201
- }
202
- this._db.prepare("DELETE FROM doc_chunks WHERE collection = ?").run(collection);
203
- this._db.prepare("DELETE FROM collections WHERE name = ?").run(collection);
204
- this._db.prepare("DELETE FROM path_contexts WHERE collection = ?").run(collection);
205
- }
206
- // ── Smart Chunking ──────────────────────────────
207
- /** Split document into chunks at natural markdown boundaries. */
208
- _smartChunk(text) {
209
- if (text.length <= TARGET_CHARS) {
210
- return [{ text, pos: 0 }];
211
- }
212
- const lines = text.split("\n");
213
- const breakPoints = this._findBreakPoints(lines);
214
- const chunks = [];
215
- let chunkStart = 0;
216
- while (chunkStart < text.length) {
217
- const remaining = text.length - chunkStart;
218
- if (remaining <= TARGET_CHARS + WINDOW_CHARS) {
219
- this._flushRemainder(text, chunkStart, chunks);
220
- break;
221
- }
222
- const bestBreak = this._findBestBreak(chunkStart, breakPoints);
223
- const chunkText = text.slice(chunkStart, bestBreak).trim();
224
- if (chunkText.length >= MIN_CHUNK_CHARS) {
225
- chunks.push({ text: chunkText, pos: chunkStart });
226
- }
227
- chunkStart = bestBreak;
228
- }
229
- return chunks;
230
- }
231
- /** Handle the last chunk: merge if too small, otherwise push. */
232
- _flushRemainder(text, chunkStart, chunks) {
233
- const lastText = text.slice(chunkStart).trim();
234
- if (lastText.length >= MIN_CHUNK_CHARS) {
235
- chunks.push({ text: lastText, pos: chunkStart });
236
- } else if (chunks.length > 0) {
237
- chunks[chunks.length - 1].text += "\n" + lastText;
238
- } else {
239
- chunks.push({ text: lastText, pos: chunkStart });
240
- }
241
- }
242
- /** Find the best break position within the target window. */
243
- _findBestBreak(chunkStart, breakPoints) {
244
- const targetEnd = chunkStart + TARGET_CHARS;
245
- const windowStart = targetEnd - WINDOW_CHARS;
246
- let bestBreak = targetEnd;
247
- let bestScore = 0;
248
- for (const bp of breakPoints) {
249
- if (bp.pos <= chunkStart) continue;
250
- if (bp.pos > targetEnd + WINDOW_CHARS / 2) break;
251
- if (bp.pos < windowStart) continue;
252
- const distance = Math.abs(bp.pos - targetEnd);
253
- const decay = 1 - (distance / WINDOW_CHARS) ** 2 * 0.7;
254
- const finalScore = bp.score * decay;
255
- if (finalScore > bestScore) {
256
- bestScore = finalScore;
257
- bestBreak = bp.pos;
258
- }
259
- }
260
- return bestBreak;
261
- }
262
- /** Find all potential break points in the document with scores. */
263
- _findBreakPoints(lines) {
264
- const points = [];
265
- let charPos = 0;
266
- let inCodeBlock = false;
267
- for (const line of lines) {
268
- if (line.trimStart().startsWith("```")) {
269
- inCodeBlock = !inCodeBlock;
270
- if (!inCodeBlock) {
271
- points.push({ pos: charPos + line.length + 1, score: 80 });
272
- }
273
- charPos += line.length + 1;
274
- continue;
275
- }
276
- if (inCodeBlock) {
277
- charPos += line.length + 1;
278
- continue;
279
- }
280
- for (const [pattern, score] of BREAK_SCORES) {
281
- if (pattern.test(line.trim())) {
282
- points.push({ pos: charPos, score });
283
- break;
284
- }
285
- }
286
- charPos += line.length + 1;
287
- }
288
- return points;
289
- }
290
- /** Extract document title from first heading or filename. */
291
- _extractTitle(content, filePath) {
292
- const match = content.match(/^#{1,3}\s+(.+)$/m);
293
- if (match) return match[1].trim();
294
- return path.basename(filePath, path.extname(filePath));
295
- }
296
- };
297
-
298
- // src/search/vector/rerank.ts
299
- async function rerank(query, results, reranker) {
300
- const documents = results.map((r) => r.content);
301
- const scores = await reranker.rank(query, documents);
302
- const blended = results.map((r, i) => {
303
- const pos = i + 1;
304
- const rrfWeight = pos <= 3 ? 0.75 : pos <= 10 ? 0.6 : 0.4;
305
- return {
306
- ...r,
307
- score: rrfWeight * r.score + (1 - rrfWeight) * (scores[i] ?? 0)
308
- };
309
- });
310
- return blended.sort((a, b) => b.score - a.score);
311
- }
312
- __name(rerank, "rerank");
313
-
314
- // src/indexers/docs/document-search.ts
315
- var DocumentSearch = class {
316
- constructor(_d) {
317
- this._d = _d;
318
- }
319
- static {
320
- __name(this, "DocumentSearch");
321
- }
322
- /** Hybrid search (vector + BM25 → RRF), with dedup and optional reranking. */
323
- async search(query, options) {
324
- const k = options?.k ?? 8;
325
- const mode = options?.mode ?? "hybrid";
326
- const minScore = options?.minScore ?? 0;
327
- if (mode === "keyword") return this._dedup(this._searchBM25(query, k * 2, minScore, options?.collection), k);
328
- if (mode === "vector") return this._dedup(await this._searchVector(query, k * 2, minScore, options?.collection), k);
329
- const fetchK = k * 2;
330
- const [vecHits, bm25Hits] = await Promise.all([
331
- this._searchVector(query, fetchK, 0, options?.collection),
332
- Promise.resolve(this._searchBM25(query, fetchK, 0, options?.collection))
333
- ]);
334
- if (vecHits.length === 0 && bm25Hits.length === 0) return [];
335
- if (bm25Hits.length === 0) return this._dedup(vecHits.filter((h) => h.score >= minScore), k);
336
- if (vecHits.length === 0) return this._dedup(bm25Hits.filter((h) => h.score >= minScore), k);
337
- const fused = reciprocalRankFusion([vecHits, bm25Hits]);
338
- const allById = /* @__PURE__ */ new Map();
339
- for (const h of [...vecHits, ...bm25Hits]) {
340
- const id = h.metadata?.chunkId;
341
- if (id != null) allById.set(id, h);
342
- }
343
- const results = [];
344
- for (const r of fused) {
345
- const chunkId = r.metadata?.chunkId;
346
- const original = allById.get(chunkId);
347
- if (!original) continue;
348
- const merged = { ...original, score: r.score };
349
- if (merged.score >= minScore) results.push(merged);
350
- }
351
- const deduped = this._dedup(results, k);
352
- return this._rerankResults(query, deduped);
353
- }
354
- /** Apply reranking if a reranker is configured. */
355
- async _rerankResults(query, results) {
356
- if (!this._d.reranker || results.length <= 1) return results;
357
- return rerank(query, results, this._d.reranker);
358
- }
359
- /** Deduplicate results by file path — keep best-scoring chunk per file. */
360
- _dedup(results, k) {
361
- const seen = /* @__PURE__ */ new Map();
362
- for (const r of results) {
363
- const key = r.filePath ?? "";
364
- if (!seen.has(key) || seen.get(key).score < r.score) {
365
- seen.set(key, r);
366
- }
367
- }
368
- return [...seen.values()].sort((a, b) => b.score - a.score).slice(0, k);
369
- }
370
- /** Vector-only search via HNSW. */
371
- async _searchVector(query, k, minScore, collection) {
372
- if (this._d.hnsw.size === 0) return [];
373
- const queryVec = await this._d.embedding.embed(query);
374
- let searchK = k;
375
- if (collection && this._d.hnsw.size > 0) {
376
- const collectionCount = this._d.db.prepare(
377
- "SELECT COUNT(*) as c FROM doc_chunks WHERE collection = ?"
378
- ).get(collection)?.c ?? 0;
379
- const totalChunks = this._d.db.prepare(
380
- "SELECT COUNT(*) as c FROM doc_chunks"
381
- ).get()?.c ?? 1;
382
- const ratio = collectionCount > 0 ? Math.max(3, Math.min(50, Math.ceil(totalChunks / collectionCount))) : 3;
383
- searchK = Math.min(k * ratio, this._d.hnsw.size);
384
- }
385
- const hits = this._d.hnsw.search(queryVec, searchK);
386
- const results = [];
387
- for (const hit of hits) {
388
- if (minScore && hit.score < minScore) continue;
389
- const chunk = this._d.db.prepare("SELECT * FROM doc_chunks WHERE id = ?").get(hit.id);
390
- if (!chunk) continue;
391
- if (collection && chunk.collection !== collection) continue;
392
- results.push({
393
- type: "document",
394
- score: hit.score,
395
- filePath: chunk.file_path,
396
- content: chunk.content,
397
- context: this._getDocContext(chunk.collection, chunk.file_path),
398
- metadata: {
399
- collection: chunk.collection,
400
- title: chunk.title,
401
- seq: chunk.seq,
402
- chunkId: chunk.id
403
- }
404
- });
405
- if (results.length >= k) break;
406
- }
407
- return results;
408
- }
409
- /** BM25 keyword search via FTS5 (OR-mode for natural language). */
410
- _searchBM25(query, k, minScore, collection) {
411
- const ftsQuery = this._buildDocsFTS(query);
412
- if (!ftsQuery) return [];
413
- try {
414
- const collectionFilter = collection ? "AND d.collection = ?" : "";
415
- const params = [ftsQuery];
416
- if (collection) params.push(collection);
417
- params.push(k * 2);
418
- const rows = this._d.db.prepare(`
419
- SELECT d.*, bm25(fts_docs, 10.0, 2.0, 5.0, 1.0) AS bm25_score
420
- FROM fts_docs f
421
- JOIN doc_chunks d ON d.id = f.rowid
422
- WHERE fts_docs MATCH ? ${collectionFilter}
423
- ORDER BY bm25_score ASC
424
- LIMIT ?
425
- `).all(...params);
426
- return rows.map((r) => ({
427
- type: "document",
428
- score: normalizeBM25(r.bm25_score),
429
- filePath: r.file_path,
430
- content: r.content,
431
- context: this._getDocContext(r.collection, r.file_path),
432
- metadata: {
433
- collection: r.collection,
434
- title: r.title,
435
- seq: r.seq,
436
- chunkId: r.id
437
- }
438
- })).filter((r) => r.score >= minScore).slice(0, k);
439
- } catch {
440
- return [];
441
- }
442
- }
443
- /** Build OR-mode FTS5 query for natural language doc search. */
444
- _buildDocsFTS(query) {
445
- const STOP_WORDS = /* @__PURE__ */ new Set([
446
- "the",
447
- "is",
448
- "at",
449
- "which",
450
- "on",
451
- "a",
452
- "an",
453
- "and",
454
- "or",
455
- "but",
456
- "in",
457
- "with",
458
- "to",
459
- "for",
460
- "of",
461
- "by",
462
- "from",
463
- "as",
464
- "it",
465
- "its",
466
- "this",
467
- "that",
468
- "be",
469
- "are",
470
- "was",
471
- "were",
472
- "been",
473
- "has",
474
- "have",
475
- "had",
476
- "do",
477
- "does",
478
- "did",
479
- "can",
480
- "could",
481
- "will",
482
- "would",
483
- "how",
484
- "what",
485
- "when",
486
- "where",
487
- "who",
488
- "why",
489
- "not",
490
- "no",
491
- "so",
492
- "if"
493
- ]);
494
- const clean = query.replace(/[{}[\]()^~*:"]/g, " ").replace(/\bAND\b|\bOR\b|\bNOT\b|\bNEAR\b/gi, "").replace(/[_\-./\\]/g, " ").trim();
495
- const words = clean.split(/\s+/).filter((w) => w.length >= 3 && !STOP_WORDS.has(w.toLowerCase()));
496
- if (words.length === 0) return "";
497
- return words.map((w) => `"${w}"`).join(" OR ");
498
- }
499
- /** Resolve context for a document (checks path_contexts tree → collection context). */
500
- _getDocContext(collection, filePath) {
501
- const parts = filePath.split("/");
502
- for (let i = parts.length; i >= 0; i--) {
503
- const checkPath = i === 0 ? "/" : "/" + parts.slice(0, i).join("/");
504
- const ctx = this._d.db.prepare(
505
- "SELECT context FROM path_contexts WHERE collection = ? AND path = ?"
506
- ).get(collection, checkPath);
507
- if (ctx) return ctx.context;
508
- }
509
- const coll = this._d.db.prepare(
510
- "SELECT context FROM collections WHERE name = ?"
511
- ).get(collection);
512
- return coll?.context ?? void 0;
513
- }
514
- };
515
-
516
- // src/indexers/docs/docs-plugin.ts
517
- var DocsPlugin = class {
518
- constructor(opts = {}) {
519
- this.opts = opts;
520
- }
521
- static {
522
- __name(this, "DocsPlugin");
523
- }
524
- name = "docs";
525
- hnsw;
526
- indexer;
527
- vecCache = /* @__PURE__ */ new Map();
528
- _db;
529
- _search;
530
- async initialize(ctx) {
531
- this._db = ctx.db;
532
- const embedding = this.opts.embeddingProvider ?? ctx.embedding;
533
- this.hnsw = await ctx.createHnsw(void 0, embedding.dims);
534
- ctx.loadVectors("doc_vectors", "chunk_id", this.hnsw, this.vecCache);
535
- this.indexer = new DocsIndexer(ctx.db, embedding, this.hnsw, this.vecCache);
536
- this._search = new DocumentSearch({
537
- db: ctx.db,
538
- embedding,
539
- hnsw: this.hnsw,
540
- vecCache: this.vecCache,
541
- reranker: ctx.config.reranker
542
- });
543
- }
544
- /** Register a document collection. */
545
- addCollection(collection) {
546
- this._db.prepare(`
547
- INSERT OR REPLACE INTO collections (name, path, pattern, ignore_json, context)
548
- VALUES (?, ?, ?, ?, ?)
549
- `).run(
550
- collection.name,
551
- collection.path,
552
- collection.pattern ?? "**/*.md",
553
- JSON.stringify(collection.ignore ?? []),
554
- collection.context ?? null
555
- );
556
- }
557
- /** Remove a collection and its indexed data. */
558
- removeCollection(name) {
559
- this.indexer.removeCollection(name);
560
- }
561
- /** List all registered collections. */
562
- listCollections() {
563
- return this._db.prepare("SELECT * FROM collections").all().map((row) => ({
564
- name: row.name,
565
- path: row.path,
566
- pattern: row.pattern,
567
- ignore: JSON.parse(row.ignore_json),
568
- context: row.context
569
- }));
570
- }
571
- /** Index all (or specific) collections. Incremental. */
572
- async indexCollections(options = {}) {
573
- const allCollections = this.listCollections();
574
- const toIndex = options.collections ? allCollections.filter((c) => options.collections.includes(c.name)) : allCollections;
575
- const results = {};
576
- for (const coll of toIndex) {
577
- results[coll.name] = await this.indexer.indexCollection(
578
- coll.name,
579
- coll.path,
580
- coll.pattern,
581
- {
582
- ignore: coll.ignore,
583
- onProgress: /* @__PURE__ */ __name((file, cur, total) => options.onProgress?.(coll.name, file, cur, total), "onProgress")
584
- }
585
- );
586
- }
587
- return results;
588
- }
589
- /** Search documents using hybrid search (vector + BM25 → RRF). */
590
- async search(query, options) {
591
- return this._search.search(query, options);
592
- }
593
- /** Add context description for a document path. */
594
- addContext(collection, path2, context) {
595
- this._db.prepare(`
596
- INSERT OR REPLACE INTO path_contexts (collection, path, context)
597
- VALUES (?, ?, ?)
598
- `).run(collection, path2, context);
599
- }
600
- /** Remove context for a path. */
601
- removeContext(collection, path2) {
602
- this._db.prepare(
603
- "DELETE FROM path_contexts WHERE collection = ? AND path = ?"
604
- ).run(collection, path2);
605
- }
606
- /** List all context entries. */
607
- listContexts() {
608
- return this._db.prepare("SELECT * FROM path_contexts").all();
609
- }
610
- stats() {
611
- return {
612
- collections: this._db.prepare("SELECT COUNT(*) as c FROM collections").get().c,
613
- documents: this._db.prepare("SELECT COUNT(DISTINCT file_path) as c FROM doc_chunks").get().c,
614
- chunks: this._db.prepare("SELECT COUNT(*) as c FROM doc_chunks").get().c,
615
- hnswSize: this.hnsw.size
616
- };
617
- }
618
- };
619
- function docs(opts) {
620
- return new DocsPlugin(opts);
621
- }
622
- __name(docs, "docs");
623
-
624
- export {
625
- rerank,
626
- DocsIndexer,
627
- docs
628
- };
629
- //# sourceMappingURL=chunk-VQ27YUHH.js.map