brainbank 0.6.0 → 0.7.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/indexers/docs/docs-indexer.ts","../src/search/vector/rerank.ts","../src/indexers/docs/document-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._flushRemainder(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 _flushRemainder(\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 DocumentSearchDeps {\n db: Database;\n embedding: EmbeddingProvider;\n hnsw: HNSWIndex;\n vecCache: Map<number, Float32Array>;\n reranker?: Reranker;\n}\n\nexport class DocumentSearch {\n constructor(private _d: DocumentSearchDeps) {}\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._rerankResults(query, deduped);\n }\n\n /** Apply reranking if a reranker is configured. */\n private async _rerankResults(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 { Plugin, PluginContext } 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 { DocumentSearch } from './document-search.ts';\n\nclass DocsPlugin implements Plugin {\n readonly name = 'docs';\n hnsw!: HNSWIndex;\n indexer!: DocsIndexer;\n vecCache = new Map<number, Float32Array>();\n private _db!: Database;\n private _search!: DocumentSearch;\n\n async initialize(ctx: PluginContext): 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 DocumentSearch({\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(): Plugin {\n return new DocsPlugin();\n}\n"],"mappings":"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","names":["path"]}
1
+ {"version":3,"sources":["../src/indexers/docs/docs-indexer.ts","../src/search/vector/rerank.ts","../src/indexers/docs/document-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._flushRemainder(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 _flushRemainder(\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 DocumentSearchDeps {\n db: Database;\n embedding: EmbeddingProvider;\n hnsw: HNSWIndex;\n vecCache: Map<number, Float32Array>;\n reranker?: Reranker;\n}\n\nexport class DocumentSearch {\n constructor(private _d: DocumentSearchDeps) {}\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._rerankResults(query, deduped);\n }\n\n /** Apply reranking if a reranker is configured. */\n private async _rerankResults(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 { Plugin, PluginContext } 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 { DocumentSearch } from './document-search.ts';\n\nclass DocsPlugin implements Plugin {\n readonly name = 'docs';\n hnsw!: HNSWIndex;\n indexer!: DocsIndexer;\n vecCache = new Map<number, Float32Array>();\n private _db!: Database;\n private _search!: DocumentSearch;\n\n constructor(private opts: { embeddingProvider?: EmbeddingProvider } = {}) {}\n\n async initialize(ctx: PluginContext): Promise<void> {\n this._db = ctx.db;\n const embedding = this.opts.embeddingProvider ?? ctx.embedding;\n\n this.hnsw = await ctx.createHnsw(undefined, embedding.dims);\n ctx.loadVectors('doc_vectors', 'chunk_id', this.hnsw, this.vecCache);\n this.indexer = new DocsIndexer(ctx.db, embedding, this.hnsw, this.vecCache);\n this._search = new DocumentSearch({\n db: ctx.db,\n 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\nexport interface DocsPluginOptions {\n /** Per-plugin embedding provider. Default: global embedding from BrainBank config. */\n embeddingProvider?: EmbeddingProvider;\n}\n\n/** Create a document collections plugin. */\nexport function docs(opts?: DocsPluginOptions): Plugin {\n return new DocsPlugin(opts);\n}\n"],"mappings":"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","names":["path"]}
package/dist/cli.js CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BrainBank
4
- } from "./chunk-YC4ZQLDN.js";
4
+ } from "./chunk-DI3H6JVZ.js";
5
5
  import "./chunk-B77KABWH.js";
6
6
  import {
7
7
  code
8
- } from "./chunk-PXK62M5W.js";
8
+ } from "./chunk-FGL32LUJ.js";
9
9
  import {
10
10
  git
11
- } from "./chunk-HPNUMUIF.js";
11
+ } from "./chunk-JRSKWF6K.js";
12
12
  import {
13
13
  docs
14
- } from "./chunk-C4KDZGRX.js";
14
+ } from "./chunk-VQ27YUHH.js";
15
15
  import "./chunk-YOLKSYWK.js";
16
16
  import "./chunk-U2Q2XGPZ.js";
17
17
  import {
@@ -107,7 +107,7 @@ import * as path2 from "path";
107
107
  // src/cli/factory.ts
108
108
  import * as path from "path";
109
109
  import * as fs from "fs";
110
- var CONFIG_NAMES = ["config.ts", "config.js", "config.mjs"];
110
+ var CONFIG_NAMES = ["config.json", "config.ts", "config.js", "config.mjs"];
111
111
  var INDEXER_EXTENSIONS = [".ts", ".js", ".mjs"];
112
112
  var NOT_LOADED = /* @__PURE__ */ Symbol("not-loaded");
113
113
  var _configCache = NOT_LOADED;
@@ -118,21 +118,34 @@ async function loadConfig() {
118
118
  const brainbankDir = path.resolve(repoPath, ".brainbank");
119
119
  for (const name of CONFIG_NAMES) {
120
120
  const configPath = path.join(brainbankDir, name);
121
- if (fs.existsSync(configPath)) {
122
- try {
121
+ if (!fs.existsSync(configPath)) continue;
122
+ try {
123
+ if (name === "config.json") {
124
+ const raw = fs.readFileSync(configPath, "utf-8");
125
+ _configCache = JSON.parse(raw);
126
+ } else {
123
127
  const mod = await import(configPath);
124
128
  _configCache = mod.default ?? mod;
125
- return _configCache;
126
- } catch (err) {
127
- console.error(c.red(`Error loading .brainbank/${name}: ${err.message}`));
128
- process.exit(1);
129
129
  }
130
+ return _configCache;
131
+ } catch (err) {
132
+ console.error(c.red(`Error loading .brainbank/${name}: ${err.message}`));
133
+ process.exit(1);
130
134
  }
131
135
  }
132
136
  _configCache = null;
133
137
  return null;
134
138
  }
135
139
  __name(loadConfig, "loadConfig");
140
+ async function getConfig() {
141
+ return loadConfig();
142
+ }
143
+ __name(getConfig, "getConfig");
144
+ async function resolveEmbeddingKey(key) {
145
+ const { resolveEmbedding } = await import("./resolve-CUJWY6HP.js");
146
+ return resolveEmbedding(key);
147
+ }
148
+ __name(resolveEmbeddingKey, "resolveEmbeddingKey");
136
149
  async function discoverFolderPlugins() {
137
150
  if (_folderPluginsCache !== NOT_LOADED) return _folderPluginsCache;
138
151
  const repoPath = getFlag("repo") ?? ".";
@@ -177,10 +190,11 @@ async function createBrain(repoPath) {
177
190
  const config = await loadConfig();
178
191
  const folderIndexers = await discoverFolderPlugins();
179
192
  const brainOpts = { repoPath: rp, ...config?.brainbank ?? {} };
180
- await setupProviders(brainOpts);
193
+ if (config?.maxFileSize) brainOpts.maxFileSize = config.maxFileSize;
194
+ await setupProviders(brainOpts, config);
181
195
  const brain = new BrainBank(brainOpts);
182
- const builtins = config?.builtins ?? ["code", "git", "docs"];
183
- registerBuiltins(brain, rp, builtins);
196
+ const builtins = config?.plugins ?? config?.builtins ?? ["code", "git", "docs"];
197
+ await registerBuiltins(brain, rp, builtins, config);
184
198
  for (const indexer of folderIndexers) brain.use(indexer);
185
199
  if (config?.indexers) {
186
200
  for (const indexer of config.indexers) brain.use(indexer);
@@ -188,38 +202,87 @@ async function createBrain(repoPath) {
188
202
  return brain;
189
203
  }
190
204
  __name(createBrain, "createBrain");
191
- async function setupProviders(brainOpts) {
192
- const rerankerFlag = getFlag("reranker");
205
+ async function setupProviders(brainOpts, config) {
206
+ const rerankerFlag = getFlag("reranker") ?? config?.reranker;
193
207
  if (rerankerFlag === "qwen3") {
194
208
  const { Qwen3Reranker } = await import("./qwen3-reranker-3MHEENT5.js");
195
209
  brainOpts.reranker = new Qwen3Reranker();
196
210
  }
197
- const embFlag = getFlag("embedding");
211
+ const embFlag = getFlag("embedding") ?? config?.embedding;
198
212
  if (embFlag) {
199
- const { resolveEmbedding } = await import("./resolve-CUJWY6HP.js");
200
- const provider = await resolveEmbedding(embFlag);
213
+ const provider = await resolveEmbeddingKey(embFlag);
201
214
  brainOpts.embeddingProvider = provider;
202
215
  brainOpts.embeddingDims = provider.dims;
203
216
  }
204
217
  }
205
218
  __name(setupProviders, "setupProviders");
206
- function registerBuiltins(brain, rp, builtins) {
219
+ async function registerBuiltins(brain, rp, builtins, config) {
207
220
  const resolvedRp = path.resolve(rp);
208
221
  const hasRootGit = fs.existsSync(path.join(resolvedRp, ".git"));
209
222
  const gitSubdirs = !hasRootGit ? detectGitSubdirs(resolvedRp) : [];
223
+ const codeEmb = config?.code?.embedding ? await resolveEmbeddingKey(config.code.embedding) : void 0;
224
+ const gitEmb = config?.git?.embedding ? await resolveEmbeddingKey(config.git.embedding) : void 0;
225
+ const docsEmb = config?.docs?.embedding ? await resolveEmbeddingKey(config.docs.embedding) : void 0;
210
226
  if (gitSubdirs.length > 0 && (builtins.includes("code") || builtins.includes("git"))) {
211
227
  console.log(c.cyan(` Multi-repo: found ${gitSubdirs.length} git repos: ${gitSubdirs.map((d) => d.name).join(", ")}`));
212
228
  for (const sub of gitSubdirs) {
213
- if (builtins.includes("code")) brain.use(code({ repoPath: sub.path, name: `code:${sub.name}` }));
214
- if (builtins.includes("git")) brain.use(git({ repoPath: sub.path, name: `git:${sub.name}` }));
229
+ if (builtins.includes("code")) {
230
+ brain.use(code({
231
+ repoPath: sub.path,
232
+ name: `code:${sub.name}`,
233
+ embeddingProvider: codeEmb,
234
+ maxFileSize: config?.code?.maxFileSize
235
+ }));
236
+ }
237
+ if (builtins.includes("git")) {
238
+ brain.use(git({
239
+ repoPath: sub.path,
240
+ name: `git:${sub.name}`,
241
+ embeddingProvider: gitEmb,
242
+ depth: config?.git?.depth,
243
+ maxDiffBytes: config?.git?.maxDiffBytes
244
+ }));
245
+ }
215
246
  }
216
247
  } else {
217
- if (builtins.includes("code")) brain.use(code({ repoPath: rp }));
218
- if (builtins.includes("git")) brain.use(git());
248
+ if (builtins.includes("code")) {
249
+ brain.use(code({
250
+ repoPath: rp,
251
+ embeddingProvider: codeEmb,
252
+ maxFileSize: config?.code?.maxFileSize
253
+ }));
254
+ }
255
+ if (builtins.includes("git")) {
256
+ brain.use(git({
257
+ embeddingProvider: gitEmb,
258
+ depth: config?.git?.depth,
259
+ maxDiffBytes: config?.git?.maxDiffBytes
260
+ }));
261
+ }
262
+ }
263
+ if (builtins.includes("docs")) {
264
+ brain.use(docs({ embeddingProvider: docsEmb }));
219
265
  }
220
- if (builtins.includes("docs")) brain.use(docs());
221
266
  }
222
267
  __name(registerBuiltins, "registerBuiltins");
268
+ async function registerConfigCollections(brain, config) {
269
+ const collections = config?.docs?.collections;
270
+ if (!collections?.length) return;
271
+ for (const coll of collections) {
272
+ const absPath = path.resolve(coll.path);
273
+ try {
274
+ await brain.addCollection({
275
+ name: coll.name,
276
+ path: absPath,
277
+ pattern: coll.pattern ?? "**/*.md",
278
+ ignore: coll.ignore,
279
+ context: coll.context
280
+ });
281
+ } catch {
282
+ }
283
+ }
284
+ }
285
+ __name(registerConfigCollections, "registerConfigCollections");
223
286
 
224
287
  // src/cli/commands/index-cmd.ts
225
288
  async function cmdIndex() {
@@ -239,6 +302,8 @@ async function cmdIndex() {
239
302
  if (modules) console.log(c.dim(` Modules: ${modules.join(", ")}`));
240
303
  if (docsPath) console.log(c.dim(` Docs path: ${docsPath}`));
241
304
  const brain = await createBrain(repoPath);
305
+ const config = await getConfig();
306
+ await registerConfigCollections(brain, config);
242
307
  if (docsPath) {
243
308
  const absDocsPath = path2.resolve(docsPath);
244
309
  const collName = path2.basename(absDocsPath);