/** * BM25 search + optional embedding hybrid search for Hippo. * Zero external dependencies when embeddings are not available. */ import { MemoryEntry } from './memory.js'; import type { PhysicsConfig } from './physics-config.js'; export declare function tokenize(text: string): string[]; /** * Tokenized BM25 corpus. Callers can pre-build this with `buildCorpus` once * and reuse across many `hybridSearch` calls on the same entry set — the * tokenization work is the bulk of per-query cost on large stores. */ export interface BM25Corpus { docs: string[][]; avgLen: number; df: Map; N: number; } export declare function buildCorpus(texts: string[]): BM25Corpus; /** * Rough token estimate: characters / 4 (works well for English text). */ export declare function estimateTokens(text: string): number; export declare function detectTemporalDirection(query: string): 'recent' | 'oldest' | null; export interface TemporalRange { minTime: number; maxTime: number; } export declare function computeTemporalRange(entries: MemoryEntry[]): TemporalRange; export declare function temporalBoost(entry: MemoryEntry, direction: 'recent' | 'oldest' | null, range: TemporalRange): number; /** * A7 recall-trace: one lifecycle re-ranking step. Records a single score * mutation applied AFTER candidate generation (interference, value, OFC * utility, reranker, goal-boost, retrieval-count-downweight). * * Defined here (NOT in api.ts) so both `SearchResult` (the CLI carrier) and * `RecallResultItem` (the api/HTTP carrier in api.ts) can reference it without * a circular import: api.ts already imports from search.ts, and goals.ts imports * the type from here too — search.ts imports neither, so the dependency stays * acyclic. * * Pure side-channel: a `RerankStep[]` is only ever populated under an explicit * opt-in (`recall --why` on the CLI, `RecallOpts.explain` on the api). The * default path never allocates one (byte-identical guarantee). */ export interface RerankStep { /** One of: interference, value, utility, reranker, goal-boost, retrieval-count-downweight. */ stage: string; /** The score multiplier applied at this stage, when the transform is a scalar multiply. */ multiplier?: number; /** Score before this stage ran. */ scoreBefore: number; /** Score after this stage ran. */ scoreAfter: number; /** Optional human-readable detail (e.g. the matched goal tags). */ note?: string; } export interface SearchResult { entry: MemoryEntry; score: number; bm25: number; cosine: number; tokens: number; /** Populated when search is called with options.explain === true. */ breakdown?: ScoreBreakdown; /** * A7 recall-trace: the ordered lifecycle re-ranking steps that mutated * `score` after candidate generation. Populated by `cmdRecall` ONLY when * `recall --why` is set; undefined on the default path (zero allocation). */ rerankTrace?: RerankStep[]; /** Populated when a reranker ran. Replaces `score` for ordering; * original `score` preserved here. See src/rerankers/types.ts. */ rerankScore?: number; preRerankRank?: number; postRerankRank?: number; /** Populated for results surfaced by E3.2 graph traversal (`recall --hops N`): how * the memory was reached from a lexical seed. Absent on normal lexical hits. */ graphVia?: { hops: number; relType: string; direction: 'from' | 'to'; }; } export interface ScoreBreakdown { /** * - `hybrid`: BM25 blended with a non-zero cosine from a cached doc vector. * - `hybrid-no-vec`: Query was embedded but this doc had no cached vector, * so the effective score came from BM25 alone even though weights say * otherwise. Usually means `hippo embed` hasn't run on this memory. * - `bm25-only`: Embedding pipeline unavailable or the model requires re-index. * - `physics`: Scored by the physics engine (gravity + momentum + cluster). */ mode: 'hybrid' | 'hybrid-no-vec' | 'bm25-only' | 'physics'; /** BM25 score after normalization by max-in-corpus (0..1). */ normBm25: number; /** Weight applied to BM25 in the hybrid blend. */ bm25Weight: number; /** Weight applied to cosine in the hybrid blend. */ embeddingWeight: number; /** Cosine similarity (0 when embeddings not used). */ cosine: number; /** Blended base score before multipliers. */ base: number; /** Multiplier from memory strength: 0.5 + 0.5*strength. */ strengthMultiplier: number; /** Multiplier from age: 0.8 + 0.2*recencyBoost. */ recencyMultiplier: number; /** 1.2 if tagged 'decision', else 1.0. */ decisionBoost: number; /** 1.0..1.3 based on cwd path tag overlap. */ pathBoost: number; /** 1.5 if scope matches, 0.5 if scope mismatches, 1.0 if neutral. */ scopeBoost: number; /** Extra multiplier applied post-hybrid (e.g. 1.2x for local hits in a * local+global merged search). 1.0 when not applicable. */ sourceBump: number; /** Retrieval-time outcome personalization: 1 + 0.15*tanh(pos - neg), clipped * to [0.85, 1.15]. Immediate nudge from `hippo outcome --good/--bad`. * Separate from the slow strength-via-reward-factor path. */ outcomeBoost: number; /** Pre-MMR rank (1-indexed). Only set when MMR re-ranking ran. */ preMmrRank?: number; /** Post-MMR rank (1-indexed). Only set when MMR re-ranking ran. */ postMmrRank?: number; /** Query terms that appeared verbatim in the doc. */ matchedTerms: string[]; /** Final composite score (= base * multipliers). */ final: number; /** Age of the memory in whole days, at scoring time. */ ageDays: number; /** v0.30 / E4 — entry.dag_level (0=raw, 1=extracted, 2=topic, 3=entity). */ dagLevel?: number; /** v0.30 / E4 — descendant_count column (refreshed by E3 rebuild). */ descendantCount?: number; /** v0.30 / E4 — last_rebuilt_at ISO; null if never rebuilt. L2 only. */ lastRebuiltAt?: string | null; /** v0.30 / E4 — cumulative rebuild_count from E3. L2 only. */ rebuildCount?: number; /** v0.30 / E4 — deboost applied (1.0 for non-summaries; default 0.85 for L2). */ summaryDeboost?: number; /** v0.30 / E4 — 1.05 if L2 + rebuilt within 7 days; 1.0 otherwise. */ summaryFreshnessBoost?: number; } /** * v0.30 / E4-E5 — single source of truth for "is this a DAG summary". * E4 originally checked dag_level === 2; E5 widens to L2 + L3 since L3 * entity profiles also get the same deboost factor. Differentiated * deboost (e.g. 0.7 for L3) is flagged as follow-up. * * Existing drill-down at search.ts:506-529/923-946 uses tag check * ('dag-summary'); structural truth is dag_level === 2 || === 3. New * E4/E5 code uses this helper; existing drill-down NOT modified. */ export declare function isDagSummary(entry: MemoryEntry): boolean; /** * Hybrid search: BM25 + cosine similarity (when embeddings are available). * score = 0.4 * bm25_norm + 0.6 * cosine_sim (with embeddings) * score = bm25_norm * strength * recency (BM25-only fallback) * * embeddingWeight: weight for the cosine similarity component (0.0 to 1.0). */ export declare function hybridSearch(query: string, entries: MemoryEntry[], options?: { budget?: number; now?: Date; hippoRoot?: string; embeddingWeight?: number; explain?: boolean; /** Disable MMR re-ranking even when embeddings are available. */ mmr?: boolean; /** MMR balance: 1.0 = pure relevance, 0.0 = pure diversity. Default 0.7. */ mmrLambda?: number; /** Scoring mode: 'blend' (weighted sum of BM25+cosine, default) or * 'rrf' (reciprocal rank fusion - combines BM25 and cosine ranks * instead of scores, more robust for long documents). */ scoring?: 'blend' | 'rrf'; /** Pre-built BM25 corpus from `buildCorpus`. Pass this across many * queries on the same entry set to skip ~O(N*docLen) tokenization * work per call. Must be built from the same `entries` in the same * order (content + tags.join(' ')). */ preparedCorpus?: BM25Corpus; /** Minimum number of results to return regardless of budget. * Prevents budget saturation when memories are large. Default 1. */ minResults?: number; /** Active scope for scope-boost scoring. Auto-detected if not provided. */ scope?: string | null; /** Include superseded memories in results. Default false. */ includeSuperseded?: boolean; /** Filter to memories current at this ISO date string. */ asOf?: string; /** Optional reranker. Runs after MMR, before budget filtering. * See src/rerankers/types.ts. */ reranker?: import('./rerankers/types.js').RerankerFn; /** Options passed through to the reranker. */ rerankerOptions?: import('./rerankers/types.js').RerankerOptions; /** v0.30 / E4 — multiplier on L2 summary composite scores. Default 0.85 * (env HIPPO_SUMMARY_DEBOOST overrides). Per-call wins. Use 1.0 to disable. */ summaryDeboost?: number; /** v0.30 / E4 — enable rebuilt-within-7-days micro-boost (1.05) on L2 * summaries with fresh last_rebuilt_at. Default true. */ summaryFreshness?: boolean; /** L1 — graph-retrieval stream (opt-in): adds a 3rd RRF input ranking in-pool * candidates by graph proximity to the strong lexical seeds. Active ONLY in * `scoring:'rrf'` mode with embeddings + a `hippoRoot`. Absent/`weight<=0`/empty * graph -> the byte-identical 2-list (BM25 + dense) fusion. See src/graph-stream.ts. */ graphStream?: { /** RRF weight for the graph list. Required (opt-in is explicit; no implicit default). */ weight: number; tenantId: string; /** Global store root, when distinct (where global seeds' graph lives). */ globalRoot?: string; hops?: number; decay?: number; maxNeighbors?: number; /** # of top lexical seeds to expand from. Default DEFAULT_GRAPH_SEED_COUNT. */ seedCount?: number; }; }): Promise; /** * MMR (Maximal Marginal Relevance) re-ranking. * * Iteratively picks the candidate that maximises * lambda * relevance - (1 - lambda) * max(cos(cand, picked)) * * Inputs must already be sorted by relevance descending. When `explain` is * true, attaches `preMmrRank` / `postMmrRank` to each result's breakdown. * Exported for unit tests; production callers go through hybridSearch. * * Determinism note (T2): the picking loop below uses strict `mmr > bestMmr` * (first-wins on ties), so it is already deterministic GIVEN a * deterministic `scored` input order — no comparator change needed here; * the tiebreak lives upstream, in how `scored` was sorted before this runs. */ export declare function mmrRerank(scored: SearchResult[], embeddingIndex: Record, lambda: number, explain: boolean): SearchResult[]; /** * Physics-based search: scores memories using gravitational force, momentum, * and cluster amplification. Falls back to classic hybrid for memories * without physics state. */ export declare function physicsSearch(query: string, entries: MemoryEntry[], options?: { budget?: number; now?: Date; hippoRoot?: string; physicsConfig?: PhysicsConfig; queryEmbedding?: number[]; explain?: boolean; minResults?: number; /** Active scope for scope-boost scoring. Auto-detected if not provided. */ scope?: string | null; /** Include superseded memories. Default false. Must be threaded through * from the CLI so `recall --include-superseded` reaches the inner * bi-temporal filter at line ~844; otherwise the superseded entries are * re-filtered out here even when the caller deliberately retained them. */ includeSuperseded?: boolean; /** Bi-temporal filter: memories current at this ISO date string. */ asOf?: string; /** v0.30 / E4 — same deboost as hybridSearch. Inherited via options spread * when physicsSearch falls back to hybridSearch (L685/689/693/707). */ summaryDeboost?: number; /** v0.30 / E4 — same freshness boost as hybridSearch. */ summaryFreshness?: boolean; }): Promise; /** * Search entries using BM25 + strength + recency composite score. * When embeddings are available and hippoRoot is provided, uses hybrid scoring. * Returns results sorted by score, capped at token budget. * * Also updates retrieval metadata on returned entries (side effect: caller * must persist the updated entries). */ export declare function search(query: string, entries: MemoryEntry[], options?: { budget?: number; now?: Date; hippoRoot?: string; minResults?: number; includeSuperseded?: boolean; asOf?: string; }): SearchResult[]; /** * Update retrieval metadata on entries that were returned by a search. * Returns the mutated copies (caller must persist to disk). * * EVAL-ONLY ablation (see ablation.ts): with HIPPO_ABLATE_RECALL_BOOST set, * this returns the entries UNMUTATED - neutralizing all three strengthening * sub-effects (clock reset, retrieval_count, half-life increment) at the * single shared write site. The entries (not an empty array) must be * returned because callers derive `last_retrieval_ids` from the return * value, and a later `hippo outcome --good/--bad` targets those ids - an * empty return would silently co-ablate the outcome channel in the * strengthen-off arm (codex round-7 P2). PERSISTENCE is gated separately at * each persisting caller (CLI recall, api context, MCP recall/context, * consolidation replay): writeEntry on identical rows still refreshes * updated_at, rewrites mirrors, and marks DAG parents dirty (codex round-6 * P2), so those write loops skip under the flag. * The default `now` honors HIPPO_FAKE_NOW (simulated-time protocols). */ export declare function markRetrieved(entries: MemoryEntry[], now?: Date): MemoryEntry[]; export interface MatchExplanation { /** Human-readable reason string */ reason: string; /** Which query terms matched in the document (BM25 component) */ matchedTerms: string[]; /** Whether BM25 contributed to the score */ hasBm25: boolean; /** Whether embedding similarity contributed to the score */ hasEmbedding: boolean; /** Raw cosine similarity (0 when embeddings not used) */ cosineSimilarity: number; /** A3 provenance envelope (kind, scope, owner, artifact_ref, session_id, confidence) */ envelope?: { kind: string; scope: string | null; owner: string | null; artifact_ref: string | null; session_id: string | null; confidence: string; }; } /** * Explain why a search result matched a query. * Computes which query terms overlapped with the document and whether * BM25 and/or embedding similarity contributed to the composite score. */ export declare function explainMatch(query: string, result: SearchResult): MatchExplanation; /** * Compute text overlap ratio between two strings (Jaccard on token sets). */ export declare function textOverlap(a: string, b: string): number; //# sourceMappingURL=search.d.ts.map