brainbank 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -9
- package/dist/{base-9vfWRHCV.d.ts → base-4SUgeRWT.d.ts} +25 -2
- package/dist/{chunk-6MFTQV3O.js → chunk-2BEWWQL2.js} +435 -386
- package/dist/chunk-2BEWWQL2.js.map +1 -0
- package/dist/{chunk-FJJY4H2Y.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-V4UJKXPK.js → chunk-E6WQM4DN.js} +9 -4
- 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-WR4WXKJT.js → chunk-MGIFEPYZ.js} +62 -42
- package/dist/chunk-MGIFEPYZ.js.map +1 -0
- package/dist/{chunk-F6SJ3U4H.js → chunk-Y3JKI6QN.js} +152 -141
- package/dist/chunk-Y3JKI6QN.js.map +1 -0
- package/dist/cli.js +61 -32
- 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-6MFTQV3O.js.map +0 -1
- package/dist/chunk-7JCEW7LT.js +0 -266
- package/dist/chunk-7JCEW7LT.js.map +0 -1
- package/dist/chunk-F6SJ3U4H.js.map +0 -1
- package/dist/chunk-FJJY4H2Y.js.map +0 -1
- package/dist/chunk-GUT5MSJT.js +0 -99
- package/dist/chunk-GUT5MSJT.js.map +0 -1
- package/dist/chunk-V4UJKXPK.js.map +0 -1
- package/dist/chunk-WR4WXKJT.js.map +0 -1
- package/dist/chunk-X6645UVR.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,240 +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 [];
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
language: r.language,
|
|
678
|
-
searchType: "bm25"
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
} catch {
|
|
663
|
+
const results = [];
|
|
664
|
+
if (codeK > 0) this._searchCode(ftsQuery, query, codeK, results);
|
|
665
|
+
if (gitK > 0) this._searchGit(ftsQuery, gitK, results);
|
|
666
|
+
if (patternK > 0) this._searchPatterns(ftsQuery, patternK, results);
|
|
667
|
+
return results.sort((a, b) => b.score - a.score);
|
|
668
|
+
}
|
|
669
|
+
/** FTS5 search across code chunks + file-path fallback. */
|
|
670
|
+
_searchCode(ftsQuery, rawQuery, k, results) {
|
|
671
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
672
|
+
try {
|
|
673
|
+
const rows = this._db.prepare(`
|
|
674
|
+
SELECT c.id, c.file_path, c.chunk_type, c.name, c.start_line, c.end_line,
|
|
675
|
+
c.content, c.language, bm25(fts_code, 5.0, 3.0, 1.0) AS score
|
|
676
|
+
FROM fts_code f
|
|
677
|
+
JOIN code_chunks c ON c.id = f.rowid
|
|
678
|
+
WHERE fts_code MATCH ?
|
|
679
|
+
ORDER BY score ASC
|
|
680
|
+
LIMIT ?
|
|
681
|
+
`).all(ftsQuery, k);
|
|
682
|
+
for (const r of rows) {
|
|
683
|
+
seenIds.add(r.id);
|
|
684
|
+
results.push(this._toCodeResult(r, normalizeBM25(r.score), "bm25"));
|
|
683
685
|
}
|
|
686
|
+
} catch {
|
|
684
687
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
metadata: {
|
|
703
|
-
hash: r.hash,
|
|
704
|
-
shortHash: r.short_hash,
|
|
705
|
-
author: r.author,
|
|
706
|
-
date: r.date,
|
|
707
|
-
files: JSON.parse(r.files_json ?? "[]"),
|
|
708
|
-
additions: r.additions,
|
|
709
|
-
deletions: r.deletions,
|
|
710
|
-
diff: r.diff,
|
|
711
|
-
searchType: "bm25"
|
|
712
|
-
}
|
|
713
|
-
});
|
|
688
|
+
this._searchCodeByPath(rawQuery, seenIds, results);
|
|
689
|
+
}
|
|
690
|
+
/** File-path fallback: match filenames via LIKE. */
|
|
691
|
+
_searchCodeByPath(rawQuery, seenIds, results) {
|
|
692
|
+
try {
|
|
693
|
+
const words = rawQuery.replace(/[^a-zA-Z0-9]/g, " ").split(/\s+/).filter((w) => w.length > 2);
|
|
694
|
+
for (const word of words.slice(0, 3)) {
|
|
695
|
+
const pathRows = this._db.prepare(`
|
|
696
|
+
SELECT id, file_path, chunk_type, name, start_line, end_line, content, language
|
|
697
|
+
FROM code_chunks
|
|
698
|
+
WHERE file_path LIKE ? AND chunk_type = 'file'
|
|
699
|
+
LIMIT 3
|
|
700
|
+
`).all(`%${word}%`);
|
|
701
|
+
for (const r of pathRows) {
|
|
702
|
+
if (seenIds.has(r.id)) continue;
|
|
703
|
+
seenIds.add(r.id);
|
|
704
|
+
results.push(this._toCodeResult(r, 0.6, "bm25-path"));
|
|
714
705
|
}
|
|
715
|
-
} catch {
|
|
716
706
|
}
|
|
707
|
+
} catch {
|
|
717
708
|
}
|
|
718
|
-
|
|
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
|
-
|
|
709
|
+
}
|
|
710
|
+
/** FTS5 search across git commits. */
|
|
711
|
+
_searchGit(ftsQuery, k, results) {
|
|
712
|
+
try {
|
|
713
|
+
const rows = this._db.prepare(`
|
|
714
|
+
SELECT c.id, c.hash, c.short_hash, c.message, c.author, c.date,
|
|
715
|
+
c.files_json, c.diff, c.additions, c.deletions,
|
|
716
|
+
bm25(fts_commits, 5.0, 2.0, 1.0) AS score
|
|
717
|
+
FROM fts_commits f
|
|
718
|
+
JOIN git_commits c ON c.id = f.rowid
|
|
719
|
+
WHERE fts_commits MATCH ? AND c.is_merge = 0
|
|
720
|
+
ORDER BY score ASC
|
|
721
|
+
LIMIT ?
|
|
722
|
+
`).all(ftsQuery, k);
|
|
723
|
+
for (const r of rows) {
|
|
724
|
+
results.push({
|
|
725
|
+
type: "commit",
|
|
726
|
+
score: normalizeBM25(r.score),
|
|
727
|
+
content: r.message,
|
|
728
|
+
metadata: {
|
|
729
|
+
hash: r.hash,
|
|
730
|
+
shortHash: r.short_hash,
|
|
731
|
+
author: r.author,
|
|
732
|
+
date: r.date,
|
|
733
|
+
files: JSON.parse(r.files_json ?? "[]"),
|
|
734
|
+
additions: r.additions,
|
|
735
|
+
deletions: r.deletions,
|
|
736
|
+
diff: r.diff,
|
|
737
|
+
searchType: "bm25"
|
|
738
|
+
}
|
|
739
|
+
});
|
|
746
740
|
}
|
|
741
|
+
} catch {
|
|
747
742
|
}
|
|
748
|
-
return results.sort((a, b) => b.score - a.score);
|
|
749
743
|
}
|
|
750
|
-
/**
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
744
|
+
/** FTS5 search across memory patterns. */
|
|
745
|
+
_searchPatterns(ftsQuery, k, results) {
|
|
746
|
+
try {
|
|
747
|
+
const rows = this._db.prepare(`
|
|
748
|
+
SELECT p.id, p.task_type, p.task, p.approach, p.outcome,
|
|
749
|
+
p.success_rate, p.critique,
|
|
750
|
+
bm25(fts_patterns, 3.0, 5.0, 5.0, 1.0) AS score
|
|
751
|
+
FROM fts_patterns f
|
|
752
|
+
JOIN memory_patterns p ON p.id = f.rowid
|
|
753
|
+
WHERE fts_patterns MATCH ? AND p.success_rate >= 0.5
|
|
754
|
+
ORDER BY score ASC
|
|
755
|
+
LIMIT ?
|
|
756
|
+
`).all(ftsQuery, k);
|
|
757
|
+
for (const r of rows) {
|
|
758
|
+
results.push({
|
|
759
|
+
type: "pattern",
|
|
760
|
+
score: normalizeBM25(r.score),
|
|
761
|
+
content: r.approach,
|
|
762
|
+
metadata: {
|
|
763
|
+
taskType: r.task_type,
|
|
764
|
+
task: r.task,
|
|
765
|
+
outcome: r.outcome,
|
|
766
|
+
successRate: r.success_rate,
|
|
767
|
+
critique: r.critique,
|
|
768
|
+
searchType: "bm25"
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/** Map a code_chunks row to a CodeResult. */
|
|
776
|
+
_toCodeResult(r, score, searchType) {
|
|
777
|
+
return {
|
|
778
|
+
type: "code",
|
|
779
|
+
score,
|
|
780
|
+
filePath: r.file_path,
|
|
781
|
+
content: r.content,
|
|
782
|
+
metadata: {
|
|
783
|
+
chunkType: r.chunk_type,
|
|
784
|
+
name: r.name,
|
|
785
|
+
startLine: r.start_line,
|
|
786
|
+
endLine: r.end_line,
|
|
787
|
+
language: r.language,
|
|
788
|
+
searchType
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
/** Rebuild the FTS index from scratch. */
|
|
754
793
|
rebuild() {
|
|
755
794
|
try {
|
|
756
795
|
this._db.prepare("INSERT INTO fts_code(fts_code) VALUES('rebuild')").run();
|
|
@@ -761,7 +800,7 @@ var BM25Search = class {
|
|
|
761
800
|
}
|
|
762
801
|
};
|
|
763
802
|
|
|
764
|
-
// src/
|
|
803
|
+
// src/core/context-builder.ts
|
|
765
804
|
var ContextBuilder = class {
|
|
766
805
|
constructor(_search, _coEdits) {
|
|
767
806
|
this._search = _search;
|
|
@@ -770,10 +809,7 @@ var ContextBuilder = class {
|
|
|
770
809
|
static {
|
|
771
810
|
__name(this, "ContextBuilder");
|
|
772
811
|
}
|
|
773
|
-
/**
|
|
774
|
-
* Build a full context block for a task.
|
|
775
|
-
* Returns clean markdown ready for system prompt injection.
|
|
776
|
-
*/
|
|
812
|
+
/** Build a full context block for a task. Returns markdown for system prompt. */
|
|
777
813
|
async build(task, options = {}) {
|
|
778
814
|
const {
|
|
779
815
|
codeResults = 6,
|
|
@@ -794,85 +830,97 @@ var ContextBuilder = class {
|
|
|
794
830
|
});
|
|
795
831
|
const parts = [`# Context for: "${task}"
|
|
796
832
|
`];
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
parts.push(c.content);
|
|
814
|
-
parts.push("```\n");
|
|
815
|
-
}
|
|
816
|
-
}
|
|
833
|
+
this._formatCodeResults(results, codeResults, parts);
|
|
834
|
+
this._formatGitResults(results, gitResults, parts);
|
|
835
|
+
this._formatCoEdits(affectedFiles, parts);
|
|
836
|
+
this._formatPatternResults(results, patternResults, parts);
|
|
837
|
+
return parts.join("\n");
|
|
838
|
+
}
|
|
839
|
+
/** Format code search results grouped by file. */
|
|
840
|
+
_formatCodeResults(results, limit, parts) {
|
|
841
|
+
const codeHits = results.filter((r) => r.type === "code").slice(0, limit);
|
|
842
|
+
if (codeHits.length === 0) return;
|
|
843
|
+
parts.push("## Relevant Code\n");
|
|
844
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
845
|
+
for (const r of codeHits) {
|
|
846
|
+
const key = r.filePath ?? "unknown";
|
|
847
|
+
if (!byFile.has(key)) byFile.set(key, []);
|
|
848
|
+
byFile.get(key).push(r);
|
|
817
849
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
for (const c of gitHits) {
|
|
850
|
+
for (const [file, chunks] of byFile) {
|
|
851
|
+
parts.push(`### ${file}`);
|
|
852
|
+
for (const c of chunks) {
|
|
822
853
|
const m = c.metadata;
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
parts.push(
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const snippet = m.diff.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@")).slice(0, 10).join("\n");
|
|
829
|
-
if (snippet) {
|
|
830
|
-
parts.push("```diff");
|
|
831
|
-
parts.push(snippet);
|
|
832
|
-
parts.push("```");
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
parts.push("");
|
|
854
|
+
const label = m.name ? `${m.chunkType} \`${m.name}\` (L${m.startLine}-${m.endLine})` : `L${m.startLine}-${m.endLine}`;
|
|
855
|
+
parts.push(`**${label}** \u2014 ${Math.round(c.score * 100)}% match`);
|
|
856
|
+
parts.push("```" + (m.language || ""));
|
|
857
|
+
parts.push(c.content);
|
|
858
|
+
parts.push("```\n");
|
|
836
859
|
}
|
|
837
860
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
861
|
+
}
|
|
862
|
+
/** Format git commit results with diff snippets. */
|
|
863
|
+
_formatGitResults(results, limit, parts) {
|
|
864
|
+
const gitHits = results.filter((r) => r.type === "commit").slice(0, limit);
|
|
865
|
+
if (gitHits.length === 0) return;
|
|
866
|
+
parts.push("## Related Git History\n");
|
|
867
|
+
for (const c of gitHits) {
|
|
868
|
+
const m = c.metadata;
|
|
869
|
+
const score = Math.round(c.score * 100);
|
|
870
|
+
const files = (m.files ?? []).slice(0, 4).join(", ");
|
|
871
|
+
parts.push(`**[${m.shortHash}]** ${c.content} *(${m.author}, ${m.date?.slice(0, 10)}, ${score}%)*`);
|
|
872
|
+
if (files) parts.push(` Files: ${files}`);
|
|
873
|
+
if (m.diff) {
|
|
874
|
+
const snippet = m.diff.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@")).slice(0, 10).join("\n");
|
|
875
|
+
if (snippet) {
|
|
876
|
+
parts.push("```diff");
|
|
877
|
+
parts.push(snippet);
|
|
878
|
+
parts.push("```");
|
|
846
879
|
}
|
|
847
880
|
}
|
|
848
|
-
|
|
849
|
-
parts.push("## Co-Edit Patterns\n");
|
|
850
|
-
parts.push(...coEditLines);
|
|
851
|
-
parts.push("");
|
|
852
|
-
}
|
|
881
|
+
parts.push("");
|
|
853
882
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
parts.push("");
|
|
883
|
+
}
|
|
884
|
+
/** Format co-edit suggestions for affected files. */
|
|
885
|
+
_formatCoEdits(affectedFiles, parts) {
|
|
886
|
+
if (affectedFiles.length === 0 || !this._coEdits) return;
|
|
887
|
+
const coEditLines = [];
|
|
888
|
+
for (const file of affectedFiles.slice(0, 3)) {
|
|
889
|
+
const suggestions = this._coEdits.suggest(file, 4);
|
|
890
|
+
if (suggestions.length > 0) {
|
|
891
|
+
coEditLines.push(
|
|
892
|
+
`- **${file}** \u2192 also tends to change: ${suggestions.map((s) => `${s.file} (${s.count}x)`).join(", ")}`
|
|
893
|
+
);
|
|
866
894
|
}
|
|
867
895
|
}
|
|
868
|
-
|
|
896
|
+
if (coEditLines.length > 0) {
|
|
897
|
+
parts.push("## Co-Edit Patterns\n");
|
|
898
|
+
parts.push(...coEditLines);
|
|
899
|
+
parts.push("");
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/** Format memory pattern results. */
|
|
903
|
+
_formatPatternResults(results, limit, parts) {
|
|
904
|
+
const memHits = results.filter((r) => r.type === "pattern").slice(0, limit);
|
|
905
|
+
if (memHits.length === 0) return;
|
|
906
|
+
parts.push("## Learned Patterns\n");
|
|
907
|
+
for (const p of memHits) {
|
|
908
|
+
const m = p.metadata;
|
|
909
|
+
const score = Math.round(p.score * 100);
|
|
910
|
+
const success = Math.round((m.successRate ?? 0) * 100);
|
|
911
|
+
parts.push(`**${m.taskType}** \u2014 ${success}% success, ${score}% match`);
|
|
912
|
+
parts.push(`Task: ${m.task}`);
|
|
913
|
+
parts.push(`Approach: ${p.content}`);
|
|
914
|
+
if (m.critique) parts.push(`Lesson: ${m.critique}`);
|
|
915
|
+
parts.push("");
|
|
916
|
+
}
|
|
869
917
|
}
|
|
870
918
|
};
|
|
871
919
|
|
|
872
|
-
// src/
|
|
920
|
+
// src/brainbank.ts
|
|
873
921
|
import { EventEmitter } from "events";
|
|
874
922
|
|
|
875
|
-
// src/
|
|
923
|
+
// src/core/registry.ts
|
|
876
924
|
var ALIASES = {};
|
|
877
925
|
var IndexerRegistry = class {
|
|
878
926
|
static {
|
|
@@ -1283,6 +1331,7 @@ var Database = class {
|
|
|
1283
1331
|
}
|
|
1284
1332
|
this.db = new BetterSqlite3(dbPath);
|
|
1285
1333
|
this.db.pragma("journal_mode = WAL");
|
|
1334
|
+
this.db.pragma("busy_timeout = 5000");
|
|
1286
1335
|
this.db.pragma("synchronous = NORMAL");
|
|
1287
1336
|
this.db.pragma("foreign_keys = ON");
|
|
1288
1337
|
createSchema(this.db);
|
|
@@ -1435,7 +1484,10 @@ async function reembedTable(db, embedding, table, batchSize, onProgress) {
|
|
|
1435
1484
|
`SELECT COUNT(*) as c FROM ${table.textTable}`
|
|
1436
1485
|
).get().c;
|
|
1437
1486
|
if (totalCount === 0) return 0;
|
|
1438
|
-
const
|
|
1487
|
+
const insertVec = db.prepare(
|
|
1488
|
+
`INSERT INTO ${table.vectorTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
|
|
1489
|
+
);
|
|
1490
|
+
db.prepare(`DELETE FROM ${table.vectorTable}`).run();
|
|
1439
1491
|
let processed = 0;
|
|
1440
1492
|
for (let offset = 0; offset < totalCount; offset += batchSize) {
|
|
1441
1493
|
const batch = db.prepare(
|
|
@@ -1443,21 +1495,14 @@ async function reembedTable(db, embedding, table, batchSize, onProgress) {
|
|
|
1443
1495
|
).all(batchSize, offset);
|
|
1444
1496
|
const texts = batch.map((r) => table.textBuilder(r));
|
|
1445
1497
|
const vectors = await embedding.embedBatch(texts);
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1498
|
+
db.transaction(() => {
|
|
1499
|
+
for (let j = 0; j < batch.length; j++) {
|
|
1500
|
+
insertVec.run(batch[j][table.idColumn], Buffer.from(vectors[j].buffer));
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1449
1503
|
processed += batch.length;
|
|
1450
1504
|
onProgress?.(table.name, processed, totalCount);
|
|
1451
1505
|
}
|
|
1452
|
-
const insertVec = db.prepare(
|
|
1453
|
-
`INSERT INTO ${table.vectorTable} (${table.fkColumn}, embedding) VALUES (?, ?)`
|
|
1454
|
-
);
|
|
1455
|
-
db.transaction(() => {
|
|
1456
|
-
db.prepare(`DELETE FROM ${table.vectorTable}`).run();
|
|
1457
|
-
for (const { id, vec } of allNewVectors) {
|
|
1458
|
-
insertVec.run(id, Buffer.from(vec.buffer));
|
|
1459
|
-
}
|
|
1460
|
-
});
|
|
1461
1506
|
return processed;
|
|
1462
1507
|
}
|
|
1463
1508
|
__name(reembedTable, "reembedTable");
|
|
@@ -1512,19 +1557,16 @@ function detectProviderMismatch(db, embedding) {
|
|
|
1512
1557
|
}
|
|
1513
1558
|
__name(detectProviderMismatch, "detectProviderMismatch");
|
|
1514
1559
|
|
|
1515
|
-
// src/
|
|
1516
|
-
async function earlyInit(config, emit) {
|
|
1560
|
+
// src/core/initializer.ts
|
|
1561
|
+
async function earlyInit(config, emit, options = {}) {
|
|
1517
1562
|
const db = new Database(config.dbPath);
|
|
1518
1563
|
const embedding = config.embeddingProvider ?? new LocalEmbedding();
|
|
1519
1564
|
const mismatch = detectProviderMismatch(db, embedding);
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
current: mismatch.current,
|
|
1526
|
-
message: "Embedding provider changed \u2014 vectors not loaded. Run brain.reembed() to regenerate."
|
|
1527
|
-
});
|
|
1565
|
+
if (mismatch?.mismatch && !options.force) {
|
|
1566
|
+
db.close();
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
`BrainBank: Embedding dimension mismatch (stored: ${mismatch.stored}, current: ${mismatch.current}). Run brain.reembed() to re-index with the new provider, or switch back to the original provider.`
|
|
1569
|
+
);
|
|
1528
1570
|
}
|
|
1529
1571
|
setEmbeddingMeta(db, embedding);
|
|
1530
1572
|
const kvHnsw = new HNSWIndex(
|
|
@@ -1535,6 +1577,7 @@ async function earlyInit(config, emit) {
|
|
|
1535
1577
|
config.hnswEfSearch
|
|
1536
1578
|
);
|
|
1537
1579
|
await kvHnsw.init();
|
|
1580
|
+
const skipVectorLoad = !!(options.force && mismatch?.mismatch);
|
|
1538
1581
|
return { db, embedding, kvHnsw, skipVectorLoad };
|
|
1539
1582
|
}
|
|
1540
1583
|
__name(earlyInit, "earlyInit");
|
|
@@ -1543,7 +1586,15 @@ async function lateInit(early, config, registry, sharedHnsw, kvVecs, getCollecti
|
|
|
1543
1586
|
if (!skipVectorLoad) {
|
|
1544
1587
|
loadVectors(db, "kv_vectors", "data_id", kvHnsw, kvVecs);
|
|
1545
1588
|
}
|
|
1546
|
-
const ctx =
|
|
1589
|
+
const ctx = buildIndexerContext(db, embedding, config, sharedHnsw, skipVectorLoad, getCollection);
|
|
1590
|
+
for (const mod of registry.all) {
|
|
1591
|
+
await mod.initialize(ctx);
|
|
1592
|
+
}
|
|
1593
|
+
return buildSearchLayer(db, embedding, config, registry, sharedHnsw);
|
|
1594
|
+
}
|
|
1595
|
+
__name(lateInit, "lateInit");
|
|
1596
|
+
function buildIndexerContext(db, embedding, config, sharedHnsw, skipVectorLoad, getCollection) {
|
|
1597
|
+
return {
|
|
1547
1598
|
db,
|
|
1548
1599
|
embedding,
|
|
1549
1600
|
config,
|
|
@@ -1574,36 +1625,30 @@ async function lateInit(early, config, registry, sharedHnsw, kvVecs, getCollecti
|
|
|
1574
1625
|
}, "getOrCreateSharedHnsw"),
|
|
1575
1626
|
collection: getCollection
|
|
1576
1627
|
};
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1628
|
+
}
|
|
1629
|
+
__name(buildIndexerContext, "buildIndexerContext");
|
|
1630
|
+
function buildSearchLayer(db, embedding, config, registry, sharedHnsw) {
|
|
1580
1631
|
const codeMod = sharedHnsw.get("code");
|
|
1581
1632
|
const gitMod = sharedHnsw.get("git");
|
|
1582
1633
|
const memMod = registry.firstByType("memory");
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
bm25 = new BM25Search(db);
|
|
1599
|
-
}
|
|
1600
|
-
if (search) {
|
|
1601
|
-
const firstGit = registry.firstByType("git");
|
|
1602
|
-
contextBuilder = new ContextBuilder(search, firstGit?.coEdits);
|
|
1603
|
-
}
|
|
1634
|
+
if (!codeMod && !gitMod && !memMod) return {};
|
|
1635
|
+
const search = new VectorSearch({
|
|
1636
|
+
db,
|
|
1637
|
+
codeHnsw: codeMod?.hnsw,
|
|
1638
|
+
gitHnsw: gitMod?.hnsw,
|
|
1639
|
+
patternHnsw: memMod?.hnsw,
|
|
1640
|
+
codeVecs: codeMod?.vecCache ?? /* @__PURE__ */ new Map(),
|
|
1641
|
+
gitVecs: gitMod?.vecCache ?? /* @__PURE__ */ new Map(),
|
|
1642
|
+
patternVecs: memMod?.vecCache ?? /* @__PURE__ */ new Map(),
|
|
1643
|
+
embedding,
|
|
1644
|
+
reranker: config.reranker
|
|
1645
|
+
});
|
|
1646
|
+
const bm25 = new KeywordSearch(db);
|
|
1647
|
+
const firstGit = registry.firstByType("git");
|
|
1648
|
+
const contextBuilder = new ContextBuilder(search, firstGit?.coEdits);
|
|
1604
1649
|
return { search, bm25, contextBuilder };
|
|
1605
1650
|
}
|
|
1606
|
-
__name(
|
|
1651
|
+
__name(buildSearchLayer, "buildSearchLayer");
|
|
1607
1652
|
function loadVectors(db, table, idCol, hnsw, cache) {
|
|
1608
1653
|
const rows = db.prepare(`SELECT ${idCol}, embedding FROM ${table}`).all();
|
|
1609
1654
|
for (const row of rows) {
|
|
@@ -1619,7 +1664,7 @@ function loadVectors(db, table, idCol, hnsw, cache) {
|
|
|
1619
1664
|
}
|
|
1620
1665
|
__name(loadVectors, "loadVectors");
|
|
1621
1666
|
|
|
1622
|
-
// src/
|
|
1667
|
+
// src/core/search-api.ts
|
|
1623
1668
|
var SearchAPI = class {
|
|
1624
1669
|
constructor(_d) {
|
|
1625
1670
|
this._d = _d;
|
|
@@ -1666,6 +1711,13 @@ var SearchAPI = class {
|
|
|
1666
1711
|
const docs = await this._d.searchDocs(query, { k: docsK });
|
|
1667
1712
|
if (docs.length > 0) resultLists.push(docs);
|
|
1668
1713
|
}
|
|
1714
|
+
await this._searchKvCollections(query, cols, resultLists);
|
|
1715
|
+
if (resultLists.length === 0) return [];
|
|
1716
|
+
const fused = reciprocalRankFusion(resultLists);
|
|
1717
|
+
return this._applyReranking(query, fused);
|
|
1718
|
+
}
|
|
1719
|
+
/** Search non-reserved KV collections and push results. */
|
|
1720
|
+
async _searchKvCollections(query, cols, resultLists) {
|
|
1669
1721
|
const reserved = /* @__PURE__ */ new Set(["code", "git", "docs"]);
|
|
1670
1722
|
for (const [name, k] of Object.entries(cols)) {
|
|
1671
1723
|
if (reserved.has(name)) continue;
|
|
@@ -1679,23 +1731,22 @@ var SearchAPI = class {
|
|
|
1679
1731
|
})));
|
|
1680
1732
|
}
|
|
1681
1733
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
}
|
|
1691
|
-
return fused;
|
|
1734
|
+
}
|
|
1735
|
+
/** Apply reranking if a reranker is configured. */
|
|
1736
|
+
async _applyReranking(query, fused) {
|
|
1737
|
+
if (!this._d.config.reranker || fused.length <= 1) return fused;
|
|
1738
|
+
const scores = await this._d.config.reranker.rank(query, fused.map((r) => r.content));
|
|
1739
|
+
return fused.map((r, i) => {
|
|
1740
|
+
const w = i < 3 ? 0.75 : i < 10 ? 0.6 : 0.4;
|
|
1741
|
+
return { ...r, score: w * r.score + (1 - w) * (scores[i] ?? 0) };
|
|
1742
|
+
}).sort((a, b) => b.score - a.score);
|
|
1692
1743
|
}
|
|
1693
1744
|
// ── Keyword ─────────────────────────────────────
|
|
1694
|
-
searchBM25(query, options) {
|
|
1745
|
+
async searchBM25(query, options) {
|
|
1695
1746
|
return this._d.bm25?.search(query, options) ?? [];
|
|
1696
1747
|
}
|
|
1697
1748
|
rebuildFTS() {
|
|
1698
|
-
this._d.bm25?.rebuild();
|
|
1749
|
+
this._d.bm25?.rebuild?.();
|
|
1699
1750
|
}
|
|
1700
1751
|
// ── Context ─────────────────────────────────────
|
|
1701
1752
|
async getContext(task, options = {}) {
|
|
@@ -1723,7 +1774,7 @@ ${body}`);
|
|
|
1723
1774
|
}
|
|
1724
1775
|
};
|
|
1725
1776
|
|
|
1726
|
-
// src/
|
|
1777
|
+
// src/core/index-api.ts
|
|
1727
1778
|
var IndexAPI = class {
|
|
1728
1779
|
constructor(_d) {
|
|
1729
1780
|
this._d = _d;
|
|
@@ -1931,7 +1982,7 @@ function createWatcher(reindexFn, indexers, repoPath, options = {}) {
|
|
|
1931
1982
|
}
|
|
1932
1983
|
__name(createWatcher, "createWatcher");
|
|
1933
1984
|
|
|
1934
|
-
// src/
|
|
1985
|
+
// src/brainbank.ts
|
|
1935
1986
|
var BrainBank = class extends EventEmitter {
|
|
1936
1987
|
static {
|
|
1937
1988
|
__name(this, "BrainBank");
|
|
@@ -1986,10 +2037,10 @@ var BrainBank = class extends EventEmitter {
|
|
|
1986
2037
|
* Only initializes registered modules.
|
|
1987
2038
|
* Automatically called by index/search methods if not yet initialized.
|
|
1988
2039
|
*/
|
|
1989
|
-
async initialize() {
|
|
2040
|
+
async initialize(options = {}) {
|
|
1990
2041
|
if (this._initialized) return;
|
|
1991
2042
|
if (this._initPromise) return this._initPromise;
|
|
1992
|
-
this._initPromise = this._runInitialize().catch((err) => {
|
|
2043
|
+
this._initPromise = this._runInitialize(options).catch((err) => {
|
|
1993
2044
|
for (const { hnsw } of this._sharedHnsw.values()) try {
|
|
1994
2045
|
hnsw.reinit();
|
|
1995
2046
|
} catch {
|
|
@@ -2013,9 +2064,9 @@ var BrainBank = class extends EventEmitter {
|
|
|
2013
2064
|
});
|
|
2014
2065
|
return this._initPromise;
|
|
2015
2066
|
}
|
|
2016
|
-
async _runInitialize() {
|
|
2067
|
+
async _runInitialize(options = {}) {
|
|
2017
2068
|
if (this._initialized) return;
|
|
2018
|
-
const early = await earlyInit(this._config, (e, d) => this.emit(e, d));
|
|
2069
|
+
const early = await earlyInit(this._config, (e, d) => this.emit(e, d), options);
|
|
2019
2070
|
this._db = early.db;
|
|
2020
2071
|
this._embedding = early.embedding;
|
|
2021
2072
|
this._kvHnsw = early.kvHnsw;
|
|
@@ -2083,21 +2134,25 @@ var BrainBank = class extends EventEmitter {
|
|
|
2083
2134
|
/** Register a document collection. */
|
|
2084
2135
|
async addCollection(collection) {
|
|
2085
2136
|
await this.initialize();
|
|
2137
|
+
this._requireDocs("addCollection");
|
|
2086
2138
|
this.indexer("docs").addCollection(collection);
|
|
2087
2139
|
}
|
|
2088
2140
|
/** Remove a collection and all its indexed data. */
|
|
2089
2141
|
async removeCollection(name) {
|
|
2090
2142
|
await this.initialize();
|
|
2143
|
+
this._requireDocs("removeCollection");
|
|
2091
2144
|
this.indexer("docs").removeCollection(name);
|
|
2092
2145
|
}
|
|
2093
2146
|
/** List all registered collections. */
|
|
2094
2147
|
listCollections() {
|
|
2095
2148
|
this._requireInit("listCollections");
|
|
2149
|
+
this._requireDocs("listCollections");
|
|
2096
2150
|
return this.indexer("docs").listCollections();
|
|
2097
2151
|
}
|
|
2098
2152
|
/** Index all (or specific) document collections. */
|
|
2099
2153
|
async indexDocs(options = {}) {
|
|
2100
2154
|
await this.initialize();
|
|
2155
|
+
this._requireDocs("indexDocs");
|
|
2101
2156
|
const results = await this.indexer("docs").indexCollections(options);
|
|
2102
2157
|
this.emit("docsIndexed", results);
|
|
2103
2158
|
return results;
|
|
@@ -2105,19 +2160,23 @@ var BrainBank = class extends EventEmitter {
|
|
|
2105
2160
|
/** Search documents only. */
|
|
2106
2161
|
async searchDocs(query, options) {
|
|
2107
2162
|
await this.initialize();
|
|
2163
|
+
if (!this.has("docs")) return [];
|
|
2108
2164
|
return this.indexer("docs").search(query, options);
|
|
2109
2165
|
}
|
|
2110
2166
|
// ── Context metadata ─────────────────────────────
|
|
2111
2167
|
/** Add context description for a collection path. */
|
|
2112
2168
|
addContext(collection, path4, context) {
|
|
2169
|
+
this._requireDocs("addContext");
|
|
2113
2170
|
this.indexer("docs").addContext(collection, path4, context);
|
|
2114
2171
|
}
|
|
2115
2172
|
/** Remove context for a collection path. */
|
|
2116
2173
|
removeContext(collection, path4) {
|
|
2174
|
+
this._requireDocs("removeContext");
|
|
2117
2175
|
this.indexer("docs").removeContext(collection, path4);
|
|
2118
2176
|
}
|
|
2119
2177
|
/** List all context entries. */
|
|
2120
2178
|
listContexts() {
|
|
2179
|
+
this._requireDocs("listContexts");
|
|
2121
2180
|
return this.indexer("docs").listContexts();
|
|
2122
2181
|
}
|
|
2123
2182
|
// ── Search (delegated to SearchAPI) ─────────────
|
|
@@ -2153,7 +2212,7 @@ var BrainBank = class extends EventEmitter {
|
|
|
2153
2212
|
return this._searchAPI.hybridSearch(query, options);
|
|
2154
2213
|
}
|
|
2155
2214
|
/** BM25 keyword search only (no embeddings needed). */
|
|
2156
|
-
searchBM25(query, options) {
|
|
2215
|
+
async searchBM25(query, options) {
|
|
2157
2216
|
this._requireInit("searchBM25");
|
|
2158
2217
|
return this._searchAPI.searchBM25(query, options);
|
|
2159
2218
|
}
|
|
@@ -2165,20 +2224,15 @@ var BrainBank = class extends EventEmitter {
|
|
|
2165
2224
|
// ── Queries ──────────────────────────────────────
|
|
2166
2225
|
/** Get git history for a specific file. */
|
|
2167
2226
|
async fileHistory(filePath, limit = 20) {
|
|
2168
|
-
this.indexer("git");
|
|
2169
2227
|
await this.initialize();
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
FROM git_commits c
|
|
2173
|
-
INNER JOIN commit_files cf ON c.id = cf.commit_id
|
|
2174
|
-
WHERE cf.file_path LIKE ? AND c.is_merge = 0
|
|
2175
|
-
ORDER BY c.timestamp DESC LIMIT ?
|
|
2176
|
-
`).all(`%${filePath}%`, limit);
|
|
2228
|
+
const gitPlugin = this.indexer("git");
|
|
2229
|
+
return gitPlugin.fileHistory(filePath, limit);
|
|
2177
2230
|
}
|
|
2178
2231
|
/** Get co-edit suggestions for a file. */
|
|
2179
2232
|
coEdits(filePath, limit = 5) {
|
|
2180
2233
|
this._requireInit("coEdits");
|
|
2181
|
-
|
|
2234
|
+
const gitPlugin = this.indexer("git");
|
|
2235
|
+
return gitPlugin.suggestCoEdits(filePath, limit);
|
|
2182
2236
|
}
|
|
2183
2237
|
// ── Stats ────────────────────────────────────────
|
|
2184
2238
|
/** Get statistics for all loaded modules. */
|
|
@@ -2186,24 +2240,13 @@ var BrainBank = class extends EventEmitter {
|
|
|
2186
2240
|
this._requireInit("stats");
|
|
2187
2241
|
const result = {};
|
|
2188
2242
|
if (this.has("code")) {
|
|
2189
|
-
|
|
2190
|
-
result.code = {
|
|
2191
|
-
files: this._db.prepare("SELECT COUNT(DISTINCT file_path) as c FROM code_chunks").get().c,
|
|
2192
|
-
chunks: this._db.prepare("SELECT COUNT(*) as c FROM code_chunks").get().c,
|
|
2193
|
-
hnswSize: sh?.hnsw.size ?? 0
|
|
2194
|
-
};
|
|
2243
|
+
result.code = this._registry.firstByType("code").stats();
|
|
2195
2244
|
}
|
|
2196
2245
|
if (this.has("git")) {
|
|
2197
|
-
|
|
2198
|
-
result.git = {
|
|
2199
|
-
commits: this._db.prepare("SELECT COUNT(*) as c FROM git_commits").get().c,
|
|
2200
|
-
filesTracked: this._db.prepare("SELECT COUNT(DISTINCT file_path) as c FROM commit_files").get().c,
|
|
2201
|
-
coEdits: this._db.prepare("SELECT COUNT(*) as c FROM co_edits").get().c,
|
|
2202
|
-
hnswSize: sh?.hnsw.size ?? 0
|
|
2203
|
-
};
|
|
2246
|
+
result.git = this._registry.firstByType("git").stats();
|
|
2204
2247
|
}
|
|
2205
2248
|
if (this.has("docs")) {
|
|
2206
|
-
result.documents = this.
|
|
2249
|
+
result.documents = this._registry.firstByType("docs").stats();
|
|
2207
2250
|
}
|
|
2208
2251
|
return result;
|
|
2209
2252
|
}
|
|
@@ -2250,6 +2293,8 @@ var BrainBank = class extends EventEmitter {
|
|
|
2250
2293
|
close() {
|
|
2251
2294
|
this._watcher?.close();
|
|
2252
2295
|
for (const indexer of this._registry.all) indexer.close?.();
|
|
2296
|
+
this._embedding?.close().catch(() => {
|
|
2297
|
+
});
|
|
2253
2298
|
this._db?.close();
|
|
2254
2299
|
this._initialized = false;
|
|
2255
2300
|
this._collections.clear();
|
|
@@ -2273,6 +2318,10 @@ var BrainBank = class extends EventEmitter {
|
|
|
2273
2318
|
if (!this._initialized)
|
|
2274
2319
|
throw new Error(`BrainBank: Not initialized. Call await brain.initialize() before ${method}().`);
|
|
2275
2320
|
}
|
|
2321
|
+
_requireDocs(method) {
|
|
2322
|
+
if (!this.has("docs"))
|
|
2323
|
+
throw new Error(`BrainBank: Docs indexer not loaded. Add .use(docs()) before calling ${method}().`);
|
|
2324
|
+
}
|
|
2276
2325
|
};
|
|
2277
2326
|
|
|
2278
2327
|
export {
|
|
@@ -2282,9 +2331,9 @@ export {
|
|
|
2282
2331
|
HNSWIndex,
|
|
2283
2332
|
LocalEmbedding,
|
|
2284
2333
|
searchMMR,
|
|
2285
|
-
|
|
2286
|
-
|
|
2334
|
+
VectorSearch,
|
|
2335
|
+
KeywordSearch,
|
|
2287
2336
|
ContextBuilder,
|
|
2288
2337
|
BrainBank
|
|
2289
2338
|
};
|
|
2290
|
-
//# sourceMappingURL=chunk-
|
|
2339
|
+
//# sourceMappingURL=chunk-2BEWWQL2.js.map
|