brainbank 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +19 -9
  2. package/dist/{base-9vfWRHCV.d.ts → base-4SUgeRWT.d.ts} +25 -2
  3. package/dist/{chunk-6MFTQV3O.js → chunk-2BEWWQL2.js} +435 -386
  4. package/dist/chunk-2BEWWQL2.js.map +1 -0
  5. package/dist/{chunk-FJJY4H2Y.js → chunk-5VUYPNH3.js} +47 -3
  6. package/dist/chunk-5VUYPNH3.js.map +1 -0
  7. package/dist/chunk-CCXVL56V.js +120 -0
  8. package/dist/chunk-CCXVL56V.js.map +1 -0
  9. package/dist/{chunk-V4UJKXPK.js → chunk-E6WQM4DN.js} +9 -4
  10. package/dist/chunk-E6WQM4DN.js.map +1 -0
  11. package/dist/chunk-FI7GWG4W.js +309 -0
  12. package/dist/chunk-FI7GWG4W.js.map +1 -0
  13. package/dist/{chunk-X6645UVR.js → chunk-FINIFKAY.js} +136 -4
  14. package/dist/chunk-FINIFKAY.js.map +1 -0
  15. package/dist/{chunk-WR4WXKJT.js → chunk-MGIFEPYZ.js} +62 -42
  16. package/dist/chunk-MGIFEPYZ.js.map +1 -0
  17. package/dist/{chunk-F6SJ3U4H.js → chunk-Y3JKI6QN.js} +152 -141
  18. package/dist/chunk-Y3JKI6QN.js.map +1 -0
  19. package/dist/cli.js +61 -32
  20. package/dist/cli.js.map +1 -1
  21. package/dist/code.d.ts +1 -1
  22. package/dist/code.js +1 -1
  23. package/dist/docs.d.ts +1 -1
  24. package/dist/docs.js +1 -1
  25. package/dist/git.d.ts +1 -1
  26. package/dist/git.js +1 -1
  27. package/dist/index.d.ts +121 -82
  28. package/dist/index.js +66 -15
  29. package/dist/index.js.map +1 -1
  30. package/dist/memory.d.ts +1 -1
  31. package/dist/memory.js +3 -137
  32. package/dist/memory.js.map +1 -1
  33. package/dist/notes.d.ts +1 -1
  34. package/dist/notes.js +4 -49
  35. package/dist/notes.js.map +1 -1
  36. package/dist/{openai-CYDMYX7X.js → openai-embedding-VQZCZQYT.js} +2 -2
  37. package/package.json +1 -1
  38. package/dist/chunk-6MFTQV3O.js.map +0 -1
  39. package/dist/chunk-7JCEW7LT.js +0 -266
  40. package/dist/chunk-7JCEW7LT.js.map +0 -1
  41. package/dist/chunk-F6SJ3U4H.js.map +0 -1
  42. package/dist/chunk-FJJY4H2Y.js.map +0 -1
  43. package/dist/chunk-GUT5MSJT.js +0 -99
  44. package/dist/chunk-GUT5MSJT.js.map +0 -1
  45. package/dist/chunk-V4UJKXPK.js.map +0 -1
  46. package/dist/chunk-WR4WXKJT.js.map +0 -1
  47. package/dist/chunk-X6645UVR.js.map +0 -1
  48. /package/dist/{openai-CYDMYX7X.js.map → openai-embedding-VQZCZQYT.js.map} +0 -0
@@ -2,12 +2,12 @@ import {
2
2
  isIgnoredDir,
3
3
  isIgnoredFile,
4
4
  isSupported
5
- } from "./chunk-WR4WXKJT.js";
5
+ } from "./chunk-MGIFEPYZ.js";
6
6
  import {
7
7
  normalizeBM25,
8
8
  reciprocalRankFusion,
9
9
  sanitizeFTS
10
- } from "./chunk-V4UJKXPK.js";
10
+ } from "./chunk-E6WQM4DN.js";
11
11
  import {
12
12
  cosineSimilarity
13
13
  } from "./chunk-QNHBCOKB.js";
@@ -51,7 +51,7 @@ function resolveConfig(partial = {}) {
51
51
  }
52
52
  __name(resolveConfig, "resolveConfig");
53
53
 
54
- // src/app/collection.ts
54
+ // src/core/collection.ts
55
55
  var Collection = class {
56
56
  constructor(_name, _db, _embedding, _hnsw, _vecs, _reranker) {
57
57
  this._name = _name;
@@ -131,14 +131,15 @@ var Collection = class {
131
131
  Promise.resolve(this._searchBM25(query, k, 0))
132
132
  ]);
133
133
  const fused = reciprocalRankFusion([
134
- vectorHits.map((h) => ({ type: "document", score: h.score ?? 0, content: h.content, metadata: { id: h.id } })),
135
- bm25Hits.map((h) => ({ type: "document", score: h.score ?? 0, content: h.content, metadata: { id: h.id } }))
134
+ vectorHits.map((h) => ({ type: "collection", score: h.score ?? 0, content: h.content, metadata: { id: h.id } })),
135
+ bm25Hits.map((h) => ({ type: "collection", score: h.score ?? 0, content: h.content, metadata: { id: h.id } }))
136
136
  ]);
137
137
  const allById = /* @__PURE__ */ new Map();
138
138
  for (const h of [...vectorHits, ...bm25Hits]) allById.set(h.id, h);
139
139
  const results = [];
140
140
  for (const r of fused) {
141
- const item = allById.get(r.metadata.id);
141
+ const meta = r.metadata;
142
+ const item = allById.get(meta?.id);
142
143
  if (!item) continue;
143
144
  const scored = { ...item, score: r.score };
144
145
  if (scored.score >= minScore) results.push(scored);
@@ -306,7 +307,7 @@ function parseDuration(s) {
306
307
  }
307
308
  __name(parseDuration, "parseDuration");
308
309
 
309
- // src/providers/vector/hnsw.ts
310
+ // src/providers/vector/hnsw-index.ts
310
311
  var HNSWIndex = class {
311
312
  constructor(_dims, _maxElements = 2e6, _M = 16, _efConstruction = 200, _efSearch = 50) {
312
313
  this._dims = _dims;
@@ -399,7 +400,7 @@ var HNSWIndex = class {
399
400
  }
400
401
  };
401
402
 
402
- // src/providers/embeddings/local.ts
403
+ // src/providers/embeddings/local-embedding.ts
403
404
  var LocalEmbedding = class {
404
405
  static {
405
406
  __name(this, "LocalEmbedding");
@@ -446,13 +447,21 @@ var LocalEmbedding = class {
446
447
  return output.data;
447
448
  }
448
449
  /**
449
- * Embed multiple texts.
450
- * Processes sequentially to avoid OOM on large batches.
450
+ * Embed multiple texts using real batch processing.
451
+ * Chunks into groups of BATCH_SIZE to balance throughput vs memory.
451
452
  */
452
453
  async embedBatch(texts) {
454
+ if (texts.length === 0) return [];
455
+ const BATCH_SIZE = 32;
456
+ const pipe = await this._getPipeline();
453
457
  const results = [];
454
- for (const text of texts) {
455
- results.push(await this.embed(text));
458
+ for (let i = 0; i < texts.length; i += BATCH_SIZE) {
459
+ const batch = texts.slice(i, i + BATCH_SIZE);
460
+ const output = await pipe(batch, { pooling: "mean", normalize: true });
461
+ for (let j = 0; j < batch.length; j++) {
462
+ const start = j * this.dims;
463
+ results.push(new Float32Array(output.data.buffer, start * 4, this.dims));
464
+ }
456
465
  }
457
466
  return results;
458
467
  }
@@ -493,19 +502,32 @@ function searchMMR(index, query, vectorCache, k, lambda = 0.7) {
493
502
  }
494
503
  __name(searchMMR, "searchMMR");
495
504
 
496
- // src/search/vector/multi-index.ts
497
- var MultiIndexSearch = class {
505
+ // src/search/vector/rerank.ts
506
+ async function rerank(query, results, reranker) {
507
+ const documents = results.map((r) => r.content);
508
+ const scores = await reranker.rank(query, documents);
509
+ const blended = results.map((r, i) => {
510
+ const pos = i + 1;
511
+ const rrfWeight = pos <= 3 ? 0.75 : pos <= 10 ? 0.6 : 0.4;
512
+ return {
513
+ ...r,
514
+ score: rrfWeight * r.score + (1 - rrfWeight) * (scores[i] ?? 0)
515
+ };
516
+ });
517
+ return blended.sort((a, b) => b.score - a.score);
518
+ }
519
+ __name(rerank, "rerank");
520
+
521
+ // src/search/vector/vector-search.ts
522
+ var VectorSearch = class {
498
523
  static {
499
- __name(this, "MultiIndexSearch");
524
+ __name(this, "VectorSearch");
500
525
  }
501
526
  _config;
502
527
  constructor(config) {
503
528
  this._config = config;
504
529
  }
505
- /**
506
- * Search across all indices.
507
- * Returns combined results sorted by score.
508
- */
530
+ /** Search across all indices. Returns combined results sorted by score. */
509
531
  async search(query, options = {}) {
510
532
  const {
511
533
  codeK = 6,
@@ -517,240 +539,257 @@ var MultiIndexSearch = class {
517
539
  } = options;
518
540
  const queryVec = await this._config.embedding.embed(query);
519
541
  const results = [];
520
- if (this._config.codeHnsw && this._config.codeHnsw.size > 0) {
521
- const hits = useMMR ? searchMMR(this._config.codeHnsw, queryVec, this._config.codeVecs, codeK, mmrLambda) : this._config.codeHnsw.search(queryVec, codeK);
522
- if (hits.length > 0) {
523
- const ids = hits.map((h) => h.id);
524
- const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
525
- const placeholders = ids.map(() => "?").join(",");
526
- const rows = this._config.db.prepare(
527
- `SELECT * FROM code_chunks WHERE id IN (${placeholders})`
528
- ).all(...ids);
529
- for (const r of rows) {
530
- const score = scoreMap.get(r.id) ?? 0;
531
- if (score >= minScore) {
532
- results.push({
533
- type: "code",
534
- score,
535
- filePath: r.file_path,
536
- content: r.content,
537
- metadata: {
538
- chunkType: r.chunk_type,
539
- name: r.name,
540
- startLine: r.start_line,
541
- endLine: r.end_line,
542
- language: r.language
543
- }
544
- });
542
+ this._searchCode(queryVec, codeK, minScore, useMMR, mmrLambda, results);
543
+ this._searchGit(queryVec, gitK, minScore, results);
544
+ this._searchPatterns(queryVec, patternK, minScore, useMMR, mmrLambda, results);
545
+ results.sort((a, b) => b.score - a.score);
546
+ if (this._config.reranker && results.length > 1) {
547
+ return rerank(query, results, this._config.reranker);
548
+ }
549
+ return results;
550
+ }
551
+ /** Vector search across code chunks. */
552
+ _searchCode(queryVec, k, minScore, useMMR, mmrLambda, results) {
553
+ const { codeHnsw, codeVecs, db } = this._config;
554
+ if (!codeHnsw || codeHnsw.size === 0) return;
555
+ const hits = useMMR ? searchMMR(codeHnsw, queryVec, codeVecs, k, mmrLambda) : codeHnsw.search(queryVec, k);
556
+ if (hits.length === 0) return;
557
+ const ids = hits.map((h) => h.id);
558
+ const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
559
+ const placeholders = ids.map(() => "?").join(",");
560
+ const rows = db.prepare(
561
+ `SELECT * FROM code_chunks WHERE id IN (${placeholders})`
562
+ ).all(...ids);
563
+ for (const r of rows) {
564
+ const score = scoreMap.get(r.id) ?? 0;
565
+ if (score >= minScore) {
566
+ results.push({
567
+ type: "code",
568
+ score,
569
+ filePath: r.file_path,
570
+ content: r.content,
571
+ metadata: {
572
+ chunkType: r.chunk_type,
573
+ name: r.name,
574
+ startLine: r.start_line,
575
+ endLine: r.end_line,
576
+ language: r.language
545
577
  }
546
- }
578
+ });
547
579
  }
548
580
  }
549
- if (this._config.gitHnsw && this._config.gitHnsw.size > 0) {
550
- const hits = this._config.gitHnsw.search(queryVec, gitK * 2);
551
- if (hits.length > 0) {
552
- const ids = hits.map((h) => h.id);
553
- const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
554
- const placeholders = ids.map(() => "?").join(",");
555
- const rows = this._config.db.prepare(
556
- `SELECT * FROM git_commits WHERE id IN (${placeholders}) AND is_merge = 0`
557
- ).all(...ids);
558
- for (const r of rows) {
559
- const score = scoreMap.get(r.id) ?? 0;
560
- if (score >= minScore) {
561
- results.push({
562
- type: "commit",
563
- score,
564
- content: r.message,
565
- metadata: {
566
- hash: r.hash,
567
- shortHash: r.short_hash,
568
- author: r.author,
569
- date: r.date,
570
- files: JSON.parse(r.files_json ?? "[]"),
571
- additions: r.additions,
572
- deletions: r.deletions,
573
- diff: r.diff
574
- }
575
- });
581
+ }
582
+ /** Vector search across git commits. */
583
+ _searchGit(queryVec, k, minScore, results) {
584
+ const { gitHnsw, db } = this._config;
585
+ if (!gitHnsw || gitHnsw.size === 0) return;
586
+ const hits = gitHnsw.search(queryVec, k * 2);
587
+ if (hits.length === 0) return;
588
+ const ids = hits.map((h) => h.id);
589
+ const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
590
+ const placeholders = ids.map(() => "?").join(",");
591
+ const rows = db.prepare(
592
+ `SELECT * FROM git_commits WHERE id IN (${placeholders}) AND is_merge = 0`
593
+ ).all(...ids);
594
+ for (const r of rows) {
595
+ const score = scoreMap.get(r.id) ?? 0;
596
+ if (score >= minScore) {
597
+ results.push({
598
+ type: "commit",
599
+ score,
600
+ content: r.message,
601
+ metadata: {
602
+ hash: r.hash,
603
+ shortHash: r.short_hash,
604
+ author: r.author,
605
+ date: r.date,
606
+ files: JSON.parse(r.files_json ?? "[]"),
607
+ additions: r.additions,
608
+ deletions: r.deletions,
609
+ diff: r.diff
576
610
  }
577
- }
611
+ });
578
612
  }
579
613
  }
580
- if (this._config.patternHnsw && this._config.patternHnsw.size > 0) {
581
- const hits = useMMR ? searchMMR(this._config.patternHnsw, queryVec, this._config.patternVecs, patternK, mmrLambda) : this._config.patternHnsw.search(queryVec, patternK);
582
- if (hits.length > 0) {
583
- const ids = hits.map((h) => h.id);
584
- const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
585
- const placeholders = ids.map(() => "?").join(",");
586
- const rows = this._config.db.prepare(
587
- `SELECT * FROM memory_patterns WHERE id IN (${placeholders}) AND success_rate >= 0.5`
588
- ).all(...ids);
589
- for (const r of rows) {
590
- const score = scoreMap.get(r.id) ?? 0;
591
- if (score >= minScore) {
592
- results.push({
593
- type: "pattern",
594
- score,
595
- content: r.approach,
596
- metadata: {
597
- taskType: r.task_type,
598
- task: r.task,
599
- outcome: r.outcome,
600
- successRate: r.success_rate,
601
- critique: r.critique
602
- }
603
- });
614
+ }
615
+ /** Vector search across memory patterns. */
616
+ _searchPatterns(queryVec, k, minScore, useMMR, mmrLambda, results) {
617
+ const { patternHnsw, patternVecs, db } = this._config;
618
+ if (!patternHnsw || patternHnsw.size === 0) return;
619
+ const hits = useMMR ? searchMMR(patternHnsw, queryVec, patternVecs, k, mmrLambda) : patternHnsw.search(queryVec, k);
620
+ if (hits.length === 0) return;
621
+ const ids = hits.map((h) => h.id);
622
+ const scoreMap = new Map(hits.map((h) => [h.id, h.score]));
623
+ const placeholders = ids.map(() => "?").join(",");
624
+ const rows = db.prepare(
625
+ `SELECT * FROM memory_patterns WHERE id IN (${placeholders}) AND success_rate >= 0.5`
626
+ ).all(...ids);
627
+ for (const r of rows) {
628
+ const score = scoreMap.get(r.id) ?? 0;
629
+ if (score >= minScore) {
630
+ results.push({
631
+ type: "pattern",
632
+ score,
633
+ content: r.approach,
634
+ metadata: {
635
+ taskType: r.task_type,
636
+ task: r.task,
637
+ outcome: r.outcome,
638
+ successRate: r.success_rate,
639
+ critique: r.critique
604
640
  }
605
- }
641
+ });
606
642
  }
607
643
  }
608
- results.sort((a, b) => b.score - a.score);
609
- if (this._config.reranker && results.length > 1) {
610
- return this._rerank(query, results);
611
- }
612
- return results;
613
- }
614
- /**
615
- * Re-rank results using position-aware blending.
616
- *
617
- * Top 1-3: 75% retrieval / 25% reranker (preserves exact matches)
618
- * Top 4-10: 60% retrieval / 40% reranker
619
- * Top 11+: 40% retrieval / 60% reranker (trust reranker more)
620
- */
621
- async _rerank(query, results) {
622
- const reranker = this._config.reranker;
623
- const documents = results.map((r) => r.content);
624
- const scores = await reranker.rank(query, documents);
625
- const blended = results.map((r, i) => {
626
- const pos = i + 1;
627
- const rrfWeight = pos <= 3 ? 0.75 : pos <= 10 ? 0.6 : 0.4;
628
- return {
629
- ...r,
630
- score: rrfWeight * r.score + (1 - rrfWeight) * (scores[i] ?? 0)
631
- };
632
- });
633
- return blended.sort((a, b) => b.score - a.score);
634
644
  }
635
645
  };
636
646
 
637
- // src/search/keyword/bm25.ts
638
- var BM25Search = class {
647
+ // src/search/keyword/keyword-search.ts
648
+ var KeywordSearch = class {
639
649
  constructor(_db) {
640
650
  this._db = _db;
641
651
  }
642
652
  static {
643
- __name(this, "BM25Search");
653
+ __name(this, "KeywordSearch");
644
654
  }
645
655
  /**
646
656
  * Full-text keyword search across all FTS5 indices.
647
657
  * Uses BM25 scoring — lower scores = better matches.
648
- * Query syntax: simple words, OR, NOT, "exact phrases", prefix*
649
658
  */
650
- search(query, options = {}) {
659
+ async search(query, options = {}) {
651
660
  const { codeK = 8, gitK = 5, patternK = 4 } = options;
652
- const results = [];
653
661
  const ftsQuery = sanitizeFTS(query);
654
662
  if (!ftsQuery) return [];
655
- if (codeK > 0) {
656
- try {
657
- const rows = this._db.prepare(`
658
- SELECT c.id, c.file_path, c.chunk_type, c.name, c.start_line, c.end_line,
659
- c.content, c.language, bm25(fts_code, 5.0, 3.0, 1.0) AS score
660
- FROM fts_code f
661
- JOIN code_chunks c ON c.id = f.rowid
662
- WHERE fts_code MATCH ?
663
- ORDER BY score ASC
664
- LIMIT ?
665
- `).all(ftsQuery, codeK);
666
- for (const r of rows) {
667
- results.push({
668
- type: "code",
669
- score: normalizeBM25(r.score),
670
- filePath: r.file_path,
671
- content: r.content,
672
- metadata: {
673
- chunkType: r.chunk_type,
674
- name: r.name,
675
- startLine: r.start_line,
676
- endLine: r.end_line,
677
- language: r.language,
678
- searchType: "bm25"
679
- }
680
- });
681
- }
682
- } catch {
663
+ const results = [];
664
+ if (codeK > 0) this._searchCode(ftsQuery, query, codeK, results);
665
+ if (gitK > 0) this._searchGit(ftsQuery, gitK, results);
666
+ if (patternK > 0) this._searchPatterns(ftsQuery, patternK, results);
667
+ return results.sort((a, b) => b.score - a.score);
668
+ }
669
+ /** FTS5 search across code chunks + file-path fallback. */
670
+ _searchCode(ftsQuery, rawQuery, k, results) {
671
+ const seenIds = /* @__PURE__ */ new Set();
672
+ try {
673
+ const rows = this._db.prepare(`
674
+ SELECT c.id, c.file_path, c.chunk_type, c.name, c.start_line, c.end_line,
675
+ c.content, c.language, bm25(fts_code, 5.0, 3.0, 1.0) AS score
676
+ FROM fts_code f
677
+ JOIN code_chunks c ON c.id = f.rowid
678
+ WHERE fts_code MATCH ?
679
+ ORDER BY score ASC
680
+ LIMIT ?
681
+ `).all(ftsQuery, k);
682
+ for (const r of rows) {
683
+ seenIds.add(r.id);
684
+ results.push(this._toCodeResult(r, normalizeBM25(r.score), "bm25"));
683
685
  }
686
+ } catch {
684
687
  }
685
- if (gitK > 0) {
686
- try {
687
- const rows = this._db.prepare(`
688
- SELECT c.id, c.hash, c.short_hash, c.message, c.author, c.date,
689
- c.files_json, c.diff, c.additions, c.deletions,
690
- bm25(fts_commits, 5.0, 2.0, 1.0) AS score
691
- FROM fts_commits f
692
- JOIN git_commits c ON c.id = f.rowid
693
- WHERE fts_commits MATCH ? AND c.is_merge = 0
694
- ORDER BY score ASC
695
- LIMIT ?
696
- `).all(ftsQuery, gitK);
697
- for (const r of rows) {
698
- results.push({
699
- type: "commit",
700
- score: normalizeBM25(r.score),
701
- content: r.message,
702
- metadata: {
703
- hash: r.hash,
704
- shortHash: r.short_hash,
705
- author: r.author,
706
- date: r.date,
707
- files: JSON.parse(r.files_json ?? "[]"),
708
- additions: r.additions,
709
- deletions: r.deletions,
710
- diff: r.diff,
711
- searchType: "bm25"
712
- }
713
- });
688
+ this._searchCodeByPath(rawQuery, seenIds, results);
689
+ }
690
+ /** File-path fallback: match filenames via LIKE. */
691
+ _searchCodeByPath(rawQuery, seenIds, results) {
692
+ try {
693
+ const words = rawQuery.replace(/[^a-zA-Z0-9]/g, " ").split(/\s+/).filter((w) => w.length > 2);
694
+ for (const word of words.slice(0, 3)) {
695
+ const pathRows = this._db.prepare(`
696
+ SELECT id, file_path, chunk_type, name, start_line, end_line, content, language
697
+ FROM code_chunks
698
+ WHERE file_path LIKE ? AND chunk_type = 'file'
699
+ LIMIT 3
700
+ `).all(`%${word}%`);
701
+ for (const r of pathRows) {
702
+ if (seenIds.has(r.id)) continue;
703
+ seenIds.add(r.id);
704
+ results.push(this._toCodeResult(r, 0.6, "bm25-path"));
714
705
  }
715
- } catch {
716
706
  }
707
+ } catch {
717
708
  }
718
- if (patternK > 0) {
719
- try {
720
- const rows = this._db.prepare(`
721
- SELECT p.id, p.task_type, p.task, p.approach, p.outcome,
722
- p.success_rate, p.critique,
723
- bm25(fts_patterns, 3.0, 5.0, 5.0, 1.0) AS score
724
- FROM fts_patterns f
725
- JOIN memory_patterns p ON p.id = f.rowid
726
- WHERE fts_patterns MATCH ? AND p.success_rate >= 0.5
727
- ORDER BY score ASC
728
- LIMIT ?
729
- `).all(ftsQuery, patternK);
730
- for (const r of rows) {
731
- results.push({
732
- type: "pattern",
733
- score: normalizeBM25(r.score),
734
- content: r.approach,
735
- metadata: {
736
- taskType: r.task_type,
737
- task: r.task,
738
- outcome: r.outcome,
739
- successRate: r.success_rate,
740
- critique: r.critique,
741
- searchType: "bm25"
742
- }
743
- });
744
- }
745
- } catch {
709
+ }
710
+ /** FTS5 search across git commits. */
711
+ _searchGit(ftsQuery, k, results) {
712
+ try {
713
+ const rows = this._db.prepare(`
714
+ SELECT c.id, c.hash, c.short_hash, c.message, c.author, c.date,
715
+ c.files_json, c.diff, c.additions, c.deletions,
716
+ bm25(fts_commits, 5.0, 2.0, 1.0) AS score
717
+ FROM fts_commits f
718
+ JOIN git_commits c ON c.id = f.rowid
719
+ WHERE fts_commits MATCH ? AND c.is_merge = 0
720
+ ORDER BY score ASC
721
+ LIMIT ?
722
+ `).all(ftsQuery, k);
723
+ for (const r of rows) {
724
+ results.push({
725
+ type: "commit",
726
+ score: normalizeBM25(r.score),
727
+ content: r.message,
728
+ metadata: {
729
+ hash: r.hash,
730
+ shortHash: r.short_hash,
731
+ author: r.author,
732
+ date: r.date,
733
+ files: JSON.parse(r.files_json ?? "[]"),
734
+ additions: r.additions,
735
+ deletions: r.deletions,
736
+ diff: r.diff,
737
+ searchType: "bm25"
738
+ }
739
+ });
746
740
  }
741
+ } catch {
747
742
  }
748
- return results.sort((a, b) => b.score - a.score);
749
743
  }
750
- /**
751
- * Rebuild the FTS index from scratch.
752
- * Call this after bulk imports or if FTS gets out of sync.
753
- */
744
+ /** FTS5 search across memory patterns. */
745
+ _searchPatterns(ftsQuery, k, results) {
746
+ try {
747
+ const rows = this._db.prepare(`
748
+ SELECT p.id, p.task_type, p.task, p.approach, p.outcome,
749
+ p.success_rate, p.critique,
750
+ bm25(fts_patterns, 3.0, 5.0, 5.0, 1.0) AS score
751
+ FROM fts_patterns f
752
+ JOIN memory_patterns p ON p.id = f.rowid
753
+ WHERE fts_patterns MATCH ? AND p.success_rate >= 0.5
754
+ ORDER BY score ASC
755
+ LIMIT ?
756
+ `).all(ftsQuery, k);
757
+ for (const r of rows) {
758
+ results.push({
759
+ type: "pattern",
760
+ score: normalizeBM25(r.score),
761
+ content: r.approach,
762
+ metadata: {
763
+ taskType: r.task_type,
764
+ task: r.task,
765
+ outcome: r.outcome,
766
+ successRate: r.success_rate,
767
+ critique: r.critique,
768
+ searchType: "bm25"
769
+ }
770
+ });
771
+ }
772
+ } catch {
773
+ }
774
+ }
775
+ /** Map a code_chunks row to a CodeResult. */
776
+ _toCodeResult(r, score, searchType) {
777
+ return {
778
+ type: "code",
779
+ score,
780
+ filePath: r.file_path,
781
+ content: r.content,
782
+ metadata: {
783
+ chunkType: r.chunk_type,
784
+ name: r.name,
785
+ startLine: r.start_line,
786
+ endLine: r.end_line,
787
+ language: r.language,
788
+ searchType
789
+ }
790
+ };
791
+ }
792
+ /** Rebuild the FTS index from scratch. */
754
793
  rebuild() {
755
794
  try {
756
795
  this._db.prepare("INSERT INTO fts_code(fts_code) VALUES('rebuild')").run();
@@ -761,7 +800,7 @@ var BM25Search = class {
761
800
  }
762
801
  };
763
802
 
764
- // src/app/context-builder.ts
803
+ // src/core/context-builder.ts
765
804
  var ContextBuilder = class {
766
805
  constructor(_search, _coEdits) {
767
806
  this._search = _search;
@@ -770,10 +809,7 @@ var ContextBuilder = class {
770
809
  static {
771
810
  __name(this, "ContextBuilder");
772
811
  }
773
- /**
774
- * Build a full context block for a task.
775
- * Returns clean markdown ready for system prompt injection.
776
- */
812
+ /** Build a full context block for a task. Returns markdown for system prompt. */
777
813
  async build(task, options = {}) {
778
814
  const {
779
815
  codeResults = 6,
@@ -794,85 +830,97 @@ var ContextBuilder = class {
794
830
  });
795
831
  const parts = [`# Context for: "${task}"
796
832
  `];
797
- const codeHits = results.filter((r) => r.type === "code").slice(0, codeResults);
798
- if (codeHits.length > 0) {
799
- parts.push("## Relevant Code\n");
800
- const byFile = /* @__PURE__ */ new Map();
801
- for (const r of codeHits) {
802
- const key = r.filePath ?? "unknown";
803
- if (!byFile.has(key)) byFile.set(key, []);
804
- byFile.get(key).push(r);
805
- }
806
- for (const [file, chunks] of byFile) {
807
- parts.push(`### ${file}`);
808
- for (const c of chunks) {
809
- const m = c.metadata;
810
- const label = m.name ? `${m.chunkType} \`${m.name}\` (L${m.startLine}-${m.endLine})` : `L${m.startLine}-${m.endLine}`;
811
- parts.push(`**${label}** \u2014 ${Math.round(c.score * 100)}% match`);
812
- parts.push("```" + (m.language || ""));
813
- parts.push(c.content);
814
- parts.push("```\n");
815
- }
816
- }
833
+ this._formatCodeResults(results, codeResults, parts);
834
+ this._formatGitResults(results, gitResults, parts);
835
+ this._formatCoEdits(affectedFiles, parts);
836
+ this._formatPatternResults(results, patternResults, parts);
837
+ return parts.join("\n");
838
+ }
839
+ /** Format code search results grouped by file. */
840
+ _formatCodeResults(results, limit, parts) {
841
+ const codeHits = results.filter((r) => r.type === "code").slice(0, limit);
842
+ if (codeHits.length === 0) return;
843
+ parts.push("## Relevant Code\n");
844
+ const byFile = /* @__PURE__ */ new Map();
845
+ for (const r of codeHits) {
846
+ const key = r.filePath ?? "unknown";
847
+ if (!byFile.has(key)) byFile.set(key, []);
848
+ byFile.get(key).push(r);
817
849
  }
818
- const gitHits = results.filter((r) => r.type === "commit").slice(0, gitResults);
819
- if (gitHits.length > 0) {
820
- parts.push("## Related Git History\n");
821
- for (const c of gitHits) {
850
+ for (const [file, chunks] of byFile) {
851
+ parts.push(`### ${file}`);
852
+ for (const c of chunks) {
822
853
  const m = c.metadata;
823
- const score = Math.round(c.score * 100);
824
- const files = (m.files ?? []).slice(0, 4).join(", ");
825
- parts.push(`**[${m.shortHash}]** ${c.content} *(${m.author}, ${m.date?.slice(0, 10)}, ${score}%)*`);
826
- if (files) parts.push(` Files: ${files}`);
827
- if (m.diff) {
828
- const snippet = m.diff.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@")).slice(0, 10).join("\n");
829
- if (snippet) {
830
- parts.push("```diff");
831
- parts.push(snippet);
832
- parts.push("```");
833
- }
834
- }
835
- parts.push("");
854
+ const label = m.name ? `${m.chunkType} \`${m.name}\` (L${m.startLine}-${m.endLine})` : `L${m.startLine}-${m.endLine}`;
855
+ parts.push(`**${label}** \u2014 ${Math.round(c.score * 100)}% match`);
856
+ parts.push("```" + (m.language || ""));
857
+ parts.push(c.content);
858
+ parts.push("```\n");
836
859
  }
837
860
  }
838
- if (affectedFiles.length > 0 && this._coEdits) {
839
- const coEditLines = [];
840
- for (const file of affectedFiles.slice(0, 3)) {
841
- const suggestions = this._coEdits.suggest(file, 4);
842
- if (suggestions.length > 0) {
843
- coEditLines.push(
844
- `- **${file}** \u2192 also tends to change: ${suggestions.map((s) => `${s.file} (${s.count}x)`).join(", ")}`
845
- );
861
+ }
862
+ /** Format git commit results with diff snippets. */
863
+ _formatGitResults(results, limit, parts) {
864
+ const gitHits = results.filter((r) => r.type === "commit").slice(0, limit);
865
+ if (gitHits.length === 0) return;
866
+ parts.push("## Related Git History\n");
867
+ for (const c of gitHits) {
868
+ const m = c.metadata;
869
+ const score = Math.round(c.score * 100);
870
+ const files = (m.files ?? []).slice(0, 4).join(", ");
871
+ parts.push(`**[${m.shortHash}]** ${c.content} *(${m.author}, ${m.date?.slice(0, 10)}, ${score}%)*`);
872
+ if (files) parts.push(` Files: ${files}`);
873
+ if (m.diff) {
874
+ const snippet = m.diff.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@")).slice(0, 10).join("\n");
875
+ if (snippet) {
876
+ parts.push("```diff");
877
+ parts.push(snippet);
878
+ parts.push("```");
846
879
  }
847
880
  }
848
- if (coEditLines.length > 0) {
849
- parts.push("## Co-Edit Patterns\n");
850
- parts.push(...coEditLines);
851
- parts.push("");
852
- }
881
+ parts.push("");
853
882
  }
854
- const memHits = results.filter((r) => r.type === "pattern").slice(0, patternResults);
855
- if (memHits.length > 0) {
856
- parts.push("## Learned Patterns\n");
857
- for (const p of memHits) {
858
- const m = p.metadata;
859
- const score = Math.round(p.score * 100);
860
- const success = Math.round((m.successRate ?? 0) * 100);
861
- parts.push(`**${m.taskType}** \u2014 ${success}% success, ${score}% match`);
862
- parts.push(`Task: ${m.task}`);
863
- parts.push(`Approach: ${p.content}`);
864
- if (m.critique) parts.push(`Lesson: ${m.critique}`);
865
- parts.push("");
883
+ }
884
+ /** Format co-edit suggestions for affected files. */
885
+ _formatCoEdits(affectedFiles, parts) {
886
+ if (affectedFiles.length === 0 || !this._coEdits) return;
887
+ const coEditLines = [];
888
+ for (const file of affectedFiles.slice(0, 3)) {
889
+ const suggestions = this._coEdits.suggest(file, 4);
890
+ if (suggestions.length > 0) {
891
+ coEditLines.push(
892
+ `- **${file}** \u2192 also tends to change: ${suggestions.map((s) => `${s.file} (${s.count}x)`).join(", ")}`
893
+ );
866
894
  }
867
895
  }
868
- return parts.join("\n");
896
+ if (coEditLines.length > 0) {
897
+ parts.push("## Co-Edit Patterns\n");
898
+ parts.push(...coEditLines);
899
+ parts.push("");
900
+ }
901
+ }
902
+ /** Format memory pattern results. */
903
+ _formatPatternResults(results, limit, parts) {
904
+ const memHits = results.filter((r) => r.type === "pattern").slice(0, limit);
905
+ if (memHits.length === 0) return;
906
+ parts.push("## Learned Patterns\n");
907
+ for (const p of memHits) {
908
+ const m = p.metadata;
909
+ const score = Math.round(p.score * 100);
910
+ const success = Math.round((m.successRate ?? 0) * 100);
911
+ parts.push(`**${m.taskType}** \u2014 ${success}% success, ${score}% match`);
912
+ parts.push(`Task: ${m.task}`);
913
+ parts.push(`Approach: ${p.content}`);
914
+ if (m.critique) parts.push(`Lesson: ${m.critique}`);
915
+ parts.push("");
916
+ }
869
917
  }
870
918
  };
871
919
 
872
- // src/app/brainbank.ts
920
+ // src/brainbank.ts
873
921
  import { EventEmitter } from "events";
874
922
 
875
- // src/app/registry.ts
923
+ // src/core/registry.ts
876
924
  var ALIASES = {};
877
925
  var IndexerRegistry = class {
878
926
  static {
@@ -1283,6 +1331,7 @@ var Database = class {
1283
1331
  }
1284
1332
  this.db = new BetterSqlite3(dbPath);
1285
1333
  this.db.pragma("journal_mode = WAL");
1334
+ this.db.pragma("busy_timeout = 5000");
1286
1335
  this.db.pragma("synchronous = NORMAL");
1287
1336
  this.db.pragma("foreign_keys = ON");
1288
1337
  createSchema(this.db);
@@ -1435,7 +1484,10 @@ async function reembedTable(db, embedding, table, batchSize, onProgress) {
1435
1484
  `SELECT COUNT(*) as c FROM ${table.textTable}`
1436
1485
  ).get().c;
1437
1486
  if (totalCount === 0) return 0;
1438
- const allNewVectors = [];
1487
+ const insertVec = db.prepare(
1488
+ `INSERT INTO ${table.vectorTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
1489
+ );
1490
+ db.prepare(`DELETE FROM ${table.vectorTable}`).run();
1439
1491
  let processed = 0;
1440
1492
  for (let offset = 0; offset < totalCount; offset += batchSize) {
1441
1493
  const batch = db.prepare(
@@ -1443,21 +1495,14 @@ async function reembedTable(db, embedding, table, batchSize, onProgress) {
1443
1495
  ).all(batchSize, offset);
1444
1496
  const texts = batch.map((r) => table.textBuilder(r));
1445
1497
  const vectors = await embedding.embedBatch(texts);
1446
- for (let j = 0; j < batch.length; j++) {
1447
- allNewVectors.push({ id: batch[j][table.idColumn], vec: vectors[j] });
1448
- }
1498
+ db.transaction(() => {
1499
+ for (let j = 0; j < batch.length; j++) {
1500
+ insertVec.run(batch[j][table.idColumn], Buffer.from(vectors[j].buffer));
1501
+ }
1502
+ });
1449
1503
  processed += batch.length;
1450
1504
  onProgress?.(table.name, processed, totalCount);
1451
1505
  }
1452
- const insertVec = db.prepare(
1453
- `INSERT INTO ${table.vectorTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
1454
- );
1455
- db.transaction(() => {
1456
- db.prepare(`DELETE FROM ${table.vectorTable}`).run();
1457
- for (const { id, vec } of allNewVectors) {
1458
- insertVec.run(id, Buffer.from(vec.buffer));
1459
- }
1460
- });
1461
1506
  return processed;
1462
1507
  }
1463
1508
  __name(reembedTable, "reembedTable");
@@ -1512,19 +1557,16 @@ function detectProviderMismatch(db, embedding) {
1512
1557
  }
1513
1558
  __name(detectProviderMismatch, "detectProviderMismatch");
1514
1559
 
1515
- // src/app/initializer.ts
1516
- async function earlyInit(config, emit) {
1560
+ // src/core/initializer.ts
1561
+ async function earlyInit(config, emit, options = {}) {
1517
1562
  const db = new Database(config.dbPath);
1518
1563
  const embedding = config.embeddingProvider ?? new LocalEmbedding();
1519
1564
  const mismatch = detectProviderMismatch(db, embedding);
1520
- const skipVectorLoad = !!mismatch?.mismatch;
1521
- if (skipVectorLoad) {
1522
- emit("warning", {
1523
- type: "provider_mismatch",
1524
- previous: mismatch.stored,
1525
- current: mismatch.current,
1526
- message: "Embedding provider changed \u2014 vectors not loaded. Run brain.reembed() to regenerate."
1527
- });
1565
+ if (mismatch?.mismatch && !options.force) {
1566
+ db.close();
1567
+ throw new Error(
1568
+ `BrainBank: Embedding dimension mismatch (stored: ${mismatch.stored}, current: ${mismatch.current}). Run brain.reembed() to re-index with the new provider, or switch back to the original provider.`
1569
+ );
1528
1570
  }
1529
1571
  setEmbeddingMeta(db, embedding);
1530
1572
  const kvHnsw = new HNSWIndex(
@@ -1535,6 +1577,7 @@ async function earlyInit(config, emit) {
1535
1577
  config.hnswEfSearch
1536
1578
  );
1537
1579
  await kvHnsw.init();
1580
+ const skipVectorLoad = !!(options.force && mismatch?.mismatch);
1538
1581
  return { db, embedding, kvHnsw, skipVectorLoad };
1539
1582
  }
1540
1583
  __name(earlyInit, "earlyInit");
@@ -1543,7 +1586,15 @@ async function lateInit(early, config, registry, sharedHnsw, kvVecs, getCollecti
1543
1586
  if (!skipVectorLoad) {
1544
1587
  loadVectors(db, "kv_vectors", "data_id", kvHnsw, kvVecs);
1545
1588
  }
1546
- const ctx = {
1589
+ const ctx = buildIndexerContext(db, embedding, config, sharedHnsw, skipVectorLoad, getCollection);
1590
+ for (const mod of registry.all) {
1591
+ await mod.initialize(ctx);
1592
+ }
1593
+ return buildSearchLayer(db, embedding, config, registry, sharedHnsw);
1594
+ }
1595
+ __name(lateInit, "lateInit");
1596
+ function buildIndexerContext(db, embedding, config, sharedHnsw, skipVectorLoad, getCollection) {
1597
+ return {
1547
1598
  db,
1548
1599
  embedding,
1549
1600
  config,
@@ -1574,36 +1625,30 @@ async function lateInit(early, config, registry, sharedHnsw, kvVecs, getCollecti
1574
1625
  }, "getOrCreateSharedHnsw"),
1575
1626
  collection: getCollection
1576
1627
  };
1577
- for (const mod of registry.all) {
1578
- await mod.initialize(ctx);
1579
- }
1628
+ }
1629
+ __name(buildIndexerContext, "buildIndexerContext");
1630
+ function buildSearchLayer(db, embedding, config, registry, sharedHnsw) {
1580
1631
  const codeMod = sharedHnsw.get("code");
1581
1632
  const gitMod = sharedHnsw.get("git");
1582
1633
  const memMod = registry.firstByType("memory");
1583
- let search;
1584
- let bm25;
1585
- let contextBuilder;
1586
- if (codeMod || gitMod || memMod) {
1587
- search = new MultiIndexSearch({
1588
- db,
1589
- codeHnsw: codeMod?.hnsw,
1590
- gitHnsw: gitMod?.hnsw,
1591
- patternHnsw: memMod?.hnsw,
1592
- codeVecs: codeMod?.vecCache ?? /* @__PURE__ */ new Map(),
1593
- gitVecs: gitMod?.vecCache ?? /* @__PURE__ */ new Map(),
1594
- patternVecs: memMod?.vecCache ?? /* @__PURE__ */ new Map(),
1595
- embedding,
1596
- reranker: config.reranker
1597
- });
1598
- bm25 = new BM25Search(db);
1599
- }
1600
- if (search) {
1601
- const firstGit = registry.firstByType("git");
1602
- contextBuilder = new ContextBuilder(search, firstGit?.coEdits);
1603
- }
1634
+ if (!codeMod && !gitMod && !memMod) return {};
1635
+ const search = new VectorSearch({
1636
+ db,
1637
+ codeHnsw: codeMod?.hnsw,
1638
+ gitHnsw: gitMod?.hnsw,
1639
+ patternHnsw: memMod?.hnsw,
1640
+ codeVecs: codeMod?.vecCache ?? /* @__PURE__ */ new Map(),
1641
+ gitVecs: gitMod?.vecCache ?? /* @__PURE__ */ new Map(),
1642
+ patternVecs: memMod?.vecCache ?? /* @__PURE__ */ new Map(),
1643
+ embedding,
1644
+ reranker: config.reranker
1645
+ });
1646
+ const bm25 = new KeywordSearch(db);
1647
+ const firstGit = registry.firstByType("git");
1648
+ const contextBuilder = new ContextBuilder(search, firstGit?.coEdits);
1604
1649
  return { search, bm25, contextBuilder };
1605
1650
  }
1606
- __name(lateInit, "lateInit");
1651
+ __name(buildSearchLayer, "buildSearchLayer");
1607
1652
  function loadVectors(db, table, idCol, hnsw, cache) {
1608
1653
  const rows = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).all();
1609
1654
  for (const row of rows) {
@@ -1619,7 +1664,7 @@ function loadVectors(db, table, idCol, hnsw, cache) {
1619
1664
  }
1620
1665
  __name(loadVectors, "loadVectors");
1621
1666
 
1622
- // src/app/search-api.ts
1667
+ // src/core/search-api.ts
1623
1668
  var SearchAPI = class {
1624
1669
  constructor(_d) {
1625
1670
  this._d = _d;
@@ -1666,6 +1711,13 @@ var SearchAPI = class {
1666
1711
  const docs = await this._d.searchDocs(query, { k: docsK });
1667
1712
  if (docs.length > 0) resultLists.push(docs);
1668
1713
  }
1714
+ await this._searchKvCollections(query, cols, resultLists);
1715
+ if (resultLists.length === 0) return [];
1716
+ const fused = reciprocalRankFusion(resultLists);
1717
+ return this._applyReranking(query, fused);
1718
+ }
1719
+ /** Search non-reserved KV collections and push results. */
1720
+ async _searchKvCollections(query, cols, resultLists) {
1669
1721
  const reserved = /* @__PURE__ */ new Set(["code", "git", "docs"]);
1670
1722
  for (const [name, k] of Object.entries(cols)) {
1671
1723
  if (reserved.has(name)) continue;
@@ -1679,23 +1731,22 @@ var SearchAPI = class {
1679
1731
  })));
1680
1732
  }
1681
1733
  }
1682
- if (resultLists.length === 0) return [];
1683
- const fused = reciprocalRankFusion(resultLists);
1684
- if (this._d.config.reranker && fused.length > 1) {
1685
- const scores = await this._d.config.reranker.rank(query, fused.map((r) => r.content));
1686
- return fused.map((r, i) => {
1687
- const w = i < 3 ? 0.75 : i < 10 ? 0.6 : 0.4;
1688
- return { ...r, score: w * r.score + (1 - w) * (scores[i] ?? 0) };
1689
- }).sort((a, b) => b.score - a.score);
1690
- }
1691
- return fused;
1734
+ }
1735
+ /** Apply reranking if a reranker is configured. */
1736
+ async _applyReranking(query, fused) {
1737
+ if (!this._d.config.reranker || fused.length <= 1) return fused;
1738
+ const scores = await this._d.config.reranker.rank(query, fused.map((r) => r.content));
1739
+ return fused.map((r, i) => {
1740
+ const w = i < 3 ? 0.75 : i < 10 ? 0.6 : 0.4;
1741
+ return { ...r, score: w * r.score + (1 - w) * (scores[i] ?? 0) };
1742
+ }).sort((a, b) => b.score - a.score);
1692
1743
  }
1693
1744
  // ── Keyword ─────────────────────────────────────
1694
- searchBM25(query, options) {
1745
+ async searchBM25(query, options) {
1695
1746
  return this._d.bm25?.search(query, options) ?? [];
1696
1747
  }
1697
1748
  rebuildFTS() {
1698
- this._d.bm25?.rebuild();
1749
+ this._d.bm25?.rebuild?.();
1699
1750
  }
1700
1751
  // ── Context ─────────────────────────────────────
1701
1752
  async getContext(task, options = {}) {
@@ -1723,7 +1774,7 @@ ${body}`);
1723
1774
  }
1724
1775
  };
1725
1776
 
1726
- // src/app/index-api.ts
1777
+ // src/core/index-api.ts
1727
1778
  var IndexAPI = class {
1728
1779
  constructor(_d) {
1729
1780
  this._d = _d;
@@ -1931,7 +1982,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
1931
1982
  }
1932
1983
  __name(createWatcher, "createWatcher");
1933
1984
 
1934
- // src/app/brainbank.ts
1985
+ // src/brainbank.ts
1935
1986
  var BrainBank = class extends EventEmitter {
1936
1987
  static {
1937
1988
  __name(this, "BrainBank");
@@ -1986,10 +2037,10 @@ var BrainBank = class extends EventEmitter {
1986
2037
  * Only initializes registered modules.
1987
2038
  * Automatically called by index/search methods if not yet initialized.
1988
2039
  */
1989
- async initialize() {
2040
+ async initialize(options = {}) {
1990
2041
  if (this._initialized) return;
1991
2042
  if (this._initPromise) return this._initPromise;
1992
- this._initPromise = this._runInitialize().catch((err) => {
2043
+ this._initPromise = this._runInitialize(options).catch((err) => {
1993
2044
  for (const { hnsw } of this._sharedHnsw.values()) try {
1994
2045
  hnsw.reinit();
1995
2046
  } catch {
@@ -2013,9 +2064,9 @@ var BrainBank = class extends EventEmitter {
2013
2064
  });
2014
2065
  return this._initPromise;
2015
2066
  }
2016
- async _runInitialize() {
2067
+ async _runInitialize(options = {}) {
2017
2068
  if (this._initialized) return;
2018
- const early = await earlyInit(this._config, (e, d) => this.emit(e, d));
2069
+ const early = await earlyInit(this._config, (e, d) => this.emit(e, d), options);
2019
2070
  this._db = early.db;
2020
2071
  this._embedding = early.embedding;
2021
2072
  this._kvHnsw = early.kvHnsw;
@@ -2083,21 +2134,25 @@ var BrainBank = class extends EventEmitter {
2083
2134
  /** Register a document collection. */
2084
2135
  async addCollection(collection) {
2085
2136
  await this.initialize();
2137
+ this._requireDocs("addCollection");
2086
2138
  this.indexer("docs").addCollection(collection);
2087
2139
  }
2088
2140
  /** Remove a collection and all its indexed data. */
2089
2141
  async removeCollection(name) {
2090
2142
  await this.initialize();
2143
+ this._requireDocs("removeCollection");
2091
2144
  this.indexer("docs").removeCollection(name);
2092
2145
  }
2093
2146
  /** List all registered collections. */
2094
2147
  listCollections() {
2095
2148
  this._requireInit("listCollections");
2149
+ this._requireDocs("listCollections");
2096
2150
  return this.indexer("docs").listCollections();
2097
2151
  }
2098
2152
  /** Index all (or specific) document collections. */
2099
2153
  async indexDocs(options = {}) {
2100
2154
  await this.initialize();
2155
+ this._requireDocs("indexDocs");
2101
2156
  const results = await this.indexer("docs").indexCollections(options);
2102
2157
  this.emit("docsIndexed", results);
2103
2158
  return results;
@@ -2105,19 +2160,23 @@ var BrainBank = class extends EventEmitter {
2105
2160
  /** Search documents only. */
2106
2161
  async searchDocs(query, options) {
2107
2162
  await this.initialize();
2163
+ if (!this.has("docs")) return [];
2108
2164
  return this.indexer("docs").search(query, options);
2109
2165
  }
2110
2166
  // ── Context metadata ─────────────────────────────
2111
2167
  /** Add context description for a collection path. */
2112
2168
  addContext(collection, path4, context) {
2169
+ this._requireDocs("addContext");
2113
2170
  this.indexer("docs").addContext(collection, path4, context);
2114
2171
  }
2115
2172
  /** Remove context for a collection path. */
2116
2173
  removeContext(collection, path4) {
2174
+ this._requireDocs("removeContext");
2117
2175
  this.indexer("docs").removeContext(collection, path4);
2118
2176
  }
2119
2177
  /** List all context entries. */
2120
2178
  listContexts() {
2179
+ this._requireDocs("listContexts");
2121
2180
  return this.indexer("docs").listContexts();
2122
2181
  }
2123
2182
  // ── Search (delegated to SearchAPI) ─────────────
@@ -2153,7 +2212,7 @@ var BrainBank = class extends EventEmitter {
2153
2212
  return this._searchAPI.hybridSearch(query, options);
2154
2213
  }
2155
2214
  /** BM25 keyword search only (no embeddings needed). */
2156
- searchBM25(query, options) {
2215
+ async searchBM25(query, options) {
2157
2216
  this._requireInit("searchBM25");
2158
2217
  return this._searchAPI.searchBM25(query, options);
2159
2218
  }
@@ -2165,20 +2224,15 @@ var BrainBank = class extends EventEmitter {
2165
2224
  // ── Queries ──────────────────────────────────────
2166
2225
  /** Get git history for a specific file. */
2167
2226
  async fileHistory(filePath, limit = 20) {
2168
- this.indexer("git");
2169
2227
  await this.initialize();
2170
- return this._db.prepare(`
2171
- SELECT c.short_hash, c.message, c.author, c.date, c.additions, c.deletions
2172
- FROM git_commits c
2173
- INNER JOIN commit_files cf ON c.id = cf.commit_id
2174
- WHERE cf.file_path LIKE ? AND c.is_merge = 0
2175
- ORDER BY c.timestamp DESC LIMIT ?
2176
- `).all(`%${filePath}%`, limit);
2228
+ const gitPlugin = this.indexer("git");
2229
+ return gitPlugin.fileHistory(filePath, limit);
2177
2230
  }
2178
2231
  /** Get co-edit suggestions for a file. */
2179
2232
  coEdits(filePath, limit = 5) {
2180
2233
  this._requireInit("coEdits");
2181
- return this.indexer("git").suggestCoEdits(filePath, limit);
2234
+ const gitPlugin = this.indexer("git");
2235
+ return gitPlugin.suggestCoEdits(filePath, limit);
2182
2236
  }
2183
2237
  // ── Stats ────────────────────────────────────────
2184
2238
  /** Get statistics for all loaded modules. */
@@ -2186,24 +2240,13 @@ var BrainBank = class extends EventEmitter {
2186
2240
  this._requireInit("stats");
2187
2241
  const result = {};
2188
2242
  if (this.has("code")) {
2189
- const sh = this._sharedHnsw.get("code");
2190
- result.code = {
2191
- files: this._db.prepare("SELECT COUNT(DISTINCT file_path) as c FROM code_chunks").get().c,
2192
- chunks: this._db.prepare("SELECT COUNT(*) as c FROM code_chunks").get().c,
2193
- hnswSize: sh?.hnsw.size ?? 0
2194
- };
2243
+ result.code = this._registry.firstByType("code").stats();
2195
2244
  }
2196
2245
  if (this.has("git")) {
2197
- const sh = this._sharedHnsw.get("git");
2198
- result.git = {
2199
- commits: this._db.prepare("SELECT COUNT(*) as c FROM git_commits").get().c,
2200
- filesTracked: this._db.prepare("SELECT COUNT(DISTINCT file_path) as c FROM commit_files").get().c,
2201
- coEdits: this._db.prepare("SELECT COUNT(*) as c FROM co_edits").get().c,
2202
- hnswSize: sh?.hnsw.size ?? 0
2203
- };
2246
+ result.git = this._registry.firstByType("git").stats();
2204
2247
  }
2205
2248
  if (this.has("docs")) {
2206
- result.documents = this.indexer("docs").stats();
2249
+ result.documents = this._registry.firstByType("docs").stats();
2207
2250
  }
2208
2251
  return result;
2209
2252
  }
@@ -2250,6 +2293,8 @@ var BrainBank = class extends EventEmitter {
2250
2293
  close() {
2251
2294
  this._watcher?.close();
2252
2295
  for (const indexer of this._registry.all) indexer.close?.();
2296
+ this._embedding?.close().catch(() => {
2297
+ });
2253
2298
  this._db?.close();
2254
2299
  this._initialized = false;
2255
2300
  this._collections.clear();
@@ -2273,6 +2318,10 @@ var BrainBank = class extends EventEmitter {
2273
2318
  if (!this._initialized)
2274
2319
  throw new Error(`BrainBank: Not initialized. Call await brain.initialize() before ${method}().`);
2275
2320
  }
2321
+ _requireDocs(method) {
2322
+ if (!this.has("docs"))
2323
+ throw new Error(`BrainBank: Docs indexer not loaded. Add .use(docs()) before calling ${method}().`);
2324
+ }
2276
2325
  };
2277
2326
 
2278
2327
  export {
@@ -2282,9 +2331,9 @@ export {
2282
2331
  HNSWIndex,
2283
2332
  LocalEmbedding,
2284
2333
  searchMMR,
2285
- MultiIndexSearch,
2286
- BM25Search,
2334
+ VectorSearch,
2335
+ KeywordSearch,
2287
2336
  ContextBuilder,
2288
2337
  BrainBank
2289
2338
  };
2290
- //# sourceMappingURL=chunk-6MFTQV3O.js.map
2339
+ //# sourceMappingURL=chunk-2BEWWQL2.js.map