brainbank 0.3.1 → 0.5.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 (50) hide show
  1. package/README.md +174 -15
  2. package/assets/architecture.png +0 -0
  3. package/dist/{base-4SUgeRWT.d.ts → base-DZWtdgIf.d.ts} +23 -27
  4. package/dist/chunk-6XOXM7MI.js +136 -0
  5. package/dist/chunk-6XOXM7MI.js.map +1 -0
  6. package/dist/{chunk-FINIFKAY.js → chunk-BNV43SEF.js} +5 -4
  7. package/dist/chunk-BNV43SEF.js.map +1 -0
  8. package/dist/{chunk-MGIFEPYZ.js → chunk-DDECTPRM.js} +22 -17
  9. package/dist/chunk-DDECTPRM.js.map +1 -0
  10. package/dist/{chunk-5VUYPNH3.js → chunk-HNPABX7L.js} +6 -3
  11. package/dist/chunk-HNPABX7L.js.map +1 -0
  12. package/dist/{chunk-2BEWWQL2.js → chunk-MY36UPPQ.js} +227 -112
  13. package/dist/chunk-MY36UPPQ.js.map +1 -0
  14. package/dist/chunk-N2OJRXSB.js +117 -0
  15. package/dist/chunk-N2OJRXSB.js.map +1 -0
  16. package/dist/{chunk-FI7GWG4W.js → chunk-TTXVJFAE.js} +5 -2
  17. package/dist/chunk-TTXVJFAE.js.map +1 -0
  18. package/dist/{chunk-QNHBCOKB.js → chunk-U2Q2XGPZ.js} +7 -2
  19. package/dist/{chunk-QNHBCOKB.js.map → chunk-U2Q2XGPZ.js.map} +1 -1
  20. package/dist/{chunk-E6WQM4DN.js → chunk-YOLKSYWK.js} +1 -1
  21. package/dist/chunk-YOLKSYWK.js.map +1 -0
  22. package/dist/{chunk-Y3JKI6QN.js → chunk-YRGUIRN5.js} +234 -57
  23. package/dist/chunk-YRGUIRN5.js.map +1 -0
  24. package/dist/cli.js +21 -10
  25. package/dist/cli.js.map +1 -1
  26. package/dist/code.d.ts +1 -1
  27. package/dist/code.js +2 -1
  28. package/dist/docs.d.ts +1 -1
  29. package/dist/docs.js +2 -1
  30. package/dist/git.d.ts +1 -1
  31. package/dist/git.js +2 -1
  32. package/dist/index.d.ts +100 -4
  33. package/dist/index.js +16 -8
  34. package/dist/index.js.map +1 -1
  35. package/dist/memory.d.ts +1 -1
  36. package/dist/memory.js +2 -2
  37. package/dist/notes.d.ts +1 -1
  38. package/dist/notes.js +3 -2
  39. package/dist/perplexity-context-embedding-KSVSZXMD.js +9 -0
  40. package/dist/perplexity-context-embedding-KSVSZXMD.js.map +1 -0
  41. package/dist/perplexity-embedding-227WQY4R.js +10 -0
  42. package/dist/perplexity-embedding-227WQY4R.js.map +1 -0
  43. package/package.json +1 -1
  44. package/dist/chunk-2BEWWQL2.js.map +0 -1
  45. package/dist/chunk-5VUYPNH3.js.map +0 -1
  46. package/dist/chunk-E6WQM4DN.js.map +0 -1
  47. package/dist/chunk-FI7GWG4W.js.map +0 -1
  48. package/dist/chunk-FINIFKAY.js.map +0 -1
  49. package/dist/chunk-MGIFEPYZ.js.map +0 -1
  50. package/dist/chunk-Y3JKI6QN.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/indexers/git/git-indexer.ts","../src/indexers/git/co-edit-analyzer.ts","../src/indexers/git/git-plugin.ts"],"sourcesContent":["/**\n * BrainBank — Git Indexer\n * \n * Reads git history, embeds commit messages + diffs,\n * and computes file co-edit relationships.\n * Incremental: only processes new commits.\n */\n\nimport type { Database } from '@/db/database.ts';\nimport { vecToBuffer } from '@/lib/math.ts';\nimport type { EmbeddingProvider, ProgressCallback, IndexResult } from '@/types.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\n\nexport interface GitIndexerDeps {\n db: Database;\n hnsw: HNSWIndex;\n vectorCache: Map<number, Float32Array>;\n embedding: EmbeddingProvider;\n}\n\nexport interface GitIndexOptions {\n depth?: number;\n onProgress?: ProgressCallback;\n}\n\ninterface CommitData {\n commit: any;\n diff: string;\n additions: number;\n deletions: number;\n filesChanged: string[];\n isMerge: boolean;\n text: string;\n}\n\n/** Prepared statements for git commit operations. */\ninterface GitStatements {\n check: any;\n deleteFiles: any;\n deleteCommit: any;\n insertCommit: any;\n insertFile: any;\n insertVec: any;\n}\n\nexport class GitIndexer {\n private _deps: GitIndexerDeps;\n private _repoPath: string;\n private _maxDiffBytes: number;\n\n constructor(repoPath: string, deps: GitIndexerDeps, maxDiffBytes: number = 8192) {\n this._deps = deps;\n this._repoPath = repoPath;\n this._maxDiffBytes = maxDiffBytes;\n }\n\n /**\n * Index git history.\n * Only processes commits not already in the database.\n */\n async index(options: GitIndexOptions = {}): Promise<IndexResult> {\n const { depth = 500, onProgress } = options;\n\n const git = await this._initGit();\n if (!git) return { indexed: 0, skipped: 0 };\n\n let log: any;\n try { log = await git.log({ maxCount: depth }); }\n catch { return { indexed: 0, skipped: 0 }; }\n\n const stmts = this._prepareStatements();\n const { toProcess, skipped } = await this._collectCommits(git, log.all, stmts, onProgress);\n\n if (toProcess.length === 0) return { indexed: 0, skipped };\n\n const vecs = await this._deps.embedding.embedBatch(toProcess.map(d => d.text));\n const { indexed, newCommitIds } = this._insertCommits(toProcess, vecs, stmts);\n\n this._updateHnsw(vecs, newCommitIds);\n\n return { indexed, skipped };\n }\n\n /** Initialize simple-git. Returns null if git is unavailable. */\n private async _initGit(): Promise<any | null> {\n try {\n const simpleGit = (await import('simple-git')).default;\n return simpleGit(this._repoPath);\n } catch {\n return null;\n }\n }\n\n /** Prepare all SQL statements (hoisted outside loops). */\n private _prepareStatements(): GitStatements {\n const db = this._deps.db;\n return {\n check: db.prepare(`\n SELECT gc.id, gv.commit_id AS has_vector\n FROM git_commits gc\n LEFT JOIN git_vectors gv ON gv.commit_id = gc.id\n WHERE gc.hash = ?`),\n deleteFiles: db.prepare('DELETE FROM commit_files WHERE commit_id = ?'),\n deleteCommit: db.prepare('DELETE FROM git_commits WHERE id = ?'),\n insertCommit: db.prepare(`\n INSERT OR IGNORE INTO git_commits (hash, short_hash, message, author, date, timestamp, files_json, diff, additions, deletions, is_merge)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),\n insertFile: db.prepare('INSERT INTO commit_files (commit_id, file_path) VALUES (?, ?)'),\n insertVec: db.prepare('INSERT OR IGNORE INTO git_vectors (commit_id, embedding) VALUES (?, ?)'),\n };\n }\n\n /** Phase 1: Collect commit data from git (async git calls). */\n private async _collectCommits(\n git: any,\n commits: any[],\n stmts: GitStatements,\n onProgress?: ProgressCallback,\n ): Promise<{ toProcess: CommitData[]; skipped: number }> {\n const toProcess: CommitData[] = [];\n let skipped = 0;\n\n for (let i = 0; i < commits.length; i++) {\n const c = commits[i];\n onProgress?.(`[${c.hash.slice(0, 7)}] ${c.message.slice(0, 50)}`, i + 1, commits.length);\n\n const exists = stmts.check.get(c.hash) as any;\n if (exists?.has_vector) { skipped++; continue; }\n\n // Zombie commit (data exists but vector missing) — clean up\n if (exists && !exists.has_vector) {\n stmts.deleteFiles.run(exists.id);\n stmts.deleteCommit.run(exists.id);\n }\n\n const data = await this._parseCommit(git, c);\n toProcess.push(data);\n }\n\n return { toProcess, skipped };\n }\n\n /** Extract diff, stat, and text from a single commit. */\n private async _parseCommit(git: any, c: any): Promise<CommitData> {\n let diff = '';\n let additions = 0, deletions = 0;\n const filesChanged: string[] = [];\n\n try {\n const numstat = await git.raw(['show', '--numstat', '--format=', c.hash]);\n for (const line of numstat.trim().split('\\n')) {\n if (!line.trim()) continue;\n const parts = line.split('\\t');\n if (parts.length < 3) continue;\n const add = parseInt(parts[0], 10);\n const del = parseInt(parts[1], 10);\n const file = parts[2].trim();\n if (file) {\n filesChanged.push(file);\n if (!isNaN(add)) additions += add;\n if (!isNaN(del)) deletions += del;\n }\n }\n\n const rawDiff = await git.raw(['show', '--format=', '--unified=3', '--no-color', c.hash]);\n diff = rawDiff.length > this._maxDiffBytes\n ? rawDiff.slice(0, this._maxDiffBytes) + '\\n... [truncated]'\n : rawDiff;\n } catch {}\n\n const isMerge = /^(Merge|merge)\\s+(branch|pull|remote|tag)\\b/.test(c.message);\n const text = [\n `Commit: ${c.message}`,\n `Author: ${c.author_name}`,\n `Date: ${c.date}`,\n filesChanged.length > 0 ? `Files: ${filesChanged.join(', ')}` : '',\n diff ? `Changes:\\n${diff.slice(0, 2000)}` : '',\n ].filter(Boolean).join('\\n');\n\n return { commit: c, diff, additions, deletions, filesChanged, isMerge, text };\n }\n\n /** Phase 3: Insert commits + vectors in a single transaction. */\n private _insertCommits(\n toProcess: CommitData[],\n vecs: Float32Array[],\n stmts: GitStatements,\n ): { indexed: number; newCommitIds: { commitId: number; vecIndex: number }[] } {\n let indexed = 0;\n const newCommitIds: { commitId: number; vecIndex: number }[] = [];\n\n this._deps.db.transaction(() => {\n for (let i = 0; i < toProcess.length; i++) {\n const d = toProcess[i];\n const c = d.commit;\n const ts = Math.floor(new Date(c.date).getTime() / 1000);\n\n const result = stmts.insertCommit.run(\n c.hash, c.hash.slice(0, 7), c.message, c.author_name, c.date,\n ts, JSON.stringify(d.filesChanged), d.diff || null,\n d.additions, d.deletions, d.isMerge ? 1 : 0,\n );\n\n if (result.changes === 0) continue;\n const commitId = Number(result.lastInsertRowid);\n\n for (const f of d.filesChanged) {\n stmts.insertFile.run(commitId, f);\n }\n\n stmts.insertVec.run(commitId, vecToBuffer(vecs[i]));\n newCommitIds.push({ commitId, vecIndex: i });\n indexed++;\n }\n });\n\n return { indexed, newCommitIds };\n }\n\n /** Phase 4: Update HNSW index and compute co-edits. */\n private _updateHnsw(\n vecs: Float32Array[],\n inserted: { commitId: number; vecIndex: number }[],\n ): void {\n const newCommitIds: number[] = [];\n for (const { commitId, vecIndex } of inserted) {\n this._deps.hnsw.add(vecs[vecIndex], commitId);\n this._deps.vectorCache.set(commitId, vecs[vecIndex]);\n newCommitIds.push(commitId);\n }\n\n if (newCommitIds.length > 0) {\n this._computeCoEdits(newCommitIds);\n }\n }\n\n /** Compute which files tend to be edited together. */\n private _computeCoEdits(newCommitIds: number[]): void {\n if (newCommitIds.length === 0) return;\n\n const rows = this._queryCommitFiles(newCommitIds);\n const byCommit = this._groupFilesByCommit(rows);\n\n const upsert = this._deps.db.prepare(\n `INSERT INTO co_edits (file_a, file_b, count)\n VALUES (?, ?, 1)\n ON CONFLICT(file_a, file_b) DO UPDATE SET count = count + 1`\n );\n\n this._deps.db.transaction(() => {\n for (const files of byCommit.values()) {\n if (files.length < 2 || files.length > 20) continue;\n for (let i = 0; i < files.length; i++) {\n for (let j = i + 1; j < files.length; j++) {\n const [a, b] = [files[i], files[j]].sort();\n upsert.run(a, b);\n }\n }\n }\n });\n }\n\n /** Query commit_files in chunks to stay under SQLite's 999-variable limit. */\n private _queryCommitFiles(commitIds: number[]): any[] {\n const CHUNK_SIZE = 500;\n const allRows: any[] = [];\n for (let i = 0; i < commitIds.length; i += CHUNK_SIZE) {\n const chunk = commitIds.slice(i, i + CHUNK_SIZE);\n const placeholders = chunk.map(() => '?').join(',');\n const rows = this._deps.db.prepare(\n `SELECT commit_id, file_path FROM commit_files WHERE commit_id IN (${placeholders}) ORDER BY commit_id`\n ).all(...chunk) as any[];\n allRows.push(...rows);\n }\n return allRows;\n }\n\n /** Group file paths by commit ID. */\n private _groupFilesByCommit(rows: any[]): Map<number, string[]> {\n const byCommit = new Map<number, string[]>();\n for (const r of rows) {\n if (!byCommit.has(r.commit_id)) byCommit.set(r.commit_id, []);\n byCommit.get(r.commit_id)!.push(r.file_path);\n }\n return byCommit;\n }\n}\n","/**\n * BrainBank — Co-Edit Analyzer\n * \n * Suggests files that historically change together.\n * Based on git commit co-occurrence analysis.\n */\n\nimport type { Database } from '@/db/database.ts';\nimport type { CoEditSuggestion } from '@/types.ts';\n\nexport class CoEditAnalyzer {\n constructor(private _db: Database) {}\n\n /**\n * Get files that frequently change alongside the given file.\n * Returns sorted by co-edit count (highest first).\n */\n suggest(filePath: string, limit: number = 5): CoEditSuggestion[] {\n const rows = this._db.prepare(`\n SELECT\n CASE WHEN file_a = ? THEN file_b ELSE file_a END AS file,\n count\n FROM co_edits\n WHERE file_a = ? OR file_b = ?\n ORDER BY count DESC\n LIMIT ?\n `).all(filePath, filePath, filePath, limit) as any[];\n\n return rows.map(r => ({ file: r.file, count: r.count }));\n }\n}\n","/**\n * BrainBank — Git Module\n * \n * Git history indexing with co-edit relationships.\n * \n * import { git } from 'brainbank/git';\n * brain.use(git({ depth: 500 }));\n * \n * // Multi-repo: namespace to avoid key collisions\n * brain\n * .use(git({ repoPath: './frontend', name: 'git:frontend' }))\n * .use(git({ repoPath: './backend', name: 'git:backend' }));\n */\n\nimport type { Indexer, IndexerContext } from '@/indexers/base.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\nimport type { Database } from '@/db/database.ts';\nimport { GitIndexer } from './git-indexer.ts';\nimport { CoEditAnalyzer } from './co-edit-analyzer.ts';\nimport type { IndexResult, ProgressCallback, CoEditSuggestion } from '@/types.ts';\n\nexport interface GitPluginOptions {\n /** Repository path. Default: from config */\n repoPath?: string;\n /** Max commits to index. Default: from config */\n depth?: number;\n /** Max diff bytes. Default: from config */\n maxDiffBytes?: number;\n /** Custom indexer name for multi-repo (e.g. 'git:frontend'). Default: 'git' */\n name?: string;\n}\n\nclass GitPlugin implements Indexer {\n readonly name: string;\n private db!: Database;\n hnsw!: HNSWIndex;\n indexer!: GitIndexer;\n coEdits!: CoEditAnalyzer;\n vecCache = new Map<number, Float32Array>();\n\n constructor(private opts: GitPluginOptions = {}) {\n this.name = opts.name ?? 'git';\n }\n\n async initialize(ctx: IndexerContext): Promise<void> {\n this.db = ctx.db;\n // Use shared HNSW so all git indexers share one index\n const shared = await ctx.getOrCreateSharedHnsw('git', 500_000);\n this.hnsw = shared.hnsw;\n this.vecCache = shared.vecCache;\n\n if (shared.isNew) {\n ctx.loadVectors('git_vectors', 'commit_id', this.hnsw, this.vecCache);\n }\n\n const repoPath = this.opts.repoPath ?? ctx.config.repoPath;\n this.indexer = new GitIndexer(repoPath, {\n db: ctx.db,\n hnsw: this.hnsw,\n vectorCache: this.vecCache,\n embedding: ctx.embedding,\n }, this.opts.maxDiffBytes ?? ctx.config.maxDiffBytes);\n\n this.coEdits = new CoEditAnalyzer(ctx.db);\n }\n\n async index(options: {\n depth?: number;\n onProgress?: ProgressCallback;\n } = {}): Promise<IndexResult> {\n return this.indexer.index(options);\n }\n\n suggestCoEdits(filePath: string, limit: number = 5): CoEditSuggestion[] {\n return this.coEdits.suggest(filePath, limit);\n }\n\n /** Get git history for a specific file. */\n fileHistory(filePath: string, limit: number = 20): any[] {\n return this.db.prepare(`\n SELECT c.short_hash, c.message, c.author, c.date, c.additions, c.deletions\n FROM git_commits c\n INNER JOIN commit_files cf ON c.id = cf.commit_id\n WHERE cf.file_path LIKE ? AND c.is_merge = 0\n ORDER BY c.timestamp DESC LIMIT ?\n `).all(`%${filePath}%`, limit) as any[];\n }\n\n stats(): Record<string, number> {\n return {\n commits: (this.db.prepare('SELECT COUNT(*) as c FROM git_commits').get() as { c: number }).c,\n filesTracked: (this.db.prepare('SELECT COUNT(DISTINCT file_path) as c FROM commit_files').get() as { c: number }).c,\n coEdits: (this.db.prepare('SELECT COUNT(*) as c FROM co_edits').get() as { c: number }).c,\n hnswSize: this.hnsw.size,\n };\n }\n}\n\n/** Create a git history plugin. */\nexport function git(opts?: GitPluginOptions): Indexer {\n return new GitPlugin(opts);\n}\n"],"mappings":"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","names":["git"]}
@@ -29,9 +29,14 @@ function normalize(vec) {
29
29
  return result;
30
30
  }
31
31
  __name(normalize, "normalize");
32
+ function vecToBuffer(vec) {
33
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
34
+ }
35
+ __name(vecToBuffer, "vecToBuffer");
32
36
 
33
37
  export {
34
38
  cosineSimilarity,
35
- normalize
39
+ normalize,
40
+ vecToBuffer
36
41
  };
37
- //# sourceMappingURL=chunk-QNHBCOKB.js.map
42
+ //# sourceMappingURL=chunk-U2Q2XGPZ.js.map
@@ -1 +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":[]}
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\n/**\n * Convert a Float32Array to a Buffer for SQLite storage.\n * Handles views with non-zero byteOffset (e.g. from batched embedding output).\n * Using Buffer.from(vec.buffer) directly is WRONG for views — it copies the entire parent buffer.\n */\nexport function vecToBuffer(vec: Float32Array): Buffer {\n return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);\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;AAmCT,SAAS,YAAY,KAA2B;AACnD,SAAO,OAAO,KAAK,IAAI,QAAQ,IAAI,YAAY,IAAI,UAAU;AACjE;AAFgB;","names":[]}
@@ -76,4 +76,4 @@ export {
76
76
  sanitizeFTS,
77
77
  normalizeBM25
78
78
  };
79
- //# sourceMappingURL=chunk-E6WQM4DN.js.map
79
+ //# sourceMappingURL=chunk-YOLKSYWK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/rrf.ts","../src/lib/fts.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 // Note: A single result always normalizes to 1.0. This is correct for RRF —\n // the score is relative to the result set, not absolute relevance.\n // Use minScore filters sparingly with RRF.\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 * Split camelCase, PascalCase, and snake_case into individual words.\n * \"MagicLinkCallback\" → \"Magic Link Callback\"\n * \"tenant_worker\" → \"tenant worker\"\n */\nfunction splitCompound(word: string): string {\n return word\n .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → camel Case\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // HTMLParser → HTML Parser\n .replace(/[_\\-./\\\\]/g, ' ') // snake_case, kebab-case, paths\n .trim();\n}\n\n/**\n * Sanitize a user query for FTS5 syntax.\n * Strips operators that would cause parse errors, splits compound words,\n * and converts 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 // Split compound words (camelCase, PascalCase, snake_case)\n const expanded = clean.split(/\\s+/)\n .map(w => splitCompound(w))\n .join(' ');\n\n const words = expanded.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;AAMxB,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;AAhDgB;AAqDhB,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;;;AChET,SAAS,cAAc,MAAsB;AACzC,SAAO,KACF,QAAQ,mBAAmB,OAAO,EAClC,QAAQ,yBAAyB,OAAO,EACxC,QAAQ,cAAc,GAAG,EACzB,KAAK;AACd;AANS;AAaF,SAAS,YAAY,OAAuB;AAC/C,QAAM,QAAQ,MACT,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,qCAAqC,EAAE,EAC/C,KAAK;AAGV,QAAM,WAAW,MAAM,MAAM,KAAK,EAC7B,IAAI,OAAK,cAAc,CAAC,CAAC,EACzB,KAAK,GAAG;AAEb,QAAM,QAAQ,SAAS,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC;AAC5D,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,SAAO,MAAM,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG;AAC5C;AAfgB;AAqBT,SAAS,cAAc,UAA0B;AACpD,QAAM,MAAM,KAAK,IAAI,QAAQ;AAC7B,SAAO,KAAO,IAAM,KAAK,IAAI,QAAQ,MAAM,EAAE;AACjD;AAHgB;","names":[]}
@@ -1,3 +1,7 @@
1
+ import {
2
+ normalizeBM25,
3
+ reciprocalRankFusion
4
+ } from "./chunk-YOLKSYWK.js";
1
5
  import {
2
6
  __name
3
7
  } from "./chunk-7QVYU63E.js";
@@ -291,6 +295,224 @@ var DocsIndexer = class {
291
295
  }
292
296
  };
293
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/docs-search.ts
315
+ var DocsSearch = class {
316
+ constructor(_d) {
317
+ this._d = _d;
318
+ }
319
+ static {
320
+ __name(this, "DocsSearch");
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._applyReranking(query, deduped);
353
+ }
354
+ /** Apply reranking if a reranker is configured. */
355
+ async _applyReranking(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
+
294
516
  // src/indexers/docs/docs-plugin.ts
295
517
  var DocsPlugin = class {
296
518
  static {
@@ -301,13 +523,19 @@ var DocsPlugin = class {
301
523
  indexer;
302
524
  vecCache = /* @__PURE__ */ new Map();
303
525
  _db;
304
- _embedding;
526
+ _search;
305
527
  async initialize(ctx) {
306
528
  this._db = ctx.db;
307
- this._embedding = ctx.embedding;
308
529
  this.hnsw = await ctx.createHnsw();
309
530
  ctx.loadVectors("doc_vectors", "chunk_id", this.hnsw, this.vecCache);
310
531
  this.indexer = new DocsIndexer(ctx.db, ctx.embedding, this.hnsw, this.vecCache);
532
+ this._search = new DocsSearch({
533
+ db: ctx.db,
534
+ embedding: ctx.embedding,
535
+ hnsw: this.hnsw,
536
+ vecCache: this.vecCache,
537
+ reranker: ctx.config.reranker
538
+ });
311
539
  }
312
540
  /** Register a document collection. */
313
541
  addCollection(collection) {
@@ -354,46 +582,9 @@ var DocsPlugin = class {
354
582
  }
355
583
  return results;
356
584
  }
357
- /** Search documents only. */
585
+ /** Search documents using hybrid search (vector + BM25 → RRF). */
358
586
  async search(query, options) {
359
- const k = options?.k ?? 8;
360
- const queryVec = await this._embedding.embed(query);
361
- let searchK = k;
362
- if (options?.collection && this.hnsw.size > 0) {
363
- const collectionCount = this._db.prepare(
364
- "SELECT COUNT(*) as c FROM doc_chunks WHERE collection = ?"
365
- ).get(options.collection)?.c ?? 0;
366
- const totalChunks = this._db.prepare(
367
- "SELECT COUNT(*) as c FROM doc_chunks"
368
- ).get()?.c ?? 1;
369
- const ratio = collectionCount > 0 ? Math.max(3, Math.min(50, Math.ceil(totalChunks / collectionCount))) : 3;
370
- searchK = Math.min(k * ratio, this.hnsw.size);
371
- }
372
- const hits = this.hnsw.search(queryVec, searchK);
373
- const results = [];
374
- for (const hit of hits) {
375
- if (options?.minScore && hit.score < options.minScore) continue;
376
- const chunk = this._db.prepare(
377
- "SELECT * FROM doc_chunks WHERE id = ?"
378
- ).get(hit.id);
379
- if (!chunk) continue;
380
- if (options?.collection && chunk.collection !== options.collection) continue;
381
- const ctx = this._getDocContext(chunk.collection, chunk.file_path);
382
- results.push({
383
- type: "document",
384
- score: hit.score,
385
- filePath: chunk.file_path,
386
- content: chunk.content,
387
- context: ctx,
388
- metadata: {
389
- collection: chunk.collection,
390
- title: chunk.title,
391
- seq: chunk.seq
392
- }
393
- });
394
- if (results.length >= k) break;
395
- }
396
- return results;
587
+ return this._search.search(query, options);
397
588
  }
398
589
  /** Add context description for a document path. */
399
590
  addContext(collection, path2, context) {
@@ -420,21 +611,6 @@ var DocsPlugin = class {
420
611
  hnswSize: this.hnsw.size
421
612
  };
422
613
  }
423
- /** Resolve context for a document (checks path_contexts tree → collection context). */
424
- _getDocContext(collection, filePath) {
425
- const parts = filePath.split("/");
426
- for (let i = parts.length; i >= 0; i--) {
427
- const checkPath = i === 0 ? "/" : "/" + parts.slice(0, i).join("/");
428
- const ctx = this._db.prepare(
429
- "SELECT context FROM path_contexts WHERE collection = ? AND path = ?"
430
- ).get(collection, checkPath);
431
- if (ctx) return ctx.context;
432
- }
433
- const coll = this._db.prepare(
434
- "SELECT context FROM collections WHERE name = ?"
435
- ).get(collection);
436
- return coll?.context ?? void 0;
437
- }
438
614
  };
439
615
  function docs() {
440
616
  return new DocsPlugin();
@@ -442,7 +618,8 @@ function docs() {
442
618
  __name(docs, "docs");
443
619
 
444
620
  export {
621
+ rerank,
445
622
  DocsIndexer,
446
623
  docs
447
624
  };
448
- //# sourceMappingURL=chunk-Y3JKI6QN.js.map
625
+ //# sourceMappingURL=chunk-YRGUIRN5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/indexers/docs/docs-indexer.ts","../src/search/vector/rerank.ts","../src/indexers/docs/docs-search.ts","../src/indexers/docs/docs-plugin.ts"],"sourcesContent":["/**\n * BrainBank — Document Indexer\n * \n * Indexes generic document collections (markdown, text, etc.)\n * with heading-aware smart chunking, inspired by qmd.\n * \n * const indexer = new DocsIndexer(db, embedding, hnsw, vecCache);\n * await indexer.indexCollection('notes', '/path/to/notes', '**\\/*.md');\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { createHash } from 'node:crypto';\n\nimport type { Database } from '@/db/database.ts';\nimport type { EmbeddingProvider, VectorIndex } from '@/types.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\n\n// ── Break Point Scoring (qmd-inspired) ──────────────\n\ninterface BreakPoint {\n pos: number; // character position\n score: number; // break quality (higher = better)\n}\n\nconst BREAK_SCORES: [RegExp, number][] = [\n [/^# /, 100], // H1\n [/^## /, 90], // H2\n [/^### /, 80], // H3\n [/^#### /, 70], // H4\n [/^##### /, 60], // H5\n [/^###### /, 50], // H6\n [/^```/, 80], // Code fence\n [/^---$/, 60], // Horizontal rule\n [/^\\*\\*\\*$/, 60], // Horizontal rule alt\n [/^$/, 20], // Blank line (paragraph break)\n [/^[-*+] /, 5], // List item\n];\n\n// ── Chunk Target ────────────────────────────────────\n\nconst TARGET_CHARS = 3000; // ~900 tokens\nconst WINDOW_CHARS = 600; // search window before cutoff\nconst MIN_CHUNK_CHARS = 200; // don't create tiny chunks\n\n\n/** Ignored output/vendor directories when walking docs. */\nconst IGNORED_DOC_DIRS = new Set([\n 'node_modules', '.git', '.hg', '.svn',\n 'dist', 'build', 'out', 'coverage', '.next',\n '__pycache__', '.tox', '.venv', 'venv',\n 'vendor', 'target', '.cache', '.turbo',\n]);\n\n// ── DocsIndexer ──────────────────────────────────────\n\nexport class DocsIndexer {\n constructor(\n private _db: Database,\n private _embedding: EmbeddingProvider,\n private _hnsw: HNSWIndex,\n private _vecCache: Map<number, Float32Array>,\n ) {}\n\n /**\n * Index all documents in a collection.\n * Incremental — skips unchanged files (by content hash).\n */\n async indexCollection(\n collection: string,\n dirPath: string,\n pattern: string = '**/*.md',\n options: {\n ignore?: string[];\n onProgress?: (file: string, current: number, total: number) => void;\n } = {},\n ): Promise<{ indexed: number; skipped: number; chunks: number }> {\n const absDir = path.resolve(dirPath);\n if (!fs.existsSync(absDir)) {\n throw new Error(`Collection path does not exist: ${absDir}`);\n }\n\n const files = this._walkFiles(absDir, pattern, options.ignore);\n let indexed = 0, skipped = 0, totalChunks = 0;\n\n for (let i = 0; i < files.length; i++) {\n const relPath = files[i];\n options.onProgress?.(relPath, i + 1, files.length);\n\n const absPath = path.join(absDir, relPath);\n const content = fs.readFileSync(absPath, 'utf-8');\n const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);\n\n if (this._isUnchanged(collection, relPath, hash)) {\n skipped++;\n continue;\n }\n\n this._removeOldChunks(collection, relPath);\n const chunkCount = await this._indexFile(collection, relPath, content, hash);\n indexed++;\n totalChunks += chunkCount;\n }\n\n return { indexed, skipped, chunks: totalChunks };\n }\n\n /** Walk directory tree and collect matching files. */\n private _walkFiles(absDir: string, pattern: string, ignore?: string[]): string[] {\n const patternExt = pattern.match(/\\.([\\w]+)$/)?.[1];\n const files: string[] = [];\n\n const walk = (dir: string, base: string): void => {\n let entries: fs.Dirent[];\n try { entries = fs.readdirSync(dir, { withFileTypes: true }); }\n catch { return; }\n for (const e of entries) {\n const rel = base ? `${base}/${e.name}` : e.name;\n if (e.isDirectory()) {\n if (IGNORED_DOC_DIRS.has(e.name)) continue;\n walk(path.join(dir, e.name), rel);\n } else if (e.isFile()) {\n if (this._isIgnoredFile(rel, ignore)) continue;\n const ext = path.extname(e.name).slice(1);\n if (!patternExt || ext === patternExt) files.push(rel);\n }\n }\n };\n walk(absDir, '');\n return files;\n }\n\n /** Check if a file matches any ignore patterns. */\n private _isIgnoredFile(relPath: string, ignore?: string[]): boolean {\n if (!ignore) return false;\n return ignore.some(ig => {\n const regex = ig\n .replace(/\\*\\*/g, '{{GLOBSTAR}}')\n .replace(/\\*/g, '{{STAR}}')\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\{\\{GLOBSTAR\\}\\}/g, '.*')\n .replace(/\\{\\{STAR\\}\\}/g, '[^/]*');\n return new RegExp(regex).test(relPath);\n });\n }\n\n /** Check if all chunks for a file match the current hash and have vectors. */\n private _isUnchanged(collection: string, relPath: string, hash: string): boolean {\n const existing = this._db.prepare(\n `SELECT dc.id, dc.content_hash, dv.chunk_id AS has_vector\n FROM doc_chunks dc\n LEFT JOIN doc_vectors dv ON dv.chunk_id = dc.id\n WHERE dc.collection = ? AND dc.file_path = ?`\n ).all(collection, relPath) as any[];\n\n return existing.length > 0 &&\n existing.every((c: any) => c.content_hash === hash && c.has_vector != null);\n }\n\n /** Remove old chunks and their HNSW vectors for a file. */\n private _removeOldChunks(collection: string, relPath: string): void {\n const oldChunks = this._db.prepare(\n 'SELECT id FROM doc_chunks WHERE collection = ? AND file_path = ?'\n ).all(collection, relPath) as any[];\n\n for (const old of oldChunks) {\n this._hnsw.remove(old.id);\n this._vecCache.delete(old.id);\n }\n this._db.prepare(\n 'DELETE FROM doc_chunks WHERE collection = ? AND file_path = ?'\n ).run(collection, relPath);\n }\n\n /** Index a single file: chunk, embed, store in DB + HNSW. */\n private async _indexFile(\n collection: string, relPath: string, content: string, hash: string,\n ): Promise<number> {\n const title = this._extractTitle(content, relPath);\n const chunks = this._smartChunk(content);\n\n const insertChunk = this._db.prepare(`\n INSERT INTO doc_chunks (collection, file_path, title, content, seq, pos, content_hash)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n const chunkIds: number[] = [];\n this._db.transaction(() => {\n for (let seq = 0; seq < chunks.length; seq++) {\n const result = insertChunk.run(\n collection, relPath, title, chunks[seq].text, seq, chunks[seq].pos, hash,\n );\n chunkIds.push(Number(result.lastInsertRowid));\n }\n });\n\n const texts = chunks.map(c => `title: ${title} | text: ${c.text}`);\n const embeddings = await this._embedding.embedBatch(texts);\n\n const insertVec = this._db.prepare(\n 'INSERT OR REPLACE INTO doc_vectors (chunk_id, embedding) VALUES (?, ?)'\n );\n this._db.transaction(() => {\n for (let j = 0; j < chunkIds.length; j++) {\n insertVec.run(chunkIds[j], Buffer.from(embeddings[j].buffer));\n }\n });\n\n for (let j = 0; j < chunkIds.length; j++) {\n this._hnsw.add(embeddings[j], chunkIds[j]);\n this._vecCache.set(chunkIds[j], embeddings[j]);\n }\n\n return chunks.length;\n }\n\n /** Remove all indexed data for a collection. */\n removeCollection(collection: string): void {\n const chunks = this._db.prepare(\n 'SELECT id FROM doc_chunks WHERE collection = ?'\n ).all(collection) as any[];\n for (const chunk of chunks) {\n this._hnsw.remove(chunk.id);\n this._vecCache.delete(chunk.id);\n }\n\n this._db.prepare('DELETE FROM doc_chunks WHERE collection = ?').run(collection);\n this._db.prepare('DELETE FROM collections WHERE name = ?').run(collection);\n this._db.prepare('DELETE FROM path_contexts WHERE collection = ?').run(collection);\n }\n\n // ── Smart Chunking ──────────────────────────────\n\n /** Split document into chunks at natural markdown boundaries. */\n private _smartChunk(text: string): { text: string; pos: number }[] {\n if (text.length <= TARGET_CHARS) {\n return [{ text, pos: 0 }];\n }\n\n const lines = text.split('\\n');\n const breakPoints = this._findBreakPoints(lines);\n const chunks: { text: string; pos: number }[] = [];\n let chunkStart = 0;\n\n while (chunkStart < text.length) {\n const remaining = text.length - chunkStart;\n if (remaining <= TARGET_CHARS + WINDOW_CHARS) {\n this._pushLastChunk(text, chunkStart, chunks);\n break;\n }\n\n const bestBreak = this._findBestBreak(chunkStart, breakPoints);\n const chunkText = text.slice(chunkStart, bestBreak).trim();\n if (chunkText.length >= MIN_CHUNK_CHARS) {\n chunks.push({ text: chunkText, pos: chunkStart });\n }\n chunkStart = bestBreak;\n }\n\n return chunks;\n }\n\n /** Handle the last chunk: merge if too small, otherwise push. */\n private _pushLastChunk(\n text: string, chunkStart: number, chunks: { text: string; pos: number }[],\n ): void {\n const lastText = text.slice(chunkStart).trim();\n if (lastText.length >= MIN_CHUNK_CHARS) {\n chunks.push({ text: lastText, pos: chunkStart });\n } else if (chunks.length > 0) {\n chunks[chunks.length - 1].text += '\\n' + lastText;\n } else {\n chunks.push({ text: lastText, pos: chunkStart });\n }\n }\n\n /** Find the best break position within the target window. */\n private _findBestBreak(chunkStart: number, breakPoints: BreakPoint[]): number {\n const targetEnd = chunkStart + TARGET_CHARS;\n const windowStart = targetEnd - WINDOW_CHARS;\n\n let bestBreak = targetEnd;\n let bestScore = 0;\n\n for (const bp of breakPoints) {\n if (bp.pos <= chunkStart) continue;\n if (bp.pos > targetEnd + WINDOW_CHARS / 2) break;\n if (bp.pos < windowStart) continue;\n\n const distance = Math.abs(bp.pos - targetEnd);\n const decay = 1 - (distance / WINDOW_CHARS) ** 2 * 0.7;\n const finalScore = bp.score * decay;\n\n if (finalScore > bestScore) {\n bestScore = finalScore;\n bestBreak = bp.pos;\n }\n }\n\n return bestBreak;\n }\n\n /** Find all potential break points in the document with scores. */\n private _findBreakPoints(lines: string[]): BreakPoint[] {\n const points: BreakPoint[] = [];\n let charPos = 0;\n let inCodeBlock = false;\n\n for (const line of lines) {\n if (line.trimStart().startsWith('```')) {\n inCodeBlock = !inCodeBlock;\n if (!inCodeBlock) {\n points.push({ pos: charPos + line.length + 1, score: 80 });\n }\n charPos += line.length + 1;\n continue;\n }\n\n if (inCodeBlock) {\n charPos += line.length + 1;\n continue;\n }\n\n for (const [pattern, score] of BREAK_SCORES) {\n if (pattern.test(line.trim())) {\n points.push({ pos: charPos, score });\n break;\n }\n }\n\n charPos += line.length + 1;\n }\n\n return points;\n }\n\n /** Extract document title from first heading or filename. */\n private _extractTitle(content: string, filePath: string): string {\n const match = content.match(/^#{1,3}\\s+(.+)$/m);\n if (match) return match[1].trim();\n return path.basename(filePath, path.extname(filePath));\n }\n}\n","/**\n * BrainBank — Rerank\n * \n * Position-aware score blending between retrieval and reranker.\n * Pure function — no state.\n * \n * Top 1-3: 75% retrieval / 25% reranker (preserves exact matches)\n * Top 4-10: 60% retrieval / 40% reranker\n * Top 11+: 40% retrieval / 60% reranker (trust reranker more)\n */\n\nimport type { Reranker, SearchResult } from '@/types.ts';\n\n/** Re-rank results using position-aware blending. */\nexport async function rerank(\n query: string,\n results: SearchResult[],\n reranker: Reranker,\n): Promise<SearchResult[]> {\n const documents = results.map(r => r.content);\n const scores = await reranker.rank(query, documents);\n\n const blended = results.map((r, i) => {\n const pos = i + 1;\n const rrfWeight = pos <= 3 ? 0.75 : pos <= 10 ? 0.60 : 0.40;\n return {\n ...r,\n score: rrfWeight * r.score + (1 - rrfWeight) * (scores[i] ?? 0),\n };\n });\n\n return blended.sort((a, b) => b.score - a.score);\n}\n","/**\n * BrainBank — Document Search\n *\n * Hybrid search (vector + BM25 → RRF) for indexed documents.\n * Extracted from DocsPlugin to keep plugin management separate from search logic.\n */\n\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\nimport type { Database } from '@/db/database.ts';\nimport type { EmbeddingProvider, SearchResult, Reranker } from '@/types.ts';\nimport { reciprocalRankFusion } from '@/lib/rrf.ts';\nimport { rerank } from '@/search/vector/rerank.ts';\nimport { normalizeBM25 } from '@/lib/fts.ts';\n\nexport interface DocsSearchDeps {\n db: Database;\n embedding: EmbeddingProvider;\n hnsw: HNSWIndex;\n vecCache: Map<number, Float32Array>;\n reranker?: Reranker;\n}\n\nexport class DocsSearch {\n constructor(private _d: DocsSearchDeps) {}\n\n /** Hybrid search (vector + BM25 → RRF), with dedup and optional reranking. */\n async search(query: string, options?: {\n collection?: string;\n k?: number;\n minScore?: number;\n mode?: 'hybrid' | 'vector' | 'keyword';\n }): Promise<SearchResult[]> {\n const k = options?.k ?? 8;\n const mode = options?.mode ?? 'hybrid';\n const minScore = options?.minScore ?? 0;\n\n if (mode === 'keyword') return this._dedup(this._searchBM25(query, k * 2, minScore, options?.collection), k);\n if (mode === 'vector') return this._dedup(await this._searchVector(query, k * 2, minScore, options?.collection), k);\n\n // Hybrid: over-fetch from both, fuse with RRF, then dedup by file\n const fetchK = k * 2;\n const [vecHits, bm25Hits] = await Promise.all([\n this._searchVector(query, fetchK, 0, options?.collection),\n Promise.resolve(this._searchBM25(query, fetchK, 0, options?.collection)),\n ]);\n\n if (vecHits.length === 0 && bm25Hits.length === 0) return [];\n if (bm25Hits.length === 0) return this._dedup(vecHits.filter(h => h.score >= minScore), k);\n if (vecHits.length === 0) return this._dedup(bm25Hits.filter(h => h.score >= minScore), k);\n\n const fused = reciprocalRankFusion([vecHits, bm25Hits]);\n\n // Map fused results back to doc SearchResults\n const allById = new Map<number, SearchResult>();\n for (const h of [...vecHits, ...bm25Hits]) {\n const id = (h.metadata as any)?.chunkId;\n if (id != null) allById.set(id, h);\n }\n\n const results: SearchResult[] = [];\n for (const r of fused) {\n const chunkId = (r.metadata as any)?.chunkId;\n const original = allById.get(chunkId);\n if (!original) continue;\n const merged = { ...original, score: r.score };\n if (merged.score >= minScore) results.push(merged);\n }\n\n const deduped = this._dedup(results, k);\n return this._applyReranking(query, deduped);\n }\n\n /** Apply reranking if a reranker is configured. */\n private async _applyReranking(query: string, results: SearchResult[]): Promise<SearchResult[]> {\n if (!this._d.reranker || results.length <= 1) return results;\n return rerank(query, results, this._d.reranker);\n }\n\n /** Deduplicate results by file path — keep best-scoring chunk per file. */\n private _dedup(results: SearchResult[], k: number): SearchResult[] {\n const seen = new Map<string, SearchResult>();\n for (const r of results) {\n const key = r.filePath ?? '';\n if (!seen.has(key) || (seen.get(key)!.score < r.score)) {\n seen.set(key, r);\n }\n }\n return [...seen.values()]\n .sort((a, b) => b.score - a.score)\n .slice(0, k);\n }\n\n /** Vector-only search via HNSW. */\n private async _searchVector(query: string, k: number, minScore: number, collection?: string): Promise<SearchResult[]> {\n if (this._d.hnsw.size === 0) return [];\n const queryVec = await this._d.embedding.embed(query);\n\n let searchK = k;\n if (collection && this._d.hnsw.size > 0) {\n const collectionCount = (this._d.db.prepare(\n 'SELECT COUNT(*) as c FROM doc_chunks WHERE collection = ?'\n ).get(collection) as any)?.c ?? 0;\n const totalChunks = (this._d.db.prepare(\n 'SELECT COUNT(*) as c FROM doc_chunks'\n ).get() as any)?.c ?? 1;\n const ratio = collectionCount > 0\n ? Math.max(3, Math.min(50, Math.ceil(totalChunks / collectionCount)))\n : 3;\n searchK = Math.min(k * ratio, this._d.hnsw.size);\n }\n\n const hits = this._d.hnsw.search(queryVec, searchK);\n const results: SearchResult[] = [];\n\n for (const hit of hits) {\n if (minScore && hit.score < minScore) continue;\n const chunk = this._d.db.prepare('SELECT * FROM doc_chunks WHERE id = ?').get(hit.id) as any;\n if (!chunk) continue;\n if (collection && chunk.collection !== collection) continue;\n\n results.push({\n type: 'document',\n score: hit.score,\n filePath: chunk.file_path,\n content: chunk.content,\n context: this._getDocContext(chunk.collection, chunk.file_path),\n metadata: {\n collection: chunk.collection,\n title: chunk.title,\n seq: chunk.seq,\n chunkId: chunk.id,\n },\n });\n\n if (results.length >= k) break;\n }\n\n return results;\n }\n\n /** BM25 keyword search via FTS5 (OR-mode for natural language). */\n private _searchBM25(query: string, k: number, minScore: number, collection?: string): SearchResult[] {\n const ftsQuery = this._buildDocsFTS(query);\n if (!ftsQuery) return [];\n\n try {\n const collectionFilter = collection ? 'AND d.collection = ?' : '';\n const params: any[] = [ftsQuery];\n if (collection) params.push(collection);\n params.push(k * 2);\n\n const rows = this._d.db.prepare(`\n SELECT d.*, bm25(fts_docs, 10.0, 2.0, 5.0, 1.0) AS bm25_score\n FROM fts_docs f\n JOIN doc_chunks d ON d.id = f.rowid\n WHERE fts_docs MATCH ? ${collectionFilter}\n ORDER BY bm25_score ASC\n LIMIT ?\n `).all(...params) as any[];\n\n return rows\n .map(r => ({\n type: 'document' as const,\n score: normalizeBM25(r.bm25_score),\n filePath: r.file_path,\n content: r.content,\n context: this._getDocContext(r.collection, r.file_path),\n metadata: {\n collection: r.collection,\n title: r.title,\n seq: r.seq,\n chunkId: r.id,\n },\n }))\n .filter(r => r.score >= minScore)\n .slice(0, k);\n } catch {\n return [];\n }\n }\n\n /** Build OR-mode FTS5 query for natural language doc search. */\n private _buildDocsFTS(query: string): string {\n const STOP_WORDS = new Set([\n 'the', 'is', 'at', 'which', 'on', 'a', 'an', 'and', 'or', 'but',\n 'in', 'with', 'to', 'for', 'of', 'by', 'from', 'as', 'it', 'its',\n 'this', 'that', 'be', 'are', 'was', 'were', 'been', 'has', 'have',\n 'had', 'do', 'does', 'did', 'can', 'could', 'will', 'would', 'how',\n 'what', 'when', 'where', 'who', 'why', 'not', 'no', 'so', 'if',\n ]);\n\n const clean = query\n .replace(/[{}[\\]()^~*:\"]/g, ' ')\n .replace(/\\bAND\\b|\\bOR\\b|\\bNOT\\b|\\bNEAR\\b/gi, '')\n .replace(/[_\\-./\\\\]/g, ' ')\n .trim();\n\n const words = clean.split(/\\s+/)\n .filter(w => w.length >= 3 && !STOP_WORDS.has(w.toLowerCase()));\n\n if (words.length === 0) return '';\n return words.map(w => `\"${w}\"`).join(' OR ');\n }\n\n /** Resolve context for a document (checks path_contexts tree → collection context). */\n private _getDocContext(collection: string, filePath: string): string | undefined {\n const parts = filePath.split('/');\n for (let i = parts.length; i >= 0; i--) {\n const checkPath = i === 0 ? '/' : '/' + parts.slice(0, i).join('/');\n const ctx = this._d.db.prepare(\n 'SELECT context FROM path_contexts WHERE collection = ? AND path = ?'\n ).get(collection, checkPath) as any;\n if (ctx) return ctx.context;\n }\n\n const coll = this._d.db.prepare(\n 'SELECT context FROM collections WHERE name = ?'\n ).get(collection) as any;\n return coll?.context ?? undefined;\n }\n}\n","/**\n * BrainBank — Docs Module\n * \n * Index any folder of markdown/text files (notes, docs, wikis).\n * Heading-aware smart chunking inspired by qmd.\n * \n * import { docs } from 'brainbank/docs';\n * brain.use(docs());\n */\n\nimport type { Indexer, IndexerContext } from '@/indexers/base.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\nimport type { Database } from '@/db/database.ts';\nimport type { EmbeddingProvider, DocumentCollection, SearchResult } from '@/types.ts';\nimport { DocsIndexer } from './docs-indexer.ts';\nimport { DocsSearch } from './docs-search.ts';\n\nclass DocsPlugin implements Indexer {\n readonly name = 'docs';\n hnsw!: HNSWIndex;\n indexer!: DocsIndexer;\n vecCache = new Map<number, Float32Array>();\n private _db!: Database;\n private _search!: DocsSearch;\n\n async initialize(ctx: IndexerContext): Promise<void> {\n this._db = ctx.db;\n this.hnsw = await ctx.createHnsw();\n ctx.loadVectors('doc_vectors', 'chunk_id', this.hnsw, this.vecCache);\n this.indexer = new DocsIndexer(ctx.db, ctx.embedding, this.hnsw, this.vecCache);\n this._search = new DocsSearch({\n db: ctx.db,\n embedding: ctx.embedding,\n hnsw: this.hnsw,\n vecCache: this.vecCache,\n reranker: ctx.config.reranker,\n });\n }\n\n /** Register a document collection. */\n addCollection(collection: DocumentCollection): void {\n this._db.prepare(`\n INSERT OR REPLACE INTO collections (name, path, pattern, ignore_json, context)\n VALUES (?, ?, ?, ?, ?)\n `).run(\n collection.name,\n collection.path,\n collection.pattern ?? '**/*.md',\n JSON.stringify(collection.ignore ?? []),\n collection.context ?? null,\n );\n }\n\n /** Remove a collection and its indexed data. */\n removeCollection(name: string): void {\n this.indexer.removeCollection(name);\n }\n\n /** List all registered collections. */\n listCollections(): DocumentCollection[] {\n return (this._db.prepare('SELECT * FROM collections').all() as any[]).map(row => ({\n name: row.name,\n path: row.path,\n pattern: row.pattern,\n ignore: JSON.parse(row.ignore_json),\n context: row.context,\n }));\n }\n\n /** Index all (or specific) collections. Incremental. */\n async indexCollections(options: {\n collections?: string[];\n onProgress?: (collection: string, file: string, current: number, total: number) => void;\n } = {}): Promise<Record<string, { indexed: number; skipped: number; chunks: number }>> {\n const allCollections = this.listCollections();\n const toIndex = options.collections\n ? allCollections.filter(c => options.collections!.includes(c.name))\n : allCollections;\n\n const results: Record<string, { indexed: number; skipped: number; chunks: number }> = {};\n\n for (const coll of toIndex) {\n results[coll.name] = await this.indexer.indexCollection(\n coll.name,\n coll.path,\n coll.pattern,\n {\n ignore: coll.ignore,\n onProgress: (file, cur, total) => options.onProgress?.(coll.name, file, cur, total),\n },\n );\n }\n\n return results;\n }\n\n /** Search documents using hybrid search (vector + BM25 → RRF). */\n async search(query: string, options?: {\n collection?: string;\n k?: number;\n minScore?: number;\n mode?: 'hybrid' | 'vector' | 'keyword';\n }): Promise<SearchResult[]> {\n return this._search.search(query, options);\n }\n\n /** Add context description for a document path. */\n addContext(collection: string, path: string, context: string): void {\n this._db.prepare(`\n INSERT OR REPLACE INTO path_contexts (collection, path, context)\n VALUES (?, ?, ?)\n `).run(collection, path, context);\n }\n\n /** Remove context for a path. */\n removeContext(collection: string, path: string): void {\n this._db.prepare(\n 'DELETE FROM path_contexts WHERE collection = ? AND path = ?'\n ).run(collection, path);\n }\n\n /** List all context entries. */\n listContexts(): { collection: string; path: string; context: string }[] {\n return this._db.prepare('SELECT * FROM path_contexts').all() as any[];\n }\n\n stats(): Record<string, any> {\n return {\n collections: (this._db.prepare('SELECT COUNT(*) as c FROM collections').get() as any).c,\n documents: (this._db.prepare('SELECT COUNT(DISTINCT file_path) as c FROM doc_chunks').get() as any).c,\n chunks: (this._db.prepare('SELECT COUNT(*) as c FROM doc_chunks').get() as any).c,\n hnswSize: this.hnsw.size,\n };\n }\n}\n\n/** Create a document collections plugin. */\nexport function docs(): Indexer {\n return new DocsPlugin();\n}\n"],"mappings":"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","names":["path"]}
package/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BrainBank
4
- } from "./chunk-2BEWWQL2.js";
4
+ } from "./chunk-MY36UPPQ.js";
5
5
  import {
6
6
  code
7
- } from "./chunk-MGIFEPYZ.js";
7
+ } from "./chunk-DDECTPRM.js";
8
8
  import {
9
9
  git
10
- } from "./chunk-FI7GWG4W.js";
10
+ } from "./chunk-TTXVJFAE.js";
11
11
  import {
12
12
  docs
13
- } from "./chunk-Y3JKI6QN.js";
14
- import "./chunk-E6WQM4DN.js";
15
- import "./chunk-QNHBCOKB.js";
13
+ } from "./chunk-YRGUIRN5.js";
14
+ import "./chunk-YOLKSYWK.js";
15
+ import "./chunk-U2Q2XGPZ.js";
16
16
  import {
17
17
  __name
18
18
  } from "./chunk-7QVYU63E.js";
@@ -108,10 +108,11 @@ import * as path from "path";
108
108
  import * as fs from "fs";
109
109
  var CONFIG_NAMES = ["config.ts", "config.js", "config.mjs"];
110
110
  var INDEXER_EXTENSIONS = [".ts", ".js", ".mjs"];
111
- var _configCache = void 0;
112
- var _folderIndexersCache = void 0;
111
+ var NOT_LOADED = /* @__PURE__ */ Symbol("not-loaded");
112
+ var _configCache = NOT_LOADED;
113
+ var _folderIndexersCache = NOT_LOADED;
113
114
  async function loadConfig() {
114
- if (_configCache !== void 0) return _configCache;
115
+ if (_configCache !== NOT_LOADED) return _configCache;
115
116
  const repoPath = getFlag("repo") ?? ".";
116
117
  const brainbankDir = path.resolve(repoPath, ".brainbank");
117
118
  for (const name of CONFIG_NAMES) {
@@ -132,7 +133,7 @@ async function loadConfig() {
132
133
  }
133
134
  __name(loadConfig, "loadConfig");
134
135
  async function discoverFolderIndexers() {
135
- if (_folderIndexersCache !== void 0) return _folderIndexersCache;
136
+ if (_folderIndexersCache !== NOT_LOADED) return _folderIndexersCache;
136
137
  const repoPath = getFlag("repo") ?? ".";
137
138
  const indexersDir = path.resolve(repoPath, ".brainbank", "indexers");
138
139
  if (!fs.existsSync(indexersDir)) {
@@ -197,6 +198,16 @@ async function setupProviders(brainOpts) {
197
198
  const provider = new OpenAIEmbedding();
198
199
  brainOpts.embeddingProvider = provider;
199
200
  brainOpts.embeddingDims = provider.dims;
201
+ } else if (process.env.BRAINBANK_EMBEDDING === "perplexity") {
202
+ const { PerplexityEmbedding } = await import("./perplexity-embedding-227WQY4R.js");
203
+ const provider = new PerplexityEmbedding();
204
+ brainOpts.embeddingProvider = provider;
205
+ brainOpts.embeddingDims = provider.dims;
206
+ } else if (process.env.BRAINBANK_EMBEDDING === "perplexity-context") {
207
+ const { PerplexityContextEmbedding } = await import("./perplexity-context-embedding-KSVSZXMD.js");
208
+ const provider = new PerplexityContextEmbedding();
209
+ brainOpts.embeddingProvider = provider;
210
+ brainOpts.embeddingDims = provider.dims;
200
211
  }
201
212
  }
202
213
  __name(setupProviders, "setupProviders");