brainbank 0.1.1 → 0.2.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 +149 -16
- package/dist/{types-Da_zLLOl.d.ts → base-9vfWRHCV.d.ts} +131 -31
- package/dist/{chunk-YGSEUWLV.js → chunk-6MFTQV3O.js} +911 -674
- package/dist/chunk-6MFTQV3O.js.map +1 -0
- package/dist/chunk-7JCEW7LT.js +266 -0
- package/dist/chunk-7JCEW7LT.js.map +1 -0
- package/dist/{chunk-GOUBW7UA.js → chunk-F6SJ3U4H.js} +98 -34
- package/dist/chunk-F6SJ3U4H.js.map +1 -0
- package/dist/{chunk-MJ3Y24H6.js → chunk-FJJY4H2Y.js} +11 -11
- package/dist/chunk-FJJY4H2Y.js.map +1 -0
- package/dist/{chunk-3GAIDXRW.js → chunk-GUT5MSJT.js} +5 -11
- package/dist/chunk-GUT5MSJT.js.map +1 -0
- package/dist/{chunk-2P3EGY6S.js → chunk-QNHBCOKB.js} +2 -2
- package/dist/chunk-QNHBCOKB.js.map +1 -0
- package/dist/{chunk-4ZKBQ33J.js → chunk-V4UJKXPK.js} +23 -5
- package/dist/chunk-V4UJKXPK.js.map +1 -0
- package/dist/chunk-WR4WXKJT.js +723 -0
- package/dist/chunk-WR4WXKJT.js.map +1 -0
- package/dist/{chunk-Z5SU54HP.js → chunk-X6645UVR.js} +3 -3
- package/dist/chunk-X6645UVR.js.map +1 -0
- package/dist/cli.js +150 -100
- package/dist/cli.js.map +1 -1
- package/dist/code.d.ts +5 -5
- package/dist/code.js +1 -1
- package/dist/docs.d.ts +4 -6
- package/dist/docs.js +1 -1
- package/dist/git.d.ts +5 -5
- package/dist/git.js +1 -1
- package/dist/index.d.ts +95 -104
- package/dist/index.js +13 -13
- package/dist/memory.d.ts +5 -7
- package/dist/memory.js +9 -12
- package/dist/memory.js.map +1 -1
- package/dist/notes.d.ts +4 -6
- package/dist/notes.js +7 -10
- package/dist/notes.js.map +1 -1
- package/dist/{openai-PCTYLOWI.js → openai-CYDMYX7X.js} +2 -2
- package/package.json +24 -4
- package/dist/chunk-2P3EGY6S.js.map +0 -1
- package/dist/chunk-3GAIDXRW.js.map +0 -1
- package/dist/chunk-4ZKBQ33J.js.map +0 -1
- package/dist/chunk-EDKSKLX4.js +0 -490
- package/dist/chunk-EDKSKLX4.js.map +0 -1
- package/dist/chunk-GOUBW7UA.js.map +0 -1
- package/dist/chunk-MJ3Y24H6.js.map +0 -1
- package/dist/chunk-N6ZMBFDE.js +0 -224
- package/dist/chunk-N6ZMBFDE.js.map +0 -1
- package/dist/chunk-YGSEUWLV.js.map +0 -1
- package/dist/chunk-Z5SU54HP.js.map +0 -1
- /package/dist/{openai-PCTYLOWI.js.map → openai-CYDMYX7X.js.map} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
normalizeBM25,
|
|
3
|
+
reciprocalRankFusion,
|
|
4
|
+
sanitizeFTS
|
|
5
|
+
} from "./chunk-V4UJKXPK.js";
|
|
4
6
|
import {
|
|
5
7
|
__name
|
|
6
8
|
} from "./chunk-7QVYU63E.js";
|
|
7
9
|
|
|
8
|
-
// src/
|
|
10
|
+
// src/indexers/notes/note-store.ts
|
|
9
11
|
var NoteStore = class {
|
|
10
12
|
static {
|
|
11
13
|
__name(this, "NoteStore");
|
|
@@ -70,8 +72,8 @@ ${patterns.join(". ")}`;
|
|
|
70
72
|
]);
|
|
71
73
|
const fusedResults = reciprocalRankFusion(
|
|
72
74
|
[
|
|
73
|
-
vectorHits.map((m) => ({ type: "
|
|
74
|
-
bm25Hits.map((m) => ({ type: "
|
|
75
|
+
vectorHits.map((m) => ({ type: "collection", score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),
|
|
76
|
+
bm25Hits.map((m) => ({ type: "collection", score: m.score ?? 0, content: m.summary, metadata: { id: m.id } }))
|
|
75
77
|
]
|
|
76
78
|
);
|
|
77
79
|
const allById = /* @__PURE__ */ new Map();
|
|
@@ -142,10 +144,8 @@ ${patterns.join(". ")}`;
|
|
|
142
144
|
}));
|
|
143
145
|
}
|
|
144
146
|
_searchBM25(query, k) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
if (words.length === 0) return [];
|
|
148
|
-
const ftsQuery = words.map((w) => `"${w}"`).join(" ");
|
|
147
|
+
const ftsQuery = sanitizeFTS(query);
|
|
148
|
+
if (!ftsQuery) return [];
|
|
149
149
|
try {
|
|
150
150
|
const rows = this._db.prepare(`
|
|
151
151
|
SELECT m.*, bm25(fts_notes, 5.0, 3.0, 2.0, 2.0, 1.0) AS score
|
|
@@ -157,7 +157,7 @@ ${patterns.join(". ")}`;
|
|
|
157
157
|
`).all(ftsQuery, k);
|
|
158
158
|
return rows.map((r) => ({
|
|
159
159
|
...this._rowToNote(r),
|
|
160
|
-
score:
|
|
160
|
+
score: normalizeBM25(r.score)
|
|
161
161
|
}));
|
|
162
162
|
} catch {
|
|
163
163
|
return [];
|
|
@@ -182,4 +182,4 @@ ${patterns.join(". ")}`;
|
|
|
182
182
|
export {
|
|
183
183
|
NoteStore
|
|
184
184
|
};
|
|
185
|
-
//# sourceMappingURL=chunk-
|
|
185
|
+
//# sourceMappingURL=chunk-FJJY4H2Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/indexers/notes/note-store.ts"],"sourcesContent":["/**\n * BrainBank — Note Memory Store\n * \n * Stores structured note digests for long-term agent memory.\n * Each digest captures decisions, files changed, patterns, and open questions.\n * Supports vector + BM25 hybrid retrieval via HNSW + FTS5.\n * \n * Memory tiers:\n * - \"short\" (default): Full digest, last ~20 notes\n * - \"long\": Compressed to patterns + decisions only\n */\n\nimport type { Database } from '../../db/database.ts';\nimport type { EmbeddingProvider, SearchResult } from '../../types.ts';\nimport type { HNSWIndex } from '../../providers/vector/hnsw.ts';\nimport { BM25Search } from '../../search/keyword/bm25.ts';\nimport { reciprocalRankFusion } from '../../search/rrf.ts';\nimport { sanitizeFTS, normalizeBM25 } from '../../search/keyword/utils.ts';\n\nexport interface NoteDigest {\n title: string;\n summary: string;\n decisions?: string[];\n filesChanged?: string[];\n patterns?: string[];\n openQuestions?: string[];\n tags?: string[];\n}\n\nexport interface StoredNote extends NoteDigest {\n id: number;\n tier: 'short' | 'long';\n createdAt: number;\n score?: number;\n}\n\nexport interface RecallOptions {\n /** Max results. Default: 5 */\n k?: number;\n /** Search mode. Default: 'hybrid' */\n mode?: 'hybrid' | 'vector' | 'keyword';\n /** Minimum score threshold. Default: 0.15 */\n minScore?: number;\n /** Filter by tier. Default: all */\n tier?: 'short' | 'long';\n}\n\nexport class NoteStore {\n private _db: Database;\n private _embedding: EmbeddingProvider;\n private _hnsw: HNSWIndex;\n private _vecs: Map<number, Float32Array>;\n\n constructor(\n db: Database,\n embedding: EmbeddingProvider,\n hnsw: HNSWIndex,\n vecs: Map<number, Float32Array>,\n ) {\n this._db = db;\n this._embedding = embedding;\n this._hnsw = hnsw;\n this._vecs = vecs;\n }\n\n /**\n * Store a note digest.\n * Embeds title + summary for vector search, auto-indexed in FTS5.\n */\n async remember(digest: NoteDigest): Promise<number> {\n const { title, summary, decisions = [], filesChanged = [], patterns = [], openQuestions = [], tags = [] } = digest;\n\n // Store in SQLite\n const result = this._db.prepare(`\n INSERT INTO note_memories (title, summary, decisions_json, files_json, patterns_json, open_json, tags_json)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `).run(\n title,\n summary,\n JSON.stringify(decisions),\n JSON.stringify(filesChanged),\n JSON.stringify(patterns),\n JSON.stringify(openQuestions),\n JSON.stringify(tags),\n );\n\n const id = Number(result.lastInsertRowid);\n\n // Embed and index\n const text = `${title}\\n${summary}\\n${decisions.join('. ')}\\n${patterns.join('. ')}`;\n const vec = await this._embedding.embed(text);\n\n this._db.prepare('INSERT INTO note_vectors (note_id, embedding) VALUES (?, ?)').run(\n id, Buffer.from(vec.buffer),\n );\n\n this._hnsw.add(vec, id);\n this._vecs.set(id, vec);\n\n return id;\n }\n\n /**\n * Recall relevant notes.\n * Supports vector, keyword, or hybrid (default) retrieval.\n */\n async recall(query: string, options: RecallOptions = {}): Promise<StoredNote[]> {\n const { k = 5, mode = 'hybrid', minScore = 0.15, tier } = options;\n\n let results: StoredNote[];\n\n if (mode === 'keyword') {\n results = this._searchBM25(query, k);\n } else if (mode === 'vector') {\n results = await this._searchVector(query, k);\n } else {\n // Hybrid: vector + BM25 → RRF\n const [vectorHits, bm25Hits] = await Promise.all([\n this._searchVector(query, k),\n Promise.resolve(this._searchBM25(query, k)),\n ]);\n\n const fusedResults = reciprocalRankFusion(\n [\n vectorHits.map(m => ({ type: 'collection' as const, score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),\n bm25Hits.map(m => ({ type: 'collection' as const, score: m.score ?? 0, content: m.summary, metadata: { id: m.id } })),\n ],\n );\n\n // Map back to full StoredNote objects\n const allById = new Map<number, StoredNote>();\n for (const m of [...vectorHits, ...bm25Hits]) allById.set(m.id, m);\n\n results = fusedResults\n .map(r => {\n const mem = allById.get((r.metadata as any).id);\n if (!mem) return null;\n return { ...mem, score: r.score };\n })\n .filter(Boolean) as StoredNote[];\n }\n\n // Apply filters\n return results\n .filter(m => (m.score ?? 0) >= minScore)\n .filter(m => !tier || m.tier === tier)\n .slice(0, k);\n }\n\n /**\n * List recent notes.\n */\n list(limit: number = 20, tier?: 'short' | 'long'): StoredNote[] {\n const sql = tier\n ? 'SELECT * FROM note_memories WHERE tier = ? ORDER BY id DESC LIMIT ?'\n : 'SELECT * FROM note_memories ORDER BY id DESC LIMIT ?';\n\n const rows = tier\n ? this._db.prepare(sql).all(tier, limit) as any[]\n : this._db.prepare(sql).all(limit) as any[];\n\n return rows.map(r => this._rowToNote(r));\n }\n\n /**\n * Get total count of notes.\n */\n count(): { total: number; short: number; long: number } {\n const total = (this._db.prepare('SELECT COUNT(*) as n FROM note_memories').get() as any).n;\n const short = (this._db.prepare(\"SELECT COUNT(*) as n FROM note_memories WHERE tier = 'short'\").get() as any).n;\n const long = (this._db.prepare(\"SELECT COUNT(*) as n FROM note_memories WHERE tier = 'long'\").get() as any).n;\n return { total, short, long };\n }\n\n /**\n * Consolidate old short-term notes into long-term.\n * Keeps the most recent `keepRecent` as short-term, compresses the rest.\n */\n consolidate(keepRecent: number = 20): { promoted: number } {\n // Find short-term notes beyond the keep window\n const old = this._db.prepare(`\n SELECT id FROM note_memories \n WHERE tier = 'short' \n ORDER BY created_at DESC \n LIMIT -1 OFFSET ?\n `).all(keepRecent) as any[];\n\n if (old.length === 0) return { promoted: 0 };\n\n const ids = old.map((r: any) => r.id);\n const placeholders = ids.map(() => '?').join(',');\n\n // Promote to long-term: clear verbose fields, keep patterns + decisions\n this._db.prepare(`\n UPDATE note_memories \n SET tier = 'long',\n open_json = '[]',\n files_json = '[]'\n WHERE id IN (${placeholders})\n `).run(...ids);\n\n return { promoted: ids.length };\n }\n\n // ── Private helpers ────────────────────────────\n\n private async _searchVector(query: string, k: number): Promise<StoredNote[]> {\n if (this._hnsw.size === 0) return [];\n\n const queryVec = await this._embedding.embed(query);\n const hits = this._hnsw.search(queryVec, k);\n\n if (hits.length === 0) return [];\n\n const ids = hits.map(h => h.id);\n const scoreMap = new Map(hits.map(h => [h.id, h.score]));\n const placeholders = ids.map(() => '?').join(',');\n\n const rows = this._db.prepare(\n `SELECT * FROM note_memories WHERE id IN (${placeholders})`\n ).all(...ids) as any[];\n\n return rows.map(r => ({\n ...this._rowToNote(r),\n score: scoreMap.get(r.id) ?? 0,\n }));\n }\n\n private _searchBM25(query: string, k: number): StoredNote[] {\n const ftsQuery = sanitizeFTS(query);\n if (!ftsQuery) return [];\n\n try {\n const rows = this._db.prepare(`\n SELECT m.*, bm25(fts_notes, 5.0, 3.0, 2.0, 2.0, 1.0) AS score\n FROM fts_notes f\n JOIN note_memories m ON m.id = f.rowid\n WHERE fts_notes MATCH ?\n ORDER BY score ASC\n LIMIT ?\n `).all(ftsQuery, k) as any[];\n\n return rows.map(r => ({\n ...this._rowToNote(r),\n score: normalizeBM25(r.score),\n }));\n } catch {\n return [];\n }\n }\n\n private _rowToNote(r: any): StoredNote {\n return {\n id: r.id,\n title: r.title,\n summary: r.summary,\n decisions: JSON.parse(r.decisions_json || '[]'),\n filesChanged: JSON.parse(r.files_json || '[]'),\n patterns: JSON.parse(r.patterns_json || '[]'),\n openQuestions: JSON.parse(r.open_json || '[]'),\n tags: JSON.parse(r.tags_json || '[]'),\n tier: r.tier,\n createdAt: r.created_at,\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
__name
|
|
3
3
|
} from "./chunk-7QVYU63E.js";
|
|
4
4
|
|
|
5
|
-
// src/embeddings/openai.ts
|
|
5
|
+
// src/providers/embeddings/openai.ts
|
|
6
6
|
var DEFAULT_MODEL = "text-embedding-3-small";
|
|
7
7
|
var DEFAULT_DIMS = {
|
|
8
8
|
"text-embedding-3-small": 1536,
|
|
@@ -20,7 +20,6 @@ var OpenAIEmbedding = class {
|
|
|
20
20
|
_model;
|
|
21
21
|
_baseUrl;
|
|
22
22
|
_requestDims;
|
|
23
|
-
_retrying = false;
|
|
24
23
|
constructor(options = {}) {
|
|
25
24
|
this._apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
|
|
26
25
|
this._model = options.model ?? DEFAULT_MODEL;
|
|
@@ -51,7 +50,7 @@ var OpenAIEmbedding = class {
|
|
|
51
50
|
_isTokenLimitError(errText) {
|
|
52
51
|
return errText.includes("maximum input length") || errText.includes("maximum context length") || errText.includes("too many tokens");
|
|
53
52
|
}
|
|
54
|
-
async _request(input) {
|
|
53
|
+
async _request(input, retryDepth = 0) {
|
|
55
54
|
if (!this._apiKey) {
|
|
56
55
|
throw new Error("OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.");
|
|
57
56
|
}
|
|
@@ -83,13 +82,8 @@ var OpenAIEmbedding = class {
|
|
|
83
82
|
}
|
|
84
83
|
return results;
|
|
85
84
|
}
|
|
86
|
-
if (isTokenLimit && safeInput.length === 1 &&
|
|
87
|
-
this.
|
|
88
|
-
try {
|
|
89
|
-
return await this._request([safeInput[0].slice(0, 6e3)]);
|
|
90
|
-
} finally {
|
|
91
|
-
this._retrying = false;
|
|
92
|
-
}
|
|
85
|
+
if (isTokenLimit && safeInput.length === 1 && retryDepth < 1) {
|
|
86
|
+
return await this._request([safeInput[0].slice(0, 6e3)], retryDepth + 1);
|
|
93
87
|
}
|
|
94
88
|
throw new Error(`OpenAI embedding API error (${res.status}): ${err}`);
|
|
95
89
|
}
|
|
@@ -102,4 +96,4 @@ var OpenAIEmbedding = class {
|
|
|
102
96
|
export {
|
|
103
97
|
OpenAIEmbedding
|
|
104
98
|
};
|
|
105
|
-
//# sourceMappingURL=chunk-
|
|
99
|
+
//# sourceMappingURL=chunk-GUT5MSJT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/embeddings/openai.ts"],"sourcesContent":["/**\n * BrainBank — OpenAI Embedding Provider\n * \n * Uses OpenAI's embedding API via fetch (no SDK dependency).\n * Supports text-embedding-3-small, text-embedding-3-large, and ada-002.\n * \n * Usage:\n * const brain = new BrainBank({\n * embeddingProvider: new OpenAIEmbedding({ model: 'text-embedding-3-small' }),\n * });\n */\n\nimport type { EmbeddingProvider } from '../../types.ts';\n\nconst DEFAULT_MODEL = 'text-embedding-3-small';\nconst DEFAULT_DIMS: Record<string, number> = {\n 'text-embedding-3-small': 1536,\n 'text-embedding-3-large': 3072,\n 'text-embedding-ada-002': 1536,\n};\nconst API_URL = 'https://api.openai.com/v1/embeddings';\nconst MAX_BATCH = 100; // OpenAI limit per request\n\nexport interface OpenAIEmbeddingOptions {\n /** OpenAI API key. Falls back to OPENAI_API_KEY env var. */\n apiKey?: string;\n /** Model name. Default: 'text-embedding-3-small' */\n model?: string;\n /** Vector dimensions. If omitted, uses model default. text-embedding-3-* supports custom dims. */\n dims?: number;\n /** Base URL override (for Azure, proxies, etc.) */\n baseUrl?: string;\n}\n\nexport class OpenAIEmbedding implements EmbeddingProvider {\n readonly dims: number;\n\n private _apiKey: string;\n private _model: string;\n private _baseUrl: string;\n private _requestDims: number | undefined;\n\n constructor(options: OpenAIEmbeddingOptions = {}) {\n this._apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? '';\n this._model = options.model ?? DEFAULT_MODEL;\n this._baseUrl = options.baseUrl ?? API_URL;\n\n // Custom dims only supported by text-embedding-3-*\n if (options.dims && this._model.startsWith('text-embedding-3')) {\n this._requestDims = options.dims;\n this.dims = options.dims;\n } else {\n this.dims = options.dims ?? DEFAULT_DIMS[this._model] ?? 1536;\n }\n }\n\n async embed(text: string): Promise<Float32Array> {\n const results = await this._request([text]);\n return results[0];\n }\n\n async embedBatch(texts: string[]): Promise<Float32Array[]> {\n if (texts.length === 0) return [];\n\n const results: Float32Array[] = [];\n\n // Split into chunks of MAX_BATCH\n for (let i = 0; i < texts.length; i += MAX_BATCH) {\n const batch = texts.slice(i, i + MAX_BATCH);\n const embeddings = await this._request(batch);\n results.push(...embeddings);\n }\n\n return results;\n }\n\n async close(): Promise<void> {\n // No resources to release\n }\n\n private _isTokenLimitError(errText: string): boolean {\n return errText.includes('maximum input length') ||\n errText.includes('maximum context length') ||\n errText.includes('too many tokens');\n }\n\n private async _request(input: string[], retryDepth: number = 0): Promise<Float32Array[]> {\n if (!this._apiKey) {\n throw new Error('OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.');\n }\n\n // Truncate texts that would exceed token limit (~4 chars per token, 8192 max)\n const MAX_CHARS = 24_000;\n const safeInput = input.map(t => t.length > MAX_CHARS ? t.slice(0, MAX_CHARS) : t);\n\n const body: Record<string, any> = {\n model: this._model,\n input: safeInput,\n };\n\n if (this._requestDims) {\n body.dimensions = this._requestDims;\n }\n\n const res = await fetch(this._baseUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this._apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const err = await res.text();\n const isTokenLimit = res.status === 400 && this._isTokenLimitError(err);\n\n // If token limit error in a batch, retry each item individually with more aggressive truncation\n if (isTokenLimit && safeInput.length > 1) {\n const results: Float32Array[] = [];\n for (const text of safeInput) {\n const r = await this._request([text.slice(0, 8_000)]);\n results.push(r[0]);\n }\n return results;\n }\n // Last resort: if single item still fails, truncate to ~2k tokens (max 1 retry)\n if (isTokenLimit && safeInput.length === 1 && retryDepth < 1) {\n return await this._request([safeInput[0].slice(0, 6_000)], retryDepth + 1);\n }\n throw new Error(`OpenAI embedding API error (${res.status}): ${err}`);\n }\n\n const json = await res.json() as {\n data: Array<{ embedding: number[]; index: number }>;\n };\n\n // Sort by index (API may return out of order)\n const sorted = json.data.sort((a, b) => a.index - b.index);\n\n return sorted.map(d => new Float32Array(d.embedding));\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
__name
|
|
3
3
|
} from "./chunk-7QVYU63E.js";
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/lib/math.ts
|
|
6
6
|
function cosineSimilarity(a, b) {
|
|
7
7
|
if (a.length !== b.length) {
|
|
8
8
|
throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
|
|
@@ -34,4 +34,4 @@ export {
|
|
|
34
34
|
cosineSimilarity,
|
|
35
35
|
normalize
|
|
36
36
|
};
|
|
37
|
-
//# sourceMappingURL=chunk-
|
|
37
|
+
//# sourceMappingURL=chunk-QNHBCOKB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/math.ts"],"sourcesContent":["/**\n * BrainBank — Math Utilities\n * \n * Pure vector math functions for similarity calculations.\n * No dependencies — works on Float32Array directly.\n */\n\n/**\n * Cosine similarity between two vectors.\n * Assumes vectors are already normalized (unit length).\n * Returns value between -1.0 and 1.0.\n */\nexport function cosineSimilarity(a: Float32Array, b: Float32Array): number {\n if (a.length !== b.length) {\n throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);\n }\n if (a.length === 0) return 0;\n\n let dot = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n }\n return dot;\n}\n\n/**\n * Full cosine similarity (normalizes first).\n * Use this when vectors may not be pre-normalized.\n */\nexport function cosineSimilarityFull(a: Float32Array, b: Float32Array): number {\n if (a.length !== b.length) {\n throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);\n }\n if (a.length === 0) return 0;\n\n let dot = 0, normA = 0, normB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n normA += a[i] * a[i];\n normB += b[i] * b[i];\n }\n const denom = Math.sqrt(normA) * Math.sqrt(normB);\n return denom === 0 ? 0 : dot / denom;\n}\n\n/**\n * L2-normalize a vector to unit length.\n * Returns a new Float32Array.\n */\nexport function normalize(vec: Float32Array): Float32Array {\n let norm = 0;\n for (let i = 0; i < vec.length; i++) {\n norm += vec[i] * vec[i];\n }\n norm = Math.sqrt(norm);\n if (norm === 0) return new Float32Array(vec.length);\n\n const result = new Float32Array(vec.length);\n for (let i = 0; i < vec.length; i++) {\n result[i] = vec[i] / norm;\n }\n return result;\n}\n\n/**\n * Euclidean distance between two vectors.\n */\nexport function euclideanDistance(a: Float32Array, b: Float32Array): number {\n if (a.length !== b.length) {\n throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);\n }\n let sum = 0;\n for (let i = 0; i < a.length; i++) {\n const d = a[i] - b[i];\n sum += d * d;\n }\n return Math.sqrt(sum);\n}\n"],"mappings":";;;;;AAYO,SAAS,iBAAiB,GAAiB,GAAyB;AACvE,MAAI,EAAE,WAAW,EAAE,QAAQ;AACvB,UAAM,IAAI,MAAM,8BAA8B,EAAE,MAAM,OAAO,EAAE,MAAM,EAAE;AAAA,EAC3E;AACA,MAAI,EAAE,WAAW,EAAG,QAAO;AAE3B,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAC/B,WAAO,EAAE,CAAC,IAAI,EAAE,CAAC;AAAA,EACrB;AACA,SAAO;AACX;AAXgB;AAqCT,SAAS,UAAU,KAAiC;AACvD,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACjC,YAAQ,IAAI,CAAC,IAAI,IAAI,CAAC;AAAA,EAC1B;AACA,SAAO,KAAK,KAAK,IAAI;AACrB,MAAI,SAAS,EAAG,QAAO,IAAI,aAAa,IAAI,MAAM;AAElD,QAAM,SAAS,IAAI,aAAa,IAAI,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACjC,WAAO,CAAC,IAAI,IAAI,CAAC,IAAI;AAAA,EACzB;AACA,SAAO;AACX;AAbgB;","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
__name
|
|
3
3
|
} from "./chunk-7QVYU63E.js";
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/search/rrf.ts
|
|
6
6
|
function reciprocalRankFusion(resultSets, k = 60, maxResults = 15) {
|
|
7
7
|
const fused = /* @__PURE__ */ new Map();
|
|
8
8
|
for (const results of resultSets) {
|
|
@@ -44,13 +44,31 @@ function resultKey(r) {
|
|
|
44
44
|
return `commit:${r.metadata.hash || r.metadata.shortHash}`;
|
|
45
45
|
case "pattern":
|
|
46
46
|
return `pattern:${r.metadata.taskType}:${r.content?.slice(0, 60)}`;
|
|
47
|
-
|
|
48
|
-
return
|
|
47
|
+
case "document":
|
|
48
|
+
return `document:${r.filePath ?? ""}:${r.metadata.collection ?? ""}:${r.metadata.seq ?? ""}:${r.content?.slice(0, 80)}`;
|
|
49
|
+
case "collection":
|
|
50
|
+
return `collection:${r.metadata.id ?? r.content?.slice(0, 80)}`;
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
__name(resultKey, "resultKey");
|
|
52
54
|
|
|
55
|
+
// src/search/keyword/utils.ts
|
|
56
|
+
function sanitizeFTS(query) {
|
|
57
|
+
const clean = query.replace(/[{}[\]()^~*:]/g, " ").replace(/\bAND\b|\bOR\b|\bNOT\b|\bNEAR\b/gi, "").trim();
|
|
58
|
+
const words = clean.split(/\s+/).filter((w) => w.length > 1);
|
|
59
|
+
if (words.length === 0) return "";
|
|
60
|
+
return words.map((w) => `"${w}"`).join(" ");
|
|
61
|
+
}
|
|
62
|
+
__name(sanitizeFTS, "sanitizeFTS");
|
|
63
|
+
function normalizeBM25(rawScore) {
|
|
64
|
+
const abs = Math.abs(rawScore);
|
|
65
|
+
return 1 / (1 + Math.exp(-0.3 * (abs - 5)));
|
|
66
|
+
}
|
|
67
|
+
__name(normalizeBM25, "normalizeBM25");
|
|
68
|
+
|
|
53
69
|
export {
|
|
54
|
-
reciprocalRankFusion
|
|
70
|
+
reciprocalRankFusion,
|
|
71
|
+
sanitizeFTS,
|
|
72
|
+
normalizeBM25
|
|
55
73
|
};
|
|
56
|
-
//# sourceMappingURL=chunk-
|
|
74
|
+
//# sourceMappingURL=chunk-V4UJKXPK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/search/rrf.ts","../src/search/keyword/utils.ts"],"sourcesContent":["/**\n * BrainBank — Reciprocal Rank Fusion (RRF)\n * \n * Combines results from multiple search systems (vector + BM25)\n * using the RRF algorithm: score = Σ 1/(k + rank_i)\n * \n * This is the same algorithm used by Elasticsearch, QMD, and most\n * production hybrid search systems. Simple but very effective.\n * \n * Reference: Cormack et al., \"Reciprocal Rank Fusion outperforms\n * Condorcet and individual Rank Learning Methods\" (2009)\n */\n\nimport type { SearchResult } from '../types.ts';\n\n/**\n * Fuse ranked lists from different search systems into a single ranked list.\n * \n * @param resultSets - Arrays of SearchResult from different systems (e.g. vector, BM25)\n * @param k - Smoothing constant. Default: 60 (standard value). Higher = less emphasis on top ranks.\n * @param maxResults - Maximum results to return.\n */\nexport function reciprocalRankFusion(\n resultSets: SearchResult[][],\n k: number = 60,\n maxResults: number = 15,\n): SearchResult[] {\n // Build a map: unique key → { bestResult, rrfScore }\n const fused = new Map<string, { result: SearchResult; rrfScore: number }>();\n\n for (const results of resultSets) {\n for (let rank = 0; rank < results.length; rank++) {\n const r = results[rank];\n const key = resultKey(r);\n const rrfContribution = 1.0 / (k + rank + 1);\n\n const existing = fused.get(key);\n if (existing) {\n existing.rrfScore += rrfContribution;\n // Keep the result with the higher original score\n if (r.score > existing.result.score) {\n existing.result = { ...r };\n }\n } else {\n fused.set(key, {\n result: { ...r },\n rrfScore: rrfContribution,\n });\n }\n }\n }\n\n // Sort by RRF score descending, normalize, and return\n const sorted = Array.from(fused.values())\n .sort((a, b) => b.rrfScore - a.rrfScore)\n .slice(0, maxResults);\n\n // Normalize RRF scores to 0..1 range\n const maxRRF = sorted[0]?.rrfScore ?? 1;\n return sorted.map(entry => ({\n ...entry.result,\n score: entry.rrfScore / maxRRF,\n metadata: {\n ...entry.result.metadata,\n rrfScore: entry.rrfScore,\n } as any,\n }));\n}\n\n/**\n * Generate a unique key for a search result to detect duplicates across systems.\n */\nfunction resultKey(r: SearchResult): string {\n switch (r.type) {\n case 'code':\n return `code:${r.filePath}:${r.metadata.startLine}-${r.metadata.endLine}`;\n case 'commit':\n return `commit:${r.metadata.hash || r.metadata.shortHash}`;\n case 'pattern':\n return `pattern:${r.metadata.taskType}:${r.content?.slice(0, 60)}`;\n case 'document':\n return `document:${r.filePath ?? ''}:${(r.metadata as any).collection ?? ''}:${(r.metadata as any).seq ?? ''}:${r.content?.slice(0, 80)}`;\n case 'collection':\n return `collection:${(r.metadata as any).id ?? r.content?.slice(0, 80)}`;\n }\n}\n","/**\n * BrainBank — FTS Utilities\n * \n * Shared helpers for SQLite FTS5 query sanitization.\n */\n\n/**\n * Sanitize a user query for FTS5 syntax.\n * Strips operators that would cause parse errors and converts\n * words to implicit AND with exact-match quoting.\n */\nexport function sanitizeFTS(query: string): string {\n const clean = query\n .replace(/[{}[\\]()^~*:]/g, ' ')\n .replace(/\\bAND\\b|\\bOR\\b|\\bNOT\\b|\\bNEAR\\b/gi, '')\n .trim();\n\n const words = clean.split(/\\s+/).filter(w => w.length > 1);\n if (words.length === 0) return '';\n\n return words.map(w => `\"${w}\"`).join(' ');\n}\n\n/**\n * Normalize BM25 score from SQLite (negative, lower = better)\n * to 0.0–1.0 (higher = better) for consistency with vector search.\n */\nexport function normalizeBM25(rawScore: number): number {\n const abs = Math.abs(rawScore);\n return 1.0 / (1.0 + Math.exp(-0.3 * (abs - 5)));\n}\n"],"mappings":";;;;;AAsBO,SAAS,qBACZ,YACA,IAAY,IACZ,aAAqB,IACP;AAEd,QAAM,QAAQ,oBAAI,IAAwD;AAE1E,aAAW,WAAW,YAAY;AAC9B,aAAS,OAAO,GAAG,OAAO,QAAQ,QAAQ,QAAQ;AAC9C,YAAM,IAAI,QAAQ,IAAI;AACtB,YAAM,MAAM,UAAU,CAAC;AACvB,YAAM,kBAAkB,KAAO,IAAI,OAAO;AAE1C,YAAM,WAAW,MAAM,IAAI,GAAG;AAC9B,UAAI,UAAU;AACV,iBAAS,YAAY;AAErB,YAAI,EAAE,QAAQ,SAAS,OAAO,OAAO;AACjC,mBAAS,SAAS,EAAE,GAAG,EAAE;AAAA,QAC7B;AAAA,MACJ,OAAO;AACH,cAAM,IAAI,KAAK;AAAA,UACX,QAAQ,EAAE,GAAG,EAAE;AAAA,UACf,UAAU;AAAA,QACd,CAAC;AAAA,MACL;AAAA,IACJ;AAAA,EACJ;AAGA,QAAM,SAAS,MAAM,KAAK,MAAM,OAAO,CAAC,EACnC,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ,EACtC,MAAM,GAAG,UAAU;AAGxB,QAAM,SAAS,OAAO,CAAC,GAAG,YAAY;AACtC,SAAO,OAAO,IAAI,YAAU;AAAA,IACxB,GAAG,MAAM;AAAA,IACT,OAAO,MAAM,WAAW;AAAA,IACxB,UAAU;AAAA,MACN,GAAG,MAAM,OAAO;AAAA,MAChB,UAAU,MAAM;AAAA,IACpB;AAAA,EACJ,EAAE;AACN;AA7CgB;AAkDhB,SAAS,UAAU,GAAyB;AACxC,UAAQ,EAAE,MAAM;AAAA,IACZ,KAAK;AACD,aAAO,QAAQ,EAAE,QAAQ,IAAI,EAAE,SAAS,SAAS,IAAI,EAAE,SAAS,OAAO;AAAA,IAC3E,KAAK;AACD,aAAO,UAAU,EAAE,SAAS,QAAQ,EAAE,SAAS,SAAS;AAAA,IAC5D,KAAK;AACD,aAAO,WAAW,EAAE,SAAS,QAAQ,IAAI,EAAE,SAAS,MAAM,GAAG,EAAE,CAAC;AAAA,IACpE,KAAK;AACD,aAAO,YAAY,EAAE,YAAY,EAAE,IAAK,EAAE,SAAiB,cAAc,EAAE,IAAK,EAAE,SAAiB,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,GAAG,EAAE,CAAC;AAAA,IAC3I,KAAK;AACD,aAAO,cAAe,EAAE,SAAiB,MAAM,EAAE,SAAS,MAAM,GAAG,EAAE,CAAC;AAAA,EAC9E;AACJ;AAbS;;;AC7DF,SAAS,YAAY,OAAuB;AAC/C,QAAM,QAAQ,MACT,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,qCAAqC,EAAE,EAC/C,KAAK;AAEV,QAAM,QAAQ,MAAM,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC;AACzD,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,SAAO,MAAM,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG;AAC5C;AAVgB;AAgBT,SAAS,cAAc,UAA0B;AACpD,QAAM,MAAM,KAAK,IAAI,QAAQ;AAC7B,SAAO,KAAO,IAAM,KAAK,IAAI,QAAQ,MAAM,EAAE;AACjD;AAHgB;","names":[]}
|