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.
- package/README.md +19 -9
- package/dist/{base-9vfWRHCV.d.ts → base-4SUgeRWT.d.ts} +25 -2
- package/dist/{chunk-ZJ5LLMGM.js → chunk-2BEWWQL2.js} +432 -415
- package/dist/chunk-2BEWWQL2.js.map +1 -0
- package/dist/{chunk-OPQ3ZIPV.js → chunk-5VUYPNH3.js} +47 -3
- package/dist/chunk-5VUYPNH3.js.map +1 -0
- package/dist/chunk-CCXVL56V.js +120 -0
- package/dist/chunk-CCXVL56V.js.map +1 -0
- package/dist/{chunk-FEAMUZGJ.js → chunk-E6WQM4DN.js} +3 -3
- package/dist/chunk-E6WQM4DN.js.map +1 -0
- package/dist/chunk-FI7GWG4W.js +309 -0
- package/dist/chunk-FI7GWG4W.js.map +1 -0
- package/dist/{chunk-X6645UVR.js → chunk-FINIFKAY.js} +136 -4
- package/dist/chunk-FINIFKAY.js.map +1 -0
- package/dist/{chunk-4DM3XWO6.js → chunk-MGIFEPYZ.js} +54 -42
- package/dist/chunk-MGIFEPYZ.js.map +1 -0
- package/dist/{chunk-T2VXF5S5.js → chunk-Y3JKI6QN.js} +152 -137
- package/dist/chunk-Y3JKI6QN.js.map +1 -0
- package/dist/cli.js +34 -28
- package/dist/cli.js.map +1 -1
- package/dist/code.d.ts +1 -1
- package/dist/code.js +1 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +1 -1
- package/dist/git.d.ts +1 -1
- package/dist/git.js +1 -1
- package/dist/index.d.ts +121 -82
- package/dist/index.js +66 -15
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +1 -1
- package/dist/memory.js +3 -137
- package/dist/memory.js.map +1 -1
- package/dist/notes.d.ts +1 -1
- package/dist/notes.js +4 -49
- package/dist/notes.js.map +1 -1
- package/dist/{openai-CYDMYX7X.js → openai-embedding-VQZCZQYT.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-4DM3XWO6.js.map +0 -1
- package/dist/chunk-7JCEW7LT.js +0 -266
- package/dist/chunk-7JCEW7LT.js.map +0 -1
- package/dist/chunk-FEAMUZGJ.js.map +0 -1
- package/dist/chunk-GUT5MSJT.js +0 -99
- package/dist/chunk-GUT5MSJT.js.map +0 -1
- package/dist/chunk-OPQ3ZIPV.js.map +0 -1
- package/dist/chunk-T2VXF5S5.js.map +0 -1
- package/dist/chunk-X6645UVR.js.map +0 -1
- package/dist/chunk-ZJ5LLMGM.js.map +0 -1
- /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-
|
|
5
|
+
} from "./chunk-MGIFEPYZ.js";
|
|
6
6
|
import {
|
|
7
7
|
normalizeBM25,
|
|
8
8
|
reciprocalRankFusion,
|
|
9
9
|
sanitizeFTS
|
|
10
|
-
} from "./chunk-
|
|
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/
|
|
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: "
|
|
135
|
-
bm25Hits.map((h) => ({ type: "
|
|
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
|
|
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
|
-
*
|
|
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 (
|
|
455
|
-
|
|
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/
|
|
497
|
-
|
|
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, "
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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/
|
|
638
|
-
var
|
|
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, "
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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/
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
|
856
|
-
|
|
857
|
-
parts.push(
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
881
|
-
parts.push("## Co-Edit Patterns\n");
|
|
882
|
-
parts.push(...coEditLines);
|
|
883
|
-
parts.push("");
|
|
884
|
-
}
|
|
881
|
+
parts.push("");
|
|
885
882
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
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/
|
|
920
|
+
// src/brainbank.ts
|
|
905
921
|
import { EventEmitter } from "events";
|
|
906
922
|
|
|
907
|
-
// src/
|
|
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
|
|
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
|
-
|
|
1479
|
-
|
|
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/
|
|
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
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
|
|
1610
|
-
|
|
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
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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(
|
|
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/
|
|
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
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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/
|
|
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/
|
|
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
|
-
|
|
2203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
2318
|
-
|
|
2334
|
+
VectorSearch,
|
|
2335
|
+
KeywordSearch,
|
|
2319
2336
|
ContextBuilder,
|
|
2320
2337
|
BrainBank
|
|
2321
2338
|
};
|
|
2322
|
-
//# sourceMappingURL=chunk-
|
|
2339
|
+
//# sourceMappingURL=chunk-2BEWWQL2.js.map
|