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.
- package/README.md +76 -1398
- package/bin/brainbank +5 -1
- package/dist/{chunk-N2OJRXSB.js → chunk-3HVCONGF.js} +1 -1
- package/dist/{chunk-N2OJRXSB.js.map → chunk-3HVCONGF.js.map} +1 -1
- package/dist/{chunk-CCXVL56V.js → chunk-3JZIM5AU.js} +6 -3
- package/dist/chunk-3JZIM5AU.js.map +1 -0
- package/dist/{chunk-6XOXM7MI.js → chunk-5KU2PP34.js} +2 -2
- package/dist/{chunk-6XOXM7MI.js.map → chunk-5KU2PP34.js.map} +1 -1
- package/dist/chunk-7JDCHUJV.js +89 -0
- package/dist/chunk-7JDCHUJV.js.map +1 -0
- package/dist/{chunk-B77KABWH.js → chunk-7T2ZCZQA.js} +17 -15
- package/dist/chunk-7T2ZCZQA.js.map +1 -0
- package/dist/chunk-E3J37GDA.js +74 -0
- package/dist/chunk-E3J37GDA.js.map +1 -0
- package/dist/chunk-JEFWMS5Z.js +217 -0
- package/dist/chunk-JEFWMS5Z.js.map +1 -0
- package/dist/chunk-JRVYSTMP.js +3256 -0
- package/dist/chunk-JRVYSTMP.js.map +1 -0
- package/dist/chunk-OLDHLOMT.js +69 -0
- package/dist/chunk-OLDHLOMT.js.map +1 -0
- package/dist/{chunk-424UFCY7.js → chunk-QTZNB6AK.js} +6 -2
- package/dist/chunk-QTZNB6AK.js.map +1 -0
- package/dist/chunk-RFF7HMP6.js +109 -0
- package/dist/chunk-RFF7HMP6.js.map +1 -0
- package/dist/{chunk-ZNLN2VWV.js → chunk-TD3TEFI3.js} +1 -1
- package/dist/chunk-TD3TEFI3.js.map +1 -0
- package/dist/cli.js +1036 -341
- package/dist/cli.js.map +1 -1
- package/dist/haiku-expander-WOVJIVXD.js +8 -0
- package/dist/haiku-pruner-DB77ZQLJ.js +8 -0
- package/dist/http-server-GIRELCCL.js +9 -0
- package/dist/index.d.ts +1774 -611
- package/dist/index.js +282 -70
- package/dist/index.js.map +1 -1
- package/dist/{local-embedding-ZIMTK6PU.js → local-embedding-2RNCC5EU.js} +2 -2
- package/dist/{openai-embedding-VQZCZQYT.js → openai-embedding-Z5I4K4CN.js} +2 -2
- package/dist/perplexity-context-embedding-V5YUMXDR.js +9 -0
- package/dist/{perplexity-embedding-227WQY4R.js → perplexity-embedding-X2S72OAC.js} +2 -2
- package/dist/plugin-FF4Q34TI.js +32 -0
- package/dist/{qwen3-reranker-3MHEENT5.js → qwen3-reranker-HVIQOLKS.js} +2 -2
- package/dist/{resolve-CUJWY6HP.js → resolve-Q5D6HECY.js} +2 -2
- package/package.json +25 -52
- package/src/brainbank.ts +620 -0
- package/src/cli/commands/collection.ts +77 -0
- package/src/cli/commands/context.ts +171 -0
- package/src/cli/commands/daemon.ts +100 -0
- package/src/cli/commands/docs.ts +71 -0
- package/src/cli/commands/files.ts +69 -0
- package/src/cli/commands/help.ts +72 -0
- package/src/cli/commands/index.ts +282 -0
- package/src/cli/commands/kv.ts +140 -0
- package/src/cli/commands/mcp.ts +13 -0
- package/src/cli/commands/reembed.ts +30 -0
- package/src/cli/commands/scan.ts +365 -0
- package/src/cli/commands/search.ts +130 -0
- package/src/cli/commands/stats.ts +44 -0
- package/src/cli/commands/status.ts +47 -0
- package/src/cli/commands/watch.ts +43 -0
- package/src/cli/factory/brain-context.ts +43 -0
- package/src/cli/factory/builtin-registration.ts +123 -0
- package/src/cli/factory/config-loader.ts +72 -0
- package/src/cli/factory/index.ts +65 -0
- package/src/cli/factory/plugin-loader.ts +146 -0
- package/src/cli/index.ts +63 -0
- package/src/cli/server-client.ts +135 -0
- package/src/cli/utils.ts +121 -0
- package/src/config.ts +50 -0
- package/src/constants.ts +13 -0
- package/src/db/adapter.ts +112 -0
- package/src/db/metadata.ts +130 -0
- package/src/db/migrations.ts +66 -0
- package/src/db/sqlite-adapter.ts +208 -0
- package/src/db/tracker.ts +91 -0
- package/src/engine/index-api.ts +85 -0
- package/src/engine/reembed.ts +206 -0
- package/src/engine/search-api.ts +222 -0
- package/src/index.ts +159 -0
- package/src/lib/fts.ts +57 -0
- package/src/lib/languages.ts +180 -0
- package/src/lib/logger.ts +125 -0
- package/src/lib/math.ts +87 -0
- package/src/lib/provider-key.ts +20 -0
- package/src/lib/prune.ts +71 -0
- package/src/lib/rerank.ts +33 -0
- package/src/lib/rrf.ts +133 -0
- package/src/lib/write-lock.ts +108 -0
- package/src/plugin.ts +323 -0
- package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
- package/src/providers/embeddings/embedding-worker.ts +141 -0
- package/src/providers/embeddings/local-embedding.ts +115 -0
- package/src/providers/embeddings/openai-embedding.ts +167 -0
- package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
- package/src/providers/embeddings/perplexity-embedding.ts +165 -0
- package/src/providers/embeddings/resolve.ts +34 -0
- package/src/providers/pruners/haiku-expander.ts +152 -0
- package/src/providers/pruners/haiku-pruner.ts +112 -0
- package/src/providers/rerankers/qwen3-reranker.ts +180 -0
- package/src/providers/vector/hnsw-index.ts +174 -0
- package/src/providers/vector/hnsw-loader.ts +129 -0
- package/src/search/bm25-boost.ts +61 -0
- package/src/search/context-builder.ts +298 -0
- package/src/search/keyword/composite-bm25-search.ts +62 -0
- package/src/search/types.ts +35 -0
- package/src/search/vector/composite-vector-search.ts +76 -0
- package/src/search/vector/mmr.ts +64 -0
- package/src/services/collection.ts +405 -0
- package/src/services/daemon.ts +87 -0
- package/src/services/http-server.ts +288 -0
- package/src/services/kv-service.ts +65 -0
- package/src/services/plugin-registry.ts +109 -0
- package/src/services/watch.ts +348 -0
- package/src/services/webhook-server.ts +100 -0
- package/src/types.ts +504 -0
- package/dist/base-3SNc_CeY.d.ts +0 -593
- package/dist/chunk-424UFCY7.js.map +0 -1
- package/dist/chunk-7EZR47JV.js +0 -232
- package/dist/chunk-7EZR47JV.js.map +0 -1
- package/dist/chunk-B77KABWH.js.map +0 -1
- package/dist/chunk-CCXVL56V.js.map +0 -1
- package/dist/chunk-DI3H6JVZ.js +0 -2432
- package/dist/chunk-DI3H6JVZ.js.map +0 -1
- package/dist/chunk-FGL32LUJ.js +0 -754
- package/dist/chunk-FGL32LUJ.js.map +0 -1
- package/dist/chunk-JRSKWF6K.js +0 -313
- package/dist/chunk-JRSKWF6K.js.map +0 -1
- package/dist/chunk-U2Q2XGPZ.js +0 -42
- package/dist/chunk-U2Q2XGPZ.js.map +0 -1
- package/dist/chunk-VQ27YUHH.js +0 -629
- package/dist/chunk-VQ27YUHH.js.map +0 -1
- package/dist/chunk-VVXYZIIB.js +0 -304
- package/dist/chunk-VVXYZIIB.js.map +0 -1
- package/dist/chunk-YOLKSYWK.js +0 -79
- package/dist/chunk-YOLKSYWK.js.map +0 -1
- package/dist/chunk-ZNLN2VWV.js.map +0 -1
- package/dist/code.d.ts +0 -33
- package/dist/code.js +0 -9
- package/dist/docs.d.ts +0 -21
- package/dist/docs.js +0 -9
- package/dist/git.d.ts +0 -33
- package/dist/git.js +0 -9
- package/dist/memory.d.ts +0 -17
- package/dist/memory.js +0 -9
- package/dist/notes.d.ts +0 -17
- package/dist/notes.js +0 -10
- package/dist/perplexity-context-embedding-KSVSZXMD.js +0 -9
- package/dist/resolve-CUJWY6HP.js.map +0 -1
- /package/dist/{code.js.map → haiku-expander-WOVJIVXD.js.map} +0 -0
- /package/dist/{docs.js.map → haiku-pruner-DB77ZQLJ.js.map} +0 -0
- /package/dist/{git.js.map → http-server-GIRELCCL.js.map} +0 -0
- /package/dist/{local-embedding-ZIMTK6PU.js.map → local-embedding-2RNCC5EU.js.map} +0 -0
- /package/dist/{memory.js.map → openai-embedding-Z5I4K4CN.js.map} +0 -0
- /package/dist/{notes.js.map → perplexity-context-embedding-V5YUMXDR.js.map} +0 -0
- /package/dist/{openai-embedding-VQZCZQYT.js.map → perplexity-embedding-X2S72OAC.js.map} +0 -0
- /package/dist/{perplexity-context-embedding-KSVSZXMD.js.map → plugin-FF4Q34TI.js.map} +0 -0
- /package/dist/{perplexity-embedding-227WQY4R.js.map → qwen3-reranker-HVIQOLKS.js.map} +0 -0
- /package/dist/{qwen3-reranker-3MHEENT5.js.map → resolve-Q5D6HECY.js.map} +0 -0
package/dist/chunk-VQ27YUHH.js
DELETED
|
@@ -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
|