/**
 * 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<string, number>;
    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 declare function computeTemporalRange(entries: MemoryEntry[]): {
    minTime: number;
    maxTime: number;
};
export declare function temporalBoost(entry: MemoryEntry, direction: 'recent' | 'oldest' | null, range: {
    minTime: number;
    maxTime: number;
}): number;
export interface SearchResult {
    entry: MemoryEntry;
    score: number;
    bm25: number;
    cosine: number;
    tokens: number;
    /** Populated when search is called with options.explain === true. */
    breakdown?: ScoreBreakdown;
    /** Populated when a reranker ran. Replaces `score` for ordering;
     *  original `score` preserved here. See src/rerankers/types.ts. */
    rerankScore?: number;
    preRerankRank?: number;
    postRerankRank?: number;
}
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;
}): Promise<SearchResult[]>;
/**
 * 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.
 */
export declare function mmrRerank(scored: SearchResult[], embeddingIndex: Record<string, number[]>, 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<SearchResult[]>;
/**
 * 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).
 */
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