/** * Full-text search across BRAIN memory using SQLite FTS5. * Uses raw SQL via nativeDb because drizzle doesn't support FTS5 virtual tables. * * Falls back to LIKE queries if FTS5 is not available (some SQLite builds lack it). * * @task T5130 * @epic T5149 */ import type { DatabaseSync } from 'node:sqlite'; import type { BrainDecisionRow, BrainLearningRow, BrainObservationRow, BrainPatternRow } from '../store/schema/memory-schema.js'; /** Search result grouped by backing memory table. */ export interface BrainMemorySearchResult { decisions: BrainDecisionRow[]; patterns: BrainPatternRow[]; learnings: BrainLearningRow[]; observations: BrainObservationRow[]; } /** Search options. */ export interface BrainSearchOptions { /** Max results per table. Default 10. */ limit?: number; /** Which tables to search. Default: all four. */ tables?: Array<'decisions' | 'patterns' | 'learnings' | 'observations'>; /** * T1085: Peer ID filter for CANT agent memory isolation (PSYCHE Wave 2). * * When provided, search results are scoped to entries where: * `peer_id = peerId OR peer_id = 'global'` * * This ensures a peer always sees its own memories plus the global pool, * but never sees another peer's private memories. * * When omitted (undefined), all entries are returned regardless of peer_id — * preserving backward-compatible behavior for callers that predate Wave 2. */ peerId?: string; /** * T1085: When true (default when peerId is provided), include entries with * `peer_id = 'global'` in addition to `peer_id = peerId`. * * Set to false for strict per-peer isolation (no global pool bleed-through). * Defaults to true — omitting this is equivalent to `includeGlobal: true`. */ includeGlobal?: boolean; } /** * Create FTS5 virtual tables and content-sync triggers if they don't exist. * * Uses content= to sync from main tables, so inserts to main tables * auto-populate FTS. UPDATE/DELETE require triggers. * * @task T5130 */ export declare function ensureFts5Tables(nativeDb: DatabaseSync): boolean; /** * Rebuild FTS5 indexes from the content tables. * Useful after bulk inserts that bypass triggers. * * @task T5130 */ export declare function rebuildFts5Index(nativeDb: DatabaseSync): void; /** * Unified search across all BRAIN memory tables. * * Uses FTS5 MATCH for full-text search with BM25 ranking when available, * falls back to LIKE queries otherwise. * * @task T5130 */ export declare function searchBrain(projectRoot: string, query: string, options?: BrainSearchOptions): Promise; /** * Escape special FTS5 characters in query string. * * Wraps each meaningful token in double quotes and joins with OR so that * partial matches are returned even when some tokens are not indexable * (e.g. task prefixes like "EPIC:", em-dashes "—", or short stop-words). * * Strategy: * 1. Split on whitespace. * 2. Keep only tokens that contain at least one word character (\w), which * ensures punctuation-only tokens (em dashes, colons standalone, etc.) * are dropped before they zero-out the entire result set. * 3. Deduplicate case-insensitively. * 4. Join with OR so the query broadens rather than requiring ALL tokens. * * Using AND (implicit FTS5 join) caused empty results whenever a task title * contained non-word tokens such as em dashes ("—") or trailing colons * ("EPIC:"), because FTS5's default tokenizer cannot index them and the * AND semantics then guaranteed zero matches for the whole query. (T553 bug fix) */ export declare function escapeFts5Query(query: string): string; /** * The two MATCH expressions used by the conjunctive-first retrieval strategy. * * @task T12073 */ export interface Fts5QueryPair { /** All terms required — high precision, may return nothing. */ readonly and: string; /** Any term matches — high recall, used only as a fallback. */ readonly or: string; /** Number of usable terms extracted from the query. */ readonly termCount: number; } /** * Build the AND and OR MATCH expressions for a natural-language query. * * ## Why both, and why prefixes (T12073) * * The previous builder emitted `"tok1" OR "tok2"` — quoted terms joined by OR. * Measured against the live 6,985-observation corpus, that has two failure * modes that together make recall useless for anything but an exact rare token: * * **Quoted terms are exact, so morphological variants are invisible.** A search * for `orphan` matched 113 documents; `orphan*` matched 189. Seventy-six * documents saying "orphaned" or "orphans" could not be found by searching for * "orphan". SQLite FTS5 has no stemmer on the `unicode61` tokenizer, so a * prefix wildcard is the available substitute — and it is a good one, because * it costs nothing on a prefix-indexed b-tree. * * **Pure OR plus BM25 ranks by term rarity, not by term coverage.** A short * document matching ONE query term outranks a long one matching ALL of them, * because BM25 normalises for length. On this corpus that is catastrophic: * 30% of observations are sub-60-character auto-capture stubs, so the * shortest, emptiest records win. Searching `nexus_symbols_fts orphan` under * the old builder returned *"Consolidated: change observations (5 entries)"* * as the top hit — a record with no content at all — while the conjunctive * form returns only documents about orphans in nexus. * * So: require all terms first, and fall back to ANY only when the conjunction * finds nothing. That preserves the recall the OR form was introduced for * (T553: non-word tokens like em dashes zeroing out an implicit-AND query) * without letting it dominate the common case. * * @param query - the raw user query. * @returns AND/OR MATCH expressions and the usable term count. * * @example * ```ts * const q = buildFts5Queries('nexus_symbols_fts orphan'); * // q.and === 'nexus_symbols_fts* AND orphan*' * // q.or === 'nexus_symbols_fts* OR orphan*' * ``` * * @task T553 * @task T12073 */ export declare function buildFts5Queries(query: string): Fts5QueryPair; /** * Run an FTS5-backed query conjunctively, falling back to disjunctive. * * Single-term queries skip the fallback entirely (AND and OR are identical). * * @param run - executes one MATCH expression and returns its rows. * @param queries - the pair from {@link buildFts5Queries}. * @returns conjunctive rows when non-empty, else disjunctive rows. * * @task T12073 */ export declare function matchConjunctiveFirst(run: (matchExpr: string) => T[], queries: Fts5QueryPair): T[]; /** * Reset the cached FTS5 availability flag and the initialized-path marker. * Used in tests to force re-detection and a rebuild on the next search. * * @task T12101 — also clears the per-path rebuild marker (was a * process-wide boolean before T12101). */ export declare function resetFts5Cache(): void; /** * The RRF smoothing constant (research-proven at 60). * * Balances noise vs. signal: small values amplify top-rank differences; * large values compress ranks toward a flat distribution. 60 is the * standard value from Cormack, Clarke & Buettcher (SIGIR 2009). */ export declare const RRF_K = 60; /** A single ranked hit from one retrieval source before fusion. */ export interface RrfHit { id: string; type: string; title: string; text: string; } /** * Round-robin interleave several independently-ranked lists. * * Takes rank 1 from every list, then rank 2 from every list, and so on, so a * downstream consumer that scores by array position does not systematically * favour whichever list happened to be concatenated first. * * @param lists - per-source lists, each already ordered best-first. * @returns a single list preserving each source's internal order. * * @example * ```ts * interleaveRanked([['a1', 'a2'], ['b1'], ['c1', 'c2', 'c3']]); * // ['a1', 'b1', 'c1', 'a2', 'c2', 'c3'] * ``` * * @task T12073 */ export declare function interleaveRanked(lists: ReadonlyArray): T[]; /** Fused result produced by reciprocalRankFusion. */ export interface RrfResult { id: string; /** Combined RRF score: sum of 1/(rank+RRF_K) across all source lists. */ rrfScore: number; type: string; title: string; text: string; /** Which retrieval sources contributed to this result. */ sources: Array<'fts' | 'vec' | 'graph' | 'code'>; /** BM25-derived FTS rank (0-based) — undefined if not in FTS results. */ ftsRank?: number; /** Vector distance rank (0-based) — undefined if not in vector results. */ vecRank?: number; } /** * Fuse ranked lists from multiple retrieval sources using Reciprocal Rank Fusion. * * Implements the RRF algorithm from Cormack, Clarke & Buettcher (SIGIR 2009): * * score(d) = Σ 1 / (k + rank(d, list)) for each list containing d * * where k=60 is the research-proven smoothing constant. * * Properties: * - Rank-based: actual scores from each source are ignored (only rank matters). * - Additive: items appearing in multiple lists accumulate higher scores. * - Robust: the +60 constant prevents rank-1 items from dominating. * * @param sources - Named arrays of ranked hits (order = rank, index 0 = best) * @param k - RRF smoothing constant (default: RRF_K = 60) * @returns Array of fused results sorted by rrfScore descending * * @example * ```ts * const fused = reciprocalRankFusion([ * { source: 'fts', hits: ftsHits }, * { source: 'vec', hits: vecHits }, * ]); * ``` */ export declare function reciprocalRankFusion(sources: Array<{ source: 'fts' | 'vec' | 'graph' | 'code'; hits: RrfHit[]; }>, k?: number): RrfResult[]; /** Result from hybridSearch combining multiple search signals. */ export interface HybridResult { id: string; /** RRF-fused score: sum of 1/(rank+60) across all source lists. */ score: number; type: string; title: string; text: string; sources: Array<'fts' | 'vec' | 'graph' | 'code'>; /** Raw FTS rank (0-based) for transparency — undefined if FTS did not return this item. */ ftsRank?: number; /** Raw vector rank (0-based) for transparency — undefined if vector did not return this item. */ vecRank?: number; } /** Options for hybridSearch. */ export interface HybridSearchOptions { limit?: number; /** * RRF smoothing constant k. Default: 60 (research-proven). * Larger k flattens rank differences; smaller k amplifies top-rank advantage. */ rrfK?: number; /** * When true, also search code symbols via @cleocode/nexus smartSearch. * Default: false. Code symbol hits are mapped to memory-compatible type/title. * * @task T1058 */ includeCode?: boolean; } /** * Hybrid search across FTS5, vector similarity, graph neighbors, and optionally code symbols. * Uses Reciprocal Rank Fusion (RRF) for result combination. * * Algorithm: * 1. Run FTS5 search, vector similarity search, and optionally code symbol search in parallel. * 2. Optionally expand via graph neighbors (best-effort). * 3. Fuse all ranked lists with RRF: score = Σ 1/(rank+rrfK). * 4. Return top-N sorted by fused RRF score. * * Graceful degradation: vector, graph, and code sources are silently skipped when * unavailable — RRF naturally handles partial source lists. * * @param query - Search query text * @param projectRoot - Project root directory * @param options - Limit, RRF tuning, and includeCode flag for code symbol search * @returns Array of hybrid results ranked by RRF score descending * * @task T5130 (hybrid search), T1058 (code symbol integration) */ export declare function hybridSearch(query: string, projectRoot: string, options?: HybridSearchOptions): Promise; //# sourceMappingURL=brain-search.d.ts.map