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.
- package/README.md +132 -61
- package/dist/{base-B_vJSAbj.d.ts → base-3SNc_CeY.d.ts} +4 -4
- package/dist/{chunk-YC4ZQLDN.js → chunk-DI3H6JVZ.js} +8 -7
- package/dist/chunk-DI3H6JVZ.js.map +1 -0
- package/dist/{chunk-PXK62M5W.js → chunk-FGL32LUJ.js} +4 -3
- package/dist/{chunk-PXK62M5W.js.map → chunk-FGL32LUJ.js.map} +1 -1
- package/dist/{chunk-HPNUMUIF.js → chunk-JRSKWF6K.js} +4 -3
- package/dist/{chunk-HPNUMUIF.js.map → chunk-JRSKWF6K.js.map} +1 -1
- package/dist/{chunk-C4KDZGRX.js → chunk-VQ27YUHH.js} +10 -6
- package/dist/{chunk-C4KDZGRX.js.map → chunk-VQ27YUHH.js.map} +1 -1
- package/dist/cli.js +90 -25
- package/dist/cli.js.map +1 -1
- package/dist/code.d.ts +3 -1
- package/dist/code.js +1 -1
- package/dist/docs.d.ts +7 -3
- package/dist/docs.js +1 -1
- package/dist/git.d.ts +3 -1
- package/dist/git.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/memory.d.ts +1 -1
- package/dist/notes.d.ts +1 -1
- package/package.json +2 -2
- package/dist/chunk-YC4ZQLDN.js.map +0 -1
|
@@ -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":";;;;;;;;;AAUA,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,kBAAkB;AAa3B,IAAM,eAAmC;AAAA,EACrC,CAAC,OAAY,GAAG;AAAA;AAAA,EAChB,CAAC,QAAa,EAAE;AAAA;AAAA,EAChB,CAAC,SAAa,EAAE;AAAA;AAAA,EAChB,CAAC,UAAa,EAAE;AAAA;AAAA,EAChB,CAAC,WAAa,EAAE;AAAA;AAAA,EAChB,CAAC,YAAa,EAAE;AAAA;AAAA,EAChB,CAAC,QAAa,EAAE;AAAA;AAAA,EAChB,CAAC,SAAa,EAAE;AAAA;AAAA,EAChB,CAAC,YAAa,EAAE;AAAA;AAAA,EAChB,CAAC,MAAa,EAAE;AAAA;AAAA,EAChB,CAAC,WAAc,CAAC;AAAA;AACpB;AAIA,IAAM,eAAe;AACrB,IAAM,eAAe;AACrB,IAAM,kBAAkB;AAIxB,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC7B;AAAA,EAAgB;AAAA,EAAQ;AAAA,EAAO;AAAA,EAC/B;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAO;AAAA,EAAY;AAAA,EACpC;AAAA,EAAe;AAAA,EAAQ;AAAA,EAAS;AAAA,EAChC;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAClC,CAAC;AAIM,IAAM,cAAN,MAAkB;AAAA,EACrB,YACY,KACA,YACA,OACA,WACV;AAJU;AACA;AACA;AACA;AAAA,EACT;AAAA,EA9DP,OAwDyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYrB,MAAM,gBACF,YACA,SACA,UAAkB,WAClB,UAGI,CAAC,GACwD;AAC7D,UAAM,SAAc,aAAQ,OAAO;AACnC,QAAI,CAAI,cAAW,MAAM,GAAG;AACxB,YAAM,IAAI,MAAM,mCAAmC,MAAM,EAAE;AAAA,IAC/D;AAEA,UAAM,QAAQ,KAAK,WAAW,QAAQ,SAAS,QAAQ,MAAM;AAC7D,QAAI,UAAU,GAAG,UAAU,GAAG,cAAc;AAE5C,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACnC,YAAM,UAAU,MAAM,CAAC;AACvB,cAAQ,aAAa,SAAS,IAAI,GAAG,MAAM,MAAM;AAEjD,YAAM,UAAe,UAAK,QAAQ,OAAO;AACzC,YAAM,UAAa,gBAAa,SAAS,OAAO;AAChD,YAAM,OAAO,WAAW,QAAQ,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAE3E,UAAI,KAAK,aAAa,YAAY,SAAS,IAAI,GAAG;AAC9C;AACA;AAAA,MACJ;AAEA,WAAK,iBAAiB,YAAY,OAAO;AACzC,YAAM,aAAa,MAAM,KAAK,WAAW,YAAY,SAAS,SAAS,IAAI;AAC3E;AACA,qBAAe;AAAA,IACnB;AAEA,WAAO,EAAE,SAAS,SAAS,QAAQ,YAAY;AAAA,EACnD;AAAA;AAAA,EAGQ,WAAW,QAAgB,SAAiB,QAA6B;AAC7E,UAAM,aAAa,QAAQ,MAAM,YAAY,IAAI,CAAC;AAClD,UAAM,QAAkB,CAAC;AAEzB,UAAM,OAAO,wBAAC,KAAa,SAAuB;AAC9C,UAAI;AACJ,UAAI;AAAE,kBAAa,eAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,MAAG,QACxD;AAAE;AAAA,MAAQ;AAChB,iBAAW,KAAK,SAAS;AACrB,cAAM,MAAM,OAAO,GAAG,IAAI,IAAI,EAAE,IAAI,KAAK,EAAE;AAC3C,YAAI,EAAE,YAAY,GAAG;AACjB,cAAI,iBAAiB,IAAI,EAAE,IAAI,EAAG;AAClC,eAAU,UAAK,KAAK,EAAE,IAAI,GAAG,GAAG;AAAA,QACpC,WAAW,EAAE,OAAO,GAAG;AACnB,cAAI,KAAK,eAAe,KAAK,MAAM,EAAG;AACtC,gBAAM,MAAW,aAAQ,EAAE,IAAI,EAAE,MAAM,CAAC;AACxC,cAAI,CAAC,cAAc,QAAQ,WAAY,OAAM,KAAK,GAAG;AAAA,QACzD;AAAA,MACJ;AAAA,IACJ,GAfa;AAgBb,SAAK,QAAQ,EAAE;AACf,WAAO;AAAA,EACX;AAAA;AAAA,EAGQ,eAAe,SAAiB,QAA4B;AAChE,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,OAAO,KAAK,QAAM;AACrB,YAAM,QAAQ,GACT,QAAQ,SAAS,cAAc,EAC/B,QAAQ,OAAO,UAAU,EACzB,QAAQ,sBAAsB,MAAM,EACpC,QAAQ,qBAAqB,IAAI,EACjC,QAAQ,iBAAiB,OAAO;AACrC,aAAO,IAAI,OAAO,KAAK,EAAE,KAAK,OAAO;AAAA,IACzC,CAAC;AAAA,EACL;AAAA;AAAA,EAGQ,aAAa,YAAoB,SAAiB,MAAuB;AAC7E,UAAM,WAAW,KAAK,IAAI;AAAA,MACtB;AAAA;AAAA;AAAA;AAAA,IAIJ,EAAE,IAAI,YAAY,OAAO;AAEzB,WAAO,SAAS,SAAS,KACrB,SAAS,MAAM,CAAC,MAAW,EAAE,iBAAiB,QAAQ,EAAE,cAAc,IAAI;AAAA,EAClF;AAAA;AAAA,EAGQ,iBAAiB,YAAoB,SAAuB;AAChE,UAAM,YAAY,KAAK,IAAI;AAAA,MACvB;AAAA,IACJ,EAAE,IAAI,YAAY,OAAO;AAEzB,eAAW,OAAO,WAAW;AACzB,WAAK,MAAM,OAAO,IAAI,EAAE;AACxB,WAAK,UAAU,OAAO,IAAI,EAAE;AAAA,IAChC;AACA,SAAK,IAAI;AAAA,MACL;AAAA,IACJ,EAAE,IAAI,YAAY,OAAO;AAAA,EAC7B;AAAA;AAAA,EAGA,MAAc,WACV,YAAoB,SAAiB,SAAiB,MACvC;AACf,UAAM,QAAQ,KAAK,cAAc,SAAS,OAAO;AACjD,UAAM,SAAS,KAAK,YAAY,OAAO;AAEvC,UAAM,cAAc,KAAK,IAAI,QAAQ;AAAA;AAAA;AAAA,SAGpC;AAED,UAAM,WAAqB,CAAC;AAC5B,SAAK,IAAI,YAAY,MAAM;AACvB,eAAS,MAAM,GAAG,MAAM,OAAO,QAAQ,OAAO;AAC1C,cAAM,SAAS,YAAY;AAAA,UACvB;AAAA,UAAY;AAAA,UAAS;AAAA,UAAO,OAAO,GAAG,EAAE;AAAA,UAAM;AAAA,UAAK,OAAO,GAAG,EAAE;AAAA,UAAK;AAAA,QACxE;AACA,iBAAS,KAAK,OAAO,OAAO,eAAe,CAAC;AAAA,MAChD;AAAA,IACJ,CAAC;AAED,UAAM,QAAQ,OAAO,IAAI,OAAK,UAAU,KAAK,YAAY,EAAE,IAAI,EAAE;AACjE,UAAM,aAAa,MAAM,KAAK,WAAW,WAAW,KAAK;AAEzD,UAAM,YAAY,KAAK,IAAI;AAAA,MACvB;AAAA,IACJ;AACA,SAAK,IAAI,YAAY,MAAM;AACvB,eAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACtC,kBAAU,IAAI,SAAS,CAAC,GAAG,OAAO,KAAK,WAAW,CAAC,EAAE,MAAM,CAAC;AAAA,MAChE;AAAA,IACJ,CAAC;AAED,aAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACtC,WAAK,MAAM,IAAI,WAAW,CAAC,GAAG,SAAS,CAAC,CAAC;AACzC,WAAK,UAAU,IAAI,SAAS,CAAC,GAAG,WAAW,CAAC,CAAC;AAAA,IACjD;AAEA,WAAO,OAAO;AAAA,EAClB;AAAA;AAAA,EAGA,iBAAiB,YAA0B;AACvC,UAAM,SAAS,KAAK,IAAI;AAAA,MACpB;AAAA,IACJ,EAAE,IAAI,UAAU;AAChB,eAAW,SAAS,QAAQ;AACxB,WAAK,MAAM,OAAO,MAAM,EAAE;AAC1B,WAAK,UAAU,OAAO,MAAM,EAAE;AAAA,IAClC;AAEA,SAAK,IAAI,QAAQ,6CAA6C,EAAE,IAAI,UAAU;AAC9E,SAAK,IAAI,QAAQ,wCAAwC,EAAE,IAAI,UAAU;AACzE,SAAK,IAAI,QAAQ,gDAAgD,EAAE,IAAI,UAAU;AAAA,EACrF;AAAA;AAAA;AAAA,EAKQ,YAAY,MAA+C;AAC/D,QAAI,KAAK,UAAU,cAAc;AAC7B,aAAO,CAAC,EAAE,MAAM,KAAK,EAAE,CAAC;AAAA,IAC5B;AAEA,UAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,UAAM,cAAc,KAAK,iBAAiB,KAAK;AAC/C,UAAM,SAA0C,CAAC;AACjD,QAAI,aAAa;AAEjB,WAAO,aAAa,KAAK,QAAQ;AAC7B,YAAM,YAAY,KAAK,SAAS;AAChC,UAAI,aAAa,eAAe,cAAc;AAC1C,aAAK,gBAAgB,MAAM,YAAY,MAAM;AAC7C;AAAA,MACJ;AAEA,YAAM,YAAY,KAAK,eAAe,YAAY,WAAW;AAC7D,YAAM,YAAY,KAAK,MAAM,YAAY,SAAS,EAAE,KAAK;AACzD,UAAI,UAAU,UAAU,iBAAiB;AACrC,eAAO,KAAK,EAAE,MAAM,WAAW,KAAK,WAAW,CAAC;AAAA,MACpD;AACA,mBAAa;AAAA,IACjB;AAEA,WAAO;AAAA,EACX;AAAA;AAAA,EAGQ,gBACJ,MAAc,YAAoB,QAC9B;AACJ,UAAM,WAAW,KAAK,MAAM,UAAU,EAAE,KAAK;AAC7C,QAAI,SAAS,UAAU,iBAAiB;AACpC,aAAO,KAAK,EAAE,MAAM,UAAU,KAAK,WAAW,CAAC;AAAA,IACnD,WAAW,OAAO,SAAS,GAAG;AAC1B,aAAO,OAAO,SAAS,CAAC,EAAE,QAAQ,OAAO;AAAA,IAC7C,OAAO;AACH,aAAO,KAAK,EAAE,MAAM,UAAU,KAAK,WAAW,CAAC;AAAA,IACnD;AAAA,EACJ;AAAA;AAAA,EAGQ,eAAe,YAAoB,aAAmC;AAC1E,UAAM,YAAY,aAAa;AAC/B,UAAM,cAAc,YAAY;AAEhC,QAAI,YAAY;AAChB,QAAI,YAAY;AAEhB,eAAW,MAAM,aAAa;AAC1B,UAAI,GAAG,OAAO,WAAY;AAC1B,UAAI,GAAG,MAAM,YAAY,eAAe,EAAG;AAC3C,UAAI,GAAG,MAAM,YAAa;AAE1B,YAAM,WAAW,KAAK,IAAI,GAAG,MAAM,SAAS;AAC5C,YAAM,QAAQ,KAAK,WAAW,iBAAiB,IAAI;AACnD,YAAM,aAAa,GAAG,QAAQ;AAE9B,UAAI,aAAa,WAAW;AACxB,oBAAY;AACZ,oBAAY,GAAG;AAAA,MACnB;AAAA,IACJ;AAEA,WAAO;AAAA,EACX;AAAA;AAAA,EAGQ,iBAAiB,OAA+B;AACpD,UAAM,SAAuB,CAAC;AAC9B,QAAI,UAAU;AACd,QAAI,cAAc;AAElB,eAAW,QAAQ,OAAO;AACtB,UAAI,KAAK,UAAU,EAAE,WAAW,KAAK,GAAG;AACpC,sBAAc,CAAC;AACf,YAAI,CAAC,aAAa;AACd,iBAAO,KAAK,EAAE,KAAK,UAAU,KAAK,SAAS,GAAG,OAAO,GAAG,CAAC;AAAA,QAC7D;AACA,mBAAW,KAAK,SAAS;AACzB;AAAA,MACJ;AAEA,UAAI,aAAa;AACb,mBAAW,KAAK,SAAS;AACzB;AAAA,MACJ;AAEA,iBAAW,CAAC,SAAS,KAAK,KAAK,cAAc;AACzC,YAAI,QAAQ,KAAK,KAAK,KAAK,CAAC,GAAG;AAC3B,iBAAO,KAAK,EAAE,KAAK,SAAS,MAAM,CAAC;AACnC;AAAA,QACJ;AAAA,MACJ;AAEA,iBAAW,KAAK,SAAS;AAAA,IAC7B;AAEA,WAAO;AAAA,EACX;AAAA;AAAA,EAGQ,cAAc,SAAiB,UAA0B;AAC7D,UAAM,QAAQ,QAAQ,MAAM,kBAAkB;AAC9C,QAAI,MAAO,QAAO,MAAM,CAAC,EAAE,KAAK;AAChC,WAAY,cAAS,UAAe,aAAQ,QAAQ,CAAC;AAAA,EACzD;AACJ;;;ACxUA,eAAsB,OAClB,OACA,SACA,UACuB;AACvB,QAAM,YAAY,QAAQ,IAAI,OAAK,EAAE,OAAO;AAC5C,QAAM,SAAS,MAAM,SAAS,KAAK,OAAO,SAAS;AAEnD,QAAM,UAAU,QAAQ,IAAI,CAAC,GAAG,MAAM;AAClC,UAAM,MAAM,IAAI;AAChB,UAAM,YAAY,OAAO,IAAI,OAAO,OAAO,KAAK,MAAO;AACvD,WAAO;AAAA,MACH,GAAG;AAAA,MACH,OAAO,YAAY,EAAE,SAAS,IAAI,cAAc,OAAO,CAAC,KAAK;AAAA,IACjE;AAAA,EACJ,CAAC;AAED,SAAO,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACnD;AAlBsB;;;ACQf,IAAM,iBAAN,MAAqB;AAAA,EACxB,YAAoB,IAAwB;AAAxB;AAAA,EAAyB;AAAA,EAvBjD,OAsB4B;AAAA;AAAA;AAAA;AAAA,EAIxB,MAAM,OAAO,OAAe,SAKA;AACxB,UAAM,IAAI,SAAS,KAAK;AACxB,UAAM,OAAO,SAAS,QAAQ;AAC9B,UAAM,WAAW,SAAS,YAAY;AAEtC,QAAI,SAAS,UAAW,QAAO,KAAK,OAAO,KAAK,YAAY,OAAO,IAAI,GAAG,UAAU,SAAS,UAAU,GAAG,CAAC;AAC3G,QAAI,SAAS,SAAU,QAAO,KAAK,OAAO,MAAM,KAAK,cAAc,OAAO,IAAI,GAAG,UAAU,SAAS,UAAU,GAAG,CAAC;AAGlH,UAAM,SAAS,IAAI;AACnB,UAAM,CAAC,SAAS,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC1C,KAAK,cAAc,OAAO,QAAQ,GAAG,SAAS,UAAU;AAAA,MACxD,QAAQ,QAAQ,KAAK,YAAY,OAAO,QAAQ,GAAG,SAAS,UAAU,CAAC;AAAA,IAC3E,CAAC;AAED,QAAI,QAAQ,WAAW,KAAK,SAAS,WAAW,EAAG,QAAO,CAAC;AAC3D,QAAI,SAAS,WAAW,EAAG,QAAO,KAAK,OAAO,QAAQ,OAAO,OAAK,EAAE,SAAS,QAAQ,GAAG,CAAC;AACzF,QAAI,QAAQ,WAAW,EAAG,QAAO,KAAK,OAAO,SAAS,OAAO,OAAK,EAAE,SAAS,QAAQ,GAAG,CAAC;AAEzF,UAAM,QAAQ,qBAAqB,CAAC,SAAS,QAAQ,CAAC;AAGtD,UAAM,UAAU,oBAAI,IAA0B;AAC9C,eAAW,KAAK,CAAC,GAAG,SAAS,GAAG,QAAQ,GAAG;AACvC,YAAM,KAAM,EAAE,UAAkB;AAChC,UAAI,MAAM,KAAM,SAAQ,IAAI,IAAI,CAAC;AAAA,IACrC;AAEA,UAAM,UAA0B,CAAC;AACjC,eAAW,KAAK,OAAO;AACnB,YAAM,UAAW,EAAE,UAAkB;AACrC,YAAM,WAAW,QAAQ,IAAI,OAAO;AACpC,UAAI,CAAC,SAAU;AACf,YAAM,SAAS,EAAE,GAAG,UAAU,OAAO,EAAE,MAAM;AAC7C,UAAI,OAAO,SAAS,SAAU,SAAQ,KAAK,MAAM;AAAA,IACrD;AAEA,UAAM,UAAU,KAAK,OAAO,SAAS,CAAC;AACtC,WAAO,KAAK,eAAe,OAAO,OAAO;AAAA,EAC7C;AAAA;AAAA,EAGA,MAAc,eAAe,OAAe,SAAkD;AAC1F,QAAI,CAAC,KAAK,GAAG,YAAY,QAAQ,UAAU,EAAG,QAAO;AACrD,WAAO,OAAO,OAAO,SAAS,KAAK,GAAG,QAAQ;AAAA,EAClD;AAAA;AAAA,EAGQ,OAAO,SAAyB,GAA2B;AAC/D,UAAM,OAAO,oBAAI,IAA0B;AAC3C,eAAW,KAAK,SAAS;AACrB,YAAM,MAAM,EAAE,YAAY;AAC1B,UAAI,CAAC,KAAK,IAAI,GAAG,KAAM,KAAK,IAAI,GAAG,EAAG,QAAQ,EAAE,OAAQ;AACpD,aAAK,IAAI,KAAK,CAAC;AAAA,MACnB;AAAA,IACJ;AACA,WAAO,CAAC,GAAG,KAAK,OAAO,CAAC,EACnB,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAChC,MAAM,GAAG,CAAC;AAAA,EACnB;AAAA;AAAA,EAGA,MAAc,cAAc,OAAe,GAAW,UAAkB,YAA8C;AAClH,QAAI,KAAK,GAAG,KAAK,SAAS,EAAG,QAAO,CAAC;AACrC,UAAM,WAAW,MAAM,KAAK,GAAG,UAAU,MAAM,KAAK;AAEpD,QAAI,UAAU;AACd,QAAI,cAAc,KAAK,GAAG,KAAK,OAAO,GAAG;AACrC,YAAM,kBAAmB,KAAK,GAAG,GAAG;AAAA,QAChC;AAAA,MACJ,EAAE,IAAI,UAAU,GAAW,KAAK;AAChC,YAAM,cAAe,KAAK,GAAG,GAAG;AAAA,QAC5B;AAAA,MACJ,EAAE,IAAI,GAAW,KAAK;AACtB,YAAM,QAAQ,kBAAkB,IAC1B,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,KAAK,cAAc,eAAe,CAAC,CAAC,IAClE;AACN,gBAAU,KAAK,IAAI,IAAI,OAAO,KAAK,GAAG,KAAK,IAAI;AAAA,IACnD;AAEA,UAAM,OAAO,KAAK,GAAG,KAAK,OAAO,UAAU,OAAO;AAClD,UAAM,UAA0B,CAAC;AAEjC,eAAW,OAAO,MAAM;AACpB,UAAI,YAAY,IAAI,QAAQ,SAAU;AACtC,YAAM,QAAQ,KAAK,GAAG,GAAG,QAAQ,uCAAuC,EAAE,IAAI,IAAI,EAAE;AACpF,UAAI,CAAC,MAAO;AACZ,UAAI,cAAc,MAAM,eAAe,WAAY;AAEnD,cAAQ,KAAK;AAAA,QACT,MAAM;AAAA,QACN,OAAO,IAAI;AAAA,QACX,UAAU,MAAM;AAAA,QAChB,SAAS,MAAM;AAAA,QACf,SAAS,KAAK,eAAe,MAAM,YAAY,MAAM,SAAS;AAAA,QAC9D,UAAU;AAAA,UACN,YAAY,MAAM;AAAA,UAClB,OAAO,MAAM;AAAA,UACb,KAAK,MAAM;AAAA,UACX,SAAS,MAAM;AAAA,QACnB;AAAA,MACJ,CAAC;AAED,UAAI,QAAQ,UAAU,EAAG;AAAA,IAC7B;AAEA,WAAO;AAAA,EACX;AAAA;AAAA,EAGQ,YAAY,OAAe,GAAW,UAAkB,YAAqC;AACjG,UAAM,WAAW,KAAK,cAAc,KAAK;AACzC,QAAI,CAAC,SAAU,QAAO,CAAC;AAEvB,QAAI;AACA,YAAM,mBAAmB,aAAa,yBAAyB;AAC/D,YAAM,SAAgB,CAAC,QAAQ;AAC/B,UAAI,WAAY,QAAO,KAAK,UAAU;AACtC,aAAO,KAAK,IAAI,CAAC;AAEjB,YAAM,OAAO,KAAK,GAAG,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,yCAIH,gBAAgB;AAAA;AAAA;AAAA,aAG5C,EAAE,IAAI,GAAG,MAAM;AAEhB,aAAO,KACF,IAAI,QAAM;AAAA,QACP,MAAM;AAAA,QACN,OAAO,cAAc,EAAE,UAAU;AAAA,QACjC,UAAU,EAAE;AAAA,QACZ,SAAS,EAAE;AAAA,QACX,SAAS,KAAK,eAAe,EAAE,YAAY,EAAE,SAAS;AAAA,QACtD,UAAU;AAAA,UACN,YAAY,EAAE;AAAA,UACd,OAAO,EAAE;AAAA,UACT,KAAK,EAAE;AAAA,UACP,SAAS,EAAE;AAAA,QACf;AAAA,MACJ,EAAE,EACD,OAAO,OAAK,EAAE,SAAS,QAAQ,EAC/B,MAAM,GAAG,CAAC;AAAA,IACnB,QAAQ;AACJ,aAAO,CAAC;AAAA,IACZ;AAAA,EACJ;AAAA;AAAA,EAGQ,cAAc,OAAuB;AACzC,UAAM,aAAa,oBAAI,IAAI;AAAA,MACvB;AAAA,MAAO;AAAA,MAAM;AAAA,MAAM;AAAA,MAAS;AAAA,MAAM;AAAA,MAAK;AAAA,MAAM;AAAA,MAAO;AAAA,MAAM;AAAA,MAC1D;AAAA,MAAM;AAAA,MAAQ;AAAA,MAAM;AAAA,MAAO;AAAA,MAAM;AAAA,MAAM;AAAA,MAAQ;AAAA,MAAM;AAAA,MAAM;AAAA,MAC3D;AAAA,MAAQ;AAAA,MAAQ;AAAA,MAAM;AAAA,MAAO;AAAA,MAAO;AAAA,MAAQ;AAAA,MAAQ;AAAA,MAAO;AAAA,MAC3D;AAAA,MAAO;AAAA,MAAM;AAAA,MAAQ;AAAA,MAAO;AAAA,MAAO;AAAA,MAAS;AAAA,MAAQ;AAAA,MAAS;AAAA,MAC7D;AAAA,MAAQ;AAAA,MAAQ;AAAA,MAAS;AAAA,MAAO;AAAA,MAAO;AAAA,MAAO;AAAA,MAAM;AAAA,MAAM;AAAA,IAC9D,CAAC;AAED,UAAM,QAAQ,MACT,QAAQ,mBAAmB,GAAG,EAC9B,QAAQ,qCAAqC,EAAE,EAC/C,QAAQ,cAAc,GAAG,EACzB,KAAK;AAEV,UAAM,QAAQ,MAAM,MAAM,KAAK,EAC1B,OAAO,OAAK,EAAE,UAAU,KAAK,CAAC,WAAW,IAAI,EAAE,YAAY,CAAC,CAAC;AAElE,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,WAAO,MAAM,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,MAAM;AAAA,EAC/C;AAAA;AAAA,EAGQ,eAAe,YAAoB,UAAsC;AAC7E,UAAM,QAAQ,SAAS,MAAM,GAAG;AAChC,aAAS,IAAI,MAAM,QAAQ,KAAK,GAAG,KAAK;AACpC,YAAM,YAAY,MAAM,IAAI,MAAM,MAAM,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AAClE,YAAM,MAAM,KAAK,GAAG,GAAG;AAAA,QACnB;AAAA,MACJ,EAAE,IAAI,YAAY,SAAS;AAC3B,UAAI,IAAK,QAAO,IAAI;AAAA,IACxB;AAEA,UAAM,OAAO,KAAK,GAAG,GAAG;AAAA,MACpB;AAAA,IACJ,EAAE,IAAI,UAAU;AAChB,WAAO,MAAM,WAAW;AAAA,EAC5B;AACJ;;;AC3MA,IAAM,aAAN,MAAmC;AAAA,EAjBnC,OAiBmC;AAAA;AAAA;AAAA,EACtB,OAAO;AAAA,EAChB;AAAA,EACA;AAAA,EACA,WAAW,oBAAI,IAA0B;AAAA,EACjC;AAAA,EACA;AAAA,EAER,MAAM,WAAW,KAAmC;AAChD,SAAK,MAAM,IAAI;AACf,SAAK,OAAO,MAAM,IAAI,WAAW;AACjC,QAAI,YAAY,eAAe,YAAY,KAAK,MAAM,KAAK,QAAQ;AACnE,SAAK,UAAU,IAAI,YAAY,IAAI,IAAI,IAAI,WAAW,KAAK,MAAM,KAAK,QAAQ;AAC9E,SAAK,UAAU,IAAI,eAAe;AAAA,MAC9B,IAAI,IAAI;AAAA,MACR,WAAW,IAAI;AAAA,MACf,MAAM,KAAK;AAAA,MACX,UAAU,KAAK;AAAA,MACf,UAAU,IAAI,OAAO;AAAA,IACzB,CAAC;AAAA,EACL;AAAA;AAAA,EAGA,cAAc,YAAsC;AAChD,SAAK,IAAI,QAAQ;AAAA;AAAA;AAAA,SAGhB,EAAE;AAAA,MACC,WAAW;AAAA,MACX,WAAW;AAAA,MACX,WAAW,WAAW;AAAA,MACtB,KAAK,UAAU,WAAW,UAAU,CAAC,CAAC;AAAA,MACtC,WAAW,WAAW;AAAA,IAC1B;AAAA,EACJ;AAAA;AAAA,EAGA,iBAAiB,MAAoB;AACjC,SAAK,QAAQ,iBAAiB,IAAI;AAAA,EACtC;AAAA;AAAA,EAGA,kBAAwC;AACpC,WAAQ,KAAK,IAAI,QAAQ,2BAA2B,EAAE,IAAI,EAAY,IAAI,UAAQ;AAAA,MAC9E,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,SAAS,IAAI;AAAA,MACb,QAAQ,KAAK,MAAM,IAAI,WAAW;AAAA,MAClC,SAAS,IAAI;AAAA,IACjB,EAAE;AAAA,EACN;AAAA;AAAA,EAGA,MAAM,iBAAiB,UAGnB,CAAC,GAAkF;AACnF,UAAM,iBAAiB,KAAK,gBAAgB;AAC5C,UAAM,UAAU,QAAQ,cAClB,eAAe,OAAO,OAAK,QAAQ,YAAa,SAAS,EAAE,IAAI,CAAC,IAChE;AAEN,UAAM,UAAgF,CAAC;AAEvF,eAAW,QAAQ,SAAS;AACxB,cAAQ,KAAK,IAAI,IAAI,MAAM,KAAK,QAAQ;AAAA,QACpC,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,QACL;AAAA,UACI,QAAQ,KAAK;AAAA,UACb,YAAY,wBAAC,MAAM,KAAK,UAAU,QAAQ,aAAa,KAAK,MAAM,MAAM,KAAK,KAAK,GAAtE;AAAA,QAChB;AAAA,MACJ;AAAA,IACJ;AAEA,WAAO;AAAA,EACX;AAAA;AAAA,EAGA,MAAM,OAAO,OAAe,SAKA;AACxB,WAAO,KAAK,QAAQ,OAAO,OAAO,OAAO;AAAA,EAC7C;AAAA;AAAA,EAGA,WAAW,YAAoBA,OAAc,SAAuB;AAChE,SAAK,IAAI,QAAQ;AAAA;AAAA;AAAA,SAGhB,EAAE,IAAI,YAAYA,OAAM,OAAO;AAAA,EACpC;AAAA;AAAA,EAGA,cAAc,YAAoBA,OAAoB;AAClD,SAAK,IAAI;AAAA,MACL;AAAA,IACJ,EAAE,IAAI,YAAYA,KAAI;AAAA,EAC1B;AAAA;AAAA,EAGA,eAAwE;AACpE,WAAO,KAAK,IAAI,QAAQ,6BAA6B,EAAE,IAAI;AAAA,EAC/D;AAAA,EAEA,QAA6B;AACzB,WAAO;AAAA,MACH,aAAc,KAAK,IAAI,QAAQ,uCAAuC,EAAE,IAAI,EAAU;AAAA,MACtF,WAAY,KAAK,IAAI,QAAQ,uDAAuD,EAAE,IAAI,EAAU;AAAA,MACpG,QAAS,KAAK,IAAI,QAAQ,sCAAsC,EAAE,IAAI,EAAU;AAAA,MAChF,UAAU,KAAK,KAAK;AAAA,IACxB;AAAA,EACJ;AACJ;AAGO,SAAS,OAAe;AAC3B,SAAO,IAAI,WAAW;AAC1B;AAFgB;","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-
|
|
4
|
+
} from "./chunk-DI3H6JVZ.js";
|
|
5
5
|
import "./chunk-B77KABWH.js";
|
|
6
6
|
import {
|
|
7
7
|
code
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-FGL32LUJ.js";
|
|
9
9
|
import {
|
|
10
10
|
git
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-JRSKWF6K.js";
|
|
12
12
|
import {
|
|
13
13
|
docs
|
|
14
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"))
|
|
214
|
-
|
|
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"))
|
|
218
|
-
|
|
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);
|