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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-7QVYU63E.js";
|
|
4
|
+
|
|
5
|
+
// src/providers/embeddings/openai-embedding.ts
|
|
6
|
+
var DEFAULT_MODEL = "text-embedding-3-small";
|
|
7
|
+
var DEFAULT_DIMS = {
|
|
8
|
+
"text-embedding-3-small": 1536,
|
|
9
|
+
"text-embedding-3-large": 3072,
|
|
10
|
+
"text-embedding-ada-002": 1536
|
|
11
|
+
};
|
|
12
|
+
var API_URL = "https://api.openai.com/v1/embeddings";
|
|
13
|
+
var MAX_BATCH = 100;
|
|
14
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
15
|
+
var BATCH_DELAY_MS = 100;
|
|
16
|
+
var OpenAIEmbedding = class {
|
|
17
|
+
static {
|
|
18
|
+
__name(this, "OpenAIEmbedding");
|
|
19
|
+
}
|
|
20
|
+
dims;
|
|
21
|
+
_apiKey;
|
|
22
|
+
_model;
|
|
23
|
+
_baseUrl;
|
|
24
|
+
_requestDims;
|
|
25
|
+
_timeout;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this._apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
|
|
28
|
+
this._model = options.model ?? DEFAULT_MODEL;
|
|
29
|
+
this._baseUrl = options.baseUrl ?? API_URL;
|
|
30
|
+
this._timeout = options.timeout ?? REQUEST_TIMEOUT_MS;
|
|
31
|
+
if (options.dims && this._model.startsWith("text-embedding-3")) {
|
|
32
|
+
this._requestDims = options.dims;
|
|
33
|
+
this.dims = options.dims;
|
|
34
|
+
} else {
|
|
35
|
+
this.dims = options.dims ?? DEFAULT_DIMS[this._model] ?? 1536;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async embed(text) {
|
|
39
|
+
const results = await this._request([text]);
|
|
40
|
+
return results[0];
|
|
41
|
+
}
|
|
42
|
+
async embedBatch(texts) {
|
|
43
|
+
if (texts.length === 0) return [];
|
|
44
|
+
const results = [];
|
|
45
|
+
for (let i = 0; i < texts.length; i += MAX_BATCH) {
|
|
46
|
+
if (i > 0) await sleep(BATCH_DELAY_MS);
|
|
47
|
+
const batch = texts.slice(i, i + MAX_BATCH);
|
|
48
|
+
const embeddings = await this._request(batch);
|
|
49
|
+
results.push(...embeddings);
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
async close() {
|
|
54
|
+
}
|
|
55
|
+
_isTokenLimitError(errText) {
|
|
56
|
+
return errText.includes("maximum input length") || errText.includes("maximum context length") || errText.includes("too many tokens");
|
|
57
|
+
}
|
|
58
|
+
async _request(input, retryDepth = 0) {
|
|
59
|
+
if (!this._apiKey) {
|
|
60
|
+
throw new Error("OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.");
|
|
61
|
+
}
|
|
62
|
+
const MAX_CHARS = 24e3;
|
|
63
|
+
const safeInput = input.map((t) => t.length > MAX_CHARS ? t.slice(0, MAX_CHARS) : t);
|
|
64
|
+
const body = { model: this._model, input: safeInput };
|
|
65
|
+
if (this._requestDims) body.dimensions = this._requestDims;
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), this._timeout);
|
|
68
|
+
let res;
|
|
69
|
+
try {
|
|
70
|
+
res = await fetch(this._baseUrl, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"Authorization": `Bearer ${this._apiKey}`
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
signal: controller.signal
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
if (err.name === "AbortError") {
|
|
82
|
+
throw new Error(`OpenAI embedding request timed out after ${this._timeout}ms.`);
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
return this._handleApiError(res, safeInput, retryDepth);
|
|
90
|
+
}
|
|
91
|
+
const json = await res.json();
|
|
92
|
+
return json.data.sort((a, b) => a.index - b.index).map((d) => new Float32Array(d.embedding));
|
|
93
|
+
}
|
|
94
|
+
/** Handle API errors with token-limit retry logic. */
|
|
95
|
+
async _handleApiError(res, safeInput, retryDepth) {
|
|
96
|
+
const err = await res.text();
|
|
97
|
+
const isTokenLimit = res.status === 400 && this._isTokenLimitError(err);
|
|
98
|
+
if (isTokenLimit && safeInput.length > 1) {
|
|
99
|
+
const results = [];
|
|
100
|
+
for (const text of safeInput) {
|
|
101
|
+
const r = await this._request([text.slice(0, 8e3)]);
|
|
102
|
+
results.push(r[0]);
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
if (isTokenLimit && safeInput.length === 1 && retryDepth < 1) {
|
|
107
|
+
return this._request([safeInput[0].slice(0, 6e3)], retryDepth + 1);
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`OpenAI embedding API error (${res.status}): ${err}`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function sleep(ms) {
|
|
113
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
115
|
+
__name(sleep, "sleep");
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
OpenAIEmbedding
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=chunk-CCXVL56V.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/embeddings/openai-embedding.ts"],"sourcesContent":["/**\n * BrainBank — OpenAI Embedding Provider\n * \n * Uses OpenAI's embedding API via fetch (no SDK dependency).\n * Supports text-embedding-3-small, text-embedding-3-large, and ada-002.\n * \n * Usage:\n * const brain = new BrainBank({\n * embeddingProvider: new OpenAIEmbedding({ model: 'text-embedding-3-small' }),\n * });\n */\n\nimport type { EmbeddingProvider } from '@/types.ts';\n\nconst DEFAULT_MODEL = 'text-embedding-3-small';\nconst DEFAULT_DIMS: Record<string, number> = {\n 'text-embedding-3-small': 1536,\n 'text-embedding-3-large': 3072,\n 'text-embedding-ada-002': 1536,\n};\nconst API_URL = 'https://api.openai.com/v1/embeddings';\nconst MAX_BATCH = 100;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst BATCH_DELAY_MS = 100;\n\nexport interface OpenAIEmbeddingOptions {\n /** OpenAI API key. Falls back to OPENAI_API_KEY env var. */\n apiKey?: string;\n /** Model name. Default: 'text-embedding-3-small' */\n model?: string;\n /** Vector dimensions. If omitted, uses model default. text-embedding-3-* supports custom dims. */\n dims?: number;\n /** Base URL override (for Azure, proxies, etc.) */\n baseUrl?: string;\n /** Request timeout in ms. Default: 30000 */\n timeout?: number;\n}\n\nexport class OpenAIEmbedding implements EmbeddingProvider {\n readonly dims: number;\n\n private _apiKey: string;\n private _model: string;\n private _baseUrl: string;\n private _requestDims: number | undefined;\n private _timeout: number;\n\n constructor(options: OpenAIEmbeddingOptions = {}) {\n this._apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? '';\n this._model = options.model ?? DEFAULT_MODEL;\n this._baseUrl = options.baseUrl ?? API_URL;\n this._timeout = options.timeout ?? REQUEST_TIMEOUT_MS;\n\n // Custom dims only supported by text-embedding-3-*\n if (options.dims && this._model.startsWith('text-embedding-3')) {\n this._requestDims = options.dims;\n this.dims = options.dims;\n } else {\n this.dims = options.dims ?? DEFAULT_DIMS[this._model] ?? 1536;\n }\n }\n\n async embed(text: string): Promise<Float32Array> {\n const results = await this._request([text]);\n return results[0];\n }\n\n async embedBatch(texts: string[]): Promise<Float32Array[]> {\n if (texts.length === 0) return [];\n\n const results: Float32Array[] = [];\n\n for (let i = 0; i < texts.length; i += MAX_BATCH) {\n if (i > 0) await sleep(BATCH_DELAY_MS);\n const batch = texts.slice(i, i + MAX_BATCH);\n const embeddings = await this._request(batch);\n results.push(...embeddings);\n }\n\n return results;\n }\n\n async close(): Promise<void> {\n // No resources to release\n }\n\n private _isTokenLimitError(errText: string): boolean {\n return errText.includes('maximum input length') ||\n errText.includes('maximum context length') ||\n errText.includes('too many tokens');\n }\n\n private async _request(input: string[], retryDepth: number = 0): Promise<Float32Array[]> {\n if (!this._apiKey) {\n throw new Error('OpenAI API key required. Set OPENAI_API_KEY env var or pass apiKey option.');\n }\n\n const MAX_CHARS = 24_000;\n const safeInput = input.map(t => t.length > MAX_CHARS ? t.slice(0, MAX_CHARS) : t);\n\n const body: Record<string, any> = { model: this._model, input: safeInput };\n if (this._requestDims) body.dimensions = this._requestDims;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this._timeout);\n\n let res: Response;\n try {\n res = await fetch(this._baseUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this._apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n } catch (err: any) {\n clearTimeout(timer);\n if (err.name === 'AbortError') {\n throw new Error(`OpenAI embedding request timed out after ${this._timeout}ms.`);\n }\n throw err;\n } finally {\n clearTimeout(timer);\n }\n\n if (!res.ok) {\n return this._handleApiError(res, safeInput, retryDepth);\n }\n\n const json = await res.json() as {\n data: Array<{ embedding: number[]; index: number }>;\n };\n return json.data.sort((a, b) => a.index - b.index).map(d => new Float32Array(d.embedding));\n }\n\n /** Handle API errors with token-limit retry logic. */\n private async _handleApiError(\n res: Response, safeInput: string[], retryDepth: number,\n ): Promise<Float32Array[]> {\n const err = await res.text();\n const isTokenLimit = res.status === 400 && this._isTokenLimitError(err);\n\n // Batch token limit → retry each item individually with aggressive truncation\n if (isTokenLimit && safeInput.length > 1) {\n const results: Float32Array[] = [];\n for (const text of safeInput) {\n const r = await this._request([text.slice(0, 8_000)]);\n results.push(r[0]);\n }\n return results;\n }\n // Single item still failing → truncate to ~2k tokens (max 1 retry)\n if (isTokenLimit && safeInput.length === 1 && retryDepth < 1) {\n return this._request([safeInput[0].slice(0, 6_000)], retryDepth + 1);\n }\n throw new Error(`OpenAI embedding API error (${res.status}): ${err}`);\n }\n}\n\n/** Simple delay helper. */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n"],"mappings":";;;;;AAcA,IAAM,gBAAgB;AACtB,IAAM,eAAuC;AAAA,EACzC,0BAA0B;AAAA,EAC1B,0BAA0B;AAAA,EAC1B,0BAA0B;AAC9B;AACA,IAAM,UAAU;AAChB,IAAM,YAAY;AAClB,IAAM,qBAAqB;AAC3B,IAAM,iBAAiB;AAehB,IAAM,kBAAN,MAAmD;AAAA,EAtC1D,OAsC0D;AAAA;AAAA;AAAA,EAC7C;AAAA,EAED;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,UAAkC,CAAC,GAAG;AAC9C,SAAK,UAAU,QAAQ,UAAU,QAAQ,IAAI,kBAAkB;AAC/D,SAAK,SAAS,QAAQ,SAAS;AAC/B,SAAK,WAAW,QAAQ,WAAW;AACnC,SAAK,WAAW,QAAQ,WAAW;AAGnC,QAAI,QAAQ,QAAQ,KAAK,OAAO,WAAW,kBAAkB,GAAG;AAC5D,WAAK,eAAe,QAAQ;AAC5B,WAAK,OAAO,QAAQ;AAAA,IACxB,OAAO;AACH,WAAK,OAAO,QAAQ,QAAQ,aAAa,KAAK,MAAM,KAAK;AAAA,IAC7D;AAAA,EACJ;AAAA,EAEA,MAAM,MAAM,MAAqC;AAC7C,UAAM,UAAU,MAAM,KAAK,SAAS,CAAC,IAAI,CAAC;AAC1C,WAAO,QAAQ,CAAC;AAAA,EACpB;AAAA,EAEA,MAAM,WAAW,OAA0C;AACvD,QAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAEhC,UAAM,UAA0B,CAAC;AAEjC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAC9C,UAAI,IAAI,EAAG,OAAM,MAAM,cAAc;AACrC,YAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,SAAS;AAC1C,YAAM,aAAa,MAAM,KAAK,SAAS,KAAK;AAC5C,cAAQ,KAAK,GAAG,UAAU;AAAA,IAC9B;AAEA,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,QAAuB;AAAA,EAE7B;AAAA,EAEQ,mBAAmB,SAA0B;AACjD,WAAO,QAAQ,SAAS,sBAAsB,KACvC,QAAQ,SAAS,wBAAwB,KACzC,QAAQ,SAAS,iBAAiB;AAAA,EAC7C;AAAA,EAEA,MAAc,SAAS,OAAiB,aAAqB,GAA4B;AACrF,QAAI,CAAC,KAAK,SAAS;AACf,YAAM,IAAI,MAAM,4EAA4E;AAAA,IAChG;AAEA,UAAM,YAAY;AAClB,UAAM,YAAY,MAAM,IAAI,OAAK,EAAE,SAAS,YAAY,EAAE,MAAM,GAAG,SAAS,IAAI,CAAC;AAEjF,UAAM,OAA4B,EAAE,OAAO,KAAK,QAAQ,OAAO,UAAU;AACzE,QAAI,KAAK,aAAc,MAAK,aAAa,KAAK;AAE9C,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,QAAQ;AAEhE,QAAI;AACJ,QAAI;AACA,YAAM,MAAM,MAAM,KAAK,UAAU;AAAA,QAC7B,QAAQ;AAAA,QACR,SAAS;AAAA,UACL,gBAAgB;AAAA,UAChB,iBAAiB,UAAU,KAAK,OAAO;AAAA,QAC3C;AAAA,QACA,MAAM,KAAK,UAAU,IAAI;AAAA,QACzB,QAAQ,WAAW;AAAA,MACvB,CAAC;AAAA,IACL,SAAS,KAAU;AACf,mBAAa,KAAK;AAClB,UAAI,IAAI,SAAS,cAAc;AAC3B,cAAM,IAAI,MAAM,4CAA4C,KAAK,QAAQ,KAAK;AAAA,MAClF;AACA,YAAM;AAAA,IACV,UAAE;AACE,mBAAa,KAAK;AAAA,IACtB;AAEA,QAAI,CAAC,IAAI,IAAI;AACT,aAAO,KAAK,gBAAgB,KAAK,WAAW,UAAU;AAAA,IAC1D;AAEA,UAAM,OAAO,MAAM,IAAI,KAAK;AAG5B,WAAO,KAAK,KAAK,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,OAAK,IAAI,aAAa,EAAE,SAAS,CAAC;AAAA,EAC7F;AAAA;AAAA,EAGA,MAAc,gBACV,KAAe,WAAqB,YACb;AACvB,UAAM,MAAM,MAAM,IAAI,KAAK;AAC3B,UAAM,eAAe,IAAI,WAAW,OAAO,KAAK,mBAAmB,GAAG;AAGtE,QAAI,gBAAgB,UAAU,SAAS,GAAG;AACtC,YAAM,UAA0B,CAAC;AACjC,iBAAW,QAAQ,WAAW;AAC1B,cAAM,IAAI,MAAM,KAAK,SAAS,CAAC,KAAK,MAAM,GAAG,GAAK,CAAC,CAAC;AACpD,gBAAQ,KAAK,EAAE,CAAC,CAAC;AAAA,MACrB;AACA,aAAO;AAAA,IACX;AAEA,QAAI,gBAAgB,UAAU,WAAW,KAAK,aAAa,GAAG;AAC1D,aAAO,KAAK,SAAS,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,GAAK,CAAC,GAAG,aAAa,CAAC;AAAA,IACvE;AACA,UAAM,IAAI,MAAM,+BAA+B,IAAI,MAAM,MAAM,GAAG,EAAE;AAAA,EACxE;AACJ;AAGA,SAAS,MAAM,IAA2B;AACtC,SAAO,IAAI,QAAQ,aAAW,WAAW,SAAS,EAAE,CAAC;AACzD;AAFS;","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
__name
|
|
3
3
|
} from "./chunk-7QVYU63E.js";
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/lib/rrf.ts
|
|
6
6
|
function reciprocalRankFusion(resultSets, k = 60, maxResults = 15) {
|
|
7
7
|
const fused = /* @__PURE__ */ new Map();
|
|
8
8
|
for (const results of resultSets) {
|
|
@@ -52,10 +52,15 @@ function resultKey(r) {
|
|
|
52
52
|
}
|
|
53
53
|
__name(resultKey, "resultKey");
|
|
54
54
|
|
|
55
|
-
// src/
|
|
55
|
+
// src/lib/fts.ts
|
|
56
|
+
function splitCompound(word) {
|
|
57
|
+
return word.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_\-./\\]/g, " ").trim();
|
|
58
|
+
}
|
|
59
|
+
__name(splitCompound, "splitCompound");
|
|
56
60
|
function sanitizeFTS(query) {
|
|
57
61
|
const clean = query.replace(/[{}[\]()^~*:]/g, " ").replace(/\bAND\b|\bOR\b|\bNOT\b|\bNEAR\b/gi, "").trim();
|
|
58
|
-
const
|
|
62
|
+
const expanded = clean.split(/\s+/).map((w) => splitCompound(w)).join(" ");
|
|
63
|
+
const words = expanded.split(/\s+/).filter((w) => w.length > 1);
|
|
59
64
|
if (words.length === 0) return "";
|
|
60
65
|
return words.map((w) => `"${w}"`).join(" ");
|
|
61
66
|
}
|
|
@@ -71,4 +76,4 @@ export {
|
|
|
71
76
|
sanitizeFTS,
|
|
72
77
|
normalizeBM25
|
|
73
78
|
};
|
|
74
|
-
//# sourceMappingURL=chunk-
|
|
79
|
+
//# sourceMappingURL=chunk-E6WQM4DN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/rrf.ts","../src/lib/fts.ts"],"sourcesContent":["/**\n * BrainBank — Reciprocal Rank Fusion (RRF)\n * \n * Combines results from multiple search systems (vector + BM25)\n * using the RRF algorithm: score = Σ 1/(k + rank_i)\n * \n * This is the same algorithm used by Elasticsearch, QMD, and most\n * production hybrid search systems. Simple but very effective.\n * \n * Reference: Cormack et al., \"Reciprocal Rank Fusion outperforms\n * Condorcet and individual Rank Learning Methods\" (2009)\n */\n\nimport type { SearchResult } from '@/types.ts';\n\n/**\n * Fuse ranked lists from different search systems into a single ranked list.\n * \n * @param resultSets - Arrays of SearchResult from different systems (e.g. vector, BM25)\n * @param k - Smoothing constant. Default: 60 (standard value). Higher = less emphasis on top ranks.\n * @param maxResults - Maximum results to return.\n */\nexport function reciprocalRankFusion(\n resultSets: SearchResult[][],\n k: number = 60,\n maxResults: number = 15,\n): SearchResult[] {\n // Build a map: unique key → { bestResult, rrfScore }\n const fused = new Map<string, { result: SearchResult; rrfScore: number }>();\n\n for (const results of resultSets) {\n for (let rank = 0; rank < results.length; rank++) {\n const r = results[rank];\n const key = resultKey(r);\n const rrfContribution = 1.0 / (k + rank + 1);\n\n const existing = fused.get(key);\n if (existing) {\n existing.rrfScore += rrfContribution;\n // Keep the result with the higher original score\n if (r.score > existing.result.score) {\n existing.result = { ...r };\n }\n } else {\n fused.set(key, {\n result: { ...r },\n rrfScore: rrfContribution,\n });\n }\n }\n }\n\n // Sort by RRF score descending, normalize, and return\n const sorted = Array.from(fused.values())\n .sort((a, b) => b.rrfScore - a.rrfScore)\n .slice(0, maxResults);\n\n // Normalize RRF scores to 0..1 range\n const maxRRF = sorted[0]?.rrfScore ?? 1;\n return sorted.map(entry => ({\n ...entry.result,\n score: entry.rrfScore / maxRRF,\n metadata: {\n ...entry.result.metadata,\n rrfScore: entry.rrfScore,\n } as any,\n }));\n}\n\n/**\n * Generate a unique key for a search result to detect duplicates across systems.\n */\nfunction resultKey(r: SearchResult): string {\n switch (r.type) {\n case 'code':\n return `code:${r.filePath}:${r.metadata.startLine}-${r.metadata.endLine}`;\n case 'commit':\n return `commit:${r.metadata.hash || r.metadata.shortHash}`;\n case 'pattern':\n return `pattern:${r.metadata.taskType}:${r.content?.slice(0, 60)}`;\n case 'document':\n return `document:${r.filePath ?? ''}:${(r.metadata as any).collection ?? ''}:${(r.metadata as any).seq ?? ''}:${r.content?.slice(0, 80)}`;\n case 'collection':\n return `collection:${(r.metadata as any).id ?? r.content?.slice(0, 80)}`;\n }\n}\n","/**\n * BrainBank — FTS Utilities\n * \n * Shared helpers for SQLite FTS5 query sanitization.\n */\n\n/**\n * Split camelCase, PascalCase, and snake_case into individual words.\n * \"MagicLinkCallback\" → \"Magic Link Callback\"\n * \"tenant_worker\" → \"tenant worker\"\n */\nfunction splitCompound(word: string): string {\n return word\n .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → camel Case\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // HTMLParser → HTML Parser\n .replace(/[_\\-./\\\\]/g, ' ') // snake_case, kebab-case, paths\n .trim();\n}\n\n/**\n * Sanitize a user query for FTS5 syntax.\n * Strips operators that would cause parse errors, splits compound words,\n * and converts words to implicit AND with exact-match quoting.\n */\nexport function sanitizeFTS(query: string): string {\n const clean = query\n .replace(/[{}[\\]()^~*:]/g, ' ')\n .replace(/\\bAND\\b|\\bOR\\b|\\bNOT\\b|\\bNEAR\\b/gi, '')\n .trim();\n\n // Split compound words (camelCase, PascalCase, snake_case)\n const expanded = clean.split(/\\s+/)\n .map(w => splitCompound(w))\n .join(' ');\n\n const words = expanded.split(/\\s+/).filter(w => w.length > 1);\n if (words.length === 0) return '';\n\n return words.map(w => `\"${w}\"`).join(' ');\n}\n\n/**\n * Normalize BM25 score from SQLite (negative, lower = better)\n * to 0.0–1.0 (higher = better) for consistency with vector search.\n */\nexport function normalizeBM25(rawScore: number): number {\n const abs = Math.abs(rawScore);\n return 1.0 / (1.0 + Math.exp(-0.3 * (abs - 5)));\n}\n"],"mappings":";;;;;AAsBO,SAAS,qBACZ,YACA,IAAY,IACZ,aAAqB,IACP;AAEd,QAAM,QAAQ,oBAAI,IAAwD;AAE1E,aAAW,WAAW,YAAY;AAC9B,aAAS,OAAO,GAAG,OAAO,QAAQ,QAAQ,QAAQ;AAC9C,YAAM,IAAI,QAAQ,IAAI;AACtB,YAAM,MAAM,UAAU,CAAC;AACvB,YAAM,kBAAkB,KAAO,IAAI,OAAO;AAE1C,YAAM,WAAW,MAAM,IAAI,GAAG;AAC9B,UAAI,UAAU;AACV,iBAAS,YAAY;AAErB,YAAI,EAAE,QAAQ,SAAS,OAAO,OAAO;AACjC,mBAAS,SAAS,EAAE,GAAG,EAAE;AAAA,QAC7B;AAAA,MACJ,OAAO;AACH,cAAM,IAAI,KAAK;AAAA,UACX,QAAQ,EAAE,GAAG,EAAE;AAAA,UACf,UAAU;AAAA,QACd,CAAC;AAAA,MACL;AAAA,IACJ;AAAA,EACJ;AAGA,QAAM,SAAS,MAAM,KAAK,MAAM,OAAO,CAAC,EACnC,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ,EACtC,MAAM,GAAG,UAAU;AAGxB,QAAM,SAAS,OAAO,CAAC,GAAG,YAAY;AACtC,SAAO,OAAO,IAAI,YAAU;AAAA,IACxB,GAAG,MAAM;AAAA,IACT,OAAO,MAAM,WAAW;AAAA,IACxB,UAAU;AAAA,MACN,GAAG,MAAM,OAAO;AAAA,MAChB,UAAU,MAAM;AAAA,IACpB;AAAA,EACJ,EAAE;AACN;AA7CgB;AAkDhB,SAAS,UAAU,GAAyB;AACxC,UAAQ,EAAE,MAAM;AAAA,IACZ,KAAK;AACD,aAAO,QAAQ,EAAE,QAAQ,IAAI,EAAE,SAAS,SAAS,IAAI,EAAE,SAAS,OAAO;AAAA,IAC3E,KAAK;AACD,aAAO,UAAU,EAAE,SAAS,QAAQ,EAAE,SAAS,SAAS;AAAA,IAC5D,KAAK;AACD,aAAO,WAAW,EAAE,SAAS,QAAQ,IAAI,EAAE,SAAS,MAAM,GAAG,EAAE,CAAC;AAAA,IACpE,KAAK;AACD,aAAO,YAAY,EAAE,YAAY,EAAE,IAAK,EAAE,SAAiB,cAAc,EAAE,IAAK,EAAE,SAAiB,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,GAAG,EAAE,CAAC;AAAA,IAC3I,KAAK;AACD,aAAO,cAAe,EAAE,SAAiB,MAAM,EAAE,SAAS,MAAM,GAAG,EAAE,CAAC;AAAA,EAC9E;AACJ;AAbS;;;AC7DT,SAAS,cAAc,MAAsB;AACzC,SAAO,KACF,QAAQ,mBAAmB,OAAO,EAClC,QAAQ,yBAAyB,OAAO,EACxC,QAAQ,cAAc,GAAG,EACzB,KAAK;AACd;AANS;AAaF,SAAS,YAAY,OAAuB;AAC/C,QAAM,QAAQ,MACT,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,qCAAqC,EAAE,EAC/C,KAAK;AAGV,QAAM,WAAW,MAAM,MAAM,KAAK,EAC7B,IAAI,OAAK,cAAc,CAAC,CAAC,EACzB,KAAK,GAAG;AAEb,QAAM,QAAQ,SAAS,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC;AAC5D,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,SAAO,MAAM,IAAI,OAAK,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG;AAC5C;AAfgB;AAqBT,SAAS,cAAc,UAA0B;AACpD,QAAM,MAAM,KAAK,IAAI,QAAQ;AAC7B,SAAO,KAAO,IAAM,KAAK,IAAI,QAAQ,MAAM,EAAE;AACjD;AAHgB;","names":[]}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-7QVYU63E.js";
|
|
4
|
+
|
|
5
|
+
// src/indexers/git/git-indexer.ts
|
|
6
|
+
var GitIndexer = class {
|
|
7
|
+
static {
|
|
8
|
+
__name(this, "GitIndexer");
|
|
9
|
+
}
|
|
10
|
+
_deps;
|
|
11
|
+
_repoPath;
|
|
12
|
+
_maxDiffBytes;
|
|
13
|
+
constructor(repoPath, deps, maxDiffBytes = 8192) {
|
|
14
|
+
this._deps = deps;
|
|
15
|
+
this._repoPath = repoPath;
|
|
16
|
+
this._maxDiffBytes = maxDiffBytes;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Index git history.
|
|
20
|
+
* Only processes commits not already in the database.
|
|
21
|
+
*/
|
|
22
|
+
async index(options = {}) {
|
|
23
|
+
const { depth = 500, onProgress } = options;
|
|
24
|
+
const git2 = await this._initGit();
|
|
25
|
+
if (!git2) return { indexed: 0, skipped: 0 };
|
|
26
|
+
let log;
|
|
27
|
+
try {
|
|
28
|
+
log = await git2.log({ maxCount: depth });
|
|
29
|
+
} catch {
|
|
30
|
+
return { indexed: 0, skipped: 0 };
|
|
31
|
+
}
|
|
32
|
+
const stmts = this._prepareStatements();
|
|
33
|
+
const { toProcess, skipped } = await this._collectCommits(git2, log.all, stmts, onProgress);
|
|
34
|
+
if (toProcess.length === 0) return { indexed: 0, skipped };
|
|
35
|
+
const vecs = await this._deps.embedding.embedBatch(toProcess.map((d) => d.text));
|
|
36
|
+
const { indexed, newCommitIds } = this._insertCommits(toProcess, vecs, stmts);
|
|
37
|
+
this._updateHnsw(vecs, newCommitIds);
|
|
38
|
+
return { indexed, skipped };
|
|
39
|
+
}
|
|
40
|
+
/** Initialize simple-git. Returns null if git is unavailable. */
|
|
41
|
+
async _initGit() {
|
|
42
|
+
try {
|
|
43
|
+
const simpleGit = (await import("simple-git")).default;
|
|
44
|
+
return simpleGit(this._repoPath);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Prepare all SQL statements (hoisted outside loops). */
|
|
50
|
+
_prepareStatements() {
|
|
51
|
+
const db = this._deps.db;
|
|
52
|
+
return {
|
|
53
|
+
check: db.prepare(`
|
|
54
|
+
SELECT gc.id, gv.commit_id AS has_vector
|
|
55
|
+
FROM git_commits gc
|
|
56
|
+
LEFT JOIN git_vectors gv ON gv.commit_id = gc.id
|
|
57
|
+
WHERE gc.hash = ?`),
|
|
58
|
+
deleteFiles: db.prepare("DELETE FROM commit_files WHERE commit_id = ?"),
|
|
59
|
+
deleteCommit: db.prepare("DELETE FROM git_commits WHERE id = ?"),
|
|
60
|
+
insertCommit: db.prepare(`
|
|
61
|
+
INSERT OR IGNORE INTO git_commits (hash, short_hash, message, author, date, timestamp, files_json, diff, additions, deletions, is_merge)
|
|
62
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
63
|
+
insertFile: db.prepare("INSERT INTO commit_files (commit_id, file_path) VALUES (?, ?)"),
|
|
64
|
+
insertVec: db.prepare("INSERT OR IGNORE INTO git_vectors (commit_id, embedding) VALUES (?, ?)")
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Phase 1: Collect commit data from git (async git calls). */
|
|
68
|
+
async _collectCommits(git2, commits, stmts, onProgress) {
|
|
69
|
+
const toProcess = [];
|
|
70
|
+
let skipped = 0;
|
|
71
|
+
for (let i = 0; i < commits.length; i++) {
|
|
72
|
+
const c = commits[i];
|
|
73
|
+
onProgress?.(`[${c.hash.slice(0, 7)}] ${c.message.slice(0, 50)}`, i + 1, commits.length);
|
|
74
|
+
const exists = stmts.check.get(c.hash);
|
|
75
|
+
if (exists?.has_vector) {
|
|
76
|
+
skipped++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (exists && !exists.has_vector) {
|
|
80
|
+
stmts.deleteFiles.run(exists.id);
|
|
81
|
+
stmts.deleteCommit.run(exists.id);
|
|
82
|
+
}
|
|
83
|
+
const data = await this._parseCommit(git2, c);
|
|
84
|
+
toProcess.push(data);
|
|
85
|
+
}
|
|
86
|
+
return { toProcess, skipped };
|
|
87
|
+
}
|
|
88
|
+
/** Extract diff, stat, and text from a single commit. */
|
|
89
|
+
async _parseCommit(git2, c) {
|
|
90
|
+
let diff = "";
|
|
91
|
+
let additions = 0, deletions = 0;
|
|
92
|
+
const filesChanged = [];
|
|
93
|
+
try {
|
|
94
|
+
const numstat = await git2.raw(["show", "--numstat", "--format=", c.hash]);
|
|
95
|
+
for (const line of numstat.trim().split("\n")) {
|
|
96
|
+
if (!line.trim()) continue;
|
|
97
|
+
const parts = line.split(" ");
|
|
98
|
+
if (parts.length < 3) continue;
|
|
99
|
+
const add = parseInt(parts[0], 10);
|
|
100
|
+
const del = parseInt(parts[1], 10);
|
|
101
|
+
const file = parts[2].trim();
|
|
102
|
+
if (file) {
|
|
103
|
+
filesChanged.push(file);
|
|
104
|
+
if (!isNaN(add)) additions += add;
|
|
105
|
+
if (!isNaN(del)) deletions += del;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const rawDiff = await git2.raw(["show", "--format=", "--unified=3", "--no-color", c.hash]);
|
|
109
|
+
diff = rawDiff.length > this._maxDiffBytes ? rawDiff.slice(0, this._maxDiffBytes) + "\n... [truncated]" : rawDiff;
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
const isMerge = /^(Merge|merge)\s+(branch|pull|remote|tag)\b/.test(c.message);
|
|
113
|
+
const text = [
|
|
114
|
+
`Commit: ${c.message}`,
|
|
115
|
+
`Author: ${c.author_name}`,
|
|
116
|
+
`Date: ${c.date}`,
|
|
117
|
+
filesChanged.length > 0 ? `Files: ${filesChanged.join(", ")}` : "",
|
|
118
|
+
diff ? `Changes:
|
|
119
|
+
${diff.slice(0, 2e3)}` : ""
|
|
120
|
+
].filter(Boolean).join("\n");
|
|
121
|
+
return { commit: c, diff, additions, deletions, filesChanged, isMerge, text };
|
|
122
|
+
}
|
|
123
|
+
/** Phase 3: Insert commits + vectors in a single transaction. */
|
|
124
|
+
_insertCommits(toProcess, vecs, stmts) {
|
|
125
|
+
let indexed = 0;
|
|
126
|
+
const newCommitIds = [];
|
|
127
|
+
this._deps.db.transaction(() => {
|
|
128
|
+
for (let i = 0; i < toProcess.length; i++) {
|
|
129
|
+
const d = toProcess[i];
|
|
130
|
+
const c = d.commit;
|
|
131
|
+
const ts = Math.floor(new Date(c.date).getTime() / 1e3);
|
|
132
|
+
const result = stmts.insertCommit.run(
|
|
133
|
+
c.hash,
|
|
134
|
+
c.hash.slice(0, 7),
|
|
135
|
+
c.message,
|
|
136
|
+
c.author_name,
|
|
137
|
+
c.date,
|
|
138
|
+
ts,
|
|
139
|
+
JSON.stringify(d.filesChanged),
|
|
140
|
+
d.diff || null,
|
|
141
|
+
d.additions,
|
|
142
|
+
d.deletions,
|
|
143
|
+
d.isMerge ? 1 : 0
|
|
144
|
+
);
|
|
145
|
+
if (result.changes === 0) continue;
|
|
146
|
+
const commitId = Number(result.lastInsertRowid);
|
|
147
|
+
for (const f of d.filesChanged) {
|
|
148
|
+
stmts.insertFile.run(commitId, f);
|
|
149
|
+
}
|
|
150
|
+
stmts.insertVec.run(commitId, Buffer.from(vecs[i].buffer));
|
|
151
|
+
newCommitIds.push({ commitId, vecIndex: i });
|
|
152
|
+
indexed++;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return { indexed, newCommitIds };
|
|
156
|
+
}
|
|
157
|
+
/** Phase 4: Update HNSW index and compute co-edits. */
|
|
158
|
+
_updateHnsw(vecs, inserted) {
|
|
159
|
+
const newCommitIds = [];
|
|
160
|
+
for (const { commitId, vecIndex } of inserted) {
|
|
161
|
+
this._deps.hnsw.add(vecs[vecIndex], commitId);
|
|
162
|
+
this._deps.vectorCache.set(commitId, vecs[vecIndex]);
|
|
163
|
+
newCommitIds.push(commitId);
|
|
164
|
+
}
|
|
165
|
+
if (newCommitIds.length > 0) {
|
|
166
|
+
this._computeCoEdits(newCommitIds);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Compute which files tend to be edited together. */
|
|
170
|
+
_computeCoEdits(newCommitIds) {
|
|
171
|
+
if (newCommitIds.length === 0) return;
|
|
172
|
+
const rows = this._queryCommitFiles(newCommitIds);
|
|
173
|
+
const byCommit = this._groupFilesByCommit(rows);
|
|
174
|
+
const upsert = this._deps.db.prepare(
|
|
175
|
+
`INSERT INTO co_edits (file_a, file_b, count)
|
|
176
|
+
VALUES (?, ?, 1)
|
|
177
|
+
ON CONFLICT(file_a, file_b) DO UPDATE SET count = count + 1`
|
|
178
|
+
);
|
|
179
|
+
this._deps.db.transaction(() => {
|
|
180
|
+
for (const files of byCommit.values()) {
|
|
181
|
+
if (files.length < 2 || files.length > 20) continue;
|
|
182
|
+
for (let i = 0; i < files.length; i++) {
|
|
183
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
184
|
+
const [a, b] = [files[i], files[j]].sort();
|
|
185
|
+
upsert.run(a, b);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** Query commit_files in chunks to stay under SQLite's 999-variable limit. */
|
|
192
|
+
_queryCommitFiles(commitIds) {
|
|
193
|
+
const CHUNK_SIZE = 500;
|
|
194
|
+
const allRows = [];
|
|
195
|
+
for (let i = 0; i < commitIds.length; i += CHUNK_SIZE) {
|
|
196
|
+
const chunk = commitIds.slice(i, i + CHUNK_SIZE);
|
|
197
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
198
|
+
const rows = this._deps.db.prepare(
|
|
199
|
+
`SELECT commit_id, file_path FROM commit_files WHERE commit_id IN (${placeholders}) ORDER BY commit_id`
|
|
200
|
+
).all(...chunk);
|
|
201
|
+
allRows.push(...rows);
|
|
202
|
+
}
|
|
203
|
+
return allRows;
|
|
204
|
+
}
|
|
205
|
+
/** Group file paths by commit ID. */
|
|
206
|
+
_groupFilesByCommit(rows) {
|
|
207
|
+
const byCommit = /* @__PURE__ */ new Map();
|
|
208
|
+
for (const r of rows) {
|
|
209
|
+
if (!byCommit.has(r.commit_id)) byCommit.set(r.commit_id, []);
|
|
210
|
+
byCommit.get(r.commit_id).push(r.file_path);
|
|
211
|
+
}
|
|
212
|
+
return byCommit;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/indexers/git/co-edit-analyzer.ts
|
|
217
|
+
var CoEditAnalyzer = class {
|
|
218
|
+
constructor(_db) {
|
|
219
|
+
this._db = _db;
|
|
220
|
+
}
|
|
221
|
+
static {
|
|
222
|
+
__name(this, "CoEditAnalyzer");
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get files that frequently change alongside the given file.
|
|
226
|
+
* Returns sorted by co-edit count (highest first).
|
|
227
|
+
*/
|
|
228
|
+
suggest(filePath, limit = 5) {
|
|
229
|
+
const rows = this._db.prepare(`
|
|
230
|
+
SELECT
|
|
231
|
+
CASE WHEN file_a = ? THEN file_b ELSE file_a END AS file,
|
|
232
|
+
count
|
|
233
|
+
FROM co_edits
|
|
234
|
+
WHERE file_a = ? OR file_b = ?
|
|
235
|
+
ORDER BY count DESC
|
|
236
|
+
LIMIT ?
|
|
237
|
+
`).all(filePath, filePath, filePath, limit);
|
|
238
|
+
return rows.map((r) => ({ file: r.file, count: r.count }));
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/indexers/git/git-plugin.ts
|
|
243
|
+
var GitPlugin = class {
|
|
244
|
+
constructor(opts = {}) {
|
|
245
|
+
this.opts = opts;
|
|
246
|
+
this.name = opts.name ?? "git";
|
|
247
|
+
}
|
|
248
|
+
static {
|
|
249
|
+
__name(this, "GitPlugin");
|
|
250
|
+
}
|
|
251
|
+
name;
|
|
252
|
+
db;
|
|
253
|
+
hnsw;
|
|
254
|
+
indexer;
|
|
255
|
+
coEdits;
|
|
256
|
+
vecCache = /* @__PURE__ */ new Map();
|
|
257
|
+
async initialize(ctx) {
|
|
258
|
+
this.db = ctx.db;
|
|
259
|
+
const shared = await ctx.getOrCreateSharedHnsw("git", 5e5);
|
|
260
|
+
this.hnsw = shared.hnsw;
|
|
261
|
+
this.vecCache = shared.vecCache;
|
|
262
|
+
if (shared.isNew) {
|
|
263
|
+
ctx.loadVectors("git_vectors", "commit_id", this.hnsw, this.vecCache);
|
|
264
|
+
}
|
|
265
|
+
const repoPath = this.opts.repoPath ?? ctx.config.repoPath;
|
|
266
|
+
this.indexer = new GitIndexer(repoPath, {
|
|
267
|
+
db: ctx.db,
|
|
268
|
+
hnsw: this.hnsw,
|
|
269
|
+
vectorCache: this.vecCache,
|
|
270
|
+
embedding: ctx.embedding
|
|
271
|
+
}, this.opts.maxDiffBytes ?? ctx.config.maxDiffBytes);
|
|
272
|
+
this.coEdits = new CoEditAnalyzer(ctx.db);
|
|
273
|
+
}
|
|
274
|
+
async index(options = {}) {
|
|
275
|
+
return this.indexer.index(options);
|
|
276
|
+
}
|
|
277
|
+
suggestCoEdits(filePath, limit = 5) {
|
|
278
|
+
return this.coEdits.suggest(filePath, limit);
|
|
279
|
+
}
|
|
280
|
+
/** Get git history for a specific file. */
|
|
281
|
+
fileHistory(filePath, limit = 20) {
|
|
282
|
+
return this.db.prepare(`
|
|
283
|
+
SELECT c.short_hash, c.message, c.author, c.date, c.additions, c.deletions
|
|
284
|
+
FROM git_commits c
|
|
285
|
+
INNER JOIN commit_files cf ON c.id = cf.commit_id
|
|
286
|
+
WHERE cf.file_path LIKE ? AND c.is_merge = 0
|
|
287
|
+
ORDER BY c.timestamp DESC LIMIT ?
|
|
288
|
+
`).all(`%${filePath}%`, limit);
|
|
289
|
+
}
|
|
290
|
+
stats() {
|
|
291
|
+
return {
|
|
292
|
+
commits: this.db.prepare("SELECT COUNT(*) as c FROM git_commits").get().c,
|
|
293
|
+
filesTracked: this.db.prepare("SELECT COUNT(DISTINCT file_path) as c FROM commit_files").get().c,
|
|
294
|
+
coEdits: this.db.prepare("SELECT COUNT(*) as c FROM co_edits").get().c,
|
|
295
|
+
hnswSize: this.hnsw.size
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
function git(opts) {
|
|
300
|
+
return new GitPlugin(opts);
|
|
301
|
+
}
|
|
302
|
+
__name(git, "git");
|
|
303
|
+
|
|
304
|
+
export {
|
|
305
|
+
GitIndexer,
|
|
306
|
+
CoEditAnalyzer,
|
|
307
|
+
git
|
|
308
|
+
};
|
|
309
|
+
//# sourceMappingURL=chunk-FI7GWG4W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/indexers/git/git-indexer.ts","../src/indexers/git/co-edit-analyzer.ts","../src/indexers/git/git-plugin.ts"],"sourcesContent":["/**\n * BrainBank — Git Indexer\n * \n * Reads git history, embeds commit messages + diffs,\n * and computes file co-edit relationships.\n * Incremental: only processes new commits.\n */\n\nimport type { Database } from '@/db/database.ts';\nimport type { EmbeddingProvider, ProgressCallback, IndexResult } from '@/types.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\n\nexport interface GitIndexerDeps {\n db: Database;\n hnsw: HNSWIndex;\n vectorCache: Map<number, Float32Array>;\n embedding: EmbeddingProvider;\n}\n\nexport interface GitIndexOptions {\n depth?: number;\n onProgress?: ProgressCallback;\n}\n\ninterface CommitData {\n commit: any;\n diff: string;\n additions: number;\n deletions: number;\n filesChanged: string[];\n isMerge: boolean;\n text: string;\n}\n\n/** Prepared statements for git commit operations. */\ninterface GitStatements {\n check: any;\n deleteFiles: any;\n deleteCommit: any;\n insertCommit: any;\n insertFile: any;\n insertVec: any;\n}\n\nexport class GitIndexer {\n private _deps: GitIndexerDeps;\n private _repoPath: string;\n private _maxDiffBytes: number;\n\n constructor(repoPath: string, deps: GitIndexerDeps, maxDiffBytes: number = 8192) {\n this._deps = deps;\n this._repoPath = repoPath;\n this._maxDiffBytes = maxDiffBytes;\n }\n\n /**\n * Index git history.\n * Only processes commits not already in the database.\n */\n async index(options: GitIndexOptions = {}): Promise<IndexResult> {\n const { depth = 500, onProgress } = options;\n\n const git = await this._initGit();\n if (!git) return { indexed: 0, skipped: 0 };\n\n let log: any;\n try { log = await git.log({ maxCount: depth }); }\n catch { return { indexed: 0, skipped: 0 }; }\n\n const stmts = this._prepareStatements();\n const { toProcess, skipped } = await this._collectCommits(git, log.all, stmts, onProgress);\n\n if (toProcess.length === 0) return { indexed: 0, skipped };\n\n const vecs = await this._deps.embedding.embedBatch(toProcess.map(d => d.text));\n const { indexed, newCommitIds } = this._insertCommits(toProcess, vecs, stmts);\n\n this._updateHnsw(vecs, newCommitIds);\n\n return { indexed, skipped };\n }\n\n /** Initialize simple-git. Returns null if git is unavailable. */\n private async _initGit(): Promise<any | null> {\n try {\n const simpleGit = (await import('simple-git')).default;\n return simpleGit(this._repoPath);\n } catch {\n return null;\n }\n }\n\n /** Prepare all SQL statements (hoisted outside loops). */\n private _prepareStatements(): GitStatements {\n const db = this._deps.db;\n return {\n check: db.prepare(`\n SELECT gc.id, gv.commit_id AS has_vector\n FROM git_commits gc\n LEFT JOIN git_vectors gv ON gv.commit_id = gc.id\n WHERE gc.hash = ?`),\n deleteFiles: db.prepare('DELETE FROM commit_files WHERE commit_id = ?'),\n deleteCommit: db.prepare('DELETE FROM git_commits WHERE id = ?'),\n insertCommit: db.prepare(`\n INSERT OR IGNORE INTO git_commits (hash, short_hash, message, author, date, timestamp, files_json, diff, additions, deletions, is_merge)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),\n insertFile: db.prepare('INSERT INTO commit_files (commit_id, file_path) VALUES (?, ?)'),\n insertVec: db.prepare('INSERT OR IGNORE INTO git_vectors (commit_id, embedding) VALUES (?, ?)'),\n };\n }\n\n /** Phase 1: Collect commit data from git (async git calls). */\n private async _collectCommits(\n git: any,\n commits: any[],\n stmts: GitStatements,\n onProgress?: ProgressCallback,\n ): Promise<{ toProcess: CommitData[]; skipped: number }> {\n const toProcess: CommitData[] = [];\n let skipped = 0;\n\n for (let i = 0; i < commits.length; i++) {\n const c = commits[i];\n onProgress?.(`[${c.hash.slice(0, 7)}] ${c.message.slice(0, 50)}`, i + 1, commits.length);\n\n const exists = stmts.check.get(c.hash) as any;\n if (exists?.has_vector) { skipped++; continue; }\n\n // Zombie commit (data exists but vector missing) — clean up\n if (exists && !exists.has_vector) {\n stmts.deleteFiles.run(exists.id);\n stmts.deleteCommit.run(exists.id);\n }\n\n const data = await this._parseCommit(git, c);\n toProcess.push(data);\n }\n\n return { toProcess, skipped };\n }\n\n /** Extract diff, stat, and text from a single commit. */\n private async _parseCommit(git: any, c: any): Promise<CommitData> {\n let diff = '';\n let additions = 0, deletions = 0;\n const filesChanged: string[] = [];\n\n try {\n const numstat = await git.raw(['show', '--numstat', '--format=', c.hash]);\n for (const line of numstat.trim().split('\\n')) {\n if (!line.trim()) continue;\n const parts = line.split('\\t');\n if (parts.length < 3) continue;\n const add = parseInt(parts[0], 10);\n const del = parseInt(parts[1], 10);\n const file = parts[2].trim();\n if (file) {\n filesChanged.push(file);\n if (!isNaN(add)) additions += add;\n if (!isNaN(del)) deletions += del;\n }\n }\n\n const rawDiff = await git.raw(['show', '--format=', '--unified=3', '--no-color', c.hash]);\n diff = rawDiff.length > this._maxDiffBytes\n ? rawDiff.slice(0, this._maxDiffBytes) + '\\n... [truncated]'\n : rawDiff;\n } catch {}\n\n const isMerge = /^(Merge|merge)\\s+(branch|pull|remote|tag)\\b/.test(c.message);\n const text = [\n `Commit: ${c.message}`,\n `Author: ${c.author_name}`,\n `Date: ${c.date}`,\n filesChanged.length > 0 ? `Files: ${filesChanged.join(', ')}` : '',\n diff ? `Changes:\\n${diff.slice(0, 2000)}` : '',\n ].filter(Boolean).join('\\n');\n\n return { commit: c, diff, additions, deletions, filesChanged, isMerge, text };\n }\n\n /** Phase 3: Insert commits + vectors in a single transaction. */\n private _insertCommits(\n toProcess: CommitData[],\n vecs: Float32Array[],\n stmts: GitStatements,\n ): { indexed: number; newCommitIds: { commitId: number; vecIndex: number }[] } {\n let indexed = 0;\n const newCommitIds: { commitId: number; vecIndex: number }[] = [];\n\n this._deps.db.transaction(() => {\n for (let i = 0; i < toProcess.length; i++) {\n const d = toProcess[i];\n const c = d.commit;\n const ts = Math.floor(new Date(c.date).getTime() / 1000);\n\n const result = stmts.insertCommit.run(\n c.hash, c.hash.slice(0, 7), c.message, c.author_name, c.date,\n ts, JSON.stringify(d.filesChanged), d.diff || null,\n d.additions, d.deletions, d.isMerge ? 1 : 0,\n );\n\n if (result.changes === 0) continue;\n const commitId = Number(result.lastInsertRowid);\n\n for (const f of d.filesChanged) {\n stmts.insertFile.run(commitId, f);\n }\n\n stmts.insertVec.run(commitId, Buffer.from(vecs[i].buffer));\n newCommitIds.push({ commitId, vecIndex: i });\n indexed++;\n }\n });\n\n return { indexed, newCommitIds };\n }\n\n /** Phase 4: Update HNSW index and compute co-edits. */\n private _updateHnsw(\n vecs: Float32Array[],\n inserted: { commitId: number; vecIndex: number }[],\n ): void {\n const newCommitIds: number[] = [];\n for (const { commitId, vecIndex } of inserted) {\n this._deps.hnsw.add(vecs[vecIndex], commitId);\n this._deps.vectorCache.set(commitId, vecs[vecIndex]);\n newCommitIds.push(commitId);\n }\n\n if (newCommitIds.length > 0) {\n this._computeCoEdits(newCommitIds);\n }\n }\n\n /** Compute which files tend to be edited together. */\n private _computeCoEdits(newCommitIds: number[]): void {\n if (newCommitIds.length === 0) return;\n\n const rows = this._queryCommitFiles(newCommitIds);\n const byCommit = this._groupFilesByCommit(rows);\n\n const upsert = this._deps.db.prepare(\n `INSERT INTO co_edits (file_a, file_b, count)\n VALUES (?, ?, 1)\n ON CONFLICT(file_a, file_b) DO UPDATE SET count = count + 1`\n );\n\n this._deps.db.transaction(() => {\n for (const files of byCommit.values()) {\n if (files.length < 2 || files.length > 20) continue;\n for (let i = 0; i < files.length; i++) {\n for (let j = i + 1; j < files.length; j++) {\n const [a, b] = [files[i], files[j]].sort();\n upsert.run(a, b);\n }\n }\n }\n });\n }\n\n /** Query commit_files in chunks to stay under SQLite's 999-variable limit. */\n private _queryCommitFiles(commitIds: number[]): any[] {\n const CHUNK_SIZE = 500;\n const allRows: any[] = [];\n for (let i = 0; i < commitIds.length; i += CHUNK_SIZE) {\n const chunk = commitIds.slice(i, i + CHUNK_SIZE);\n const placeholders = chunk.map(() => '?').join(',');\n const rows = this._deps.db.prepare(\n `SELECT commit_id, file_path FROM commit_files WHERE commit_id IN (${placeholders}) ORDER BY commit_id`\n ).all(...chunk) as any[];\n allRows.push(...rows);\n }\n return allRows;\n }\n\n /** Group file paths by commit ID. */\n private _groupFilesByCommit(rows: any[]): Map<number, string[]> {\n const byCommit = new Map<number, string[]>();\n for (const r of rows) {\n if (!byCommit.has(r.commit_id)) byCommit.set(r.commit_id, []);\n byCommit.get(r.commit_id)!.push(r.file_path);\n }\n return byCommit;\n }\n}\n","/**\n * BrainBank — Co-Edit Analyzer\n * \n * Suggests files that historically change together.\n * Based on git commit co-occurrence analysis.\n */\n\nimport type { Database } from '@/db/database.ts';\nimport type { CoEditSuggestion } from '@/types.ts';\n\nexport class CoEditAnalyzer {\n constructor(private _db: Database) {}\n\n /**\n * Get files that frequently change alongside the given file.\n * Returns sorted by co-edit count (highest first).\n */\n suggest(filePath: string, limit: number = 5): CoEditSuggestion[] {\n const rows = this._db.prepare(`\n SELECT\n CASE WHEN file_a = ? THEN file_b ELSE file_a END AS file,\n count\n FROM co_edits\n WHERE file_a = ? OR file_b = ?\n ORDER BY count DESC\n LIMIT ?\n `).all(filePath, filePath, filePath, limit) as any[];\n\n return rows.map(r => ({ file: r.file, count: r.count }));\n }\n}\n","/**\n * BrainBank — Git Module\n * \n * Git history indexing with co-edit relationships.\n * \n * import { git } from 'brainbank/git';\n * brain.use(git({ depth: 500 }));\n * \n * // Multi-repo: namespace to avoid key collisions\n * brain\n * .use(git({ repoPath: './frontend', name: 'git:frontend' }))\n * .use(git({ repoPath: './backend', name: 'git:backend' }));\n */\n\nimport type { Indexer, IndexerContext } from '@/indexers/base.ts';\nimport type { HNSWIndex } from '@/providers/vector/hnsw-index.ts';\nimport type { Database } from '@/db/database.ts';\nimport { GitIndexer } from './git-indexer.ts';\nimport { CoEditAnalyzer } from './co-edit-analyzer.ts';\nimport type { IndexResult, ProgressCallback, CoEditSuggestion } from '@/types.ts';\n\nexport interface GitPluginOptions {\n /** Repository path. Default: from config */\n repoPath?: string;\n /** Max commits to index. Default: from config */\n depth?: number;\n /** Max diff bytes. Default: from config */\n maxDiffBytes?: number;\n /** Custom indexer name for multi-repo (e.g. 'git:frontend'). Default: 'git' */\n name?: string;\n}\n\nclass GitPlugin implements Indexer {\n readonly name: string;\n private db!: Database;\n hnsw!: HNSWIndex;\n indexer!: GitIndexer;\n coEdits!: CoEditAnalyzer;\n vecCache = new Map<number, Float32Array>();\n\n constructor(private opts: GitPluginOptions = {}) {\n this.name = opts.name ?? 'git';\n }\n\n async initialize(ctx: IndexerContext): Promise<void> {\n this.db = ctx.db;\n // Use shared HNSW so all git indexers share one index\n const shared = await ctx.getOrCreateSharedHnsw('git', 500_000);\n this.hnsw = shared.hnsw;\n this.vecCache = shared.vecCache;\n\n if (shared.isNew) {\n ctx.loadVectors('git_vectors', 'commit_id', this.hnsw, this.vecCache);\n }\n\n const repoPath = this.opts.repoPath ?? ctx.config.repoPath;\n this.indexer = new GitIndexer(repoPath, {\n db: ctx.db,\n hnsw: this.hnsw,\n vectorCache: this.vecCache,\n embedding: ctx.embedding,\n }, this.opts.maxDiffBytes ?? ctx.config.maxDiffBytes);\n\n this.coEdits = new CoEditAnalyzer(ctx.db);\n }\n\n async index(options: {\n depth?: number;\n onProgress?: ProgressCallback;\n } = {}): Promise<IndexResult> {\n return this.indexer.index(options);\n }\n\n suggestCoEdits(filePath: string, limit: number = 5): CoEditSuggestion[] {\n return this.coEdits.suggest(filePath, limit);\n }\n\n /** Get git history for a specific file. */\n fileHistory(filePath: string, limit: number = 20): any[] {\n return this.db.prepare(`\n SELECT c.short_hash, c.message, c.author, c.date, c.additions, c.deletions\n FROM git_commits c\n INNER JOIN commit_files cf ON c.id = cf.commit_id\n WHERE cf.file_path LIKE ? AND c.is_merge = 0\n ORDER BY c.timestamp DESC LIMIT ?\n `).all(`%${filePath}%`, limit) as any[];\n }\n\n stats(): Record<string, number> {\n return {\n commits: (this.db.prepare('SELECT COUNT(*) as c FROM git_commits').get() as { c: number }).c,\n filesTracked: (this.db.prepare('SELECT COUNT(DISTINCT file_path) as c FROM commit_files').get() as { c: number }).c,\n coEdits: (this.db.prepare('SELECT COUNT(*) as c FROM co_edits').get() as { c: number }).c,\n hnswSize: this.hnsw.size,\n };\n }\n}\n\n/** Create a git history plugin. */\nexport function git(opts?: GitPluginOptions): Indexer {\n return new GitPlugin(opts);\n}\n"],"mappings":"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","names":["git"]}
|