brainbank 0.1.2-beta.1 → 0.1.3-beta.1

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.
@@ -4,6 +4,11 @@ import {
4
4
 
5
5
  // src/providers/pruners/haiku-pruner.ts
6
6
  var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
7
+ var _debug = !!process.env.BRAINBANK_DEBUG;
8
+ function dbg(msg) {
9
+ if (_debug) console.error(msg);
10
+ }
11
+ __name(dbg, "dbg");
7
12
  var HaikuPruner = class {
8
13
  static {
9
14
  __name(this, "HaikuPruner");
@@ -66,16 +71,24 @@ Respond with ONLY the JSON array. Example: [3, 0, 5, 1]`;
66
71
  })
67
72
  });
68
73
  if (!response.ok) {
74
+ dbg(`[HaikuPruner] API error: ${response.status} ${response.statusText} \u2014 keeping all`);
69
75
  return items.map((i) => i.id);
70
76
  }
71
77
  const data = await response.json();
72
78
  const text = data.content?.[0]?.text ?? "";
79
+ dbg(`[HaikuPruner] Raw response: ${text}`);
73
80
  const match = text.match(/\[[\d\s,]+\]/);
74
- if (!match) return items.map((i) => i.id);
81
+ if (!match) {
82
+ dbg(`[HaikuPruner] No JSON array found in response \u2014 keeping all ${items.length} items`);
83
+ return items.map((i) => i.id);
84
+ }
75
85
  const keepIds = JSON.parse(match[0]);
76
86
  const validIds = new Set(items.map((i) => i.id));
77
- return keepIds.filter((id) => validIds.has(id));
78
- } catch {
87
+ const filtered = keepIds.filter((id) => validIds.has(id));
88
+ dbg(`[HaikuPruner] Keep IDs: [${filtered.join(", ")}] (${items.length - filtered.length} dropped)`);
89
+ return filtered;
90
+ } catch (err) {
91
+ dbg(`[HaikuPruner] Error: ${err instanceof Error ? err.message : String(err)} \u2014 keeping all`);
79
92
  return items.map((i) => i.id);
80
93
  }
81
94
  }
@@ -86,4 +99,4 @@ Respond with ONLY the JSON array. Example: [3, 0, 5, 1]`;
86
99
  export {
87
100
  HaikuPruner
88
101
  };
89
- //# sourceMappingURL=chunk-PXEWQMN7.js.map
102
+ //# sourceMappingURL=chunk-3QVAKPTK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/pruners/haiku-pruner.ts"],"sourcesContent":["/**\n * BrainBank — Haiku Pruner\n *\n * LLM-based noise filter using Anthropic's Haiku 4.5 model.\n * Binary classification: for each search result, Haiku decides\n * \"relevant\" or \"noise\" based on filePath, metadata, and full\n * file content (capped at ~8K chars per item by prune.ts).\n *\n * Latency: ~300-600ms.\n */\n\nimport type { Pruner, PrunerItem } from '@/types.ts';\n\nconst DEFAULT_MODEL = 'claude-haiku-4-5-20251001';\nconst _debug = !!process.env.BRAINBANK_DEBUG;\nfunction dbg(msg: string): void { if (_debug) console.error(msg); }\n\nexport interface HaikuPrunerOptions {\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var. */\n apiKey?: string;\n /** Model to use. Default: claude-haiku-4-5-20251001 */\n model?: string;\n}\n\nexport class HaikuPruner implements Pruner {\n private readonly _apiKey: string;\n private readonly _model: string;\n\n constructor(options: HaikuPrunerOptions = {}) {\n this._apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? '';\n this._model = options.model ?? DEFAULT_MODEL;\n\n if (!this._apiKey) {\n throw new Error(\n 'HaikuPruner: No API key provided. Set ANTHROPIC_API_KEY env var or pass apiKey option.',\n );\n }\n }\n\n async prune(query: string, items: PrunerItem[]): Promise<number[]> {\n if (items.length === 0) return [];\n if (items.length === 1) return [items[0].id];\n\n const itemLines = items.map(item => {\n // Only show useful metadata fields (skip raw scores, IDs, large arrays)\n const SKIP_KEYS = new Set(['id', 'chunkIds', 'rrfScore', 'filePath']);\n const meta = Object.entries(item.metadata)\n .filter(([k, v]) => v !== undefined && v !== null && !SKIP_KEYS.has(k))\n .map(([k, v]) => `${k}=${v}`)\n .join(' | ');\n return `#${item.id} ${item.filePath} | ${meta}\\n${item.preview}`;\n }).join('\\n---\\n');\n\n const prompt =\n `Query: \"${query}\"\\n\\nSearch results (full file content):\\n${itemLines}\\n\\n` +\n `You are a precision search filter and ranker. You have the FULL source code of each file.\\n` +\n `Return a JSON array of #IDs to KEEP, ordered by relevance (most relevant FIRST).\\n\\n` +\n `Rules:\\n` +\n `- Understand the SPECIFIC system/feature the query targets. Don't match on shared vocabulary alone.\\n` +\n ` Example: \"snackbar toast notification\" targets the toast popup system, NOT a notification center/bell icon.\\n` +\n `- KEEP files that directly implement, define types for, or configure the queried system.\\n` +\n `- KEEP files where the queried system is mounted, initialized, or composed into a workflow.\\n` +\n `- DROP files that only CONSUME the system (e.g. a component that calls showNotification once but has 400 lines of unrelated logic).\\n` +\n `- DROP files that implement a DIFFERENT system sharing similar vocabulary.\\n` +\n `- DROP infrastructure/boilerplate (font loaders, theme definitions, CSS-only layout shells) unless they directly configure the queried feature.\\n` +\n `- Aim for 40-70% keep rate. Returning fewer, highly relevant files is BETTER than returning many tangential ones.\\n` +\n `- ORDER: core implementation → types/config → mount points → peripheral.\\n\\n` +\n `Respond with ONLY the JSON array. Example: [3, 0, 5, 1]`;\n\n try {\n const response = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': this._apiKey,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: this._model,\n max_tokens: 512,\n messages: [{\n role: 'user',\n content: prompt,\n }],\n }),\n });\n\n if (!response.ok) {\n // API error → fail-open, return all\n dbg(`[HaikuPruner] API error: ${response.status} ${response.statusText} — keeping all`);\n return items.map(i => i.id);\n }\n\n const data = await response.json() as {\n content: { type: string; text: string }[];\n };\n\n const text = data.content?.[0]?.text ?? '';\n dbg(`[HaikuPruner] Raw response: ${text}`);\n // Haiku may wrap in ```json ... ``` — extract any JSON array\n const match = text.match(/\\[[\\d\\s,]+\\]/);\n if (!match) {\n dbg(`[HaikuPruner] No JSON array found in response — keeping all ${items.length} items`);\n return items.map(i => i.id);\n }\n\n const keepIds = JSON.parse(match[0]) as number[];\n const validIds = new Set(items.map(i => i.id));\n const filtered = keepIds.filter(id => validIds.has(id));\n dbg(`[HaikuPruner] Keep IDs: [${filtered.join(', ')}] (${items.length - filtered.length} dropped)`);\n return filtered;\n } catch (err) {\n // Network error → fail-open, return all\n dbg(`[HaikuPruner] Error: ${err instanceof Error ? err.message : String(err)} — keeping all`);\n return items.map(i => i.id);\n }\n }\n\n async close(): Promise<void> {\n // No resources to release (stateless HTTP)\n }\n}\n"],"mappings":";;;;;AAaA,IAAM,gBAAgB;AACtB,IAAM,SAAS,CAAC,CAAC,QAAQ,IAAI;AAC7B,SAAS,IAAI,KAAmB;AAAE,MAAI,OAAQ,SAAQ,MAAM,GAAG;AAAG;AAAzD;AASF,IAAM,cAAN,MAAoC;AAAA,EAxB3C,OAwB2C;AAAA;AAAA;AAAA,EACtB;AAAA,EACA;AAAA,EAEjB,YAAY,UAA8B,CAAC,GAAG;AAC1C,SAAK,UAAU,QAAQ,UAAU,QAAQ,IAAI,qBAAqB;AAClE,SAAK,SAAS,QAAQ,SAAS;AAE/B,QAAI,CAAC,KAAK,SAAS;AACf,YAAM,IAAI;AAAA,QACN;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAAA,EAEA,MAAM,MAAM,OAAe,OAAwC;AAC/D,QAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAChC,QAAI,MAAM,WAAW,EAAG,QAAO,CAAC,MAAM,CAAC,EAAE,EAAE;AAE3C,UAAM,YAAY,MAAM,IAAI,UAAQ;AAEhC,YAAM,YAAY,oBAAI,IAAI,CAAC,MAAM,YAAY,YAAY,UAAU,CAAC;AACpE,YAAM,OAAO,OAAO,QAAQ,KAAK,QAAQ,EACpC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,MAAM,UAAa,MAAM,QAAQ,CAAC,UAAU,IAAI,CAAC,CAAC,EACrE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAC3B,KAAK,KAAK;AACf,aAAO,IAAI,KAAK,EAAE,IAAI,KAAK,QAAQ,MAAM,IAAI;AAAA,EAAK,KAAK,OAAO;AAAA,IAClE,CAAC,EAAE,KAAK,SAAS;AAEjB,UAAM,SACF,WAAW,KAAK;AAAA;AAAA;AAAA,EAA6C,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAe1E,QAAI;AACA,YAAM,WAAW,MAAM,MAAM,yCAAyC;AAAA,QAClE,QAAQ;AAAA,QACR,SAAS;AAAA,UACL,gBAAgB;AAAA,UAChB,aAAa,KAAK;AAAA,UAClB,qBAAqB;AAAA,QACzB;AAAA,QACA,MAAM,KAAK,UAAU;AAAA,UACjB,OAAO,KAAK;AAAA,UACZ,YAAY;AAAA,UACZ,UAAU,CAAC;AAAA,YACP,MAAM;AAAA,YACN,SAAS;AAAA,UACb,CAAC;AAAA,QACL,CAAC;AAAA,MACL,CAAC;AAED,UAAI,CAAC,SAAS,IAAI;AAEd,YAAI,4BAA4B,SAAS,MAAM,IAAI,SAAS,UAAU,qBAAgB;AACtF,eAAO,MAAM,IAAI,OAAK,EAAE,EAAE;AAAA,MAC9B;AAEA,YAAM,OAAO,MAAM,SAAS,KAAK;AAIjC,YAAM,OAAO,KAAK,UAAU,CAAC,GAAG,QAAQ;AACxC,UAAI,+BAA+B,IAAI,EAAE;AAEzC,YAAM,QAAQ,KAAK,MAAM,cAAc;AACvC,UAAI,CAAC,OAAO;AACR,YAAI,oEAA+D,MAAM,MAAM,QAAQ;AACvF,eAAO,MAAM,IAAI,OAAK,EAAE,EAAE;AAAA,MAC9B;AAEA,YAAM,UAAU,KAAK,MAAM,MAAM,CAAC,CAAC;AACnC,YAAM,WAAW,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,EAAE,CAAC;AAC7C,YAAM,WAAW,QAAQ,OAAO,QAAM,SAAS,IAAI,EAAE,CAAC;AACtD,UAAI,4BAA4B,SAAS,KAAK,IAAI,CAAC,MAAM,MAAM,SAAS,SAAS,MAAM,WAAW;AAClG,aAAO;AAAA,IACX,SAAS,KAAK;AAEV,UAAI,wBAAwB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,qBAAgB;AAC5F,aAAO,MAAM,IAAI,OAAK,EAAE,EAAE;AAAA,IAC9B;AAAA,EACJ;AAAA,EAEA,MAAM,QAAuB;AAAA,EAE7B;AACJ;","names":[]}
@@ -4,6 +4,11 @@ import {
4
4
 
5
5
  // src/providers/pruners/haiku-expander.ts
6
6
  var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
7
+ var _debug = !!process.env.BRAINBANK_DEBUG;
8
+ function dbg(msg) {
9
+ if (_debug) console.error(msg);
10
+ }
11
+ __name(dbg, "dbg");
7
12
  var HaikuExpander = class {
8
13
  static {
9
14
  __name(this, "HaikuExpander");
@@ -29,13 +34,16 @@ var HaikuExpander = class {
29
34
  const currentSummary = manifest.filter((m) => currentSet.has(m.id)).map((m) => `#${m.id} ${m.filePath} | ${m.chunkType} ${m.name}`).join("\n");
30
35
  let manifestSection = "";
31
36
  if (priorityChunks.length > 0) {
32
- const prioLines = priorityChunks.map(
33
- (m) => `#${m.id} ${m.filePath} | ${m.chunkType} ${m.name} ${m.lines}`
34
- ).join("\n");
37
+ const prioLines = priorityChunks.map((m) => {
38
+ const base = `#${m.id} ${m.filePath} | ${m.chunkType} ${m.name} ${m.lines}`;
39
+ return m.synopsis ? `${base} \u2014 ${m.synopsis}` : base;
40
+ }).join("\n");
35
41
  manifestSection += `DEPENDENCY chunks (imported by or importing the search result files):
36
42
  ${prioLines}
37
43
 
38
44
  `;
45
+ const synopsisCount = priorityChunks.filter((m) => m.synopsis).length;
46
+ dbg(`[HaikuExpander] ${priorityChunks.length} priority chunks (${synopsisCount} with synopsis)`);
39
47
  }
40
48
  if (otherChunks.length > 0) {
41
49
  const otherLines = otherChunks.map(
@@ -89,8 +97,10 @@ If nothing to add: { "ids": [] }`;
89
97
  }
90
98
  const data = await response.json();
91
99
  const text = data.content?.[0]?.text ?? "";
100
+ dbg(`[HaikuExpander] Raw response: ${text.slice(0, 300)}${text.length > 300 ? "..." : ""}`);
92
101
  return this._parseResponse(text, available);
93
- } catch {
102
+ } catch (err) {
103
+ dbg(`[HaikuExpander] Error: ${err instanceof Error ? err.message : String(err)}`);
94
104
  return { ids: [] };
95
105
  }
96
106
  }
@@ -121,4 +131,4 @@ If nothing to add: { "ids": [] }`;
121
131
  export {
122
132
  HaikuExpander
123
133
  };
124
- //# sourceMappingURL=chunk-OPH7GZ7U.js.map
134
+ //# sourceMappingURL=chunk-5QUZ6CYK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/pruners/haiku-expander.ts"],"sourcesContent":["/**\n * BrainBank — Haiku Expander\n *\n * LLM-powered context expansion using Anthropic's Haiku 4.5 model.\n * After search + pruning, reviews a manifest of available chunks\n * and requests additional IDs to include.\n *\n * Flow:\n * 1. Receives lightweight manifest (~20 chars per chunk)\n * 2. Haiku selects additional chunk IDs (just numbers, fast)\n * 3. Caller fetches those chunks from DB and splices into results\n *\n * Designed for minimal token usage:\n * - Input: ~2,000-3,000 tokens (manifest)\n * - Output: ~50-100 tokens (ID array)\n * - Cost: ~$0.001 per call\n * - Latency: ~300-600ms\n *\n * Fail-open: any error returns empty array (no expansion).\n */\n\nimport type { Expander, ExpanderManifestItem, ExpanderResult } from '@/types.ts';\n\nconst DEFAULT_MODEL = 'claude-haiku-4-5-20251001';\nconst _debug = !!process.env.BRAINBANK_DEBUG;\nfunction dbg(msg: string): void { if (_debug) console.error(msg); }\n\nexport interface HaikuExpanderOptions {\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var. */\n apiKey?: string;\n /** Model to use. Default: claude-haiku-4-5-20251001 */\n model?: string;\n}\n\nexport class HaikuExpander implements Expander {\n private readonly _apiKey: string;\n private readonly _model: string;\n\n constructor(options: HaikuExpanderOptions = {}) {\n this._apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? '';\n this._model = options.model ?? DEFAULT_MODEL;\n\n if (!this._apiKey) {\n throw new Error(\n 'HaikuExpander: No API key provided. Set ANTHROPIC_API_KEY env var or pass apiKey option.',\n );\n }\n }\n\n async expand(\n query: string,\n currentIds: number[],\n manifest: ExpanderManifestItem[],\n ): Promise<ExpanderResult> {\n if (manifest.length === 0) return { ids: [] };\n\n // Filter out chunks already in results\n const currentSet = new Set(currentIds);\n const available = manifest.filter(m => !currentSet.has(m.id));\n if (available.length === 0) return { ids: [] };\n\n // Split manifest into priority (import-graph neighbors) and general\n const priorityChunks = available.filter(m => m.priority);\n const otherChunks = available.filter(m => !m.priority);\n\n const currentSummary = manifest\n .filter(m => currentSet.has(m.id))\n .map(m => `#${m.id} ${m.filePath} | ${m.chunkType} ${m.name}`)\n .join('\\n');\n\n // Build manifest sections\n let manifestSection = '';\n if (priorityChunks.length > 0) {\n const prioLines = priorityChunks.map(m => {\n const base = `#${m.id} ${m.filePath} | ${m.chunkType} ${m.name} ${m.lines}`;\n return m.synopsis ? `${base} — ${m.synopsis}` : base;\n }).join('\\n');\n manifestSection += `DEPENDENCY chunks (imported by or importing the search result files):\\n${prioLines}\\n\\n`;\n const synopsisCount = priorityChunks.filter(m => m.synopsis).length;\n dbg(`[HaikuExpander] ${priorityChunks.length} priority chunks (${synopsisCount} with synopsis)`);\n }\n if (otherChunks.length > 0) {\n const otherLines = otherChunks.map(m =>\n `#${m.id} ${m.filePath} | ${m.chunkType} ${m.name} ${m.lines}`\n ).join('\\n');\n manifestSection += `Other available chunks:\\n${otherLines}`;\n }\n\n const prompt =\n `Task: \"${query}\"\\n\\n` +\n `Already included chunks:\\n${currentSummary}\\n\\n` +\n `${manifestSection}\\n\\n` +\n `You are a code context expander. The search already found the \"included\" chunks above.\\n` +\n `Review the available chunks and select any that would help an AI agent complete the task.\\n\\n` +\n `Rules:\\n` +\n `- STRONGLY PREFER dependency chunks — they are structurally connected to the search results via imports\\n` +\n `- Select type definitions, interfaces, models, or configs needed to understand included code\\n` +\n `- Select initialization or setup code if the task involves debugging or modifying a feature\\n` +\n `- Do NOT select test files, documentation, or unrelated utilities\\n` +\n `- Be selective: only include chunks that fill clear gaps. Quality over quantity.\\n` +\n `- If nothing useful is available, return an empty ids array\\n\\n` +\n `Respond with ONLY a JSON object:\\n` +\n `{ \"ids\": [42, 17, 89], \"note\": \"Brief 1-2 sentence observation about the codebase relevant to the task\" }\\n\\n` +\n `The \"note\" is optional — use it to mention things like missing files, architectural patterns, ` +\n `deprecated modules, or important relationships you noticed. If nothing notable, omit it.\\n` +\n `If nothing to add: { \"ids\": [] }`;\n\n try {\n const response = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': this._apiKey,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: this._model,\n max_tokens: 512,\n messages: [{\n role: 'user',\n content: prompt,\n }],\n }),\n });\n\n if (!response.ok) {\n return { ids: [] };\n }\n\n const data = await response.json() as {\n content: { type: string; text: string }[];\n };\n\n const text = data.content?.[0]?.text ?? '';\n dbg(`[HaikuExpander] Raw response: ${text.slice(0, 300)}${text.length > 300 ? '...' : ''}`);\n return this._parseResponse(text, available);\n } catch (err) {\n dbg(`[HaikuExpander] Error: ${err instanceof Error ? err.message : String(err)}`);\n return { ids: [] };\n }\n }\n\n /** Parse Haiku response — handles both `{ ids, note }` and bare `[...]` formats. */\n private _parseResponse(text: string, available: ExpanderManifestItem[]): ExpanderResult {\n const validIds = new Set(available.map(m => m.id));\n\n // Try JSON object first: { \"ids\": [...], \"note\": \"...\" }\n const objMatch = text.match(/\\{[\\s\\S]*\"ids\"\\s*:\\s*\\[[\\d\\s,]*\\][\\s\\S]*\\}/);\n if (objMatch) {\n try {\n const parsed = JSON.parse(objMatch[0]) as { ids: number[]; note?: string };\n const ids = parsed.ids.filter(id => validIds.has(id));\n const note = parsed.note?.trim() || undefined;\n return { ids, note };\n } catch {\n // Fall through to array parsing\n }\n }\n\n // Fallback: bare array [42, 17, 89]\n const arrMatch = text.match(/\\[[\\d\\s,]*\\]/);\n if (arrMatch) {\n const ids = (JSON.parse(arrMatch[0]) as number[]).filter(id => validIds.has(id));\n return { ids };\n }\n\n return { ids: [] };\n }\n\n async close(): Promise<void> {\n // No resources to release (stateless HTTP)\n }\n}\n"],"mappings":"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","names":[]}
@@ -693,7 +693,22 @@ var ContextBuilder = class {
693
693
  const pruner = options.pruner ?? this._pruner;
694
694
  const beforePrune = results;
695
695
  if (pruner && results.length > 1) {
696
+ dbg(`[pruner] Running ${_prunerName(pruner)} on ${results.length} results...`);
697
+ const pruneT0 = Date.now();
696
698
  results = await pruneResults(task, results, pruner);
699
+ const pruneMs = Date.now() - pruneT0;
700
+ const dropped = beforePrune.filter((r) => !results.includes(r));
701
+ dbg(`[pruner] ${beforePrune.length} \u2192 ${results.length} in ${pruneMs}ms (${dropped.length} dropped)`);
702
+ if (results.length > 0) {
703
+ dbg(`[pruner] Kept: ${results.map((r) => r.filePath ?? "?").join(", ")}`);
704
+ }
705
+ if (dropped.length > 0) {
706
+ dbg(`[pruner] Dropped: ${dropped.map((r) => r.filePath ?? "?").join(", ")}`);
707
+ }
708
+ } else if (!pruner) {
709
+ dbg(`[pruner] No pruner configured \u2014 skipping`);
710
+ } else {
711
+ dbg(`[pruner] Only ${results.length} result(s) \u2014 skipping pruner (need >1)`);
697
712
  }
698
713
  if (options.excludeFiles && options.excludeFiles.size > 0) {
699
714
  results = results.filter((r) => !r.filePath || !options.excludeFiles.has(r.filePath));
@@ -881,10 +896,109 @@ var CompositeBM25Search = class {
881
896
  }
882
897
  };
883
898
 
899
+ // src/search/query-decomposer.ts
900
+ var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
901
+ var _debug2 = !!process.env.BRAINBANK_DEBUG;
902
+ function dbg2(msg) {
903
+ if (_debug2) process.stderr.write(`[query-decomposer] ${msg}
904
+ `);
905
+ }
906
+ __name(dbg2, "dbg");
907
+ var MIN_WORDS_FOR_DECOMPOSITION = 6;
908
+ var QueryDecomposer = class {
909
+ static {
910
+ __name(this, "QueryDecomposer");
911
+ }
912
+ _apiKey;
913
+ _model;
914
+ constructor(options = {}) {
915
+ this._apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
916
+ this._model = options.model ?? DEFAULT_MODEL;
917
+ }
918
+ /**
919
+ * Decompose a complex query into 2-3 focused sub-queries.
920
+ * Returns the original query + generated sub-queries.
921
+ * For simple queries (< 6 words), returns just the original.
922
+ */
923
+ async decompose(query) {
924
+ const words = query.trim().split(/\s+/);
925
+ if (words.length < MIN_WORDS_FOR_DECOMPOSITION || !this._apiKey) {
926
+ dbg2(`Skip decomposition: ${words.length} words (min: ${MIN_WORDS_FOR_DECOMPOSITION})`);
927
+ return [query];
928
+ }
929
+ try {
930
+ const prompt = `You are a code search query optimizer. Given a complex search query, decompose it into 2-3 focused sub-queries that each target a DIFFERENT aspect of the original intent.
931
+
932
+ Original query: "${query}"
933
+
934
+ Rules:
935
+ - Each sub-query should be 4-8 words, mixing natural language with code identifiers
936
+ - Sub-queries should be COMPLEMENTARY, not overlapping
937
+ - Preserve code identifiers (camelCase, PascalCase) exactly as written
938
+ - Focus on: (1) the core action/method, (2) the data/entity, (3) the workflow/context
939
+ - Return ONLY a JSON array of strings. No explanation.
940
+
941
+ Example:
942
+ Query: "offer lifecycle arrived started admin behalf responder availability"
943
+ ["offer state machine arrived started transition", "admin acting behalf responder proxy", "responder availability isAvailable update"]
944
+
945
+ Respond with ONLY the JSON array:`;
946
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
947
+ method: "POST",
948
+ headers: {
949
+ "Content-Type": "application/json",
950
+ "x-api-key": this._apiKey,
951
+ "anthropic-version": "2023-06-01"
952
+ },
953
+ body: JSON.stringify({
954
+ model: this._model,
955
+ max_tokens: 256,
956
+ messages: [{ role: "user", content: prompt }]
957
+ })
958
+ });
959
+ if (!response.ok) {
960
+ dbg2(`API error: ${response.status} \u2014 using original query only`);
961
+ return [query];
962
+ }
963
+ const data = await response.json();
964
+ const text = data.content?.[0]?.text ?? "";
965
+ dbg2(`Raw response: ${text}`);
966
+ const match = text.match(/\[[\s\S]*?\]/);
967
+ if (!match) {
968
+ dbg2(`No JSON array found \u2014 using original query only`);
969
+ return [query];
970
+ }
971
+ const subQueries = JSON.parse(match[0]);
972
+ if (!Array.isArray(subQueries) || subQueries.length === 0) {
973
+ return [query];
974
+ }
975
+ const result = [query, ...subQueries.slice(0, 3)];
976
+ dbg2(`Decomposed into ${result.length} queries: ${JSON.stringify(result)}`);
977
+ return result;
978
+ } catch (err) {
979
+ dbg2(`Error: ${err instanceof Error ? err.message : String(err)} \u2014 using original query only`);
980
+ return [query];
981
+ }
982
+ }
983
+ /** Check if the decomposer is available (has API key). */
984
+ get available() {
985
+ return !!this._apiKey;
986
+ }
987
+ };
988
+
884
989
  // src/search/vector/composite-vector-search.ts
990
+ var _debug3 = !!process.env.BRAINBANK_DEBUG;
991
+ function dbg3(msg) {
992
+ if (_debug3) process.stderr.write(`[composite] ${msg}
993
+ `);
994
+ }
995
+ __name(dbg3, "dbg");
885
996
  var CompositeVectorSearch = class _CompositeVectorSearch {
886
997
  constructor(_c) {
887
998
  this._c = _c;
999
+ if (process.env.ANTHROPIC_API_KEY) {
1000
+ this._decomposer = new QueryDecomposer();
1001
+ }
888
1002
  }
889
1003
  _c;
890
1004
  static {
@@ -892,23 +1006,46 @@ var CompositeVectorSearch = class _CompositeVectorSearch {
892
1006
  }
893
1007
  /** Default K when no source override is provided. */
894
1008
  static DEFAULT_K = 6;
1009
+ _decomposer;
895
1010
  /** Search across all registered domain strategies with score-based merge. */
896
1011
  async search(query, options = {}) {
897
1012
  const src = options.sources ?? {};
898
1013
  const { minScore = 0.25, useMMR = true, mmrLambda = 0.7 } = options;
899
- const queryVec = await this._c.embedding.embed(query);
1014
+ let queries;
1015
+ if (this._decomposer) {
1016
+ queries = await this._decomposer.decompose(query);
1017
+ } else {
1018
+ queries = [query];
1019
+ }
1020
+ const queryVecs = await this._c.embedding.embedBatch(queries);
900
1021
  const allResults = [];
901
1022
  let requestedK = 0;
902
- for (const [name, strategy] of this._c.strategies) {
903
- const k = src[name] ?? this._c.defaults?.[name] ?? _CompositeVectorSearch.DEFAULT_K;
904
- if (k <= 0) continue;
905
- requestedK = Math.max(requestedK, k);
906
- const hits = strategy.search(queryVec, k, minScore, useMMR, mmrLambda, query);
907
- allResults.push(...hits);
1023
+ for (let qi = 0; qi < queryVecs.length; qi++) {
1024
+ const qVec = queryVecs[qi];
1025
+ const qText = queries[qi];
1026
+ for (const [name, strategy] of this._c.strategies) {
1027
+ const k = src[name] ?? this._c.defaults?.[name] ?? _CompositeVectorSearch.DEFAULT_K;
1028
+ if (k <= 0) continue;
1029
+ requestedK = Math.max(requestedK, k);
1030
+ const hits = strategy.search(qVec, k, minScore, useMMR, mmrLambda, qText);
1031
+ allResults.push(...hits);
1032
+ }
908
1033
  }
909
1034
  if (allResults.length === 0) return [];
910
- allResults.sort((a, b) => b.score - a.score);
911
- const capped = allResults.slice(0, requestedK);
1035
+ const bestByFile = /* @__PURE__ */ new Map();
1036
+ for (const r of allResults) {
1037
+ const key = r.filePath ?? `_${r.content?.slice(0, 50)}`;
1038
+ const existing = bestByFile.get(key);
1039
+ if (!existing || r.score > existing.score) {
1040
+ bestByFile.set(key, r);
1041
+ }
1042
+ }
1043
+ const deduped = [...bestByFile.values()];
1044
+ if (queries.length > 1) {
1045
+ dbg3(`Multi-query dedup: ${allResults.length} raw \u2192 ${deduped.length} unique files (${queries.length} queries)`);
1046
+ }
1047
+ deduped.sort((a, b) => b.score - a.score);
1048
+ const capped = deduped.slice(0, requestedK * 4);
912
1049
  const maxScore = capped[0].score;
913
1050
  if (maxScore > 0) {
914
1051
  for (const r of capped) r.score = r.score / maxScore;
@@ -3055,13 +3192,13 @@ async function setupProviders(brainOpts, config, flags, env) {
3055
3192
  if (openaiKey) process.env.OPENAI_API_KEY = openaiKey;
3056
3193
  const prunerFlag = flags?.pruner ?? config?.pruner;
3057
3194
  if (prunerFlag === "haiku") {
3058
- const { HaikuPruner } = await import("./haiku-pruner-SHAXUPY6.js");
3195
+ const { HaikuPruner } = await import("./haiku-pruner-MT4JFKUC.js");
3059
3196
  brainOpts.pruner = new HaikuPruner({ apiKey: anthropicKey });
3060
3197
  }
3061
3198
  const expanderFlag = flags?.expander ?? config?.expander;
3062
3199
  if (expanderFlag === "haiku") {
3063
3200
  try {
3064
- const { HaikuExpander } = await import("./haiku-expander-YRSIPGKP.js");
3201
+ const { HaikuExpander } = await import("./haiku-expander-CGEHFJIA.js");
3065
3202
  brainOpts.expander = new HaikuExpander({ apiKey: anthropicKey });
3066
3203
  } catch {
3067
3204
  }
@@ -3260,4 +3397,4 @@ export {
3260
3397
  resetFactoryCache,
3261
3398
  createBrain
3262
3399
  };
3263
- //# sourceMappingURL=chunk-5KJQ6V7M.js.map
3400
+ //# sourceMappingURL=chunk-ZEUCCE6P.js.map