/**
 * Maximal Marginal Relevance (MMR) re-selection for recall candidates.
 *
 * After the reranker produces its ordered candidate list, we run an MMR pass
 * over the top N candidates (per-section) so that a cluster of near-duplicate
 * high-scoring facts cannot dominate the injected context.
 *
 *     MMR(d) = λ * sim(d, query) − (1 − λ) * max_{d' ∈ selected} sim(d, d')
 *
 * - λ defaults to 0.7 (tilted toward relevance, with meaningful diversity).
 * - Similarity uses cosine over pre-computed embeddings when available, and
 *   falls back to Jaccard over normalized tokens (lowercased alphanumerics)
 *   when embeddings are missing.
 * - Per-section application is the caller's responsibility: pass each
 *   section's ordered candidate slice independently so one cluster in one
 *   section cannot starve another section.
 *
 * Pure, deterministic, no IO. Input arrays are never mutated.
 */
/** Minimal candidate shape used by MMR. Callers may wrap their own records. */
interface MmrCandidate {
    /** Stable identifier for the candidate. Only used for tie-breaking. */
    id: string;
    /** Text content used for the Jaccard similarity fallback. */
    content: string;
    /**
     * Relevance score from the upstream ranker (e.g. rerank score or RRF).
     * Used as the `sim(d, query)` term when no query embedding is available.
     */
    score: number;
    /** Optional pre-computed embedding vector for cosine similarity. */
    embedding?: readonly number[] | null;
}
interface ApplyMmrOptions<C extends MmrCandidate> {
    /** Ordered candidate list (most relevant first). */
    candidates: readonly C[];
    /** Optional query embedding. If provided and candidates carry embeddings,
     *  relevance is measured by cosine similarity to the query. Otherwise
     *  candidate `score` is normalized and used. */
    queryEmbedding?: readonly number[] | null;
    /** λ ∈ [0, 1]. 1 = pure relevance, 0 = pure diversity. Default 0.7. */
    lambda?: number;
    /** Apply MMR only over the top N candidates. Default 40. */
    topN?: number;
    /** Maximum number of candidates to select. Default = candidates length. */
    budget?: number;
}
interface MmrDiversityReport {
    /**
     * Total number of candidates MMR considered (the full input pool, pre-MMR).
     * Previously this mirrored `kept` and was therefore uninformative; it now
     * reflects the true pool size so `kept/considered` logs carry signal even
     * though MMR is reorder-only.
     */
    considered: number;
    /**
     * Number of candidates present in the MMR output. With the current
     * orchestrator pipeline MMR is reorder-only (no drops), so this is the
     * same as `considered`. It's still reported separately so a future
     * drop-mode MMR can distinguish them without another schema change.
     */
    kept: number;
    /**
     * Head-of-list positions whose candidate identity changed between the
     * pre-MMR and post-MMR slices. This is the *actionable* diversity signal:
     * it tells the caller how many of the top `sampleSize` results MMR
     * promoted or demoted. Zero means MMR had no head-of-list effect; a value
     * greater than zero means at least one diverse candidate was swapped in.
     */
    headReorderCount: number;
    /** Average pairwise similarity of the head-of-list input slice (pre-MMR). */
    avgPairwiseSimBefore: number;
    /** Average pairwise similarity of the head-of-list MMR output slice. */
    avgPairwiseSimAfter: number;
}
declare const DEFAULT_LAMBDA = 0.7;
declare const DEFAULT_TOP_N = 40;
/**
 * Pure MMR re-selection over an ordered candidate list.
 *
 * Returns a new array — the input is never mutated. When `candidates.length`
 * is `<= 1` or `budget <= 0`, a defensive copy is returned as a no-op.
 */
declare function applyMmrToCandidates<C extends MmrCandidate>(opts: ApplyMmrOptions<C>): C[];
/**
 * Summarize how much MMR reshuffled the head of the candidate list, for
 * logging. Optional — callers can skip this if they don't care about metrics.
 *
 * IMPORTANT: `before` should be the score-ordered input (the same ordering the
 * upstream reranker emitted, *not* the MMR output), and `after` should be the
 * MMR-reordered list. Both are truncated to `sampleSize` before their pairwise
 * similarity is averaged.
 *
 * The sample size intentionally defaults to a small number
 * ({@link DEFAULT_DIVERSITY_SAMPLE_SIZE}) so that when the caller uses
 * `budget >= candidates.length` (i.e. MMR only reorders without dropping), the
 * head-of-list comparison still reflects whether MMR promoted diverse
 * candidates. Passing a sample size `>= candidates.length` in that situation
 * makes `avgPairwiseSimBefore` and `avgPairwiseSimAfter` trivially equal
 * because pairwise similarity is order-independent.
 */
declare function summarizeMmrDiversity<C extends MmrCandidate>(before: readonly C[], after: readonly C[], sampleSize?: number): MmrDiversityReport;
declare function normalizeTokens(text: string): Set<string>;
/**
 * Minimum shape the recall-MMR orchestration helper expects from a result.
 * Any richer result type (e.g. {@link QmdSearchResult}) satisfies this.
 */
interface MmrRecallResult {
    readonly docid?: string;
    readonly path?: string;
    readonly snippet?: string;
    readonly score?: number;
}
interface ReorderRecallResultsOptions {
    readonly lambda?: number;
    readonly topN?: number;
    /**
     * Head-of-list sample size used by the diversity metric. Defaults to
     * {@link DEFAULT_DIVERSITY_SAMPLE_SIZE}. Intentionally small so the metric
     * reflects head-of-list changes even when `budget >= results.length`.
     */
    readonly diversitySampleSize?: number;
}
interface ReorderRecallResultsOutcome<R extends MmrRecallResult> {
    readonly reordered: R[];
    readonly diversity: MmrDiversityReport;
    readonly lambda: number;
}
/**
 * Apply MMR to an ordered list of recall results and return the reordered
 * list plus a head-of-list diversity report.
 *
 * This helper is the single source of truth for the orchestrator's
 * per-section MMR pass. It is pure and deterministic so it can be unit
 * tested without constructing an Orchestrator.
 *
 * Key invariants:
 * 1. **No silent drops.** Candidates are keyed by a stable unique key derived
 *    from `path` first, falling back to `docid`, and always suffixed with the
 *    candidate's original index. Two results that share a basename-style
 *    docid but differ in path are treated as distinct candidates and both
 *    survive the reorder.
 * 2. **No mutation.** The input array is never mutated; a new array is
 *    returned.
 * 3. **Diversity metric is meaningful.** The report compares the *head of
 *    list* before and after MMR using a small sample size, so it reflects
 *    whether MMR promoted diverse candidates even when
 *    `budget >= results.length`.
 */
declare function reorderRecallResultsWithMmr<R extends MmrRecallResult>(results: readonly R[], options?: ReorderRecallResultsOptions): ReorderRecallResultsOutcome<R>;

export { type ApplyMmrOptions, DEFAULT_LAMBDA, DEFAULT_TOP_N, type MmrCandidate, type MmrDiversityReport, type MmrRecallResult, type ReorderRecallResultsOptions, type ReorderRecallResultsOutcome, applyMmrToCandidates, normalizeTokens, reorderRecallResultsWithMmr, summarizeMmrDiversity };
