brainbank 0.2.2 → 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-ZJ5LLMGM.js → chunk-2BEWWQL2.js} +432 -415
  4. package/dist/chunk-2BEWWQL2.js.map +1 -0
  5. package/dist/{chunk-OPQ3ZIPV.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-FEAMUZGJ.js → chunk-E6WQM4DN.js} +3 -3
  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-4DM3XWO6.js → chunk-MGIFEPYZ.js} +54 -42
  16. package/dist/chunk-MGIFEPYZ.js.map +1 -0
  17. package/dist/{chunk-T2VXF5S5.js → chunk-Y3JKI6QN.js} +152 -137
  18. package/dist/chunk-Y3JKI6QN.js.map +1 -0
  19. package/dist/cli.js +34 -28
  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-4DM3XWO6.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-FEAMUZGJ.js.map +0 -1
  42. package/dist/chunk-GUT5MSJT.js +0 -99
  43. package/dist/chunk-GUT5MSJT.js.map +0 -1
  44. package/dist/chunk-OPQ3ZIPV.js.map +0 -1
  45. package/dist/chunk-T2VXF5S5.js.map +0 -1
  46. package/dist/chunk-X6645UVR.js.map +0 -1
  47. package/dist/chunk-ZJ5LLMGM.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-4DM3XWO6.js";
5
+ } from "./chunk-MGIFEPYZ.js";
6
6
  import {
7
7
  normalizeBM25,
8
8
  reciprocalRankFusion,
9
9
  sanitizeFTS
10
- } from "./chunk-FEAMUZGJ.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,272 +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 [];
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) {
655
671
  const seenIds = /* @__PURE__ */ new Set();
656
- if (codeK > 0) {
657
- try {
658
- const rows = this._db.prepare(`
659
- SELECT c.id, c.file_path, c.chunk_type, c.name, c.start_line, c.end_line,
660
- c.content, c.language, bm25(fts_code, 5.0, 3.0, 1.0) AS score
661
- FROM fts_code f
662
- JOIN code_chunks c ON c.id = f.rowid
663
- WHERE fts_code MATCH ?
664
- ORDER BY score ASC
665
- LIMIT ?
666
- `).all(ftsQuery, codeK);
667
- for (const r of rows) {
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"));
685
+ }
686
+ } catch {
687
+ }
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;
668
703
  seenIds.add(r.id);
669
- results.push({
670
- type: "code",
671
- score: normalizeBM25(r.score),
672
- filePath: r.file_path,
673
- content: r.content,
674
- metadata: {
675
- chunkType: r.chunk_type,
676
- name: r.name,
677
- startLine: r.start_line,
678
- endLine: r.end_line,
679
- language: r.language,
680
- searchType: "bm25"
681
- }
682
- });
704
+ results.push(this._toCodeResult(r, 0.6, "bm25-path"));
683
705
  }
684
- } catch {
685
706
  }
686
- try {
687
- const words = query.replace(/[^a-zA-Z0-9]/g, " ").split(/\s+/).filter((w) => w.length > 2);
688
- for (const word of words.slice(0, 3)) {
689
- const pathRows = this._db.prepare(`
690
- SELECT id, file_path, chunk_type, name, start_line, end_line, content, language
691
- FROM code_chunks
692
- WHERE file_path LIKE ? AND chunk_type = 'file'
693
- LIMIT 3
694
- `).all(`%${word}%`);
695
- for (const r of pathRows) {
696
- if (seenIds.has(r.id)) continue;
697
- seenIds.add(r.id);
698
- results.push({
699
- type: "code",
700
- score: 0.6,
701
- filePath: r.file_path,
702
- content: r.content,
703
- metadata: {
704
- chunkType: r.chunk_type,
705
- name: r.name,
706
- startLine: r.start_line,
707
- endLine: r.end_line,
708
- language: r.language,
709
- searchType: "bm25-path"
710
- }
711
- });
707
+ } catch {
708
+ }
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"
712
738
  }
713
- }
714
- } catch {
739
+ });
715
740
  }
741
+ } catch {
716
742
  }
717
- if (gitK > 0) {
718
- try {
719
- const rows = this._db.prepare(`
720
- SELECT c.id, c.hash, c.short_hash, c.message, c.author, c.date,
721
- c.files_json, c.diff, c.additions, c.deletions,
722
- bm25(fts_commits, 5.0, 2.0, 1.0) AS score
723
- FROM fts_commits f
724
- JOIN git_commits c ON c.id = f.rowid
725
- WHERE fts_commits MATCH ? AND c.is_merge = 0
726
- ORDER BY score ASC
727
- LIMIT ?
728
- `).all(ftsQuery, gitK);
729
- for (const r of rows) {
730
- results.push({
731
- type: "commit",
732
- score: normalizeBM25(r.score),
733
- content: r.message,
734
- metadata: {
735
- hash: r.hash,
736
- shortHash: r.short_hash,
737
- author: r.author,
738
- date: r.date,
739
- files: JSON.parse(r.files_json ?? "[]"),
740
- additions: r.additions,
741
- deletions: r.deletions,
742
- diff: r.diff,
743
- searchType: "bm25"
744
- }
745
- });
746
- }
747
- } catch {
743
+ }
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
+ });
748
771
  }
772
+ } catch {
749
773
  }
750
- if (patternK > 0) {
751
- try {
752
- const rows = this._db.prepare(`
753
- SELECT p.id, p.task_type, p.task, p.approach, p.outcome,
754
- p.success_rate, p.critique,
755
- bm25(fts_patterns, 3.0, 5.0, 5.0, 1.0) AS score
756
- FROM fts_patterns f
757
- JOIN memory_patterns p ON p.id = f.rowid
758
- WHERE fts_patterns MATCH ? AND p.success_rate >= 0.5
759
- ORDER BY score ASC
760
- LIMIT ?
761
- `).all(ftsQuery, patternK);
762
- for (const r of rows) {
763
- results.push({
764
- type: "pattern",
765
- score: normalizeBM25(r.score),
766
- content: r.approach,
767
- metadata: {
768
- taskType: r.task_type,
769
- task: r.task,
770
- outcome: r.outcome,
771
- successRate: r.success_rate,
772
- critique: r.critique,
773
- searchType: "bm25"
774
- }
775
- });
776
- }
777
- } catch {
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
778
789
  }
779
- }
780
- return results.sort((a, b) => b.score - a.score);
790
+ };
781
791
  }
782
- /**
783
- * Rebuild the FTS index from scratch.
784
- * Call this after bulk imports or if FTS gets out of sync.
785
- */
792
+ /** Rebuild the FTS index from scratch. */
786
793
  rebuild() {
787
794
  try {
788
795
  this._db.prepare("INSERT INTO fts_code(fts_code) VALUES('rebuild')").run();
@@ -793,7 +800,7 @@ var BM25Search = class {
793
800
  }
794
801
  };
795
802
 
796
- // src/app/context-builder.ts
803
+ // src/core/context-builder.ts
797
804
  var ContextBuilder = class {
798
805
  constructor(_search, _coEdits) {
799
806
  this._search = _search;
@@ -802,10 +809,7 @@ var ContextBuilder = class {
802
809
  static {
803
810
  __name(this, "ContextBuilder");
804
811
  }
805
- /**
806
- * Build a full context block for a task.
807
- * Returns clean markdown ready for system prompt injection.
808
- */
812
+ /** Build a full context block for a task. Returns markdown for system prompt. */
809
813
  async build(task, options = {}) {
810
814
  const {
811
815
  codeResults = 6,
@@ -826,85 +830,97 @@ var ContextBuilder = class {
826
830
  });
827
831
  const parts = [`# Context for: "${task}"
828
832
  `];
829
- const codeHits = results.filter((r) => r.type === "code").slice(0, codeResults);
830
- if (codeHits.length > 0) {
831
- parts.push("## Relevant Code\n");
832
- const byFile = /* @__PURE__ */ new Map();
833
- for (const r of codeHits) {
834
- const key = r.filePath ?? "unknown";
835
- if (!byFile.has(key)) byFile.set(key, []);
836
- byFile.get(key).push(r);
837
- }
838
- for (const [file, chunks] of byFile) {
839
- parts.push(`### ${file}`);
840
- for (const c of chunks) {
841
- const m = c.metadata;
842
- const label = m.name ? `${m.chunkType} \`${m.name}\` (L${m.startLine}-${m.endLine})` : `L${m.startLine}-${m.endLine}`;
843
- parts.push(`**${label}** \u2014 ${Math.round(c.score * 100)}% match`);
844
- parts.push("```" + (m.language || ""));
845
- parts.push(c.content);
846
- parts.push("```\n");
847
- }
848
- }
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);
849
849
  }
850
- const gitHits = results.filter((r) => r.type === "commit").slice(0, gitResults);
851
- if (gitHits.length > 0) {
852
- parts.push("## Related Git History\n");
853
- for (const c of gitHits) {
850
+ for (const [file, chunks] of byFile) {
851
+ parts.push(`### ${file}`);
852
+ for (const c of chunks) {
854
853
  const m = c.metadata;
855
- const score = Math.round(c.score * 100);
856
- const files = (m.files ?? []).slice(0, 4).join(", ");
857
- parts.push(`**[${m.shortHash}]** ${c.content} *(${m.author}, ${m.date?.slice(0, 10)}, ${score}%)*`);
858
- if (files) parts.push(` Files: ${files}`);
859
- if (m.diff) {
860
- const snippet = m.diff.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@")).slice(0, 10).join("\n");
861
- if (snippet) {
862
- parts.push("```diff");
863
- parts.push(snippet);
864
- parts.push("```");
865
- }
866
- }
867
- 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");
868
859
  }
869
860
  }
870
- if (affectedFiles.length > 0 && this._coEdits) {
871
- const coEditLines = [];
872
- for (const file of affectedFiles.slice(0, 3)) {
873
- const suggestions = this._coEdits.suggest(file, 4);
874
- if (suggestions.length > 0) {
875
- coEditLines.push(
876
- `- **${file}** \u2192 also tends to change: ${suggestions.map((s) => `${s.file} (${s.count}x)`).join(", ")}`
877
- );
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("```");
878
879
  }
879
880
  }
880
- if (coEditLines.length > 0) {
881
- parts.push("## Co-Edit Patterns\n");
882
- parts.push(...coEditLines);
883
- parts.push("");
884
- }
881
+ parts.push("");
885
882
  }
886
- const memHits = results.filter((r) => r.type === "pattern").slice(0, patternResults);
887
- if (memHits.length > 0) {
888
- parts.push("## Learned Patterns\n");
889
- for (const p of memHits) {
890
- const m = p.metadata;
891
- const score = Math.round(p.score * 100);
892
- const success = Math.round((m.successRate ?? 0) * 100);
893
- parts.push(`**${m.taskType}** \u2014 ${success}% success, ${score}% match`);
894
- parts.push(`Task: ${m.task}`);
895
- parts.push(`Approach: ${p.content}`);
896
- if (m.critique) parts.push(`Lesson: ${m.critique}`);
897
- 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
+ );
898
894
  }
899
895
  }
900
- 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
+ }
901
917
  }
902
918
  };
903
919
 
904
- // src/app/brainbank.ts
920
+ // src/brainbank.ts
905
921
  import { EventEmitter } from "events";
906
922
 
907
- // src/app/registry.ts
923
+ // src/core/registry.ts
908
924
  var ALIASES = {};
909
925
  var IndexerRegistry = class {
910
926
  static {
@@ -1315,6 +1331,7 @@ var Database = class {
1315
1331
  }
1316
1332
  this.db = new BetterSqlite3(dbPath);
1317
1333
  this.db.pragma("journal_mode = WAL");
1334
+ this.db.pragma("busy_timeout = 5000");
1318
1335
  this.db.pragma("synchronous = NORMAL");
1319
1336
  this.db.pragma("foreign_keys = ON");
1320
1337
  createSchema(this.db);
@@ -1467,7 +1484,10 @@ async function reembedTable(db, embedding, table, batchSize, onProgress) {
1467
1484
  `SELECT COUNT(*) as c FROM ${table.textTable}`
1468
1485
  ).get().c;
1469
1486
  if (totalCount === 0) return 0;
1470
- 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();
1471
1491
  let processed = 0;
1472
1492
  for (let offset = 0; offset < totalCount; offset += batchSize) {
1473
1493
  const batch = db.prepare(
@@ -1475,21 +1495,14 @@ async function reembedTable(db, embedding, table, batchSize, onProgress) {
1475
1495
  ).all(batchSize, offset);
1476
1496
  const texts = batch.map((r) => table.textBuilder(r));
1477
1497
  const vectors = await embedding.embedBatch(texts);
1478
- for (let j = 0; j < batch.length; j++) {
1479
- allNewVectors.push({ id: batch[j][table.idColumn], vec: vectors[j] });
1480
- }
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
+ });
1481
1503
  processed += batch.length;
1482
1504
  onProgress?.(table.name, processed, totalCount);
1483
1505
  }
1484
- const insertVec = db.prepare(
1485
- `INSERT INTO ${table.vectorTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
1486
- );
1487
- db.transaction(() => {
1488
- db.prepare(`DELETE FROM ${table.vectorTable}`).run();
1489
- for (const { id, vec } of allNewVectors) {
1490
- insertVec.run(id, Buffer.from(vec.buffer));
1491
- }
1492
- });
1493
1506
  return processed;
1494
1507
  }
1495
1508
  __name(reembedTable, "reembedTable");
@@ -1544,19 +1557,16 @@ function detectProviderMismatch(db, embedding) {
1544
1557
  }
1545
1558
  __name(detectProviderMismatch, "detectProviderMismatch");
1546
1559
 
1547
- // src/app/initializer.ts
1548
- async function earlyInit(config, emit) {
1560
+ // src/core/initializer.ts
1561
+ async function earlyInit(config, emit, options = {}) {
1549
1562
  const db = new Database(config.dbPath);
1550
1563
  const embedding = config.embeddingProvider ?? new LocalEmbedding();
1551
1564
  const mismatch = detectProviderMismatch(db, embedding);
1552
- const skipVectorLoad = !!mismatch?.mismatch;
1553
- if (skipVectorLoad) {
1554
- emit("warning", {
1555
- type: "provider_mismatch",
1556
- previous: mismatch.stored,
1557
- current: mismatch.current,
1558
- message: "Embedding provider changed \u2014 vectors not loaded. Run brain.reembed() to regenerate."
1559
- });
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
+ );
1560
1570
  }
1561
1571
  setEmbeddingMeta(db, embedding);
1562
1572
  const kvHnsw = new HNSWIndex(
@@ -1567,6 +1577,7 @@ async function earlyInit(config, emit) {
1567
1577
  config.hnswEfSearch
1568
1578
  );
1569
1579
  await kvHnsw.init();
1580
+ const skipVectorLoad = !!(options.force && mismatch?.mismatch);
1570
1581
  return { db, embedding, kvHnsw, skipVectorLoad };
1571
1582
  }
1572
1583
  __name(earlyInit, "earlyInit");
@@ -1575,7 +1586,15 @@ async function lateInit(early, config, registry, sharedHnsw, kvVecs, getCollecti
1575
1586
  if (!skipVectorLoad) {
1576
1587
  loadVectors(db, "kv_vectors", "data_id", kvHnsw, kvVecs);
1577
1588
  }
1578
- 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 {
1579
1598
  db,
1580
1599
  embedding,
1581
1600
  config,
@@ -1606,36 +1625,30 @@ async function lateInit(early, config, registry, sharedHnsw, kvVecs, getCollecti
1606
1625
  }, "getOrCreateSharedHnsw"),
1607
1626
  collection: getCollection
1608
1627
  };
1609
- for (const mod of registry.all) {
1610
- await mod.initialize(ctx);
1611
- }
1628
+ }
1629
+ __name(buildIndexerContext, "buildIndexerContext");
1630
+ function buildSearchLayer(db, embedding, config, registry, sharedHnsw) {
1612
1631
  const codeMod = sharedHnsw.get("code");
1613
1632
  const gitMod = sharedHnsw.get("git");
1614
1633
  const memMod = registry.firstByType("memory");
1615
- let search;
1616
- let bm25;
1617
- let contextBuilder;
1618
- if (codeMod || gitMod || memMod) {
1619
- search = new MultiIndexSearch({
1620
- db,
1621
- codeHnsw: codeMod?.hnsw,
1622
- gitHnsw: gitMod?.hnsw,
1623
- patternHnsw: memMod?.hnsw,
1624
- codeVecs: codeMod?.vecCache ?? /* @__PURE__ */ new Map(),
1625
- gitVecs: gitMod?.vecCache ?? /* @__PURE__ */ new Map(),
1626
- patternVecs: memMod?.vecCache ?? /* @__PURE__ */ new Map(),
1627
- embedding,
1628
- reranker: config.reranker
1629
- });
1630
- bm25 = new BM25Search(db);
1631
- }
1632
- if (search) {
1633
- const firstGit = registry.firstByType("git");
1634
- contextBuilder = new ContextBuilder(search, firstGit?.coEdits);
1635
- }
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);
1636
1649
  return { search, bm25, contextBuilder };
1637
1650
  }
1638
- __name(lateInit, "lateInit");
1651
+ __name(buildSearchLayer, "buildSearchLayer");
1639
1652
  function loadVectors(db, table, idCol, hnsw, cache) {
1640
1653
  const rows = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).all();
1641
1654
  for (const row of rows) {
@@ -1651,7 +1664,7 @@ function loadVectors(db, table, idCol, hnsw, cache) {
1651
1664
  }
1652
1665
  __name(loadVectors, "loadVectors");
1653
1666
 
1654
- // src/app/search-api.ts
1667
+ // src/core/search-api.ts
1655
1668
  var SearchAPI = class {
1656
1669
  constructor(_d) {
1657
1670
  this._d = _d;
@@ -1698,6 +1711,13 @@ var SearchAPI = class {
1698
1711
  const docs = await this._d.searchDocs(query, { k: docsK });
1699
1712
  if (docs.length > 0) resultLists.push(docs);
1700
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) {
1701
1721
  const reserved = /* @__PURE__ */ new Set(["code", "git", "docs"]);
1702
1722
  for (const [name, k] of Object.entries(cols)) {
1703
1723
  if (reserved.has(name)) continue;
@@ -1711,23 +1731,22 @@ var SearchAPI = class {
1711
1731
  })));
1712
1732
  }
1713
1733
  }
1714
- if (resultLists.length === 0) return [];
1715
- const fused = reciprocalRankFusion(resultLists);
1716
- if (this._d.config.reranker && fused.length > 1) {
1717
- const scores = await this._d.config.reranker.rank(query, fused.map((r) => r.content));
1718
- return fused.map((r, i) => {
1719
- const w = i < 3 ? 0.75 : i < 10 ? 0.6 : 0.4;
1720
- return { ...r, score: w * r.score + (1 - w) * (scores[i] ?? 0) };
1721
- }).sort((a, b) => b.score - a.score);
1722
- }
1723
- 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);
1724
1743
  }
1725
1744
  // ── Keyword ─────────────────────────────────────
1726
- searchBM25(query, options) {
1745
+ async searchBM25(query, options) {
1727
1746
  return this._d.bm25?.search(query, options) ?? [];
1728
1747
  }
1729
1748
  rebuildFTS() {
1730
- this._d.bm25?.rebuild();
1749
+ this._d.bm25?.rebuild?.();
1731
1750
  }
1732
1751
  // ── Context ─────────────────────────────────────
1733
1752
  async getContext(task, options = {}) {
@@ -1755,7 +1774,7 @@ ${body}`);
1755
1774
  }
1756
1775
  };
1757
1776
 
1758
- // src/app/index-api.ts
1777
+ // src/core/index-api.ts
1759
1778
  var IndexAPI = class {
1760
1779
  constructor(_d) {
1761
1780
  this._d = _d;
@@ -1963,7 +1982,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
1963
1982
  }
1964
1983
  __name(createWatcher, "createWatcher");
1965
1984
 
1966
- // src/app/brainbank.ts
1985
+ // src/brainbank.ts
1967
1986
  var BrainBank = class extends EventEmitter {
1968
1987
  static {
1969
1988
  __name(this, "BrainBank");
@@ -2018,10 +2037,10 @@ var BrainBank = class extends EventEmitter {
2018
2037
  * Only initializes registered modules.
2019
2038
  * Automatically called by index/search methods if not yet initialized.
2020
2039
  */
2021
- async initialize() {
2040
+ async initialize(options = {}) {
2022
2041
  if (this._initialized) return;
2023
2042
  if (this._initPromise) return this._initPromise;
2024
- this._initPromise = this._runInitialize().catch((err) => {
2043
+ this._initPromise = this._runInitialize(options).catch((err) => {
2025
2044
  for (const { hnsw } of this._sharedHnsw.values()) try {
2026
2045
  hnsw.reinit();
2027
2046
  } catch {
@@ -2045,9 +2064,9 @@ var BrainBank = class extends EventEmitter {
2045
2064
  });
2046
2065
  return this._initPromise;
2047
2066
  }
2048
- async _runInitialize() {
2067
+ async _runInitialize(options = {}) {
2049
2068
  if (this._initialized) return;
2050
- 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);
2051
2070
  this._db = early.db;
2052
2071
  this._embedding = early.embedding;
2053
2072
  this._kvHnsw = early.kvHnsw;
@@ -2115,21 +2134,25 @@ var BrainBank = class extends EventEmitter {
2115
2134
  /** Register a document collection. */
2116
2135
  async addCollection(collection) {
2117
2136
  await this.initialize();
2137
+ this._requireDocs("addCollection");
2118
2138
  this.indexer("docs").addCollection(collection);
2119
2139
  }
2120
2140
  /** Remove a collection and all its indexed data. */
2121
2141
  async removeCollection(name) {
2122
2142
  await this.initialize();
2143
+ this._requireDocs("removeCollection");
2123
2144
  this.indexer("docs").removeCollection(name);
2124
2145
  }
2125
2146
  /** List all registered collections. */
2126
2147
  listCollections() {
2127
2148
  this._requireInit("listCollections");
2149
+ this._requireDocs("listCollections");
2128
2150
  return this.indexer("docs").listCollections();
2129
2151
  }
2130
2152
  /** Index all (or specific) document collections. */
2131
2153
  async indexDocs(options = {}) {
2132
2154
  await this.initialize();
2155
+ this._requireDocs("indexDocs");
2133
2156
  const results = await this.indexer("docs").indexCollections(options);
2134
2157
  this.emit("docsIndexed", results);
2135
2158
  return results;
@@ -2137,19 +2160,23 @@ var BrainBank = class extends EventEmitter {
2137
2160
  /** Search documents only. */
2138
2161
  async searchDocs(query, options) {
2139
2162
  await this.initialize();
2163
+ if (!this.has("docs")) return [];
2140
2164
  return this.indexer("docs").search(query, options);
2141
2165
  }
2142
2166
  // ── Context metadata ─────────────────────────────
2143
2167
  /** Add context description for a collection path. */
2144
2168
  addContext(collection, path4, context) {
2169
+ this._requireDocs("addContext");
2145
2170
  this.indexer("docs").addContext(collection, path4, context);
2146
2171
  }
2147
2172
  /** Remove context for a collection path. */
2148
2173
  removeContext(collection, path4) {
2174
+ this._requireDocs("removeContext");
2149
2175
  this.indexer("docs").removeContext(collection, path4);
2150
2176
  }
2151
2177
  /** List all context entries. */
2152
2178
  listContexts() {
2179
+ this._requireDocs("listContexts");
2153
2180
  return this.indexer("docs").listContexts();
2154
2181
  }
2155
2182
  // ── Search (delegated to SearchAPI) ─────────────
@@ -2185,7 +2212,7 @@ var BrainBank = class extends EventEmitter {
2185
2212
  return this._searchAPI.hybridSearch(query, options);
2186
2213
  }
2187
2214
  /** BM25 keyword search only (no embeddings needed). */
2188
- searchBM25(query, options) {
2215
+ async searchBM25(query, options) {
2189
2216
  this._requireInit("searchBM25");
2190
2217
  return this._searchAPI.searchBM25(query, options);
2191
2218
  }
@@ -2197,20 +2224,15 @@ var BrainBank = class extends EventEmitter {
2197
2224
  // ── Queries ──────────────────────────────────────
2198
2225
  /** Get git history for a specific file. */
2199
2226
  async fileHistory(filePath, limit = 20) {
2200
- this.indexer("git");
2201
2227
  await this.initialize();
2202
- return this._db.prepare(`
2203
- SELECT c.short_hash, c.message, c.author, c.date, c.additions, c.deletions
2204
- FROM git_commits c
2205
- INNER JOIN commit_files cf ON c.id = cf.commit_id
2206
- WHERE cf.file_path LIKE ? AND c.is_merge = 0
2207
- ORDER BY c.timestamp DESC LIMIT ?
2208
- `).all(`%${filePath}%`, limit);
2228
+ const gitPlugin = this.indexer("git");
2229
+ return gitPlugin.fileHistory(filePath, limit);
2209
2230
  }
2210
2231
  /** Get co-edit suggestions for a file. */
2211
2232
  coEdits(filePath, limit = 5) {
2212
2233
  this._requireInit("coEdits");
2213
- return this.indexer("git").suggestCoEdits(filePath, limit);
2234
+ const gitPlugin = this.indexer("git");
2235
+ return gitPlugin.suggestCoEdits(filePath, limit);
2214
2236
  }
2215
2237
  // ── Stats ────────────────────────────────────────
2216
2238
  /** Get statistics for all loaded modules. */
@@ -2218,24 +2240,13 @@ var BrainBank = class extends EventEmitter {
2218
2240
  this._requireInit("stats");
2219
2241
  const result = {};
2220
2242
  if (this.has("code")) {
2221
- const sh = this._sharedHnsw.get("code");
2222
- result.code = {
2223
- files: this._db.prepare("SELECT COUNT(DISTINCT file_path) as c FROM code_chunks").get().c,
2224
- chunks: this._db.prepare("SELECT COUNT(*) as c FROM code_chunks").get().c,
2225
- hnswSize: sh?.hnsw.size ?? 0
2226
- };
2243
+ result.code = this._registry.firstByType("code").stats();
2227
2244
  }
2228
2245
  if (this.has("git")) {
2229
- const sh = this._sharedHnsw.get("git");
2230
- result.git = {
2231
- commits: this._db.prepare("SELECT COUNT(*) as c FROM git_commits").get().c,
2232
- filesTracked: this._db.prepare("SELECT COUNT(DISTINCT file_path) as c FROM commit_files").get().c,
2233
- coEdits: this._db.prepare("SELECT COUNT(*) as c FROM co_edits").get().c,
2234
- hnswSize: sh?.hnsw.size ?? 0
2235
- };
2246
+ result.git = this._registry.firstByType("git").stats();
2236
2247
  }
2237
2248
  if (this.has("docs")) {
2238
- result.documents = this.indexer("docs").stats();
2249
+ result.documents = this._registry.firstByType("docs").stats();
2239
2250
  }
2240
2251
  return result;
2241
2252
  }
@@ -2282,6 +2293,8 @@ var BrainBank = class extends EventEmitter {
2282
2293
  close() {
2283
2294
  this._watcher?.close();
2284
2295
  for (const indexer of this._registry.all) indexer.close?.();
2296
+ this._embedding?.close().catch(() => {
2297
+ });
2285
2298
  this._db?.close();
2286
2299
  this._initialized = false;
2287
2300
  this._collections.clear();
@@ -2305,6 +2318,10 @@ var BrainBank = class extends EventEmitter {
2305
2318
  if (!this._initialized)
2306
2319
  throw new Error(`BrainBank: Not initialized. Call await brain.initialize() before ${method}().`);
2307
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
+ }
2308
2325
  };
2309
2326
 
2310
2327
  export {
@@ -2314,9 +2331,9 @@ export {
2314
2331
  HNSWIndex,
2315
2332
  LocalEmbedding,
2316
2333
  searchMMR,
2317
- MultiIndexSearch,
2318
- BM25Search,
2334
+ VectorSearch,
2335
+ KeywordSearch,
2319
2336
  ContextBuilder,
2320
2337
  BrainBank
2321
2338
  };
2322
- //# sourceMappingURL=chunk-ZJ5LLMGM.js.map
2339
+ //# sourceMappingURL=chunk-2BEWWQL2.js.map