Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

src/bench_mimir.rs

//! Mimir POPULATE + SEARCH bench suite (EPIC — Mimir performance).
//!
//! ~100 registered benchers driving the two north-star numbers for the Mimir
//! knowledge warehouse: **how fast it POPULATES** (ingest / index build / commit)
//! and **how fast it SEARCHES** (the MCP retrieval tools). Every bench:
//!
//!   * uses a **deterministic** fixture (seeded synthetic rows, or a real repo's
//!     own `src/` for the real-ingest arms) — no network, no model download;
//!   * does a **warmup** then `iters` timed iterations;
//!   * records **throughput** (`*_per_sec`) + **latency percentiles** (`p50_us` /
//!     `p99_us`), and the harness folds in **core-utilisation** telemetry
//!     (`cpu_*` / `cores_busy_*`) automatically per row;
//!   * is named `nornir.mimir.<populate|search>.<op>_<size>` so the mashup / the
//!     dwarves testmatrix-benches view groups them by operation × size.
//!
//! The suite is the driver for the never-ending optimize loop: baseline it, find
//! the slowest populate + slowest search, fix (gatling-only parallelism, better
//! index, fewer allocs), re-bench, prove the delta. See
//! `.nornir/mimir-bench-optimize-loop.md` for the cycle procedure.
//!
//! Registrations live in the `nornir` LIBRARY (like `bench_registry.rs`) so
//! `inventory` links them into every unit — the runner (`examples/nornir-bench.rs`)
//! and the release completeness-gate's bencher-discovery seam.
#![allow(dead_code)]

use anyhow::{Result, ensure};

use crate::bench::BenchResult;
use crate::bench::api::Bencher;

// ─────────────────────────── shared helpers ────────────────────────────────

/// p50 / p99 of a sample of per-iteration latencies (microseconds in, out).
/// Nearest-rank; empty ⇒ (0,0).
fn percentiles_us(mut xs: Vec<f64>) -> (f64, f64) {
    if xs.is_empty() {
        return (0.0, 0.0);
    }
    xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    let at = |q: f64| -> f64 {
        let idx = (((xs.len() - 1) as f64) * q).round() as usize;
        xs[idx.min(xs.len() - 1)]
    };
    (at(0.50), at(0.99))
}

/// Fold the standard latency/throughput metric block into a metrics map: the
/// `<verb>_per_sec` throughput, p50/p99 in µs, iteration count and total wall.
fn record(
    m: &mut serde_json::Map<String, serde_json::Value>,
    throughput_key: &str,
    ops: u64,
    per_iter_us: Vec<f64>,
    total: std::time::Duration,
) {
    let (p50, p99) = percentiles_us(per_iter_us.clone());
    let per_sec = if total.as_secs_f64() > 0.0 {
        ops as f64 / total.as_secs_f64()
    } else {
        0.0
    };
    m.insert(throughput_key.into(), serde_json::json!(per_sec));
    m.insert("p50_us".into(), serde_json::json!(p50));
    m.insert("p99_us".into(), serde_json::json!(p99));
    m.insert("iters".into(), serde_json::json!(per_iter_us.len()));
    m.insert("ops".into(), serde_json::json!(ops));
    m.insert(
        "elapsed_ms".into(),
        serde_json::json!(total.as_secs_f64() * 1000.0),
    );
}

/// Deterministic pseudo-embedding: a smooth, distinct, non-degenerate `dim`-D
/// vector for a seed (no model — we bench the STORE/SEARCH kernels, not the
/// embedder). Same shape the existing `nornir.vector_search` bench uses.
fn mk_vec(seed: u64, dim: usize) -> Vec<f32> {
    (0..dim)
        .map(|d| {
            let x = (seed.wrapping_mul(2_654_435_761).wrapping_add(d as u64)) as f32;
            (x * 0.000_1).sin() + 0.001 * (d as f32 + 1.0)
        })
        .collect()
}

// ── knowledge fixtures ──────────────────────────────────────────────────────

use crate::knowledge::symbols::{CallEdgeRow, SymbolRow};

fn mk_symbol(i: usize) -> SymbolRow {
    SymbolRow {
        crate_name: "app".into(),
        module_path: format!("app::m{}", i % 512),
        item_kind: if i % 4 == 0 { "struct" } else { "fn" }.into(),
        item_name: format!("f{i}"),
        visibility: if i % 3 == 0 { "pub" } else { "pub(crate)" }.into(),
        file: format!("src/m{}.rs", i % 512),
        line: (i % 4000) as u32 + 1,
        doc_lines: (i % 5) as u32,
        signature: Some(format!("fn f{i}(x: u32) -> u32")),
    }
}

/// A deep call chain `f0→f1→…` plus a shared fan-in callee (`helper`) — real work
/// for both deep `call_path` BFS and wide `callers_of`.
fn mk_call(i: usize, n: usize) -> Vec<CallEdgeRow> {
    let mut out = Vec::with_capacity(2);
    let caller = format!("app::m{}::f{}", i % 512, i);
    if i + 1 < n {
        out.push(CallEdgeRow {
            crate_name: "app".into(),
            caller_path: caller.clone(),
            callee_ident: format!("app::m{}::f{}", (i + 1) % 512, i + 1),
            call_kind: "call".into(),
            file: format!("src/m{}.rs", i % 512),
            line: (i % 4000) as u32 + 1,
        });
    }
    out.push(CallEdgeRow {
        crate_name: "app".into(),
        caller_path: caller,
        callee_ident: "util::helper".into(),
        call_kind: "call".into(),
        file: format!("src/m{}.rs", i % 512),
        line: (i % 4000) as u32 + 2,
    });
    out
}

/// `n_sym` symbols + the call sites of `n_call` chain links — the in-memory
/// knowledge view the context bench joins against.
fn mk_view(n_sym: usize, n_call: usize) -> crate::knowledge::query::KnowledgeView {
    let symbols: Vec<SymbolRow> = (0..n_sym).map(mk_symbol).collect();
    let mut calls = Vec::with_capacity(n_call * 2);
    for i in 0..n_call {
        calls.extend(mk_call(i, n_call));
    }
    crate::knowledge::query::KnowledgeView::new(symbols, calls)
}

/// A `git_heat_facts` scan of `rows` files — the Iceberg commit payload for the
/// commit-throughput and SQL arms (a live table with a live producer; the old
/// `symbol_facts` payload was retired with Mímir v2).
fn mk_heat(repo: &str, rows: usize) -> crate::knowledge::git_heat::GitHeatScan {
    let ts = chrono::Utc::now();
    crate::knowledge::git_heat::GitHeatScan {
        snapshot_id: uuid::Uuid::new_v4(),
        ts,
        repo: repo.into(),
        files: (0..rows)
            .map(|i| crate::knowledge::git_heat::GitHeatRow {
                file: format!("src/m{}/f{i}.rs", i % 512),
                commits_total: (i % 97) as i64 + 1,
                commits_30d: (i % 7) as i64,
                commits_90d: (i % 23) as i64,
                authors_total: (i % 5) as i64 + 1,
                last_commit_ts: ts,
            })
            .collect(),
    }
}

// ═══════════════════════════ POPULATE benches ══════════════════════════════

/// POPULATE — `call_edges` index build: time `KnowledgeView::new` over `edges`
/// call rows (the interned CSR / STree adjacency build the query layer stands on).
/// Pure in-memory.
struct PopCallEdgeIndex {
    id: &'static str,
    edges: usize,
    iters: usize,
}
impl Bencher for PopCallEdgeIndex {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::knowledge::query::KnowledgeView;
        let mut calls = Vec::with_capacity(self.edges * 2);
        for i in 0..self.edges {
            calls.extend(mk_call(i, self.edges));
        }
        let symbols: Vec<SymbolRow> = (0..self.edges).map(mk_symbol).collect();
        // Warmup one build (clones inside).
        let _ = KnowledgeView::new(symbols.clone(), calls.clone());
        let mut per_iter = Vec::with_capacity(self.iters);
        let t0 = std::time::Instant::now();
        let mut built = 0u64;
        for _ in 0..self.iters {
            let t = std::time::Instant::now();
            let v = KnowledgeView::new(symbols.clone(), calls.clone());
            built += v.calls.len() as u64;
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
        }
        let total = t0.elapsed();
        let mut m = serde_json::Map::new();
        m.insert(
            "edges".into(),
            serde_json::json!(built / self.iters.max(1) as u64),
        );
        record(&mut m, "edges_per_sec", built, per_iter, total);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// POPULATE — Ragnar/Eytzinger/STree64 index build: time `RagnarLog::build` over
/// `keys` version rows (STree64 exact tree + Eytzinger floor layout). The
/// time-travel index the warehouse commit-log stands on.
struct PopRagnarBuild {
    id: &'static str,
    keys: usize,
    iters: usize,
}
impl Bencher for PopRagnarBuild {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::ragnar::RagnarLog;
        let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
        let _ = RagnarLog::build(rows.clone()); // warmup
        let mut per_iter = Vec::with_capacity(self.iters);
        let t0 = std::time::Instant::now();
        let mut built = 0u64;
        for _ in 0..self.iters {
            let t = std::time::Instant::now();
            let log = RagnarLog::build(rows.clone());
            built += log.len() as u64;
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
        }
        let total = t0.elapsed();
        let mut m = serde_json::Map::new();
        m.insert("keys".into(), serde_json::json!(self.keys));
        record(&mut m, "keys_per_sec", built, per_iter, total);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// POPULATE — LinearScan baseline build: the naive path `RagnarLog` replaces, so
/// the build-cost delta is on the record too.
struct PopLinearBuild {
    id: &'static str,
    keys: usize,
    iters: usize,
}
impl Bencher for PopLinearBuild {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::ragnar::LinearScan;
        let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
        let _ = LinearScan::build(rows.clone());
        let mut per_iter = Vec::with_capacity(self.iters);
        let t0 = std::time::Instant::now();
        for _ in 0..self.iters {
            let t = std::time::Instant::now();
            let _ = LinearScan::build(rows.clone());
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
        }
        let total = t0.elapsed();
        let mut m = serde_json::Map::new();
        m.insert("keys".into(), serde_json::json!(self.keys));
        record(
            &mut m,
            "keys_per_sec",
            (self.keys * self.iters) as u64,
            per_iter,
            total,
        );
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// POPULATE — Iceberg commit throughput: append `commits` git-heat scans of
/// `rows_each` rows and report commits/sec (the warehouse write cadence).
struct PopIcebergCommit {
    id: &'static str,
    commits: usize,
    rows_each: usize,
}
impl Bencher for PopIcebergCommit {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::iceberg::IcebergWarehouse;
        let dir = tempfile::tempdir()?;
        let wh = IcebergWarehouse::open(dir.path())?;
        wh.append_git_heat_scan(&mk_heat("warm", 16))?;
        let mut per_iter = Vec::with_capacity(self.commits);
        let t0 = std::time::Instant::now();
        for _ in 0..self.commits {
            let scan = mk_heat("repo", self.rows_each);
            let t = std::time::Instant::now();
            wh.append_git_heat_scan(&scan)?;
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
        }
        let total = t0.elapsed();
        let mut m = serde_json::Map::new();
        m.insert("commits".into(), serde_json::json!(self.commits));
        m.insert("rows_each".into(), serde_json::json!(self.rows_each));
        record(
            &mut m,
            "commits_per_sec",
            self.commits as u64,
            per_iter,
            total,
        );
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// POPULATE — Iceberg commit latency **as a function of table depth**, which is
/// the only shape in which entry P1 of the serial-passage census can be measured
/// at all.
///
/// `iceberg_commit_*` above tops out at 64 commits. The per-commit
/// duplicate-file precheck (`SnapshotProducer::validate_duplicate_files`) loads
/// the manifest list and then `load_manifest()`s **every** entry in it, so its
/// cost is O(live data files) and it is invisible on a shallow table — exactly
/// the trap that produced a false "no win" in mimir-bench cycle 5. This arm
/// therefore **seeds `seed_commits` appends first** (untimed, and always with
/// the check off so the seed itself stays linear), then times `measure_commits`
/// further appends with the check forced to `check_dup`.
///
/// The two `check_dup` variants at the same `seed_commits` are the A/B. The
/// `deep_0_*` pair is the control: at depth 0 the check has nothing to scan, so
/// the pair must come out level — if it does not, the arm is measuring noise.
///
/// `data_files` in the output is the real live parquet count under the
/// warehouse, so the "≥1000 data files" precondition is reported, not assumed.
struct PopIcebergCommitDeep {
    id: &'static str,
    seed_commits: usize,
    measure_commits: usize,
    check_dup: bool,
}
impl Bencher for PopIcebergCommitDeep {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::iceberg::{FORCE_CHECK_DUPLICATE, IcebergWarehouse};
        use std::sync::atomic::Ordering;

        let dir = tempfile::tempdir()?;
        let wh = IcebergWarehouse::open(dir.path())?;

        // ── Seed (UNTIMED). Always check-off: the seed only has to produce the
        // table *shape* (N snapshots ⇒ N manifests ⇒ N data files),
        // and seeding with the check on is O(N²) — 1k commits would take the
        // better part of an hour and would not change the shape one bit. ──
        FORCE_CHECK_DUPLICATE.store(1, Ordering::Relaxed);
        wh.append_git_heat_scan(&mk_heat("warm", 16))?;
        for _ in 0..self.seed_commits {
            wh.append_git_heat_scan(&mk_heat("seed", 64))?;
        }

        // Count the live data files the measured commits will be scanned against.
        let data_files = count_parquet_files(dir.path());

        // ── Measure. ──
        FORCE_CHECK_DUPLICATE.store(if self.check_dup { 2 } else { 1 }, Ordering::Relaxed);
        let mut per_iter = Vec::with_capacity(self.measure_commits);
        let t0 = std::time::Instant::now();
        for _ in 0..self.measure_commits {
            let scan = mk_heat("repo", 200);
            let t = std::time::Instant::now();
            wh.append_git_heat_scan(&scan)?;
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
        }
        let total = t0.elapsed();
        FORCE_CHECK_DUPLICATE.store(0, Ordering::Relaxed);

        let mut m = serde_json::Map::new();
        m.insert("seed_commits".into(), serde_json::json!(self.seed_commits));
        m.insert(
            "measure_commits".into(),
            serde_json::json!(self.measure_commits),
        );
        m.insert("check_duplicate".into(), serde_json::json!(self.check_dup));
        m.insert("data_files".into(), serde_json::json!(data_files));
        record(
            &mut m,
            "commits_per_sec",
            self.measure_commits as u64,
            per_iter,
            total,
        );
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// Live `.parquet` files anywhere under `root` — the depth figure the P1 bench
/// has to report for its numbers to mean anything.
fn count_parquet_files(root: &std::path::Path) -> usize {
    fn walk(p: &std::path::Path, n: &mut usize) {
        let Ok(rd) = std::fs::read_dir(p) else { return };
        for e in rd.flatten() {
            let path = e.path();
            if path.is_dir() {
                walk(&path, n);
            } else if path.extension().is_some_and(|x| x == "parquet") {
                *n += 1;
            }
        }
    }
    let mut n = 0;
    walk(root, &mut n);
    n
}

// ── vector / embedding populate (feature-gated) ─────────────────────────────

#[cfg(feature = "vector")]
mod vec_benches {
    use super::*;
    use crate::vector::chunk::{Chunk, ChunkOptions, hash_text};
    use crate::vector::store::{
        IndexParams, ModelProfile, chunker_hash, index_snapshot, search, search_batch,
    };
    use crate::warehouse::iceberg::IcebergWarehouse;

    pub(super) fn profile(dim: usize) -> ModelProfile {
        ModelProfile {
            model_name: "bench-embed".into(),
            weights_sha: "w0".into(),
            tokenizer_sha: "t0".into(),
            pooling: "mean".into(),
            normalize: true,
            dim,
            dtype: "f32".into(),
        }
    }

    /// `n` deterministic, distinct chunks (unique content_hash each).
    pub(super) fn mk_chunks(n: usize) -> Vec<Chunk> {
        (0..n)
            .map(|i| {
                let text = format!("chunk_{i}_token_{}", i % 997);
                Chunk {
                    file: format!("src/f{}.rs", i % 512),
                    start_line: (i % 4000) + 1,
                    end_line: (i % 4000) + 12,
                    content_hash: hash_text(&text),
                    text,
                }
            })
            .collect()
    }

    /// Build an embedding snapshot (embeddings + manifest) in `wh` for `repo`.
    pub(super) fn seed_snapshot(
        wh: &IcebergWarehouse,
        repo: &str,
        chunks: &[Chunk],
        dim: usize,
    ) -> Result<()> {
        let m = profile(dim);
        let ch = chunker_hash(&ChunkOptions::default());
        let params = IndexParams {
            workspace: "ws",
            repo,
            git_sha: "sha1",
            branch: "main",
            model: &m,
            chunker_hash: &ch,
            complete: true,
        };
        index_snapshot(wh, &params, chunks, |missing| {
            Ok(missing
                .iter()
                .enumerate()
                .map(|(i, _)| mk_vec(i as u64, dim))
                .collect())
        })?;
        Ok(())
    }

    /// POPULATE — embeddings + `embedding_manifest` write: chunk `chunks` items and
    /// time `index_snapshot` (dedup + embed-missing + Parquet append of embeddings,
    /// manifest and snapshot rows). Cheap deterministic embedder isolates the
    /// STORE cost.
    pub(super) struct PopEmbeddingIndex {
        pub id: &'static str,
        pub chunks: usize,
        pub dim: usize,
    }
    impl Bencher for PopEmbeddingIndex {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            let chunks = mk_chunks(self.chunks);
            let dir = tempfile::tempdir()?;
            let wh = IcebergWarehouse::open(dir.path())?;
            let t = std::time::Instant::now();
            seed_snapshot(&wh, "repo", &chunks, self.dim)?;
            let el = t.elapsed();
            let mut m = serde_json::Map::new();
            m.insert("chunks".into(), serde_json::json!(self.chunks));
            m.insert("dim".into(), serde_json::json!(self.dim));
            record(
                &mut m,
                "chunks_per_sec",
                self.chunks as u64,
                vec![el.as_secs_f64() * 1e6],
                el,
            );
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    /// POPULATE — `VectorIndex` (znippy_zoomies vann) build: `new` + `add` of
    /// `rows` × `dim` vectors (the SIMD int8/f32 flat index construction).
    pub(super) struct PopVannBuild {
        pub id: &'static str,
        pub rows: usize,
        pub dim: usize,
    }
    impl Bencher for PopVannBuild {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            use crate::vector::VectorIndex;
            let mut flat = Vec::with_capacity(self.rows * self.dim);
            let mut ids = Vec::with_capacity(self.rows);
            for r in 0..self.rows as u64 {
                flat.extend(mk_vec(r, self.dim));
                ids.push(r);
            }
            let t = std::time::Instant::now();
            let mut idx = VectorIndex::new(self.dim)?;
            idx.add(&flat, &ids)?;
            let el = t.elapsed();
            let mut m = serde_json::Map::new();
            m.insert("rows".into(), serde_json::json!(self.rows));
            m.insert("dim".into(), serde_json::json!(self.dim));
            record(
                &mut m,
                "rows_per_sec",
                self.rows as u64,
                vec![el.as_secs_f64() * 1e6],
                el,
            );
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    // ═══════════════════════ SEARCH: vector kNN ════════════════════════════

    /// SEARCH — dense cosine kNN (the `vector_search` MCP tool path): warm a
    /// `rows`×`dim` snapshot, then time `queries` single-query top-`k` searches
    /// through `store::search` (reconstruct-cached + SIMD int8/f32 scan).
    pub(super) struct SearchVectorKnnSingle {
        pub id: &'static str,
        pub rows: usize,
        pub dim: usize,
        pub queries: usize,
        pub k: usize,
    }
    impl Bencher for SearchVectorKnnSingle {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            let repo = self.id; // unique per bench
            let chunks = mk_chunks(self.rows);
            let dir = tempfile::tempdir()?;
            let wh = IcebergWarehouse::open(dir.path())?;
            seed_snapshot(&wh, repo, &chunks, self.dim)?;
            let mp = profile(self.dim).id();
            // Warm the reconstruct cache.
            let _ = search(&wh, repo, Some("sha1"), &mp, &mk_vec(1, self.dim), self.k)?;
            let mut per_iter = Vec::with_capacity(self.queries);
            let mut hits = 0u64;
            let t0 = std::time::Instant::now();
            for q in 0..self.queries as u64 {
                let qv = mk_vec(q.wrapping_mul(31).wrapping_add(7), self.dim);
                let t = std::time::Instant::now();
                let r = search(&wh, repo, Some("sha1"), &mp, &qv, self.k)?;
                per_iter.push(t.elapsed().as_secs_f64() * 1e6);
                hits += r.len() as u64;
            }
            let total = t0.elapsed();
            let mut m = serde_json::Map::new();
            m.insert("rows".into(), serde_json::json!(self.rows));
            m.insert("dim".into(), serde_json::json!(self.dim));
            m.insert("k".into(), serde_json::json!(self.k));
            m.insert("hits".into(), serde_json::json!(hits));
            record(
                &mut m,
                "queries_per_sec",
                self.queries as u64,
                per_iter,
                total,
            );
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    /// SEARCH — batched dense kNN: `store::search_batch` amortises the
    /// reconstructed-matrix read across `batch` queries (bulk re-rank / dedup).
    pub(super) struct SearchVectorKnnBatch {
        pub id: &'static str,
        pub rows: usize,
        pub dim: usize,
        pub batch: usize,
        pub k: usize,
    }
    impl Bencher for SearchVectorKnnBatch {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            let repo = self.id;
            let chunks = mk_chunks(self.rows);
            let dir = tempfile::tempdir()?;
            let wh = IcebergWarehouse::open(dir.path())?;
            seed_snapshot(&wh, repo, &chunks, self.dim)?;
            let mp = profile(self.dim).id();
            let queries: Vec<Vec<f32>> = (0..self.batch as u64)
                .map(|q| mk_vec(q.wrapping_mul(31).wrapping_add(7), self.dim))
                .collect();
            let _ = search_batch(&wh, repo, Some("sha1"), &mp, &queries, self.k)?; // warm
            let iters = 8usize;
            let mut per_iter = Vec::with_capacity(iters);
            let mut hits = 0u64;
            let t0 = std::time::Instant::now();
            for _ in 0..iters {
                let t = std::time::Instant::now();
                let r = search_batch(&wh, repo, Some("sha1"), &mp, &queries, self.k)?;
                per_iter.push(t.elapsed().as_secs_f64() * 1e6 / self.batch as f64);
                hits += r.iter().map(|v| v.len() as u64).sum::<u64>();
            }
            let total = t0.elapsed();
            let mut m = serde_json::Map::new();
            m.insert("rows".into(), serde_json::json!(self.rows));
            m.insert("dim".into(), serde_json::json!(self.dim));
            m.insert("batch".into(), serde_json::json!(self.batch));
            m.insert("hits".into(), serde_json::json!(hits));
            record(
                &mut m,
                "queries_per_sec",
                (self.batch * iters) as u64,
                per_iter,
                total,
            );
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    /// Which corpus geometry an HNSW recall arm runs against. **This choice, not
    /// `ef_search`, is what dominates the recall number** — see
    /// [`znippy_zoomies::vann::clustered_row`] and the `hnsw` module docs.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub(super) enum HnswCorpus {
        /// Clustered, like a real embedding corpus: mean pairwise cosine within a
        /// query's true top-10 ≈ 0.66, matching the measured 0.658 of nornir's own
        /// `jina-v2-base-code` warehouse (39,559 × 768). **This is the regime the
        /// live ANN path in `vector::store` actually serves**, so it is the one a
        /// recall floor is meaningful in.
        Realistic,
        /// i.i.d. uniform random — an ADVERSARIAL worst case with intrinsic
        /// dimensionality = `dim`. A query's true top-10 are mutually
        /// near-orthogonal (measured mean pairwise cosine 0.082; only 2.6% of them
        /// appear in each other's exact 32-NN lists), so no bounded-degree
        /// proximity graph can walk between them and recall has to be bought with
        /// `ef_search` — which is why this arm pins an explicit, costed `ef`.
        /// Kept on purpose as a stress case; NOT representative of production.
        UniformAdversarial,
    }

    /// Well-spread deterministic pseudo-random vector (NOT `mk_vec`'s smooth
    /// near-tie ramp) so **recall@k** is a meaningful metric — a near-duplicate
    /// corpus makes the true top-k ill-defined and spuriously tanks recall.
    pub(super) fn rand_vec(seed: u64, dim: usize) -> Vec<f32> {
        let mut s = seed
            .wrapping_mul(0x9E37_79B9_7F4A_7C15)
            .wrapping_add(0xABCD_1234);
        (0..dim)
            .map(|_| {
                s ^= s << 13;
                s ^= s >> 7;
                s ^= s << 17;
                ((s >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0) as f32
            })
            .collect()
    }

    /// SEARCH — **HNSW ANN vs the brute-force flat oracle** (Mimir vector cycle).
    /// Builds a `rows`×`dim` flat [`VectorIndex`] (the exact oracle) and a vendored
    /// HNSW over the SAME rows, then measures, over `queries`:
    ///   * HNSW single-query latency (the O(log n) win), plus the flat baseline
    ///     latency on the same corpus for the speedup;
    ///   * **recall@`k`** of the HNSW top-k against the flat oracle's top-k.
    /// Carries a **red-when-broken** floor: if recall drops below `recall_floor`
    /// the bench turns red (break the graph → red).
    ///
    /// ⚠ **A recall floor is only meaningful together with the corpus it is
    /// measured on.** These arms used to run on i.i.d. uniform vectors and read
    /// 0.7320 @ 10k / 0.2656 @ 100k against a 0.90 floor — which was NOT an index
    /// defect but the curse of dimensionality (uniform vectors have intrinsic
    /// dimensionality `dim`; the graph itself measured within 6 points of the
    /// best *possible* degree-32 proximity graph on that data, and converged to
    /// exactly 1.0000 recall as `ef_search` grew, proving the links are sound).
    /// The live path serves clustered code embeddings, where the same index+params
    /// measure **0.998** recall@10. So the primary arms now carry
    /// [`HnswCorpus::Realistic`], and the uniform case is kept as an explicitly
    /// labelled adversarial arm with a *costed* `ef_search`. Every arm records
    /// `corpus`, `ef_search` and `top10_cohesion` so the regime is legible from
    /// the JSON alone and nobody re-litigates this from the recall number.
    pub(super) struct SearchHnswKnn {
        pub id: &'static str,
        pub rows: usize,
        pub dim: usize,
        pub queries: usize,
        pub k: usize,
        pub recall_floor: f64,
        /// Corpus geometry — see [`HnswCorpus`].
        pub corpus: HnswCorpus,
        /// Query-time candidate width. `0` = the shipped [`HnswParams`] default,
        /// which is what the live path uses; a non-zero value is an explicit,
        /// on-the-record purchase of recall with latency.
        pub ef_search: usize,
    }
    impl Bencher for SearchHnswKnn {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            use crate::vector::{
                HnswParams, REALISTIC_SPREAD, VectorIndex, clustered_row, normalized,
            };
            let (rows, dim, k) = (self.rows, self.dim, self.k);
            // ~50 rows per cluster, so a query's true top-k sits inside one — the
            // shape a real embedding corpus has.
            let clusters = ((rows / 50) as u64).max(4);
            // L2-normalized on the way out: `VectorIndex::add` would normalize
            // anyway, and keeping the local copy normalized too is what makes the
            // `top10_cohesion` dot product below an actual cosine.
            let row_at = |r: u64| match self.corpus {
                HnswCorpus::Realistic => {
                    clustered_row(0xC0DE_1234, r, dim, clusters, REALISTIC_SPREAD)
                }
                HnswCorpus::UniformAdversarial => normalized(&rand_vec(r, dim)),
            };

            // Flat oracle over the same rows the HNSW will index.
            let mut flat = VectorIndex::new(dim)?;
            let mut flatv = Vec::with_capacity(rows * dim);
            let mut ids = Vec::with_capacity(rows);
            for r in 0..rows as u64 {
                flatv.extend(row_at(r));
                ids.push(r);
            }
            flat.add(&flatv, &ids)?;

            // HNSW build (single-threaded; timed so the build cost is on record).
            let tb = std::time::Instant::now();
            let hnsw = flat.build_hnsw(HnswParams::default());
            let build_ms = tb.elapsed().as_secs_f64() * 1000.0;

            // `ef_search` is a QUERY-time knob (`search_with_ef`) — no rebuild.
            let ef = if self.ef_search == 0 {
                hnsw.ef_search()
            } else {
                self.ef_search
            };

            // Held-out queries: drawn from the same generator beyond the corpus
            // range, so they are in-distribution but never index members (no free
            // self-match inflating recall).
            let qs: Vec<Vec<f32>> = (0..self.queries as u64)
                .map(|q| row_at(9_000_000 + q))
                .collect();
            // Warm both paths.
            let _ = hnsw.search_with_ef(&qs[0], k, ef);
            let _ = flat.search(&qs[0], k);

            // recall@k vs the flat oracle, plus the corpus-geometry diagnostic
            // that explains the recall number: how tight the true top-k are to
            // EACH OTHER. High (~0.66) = clustered/realistic and walkable by a
            // proximity graph; ~0 = uniform noise, where no graph can help.
            let mut hit = 0usize;
            let mut tot = 0usize;
            let mut cohesion = 0.0f64;
            let mut cohesion_pairs = 0usize;
            for q in &qs {
                let exact = flat.search(q, k);
                let oracle: std::collections::HashSet<u64> =
                    exact.iter().map(|(id, _)| *id).collect();
                for (id, _) in hnsw.search_with_ef(q, k, ef) {
                    if oracle.contains(&id) {
                        hit += 1;
                    }
                }
                tot += oracle.len();
                for a in 0..exact.len() {
                    for b in (a + 1)..exact.len() {
                        let (ia, ib) = (exact[a].0 as usize, exact[b].0 as usize);
                        let (va, vb) = (
                            &flatv[ia * dim..(ia + 1) * dim],
                            &flatv[ib * dim..(ib + 1) * dim],
                        );
                        cohesion += va.iter().zip(vb).map(|(x, y)| (x * y) as f64).sum::<f64>();
                        cohesion_pairs += 1;
                    }
                }
            }
            let recall = hit as f64 / tot.max(1) as f64;
            let cohesion = cohesion / cohesion_pairs.max(1) as f64;

            // HNSW query latency.
            let mut per_iter = Vec::with_capacity(self.queries);
            let t0 = std::time::Instant::now();
            for q in &qs {
                let t = std::time::Instant::now();
                let _ = std::hint::black_box(hnsw.search_with_ef(q, k, ef));
                per_iter.push(t.elapsed().as_secs_f64() * 1e6);
            }
            let total = t0.elapsed();

            // Flat baseline latency on the same corpus (for the speedup number).
            let mut flat_us = Vec::with_capacity(self.queries);
            for q in &qs {
                let t = std::time::Instant::now();
                let _ = std::hint::black_box(flat.search(q, k));
                flat_us.push(t.elapsed().as_secs_f64() * 1e6);
            }
            let (flat_p50, _) = percentiles_us(flat_us);
            let (hnsw_p50, _) = percentiles_us(per_iter.clone());

            // RED-WHEN-BROKEN: the ANN index must preserve recall vs the exact
            // oracle. Break the graph/search and this bench turns red.
            anyhow::ensure!(
                recall + 1e-9 >= self.recall_floor,
                "HNSW recall@{k} {recall:.4} fell below floor {:.4} (rows={rows}, dim={dim}, \
                 corpus={:?}, ef_search={ef}, top10_cohesion={cohesion:.3}). \
                 Before touching ef_search: a correct graph converges to recall 1.0 as \
                 ef_search grows on ANY corpus — check that invariant first \
                 (znippy_zoomies::vann::hnsw tests), because a low recall at a LOW cohesion \
                 is the corpus, not the index.",
                self.recall_floor,
                self.corpus,
            );

            let mut m = serde_json::Map::new();
            m.insert("rows".into(), serde_json::json!(rows));
            m.insert("dim".into(), serde_json::json!(dim));
            m.insert("k".into(), serde_json::json!(k));
            m.insert("recall_at_k".into(), serde_json::json!(recall));
            m.insert("recall_floor".into(), serde_json::json!(self.recall_floor));
            m.insert(
                "corpus".into(),
                serde_json::json!(match self.corpus {
                    HnswCorpus::Realistic => "realistic_clustered",
                    HnswCorpus::UniformAdversarial => "uniform_adversarial",
                }),
            );
            m.insert("ef_search".into(), serde_json::json!(ef));
            m.insert("top10_cohesion".into(), serde_json::json!(cohesion));
            m.insert("build_ms".into(), serde_json::json!(build_ms));
            m.insert("flat_p50_us".into(), serde_json::json!(flat_p50));
            m.insert(
                "speedup_x".into(),
                serde_json::json!(if hnsw_p50 > 0.0 {
                    flat_p50 / hnsw_p50
                } else {
                    0.0
                }),
            );
            record(
                &mut m,
                "queries_per_sec",
                self.queries as u64,
                per_iter,
                total,
            );
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    // ═══════════════════════ SEARCH: Graph-RAG context-slicing ═════════════

    /// SEARCH — Graph-RAG context-slicing (`knowledge_context` MCP tool):
    /// `context_slices` joins embeddings ⋈ manifest ⋈ symbol_facts (optionally
    /// trimmed by a `call_edges` change-radius) into ranked prompt-injectable
    /// slices. The headline retrieval latency.
    pub(super) struct SearchContextSlice {
        pub id: &'static str,
        pub chunks: usize,
        pub k: usize,
        pub radius: bool,
    }
    impl Bencher for SearchContextSlice {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            use crate::knowledge::context::{ChangeRadius, ContextQuery, context_slices};
            let repo = self.id;
            let dim = 32usize;
            let chunks = mk_chunks(self.chunks);
            let dir = tempfile::tempdir()?;
            let wh = IcebergWarehouse::open(dir.path())?;
            seed_snapshot(&wh, repo, &chunks, dim)?;
            // A matching symbol view so the manifest span joins to symbols (the
            // view is an input since Mímir v2 — no `symbol_facts` read).
            let n_sym = self.chunks.min(20_000);
            let view = mk_view(n_sym, n_sym);
            let mp = profile(dim).id();
            let radius = if self.radius {
                Some(ChangeRadius {
                    seeds: vec!["f0".into(), "f1".into()],
                    hops: 3,
                    include_callers: true,
                    include_callees: true,
                })
            } else {
                None
            };
            let mk_q = |_qi: u64| ContextQuery {
                repo,
                git_sha: Some("sha1"),
                model_profile: &mp,
                k: self.k,
                radius: radius.clone(),
            };
            let _ = context_slices(&wh, &view, &mk_q(1), &mk_vec(1, dim))?; // warm
            let queries = 64usize;
            let mut per_iter = Vec::with_capacity(queries);
            let mut slices = 0u64;
            let t0 = std::time::Instant::now();
            for q in 0..queries as u64 {
                let qv = mk_vec(q.wrapping_mul(31).wrapping_add(7), dim);
                let t = std::time::Instant::now();
                let r = context_slices(&wh, &view, &mk_q(q), &qv)?;
                per_iter.push(t.elapsed().as_secs_f64() * 1e6);
                slices += r.len() as u64;
            }
            let total = t0.elapsed();
            let mut m = serde_json::Map::new();
            m.insert("chunks".into(), serde_json::json!(self.chunks));
            m.insert("k".into(), serde_json::json!(self.k));
            m.insert("radius".into(), serde_json::json!(self.radius));
            m.insert("slices".into(), serde_json::json!(slices));
            record(&mut m, "queries_per_sec", queries as u64, per_iter, total);
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    // ═══════════════════════ SEARCH: hybrid RRF (optimize cycle 1) ══════════

    /// SEARCH — **RRF hybrid vs dense-only** (optimize cycle 1 proof). Builds a
    /// REAL Tantivy BM25 index AND a REAL dense embedding snapshot over the SAME
    /// planted corpus, then measures recall@k for BM25-only, dense-only, and the
    /// Reciprocal-Rank-Fusion of the two — plus the fused query latency. Half the
    /// relevant docs are semantic-only (paraphrase; dense finds, BM25 misses) and
    /// half are lexical-only (rare token in an off-topic doc; BM25 finds, dense
    /// misses), so the fusion must beat EITHER engine alone — the before/after
    /// number the loop drives.
    pub(super) struct SearchHybridRrf {
        pub id: &'static str,
        pub docs: usize,
    }
    impl Bencher for SearchHybridRrf {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            use crate::index::{Corpus, Index};
            use crate::knowledge::fusion::{DEFAULT_RRF_K, fuse_bm25_dense, recall_at_k};
            use crate::vector::store::search;
            use std::path::PathBuf;

            let repo = self.id;
            let dim = 16usize;
            // The rare lexical token only the "keyword" relevant docs contain.
            let kw = "flux_capacitor_z9";
            // topic axis 0 = the query's semantic topic.
            let n = self.docs;
            // Relevant docs: indices [0..R). semantic-only: even; lexical-only: odd.
            let r_count = 8usize.min(n / 2);
            let mut relevant: Vec<String> = Vec::new();
            let corpus_dir = tempfile::tempdir()?;
            let ws = corpus_dir.path();

            // Build the planted corpus files + chunks in lockstep so BM25 doc
            // path == chunk.file (fusion key namespace).
            use crate::vector::chunk::{Chunk, hash_text};
            let mut chunks: Vec<Chunk> = Vec::with_capacity(n);
            let mut paths: Vec<PathBuf> = Vec::with_capacity(n);
            for i in 0..n {
                let fname = format!("doc_{i:05}.rs");
                let relevant_i = i < r_count;
                let semantic_only = relevant_i && i % 2 == 0;
                let lexical_only = relevant_i && i % 2 == 1;
                // Topic: relevant semantic docs and the query share topic 0; lexical
                // docs sit in an off topic; noise spreads across topics.
                let topic = if semantic_only {
                    0u64
                } else if lexical_only {
                    100 + i as u64 // far from topic 0 in embedding space
                } else {
                    1 + (i as u64 % 64)
                };
                // Body: paraphrase text for semantic docs; the rare token for
                // lexical docs (and the two keyword-bearing relevant docs).
                let mut body = format!("module doc {i}\nfn work() {{ /* topic {topic} */ }}\n");
                if lexical_only {
                    body.push_str(&format!("// {kw} appears here in an off-topic file\n"));
                }
                if semantic_only {
                    body.push_str("// warp field manifold energy resonance coupling\n");
                }
                std::fs::write(ws.join(&fname), &body)?;
                paths.push(ws.join(&fname));
                chunks.push(Chunk {
                    file: fname.clone(),
                    start_line: 1,
                    end_line: 3,
                    content_hash: hash_text(&format!("{i}-{topic}-{}", body.len())),
                    text: body,
                });
                if relevant_i {
                    relevant.push(fname);
                }
            }

            // Dense snapshot: embed each chunk near its topic axis.
            {
                use crate::vector::chunk::ChunkOptions;
                use crate::vector::store::{IndexParams, chunker_hash, index_snapshot};
                let m = profile(dim);
                let ch = chunker_hash(&ChunkOptions::default());
                let params = IndexParams {
                    workspace: "ws",
                    repo,
                    git_sha: "sha1",
                    branch: "main",
                    model: &m,
                    chunker_hash: &ch,
                    complete: true,
                };
                // Deterministic topic embedding: unit-ish vector on `topic % dim`.
                let by_hash: std::collections::HashMap<String, u64> = chunks
                    .iter()
                    .enumerate()
                    .map(|(i, c)| {
                        let semantic_only = i < r_count && i % 2 == 0;
                        let lexical_only = i < r_count && i % 2 == 1;
                        let topic = if semantic_only {
                            0
                        } else if lexical_only {
                            100 + i as u64
                        } else {
                            1 + (i as u64 % 64)
                        };
                        (c.content_hash.clone(), topic)
                    })
                    .collect();
                let dim_c = dim;
                index_snapshot(&wh_for(&corpus_dir)?, &params, &chunks, |missing| {
                    Ok(missing
                        .iter()
                        .map(|c| {
                            let topic = *by_hash.get(&c.content_hash).unwrap_or(&999);
                            let mut v = vec![0.001f32; dim_c];
                            v[(topic as usize) % dim_c] += 1.0;
                            v
                        })
                        .collect())
                })?;
            }
            // Re-open the same warehouse for search (index_snapshot used a fresh handle).
            let wh = wh_for(&corpus_dir)?;
            let mp = profile(dim).id();

            // BM25 index over the same files.
            let idx_dir = tempfile::tempdir()?;
            let index = Index::open_at(ws, idx_dir.path())?;
            index.index_files(repo, Corpus::Code, &paths)?;

            let k = r_count.max(4);
            // Dense query = topic-0 axis.
            let mut qv = vec![0.001f32; dim];
            qv[0] += 1.0;
            let dense_hits = search(&wh, repo, Some("sha1"), &mp, &qv, n.min(64))?;
            let dense_keys: Vec<String> = dense_hits.iter().map(|(_, o)| o.file.clone()).collect();
            // BM25 query = the rare token.
            let bm25_hits = index.search(kw, Some(Corpus::Code), Some(repo), n.min(64))?;
            let bm25_keys: Vec<String> = bm25_hits.iter().map(|h| h.path.clone()).collect();

            // Fuse (timed).
            let iters = 200usize;
            let mut per_iter = Vec::with_capacity(iters);
            let t0 = std::time::Instant::now();
            let mut fused = Vec::new();
            for _ in 0..iters {
                let t = std::time::Instant::now();
                fused = fuse_bm25_dense(&bm25_keys, &dense_keys, 1.0, 1.0, DEFAULT_RRF_K, k);
                per_iter.push(t.elapsed().as_secs_f64() * 1e6);
            }
            let total = t0.elapsed();

            let r_bm25 = recall_at_k(&bm25_keys, &relevant, k);
            let r_dense = recall_at_k(&dense_keys, &relevant, k);
            let r_fused = recall_at_k(&fused, &relevant, k);
            // RED-WHEN-BROKEN (runs in release, recorded as a bench TestOutcome):
            // the whole point of the fusion is that it must not LOSE recall vs
            // either engine, and on this planted corpus (half semantic-only, half
            // lexical-only relevant docs) it must STRICTLY beat both. Break
            // `reciprocal_rank_fusion` / `fuse_bm25_dense` and this bench turns red.
            anyhow::ensure!(
                r_fused + 1e-6 >= r_bm25 && r_fused + 1e-6 >= r_dense,
                "RRF hybrid recall {r_fused} regressed below bm25 {r_bm25} / dense {r_dense}"
            );
            anyhow::ensure!(
                r_fused > r_dense + 1e-6,
                "RRF hybrid must beat the dense-only MCP path (fused {r_fused} vs dense {r_dense})"
            );

            let mut m = serde_json::Map::new();
            m.insert("docs".into(), serde_json::json!(n));
            m.insert("relevant".into(), serde_json::json!(relevant.len()));
            m.insert("recall_bm25_pct".into(), serde_json::json!(r_bm25 * 100.0));
            m.insert(
                "recall_dense_pct".into(),
                serde_json::json!(r_dense * 100.0),
            );
            m.insert(
                "recall_hybrid_pct".into(),
                serde_json::json!(r_fused * 100.0),
            );
            // The optimize-loop headline: recall points GAINED over the dense-only
            // MCP path that ships today.
            m.insert(
                "recall_gain_pct".into(),
                serde_json::json!((r_fused - r_dense) * 100.0),
            );
            record(&mut m, "fuse_per_sec", iters as u64, per_iter, total);
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    /// Re-open the warehouse living under a corpus tempdir (search + populate use
    /// the same on-disk tree, distinct handles).
    fn wh_for(dir: &tempfile::TempDir) -> Result<IcebergWarehouse> {
        IcebergWarehouse::open(&dir.path().join(".wh"))
    }

    // ═══════════════════════ SEARCH: real-project ingest ═══════════════════

    /// POPULATE (real) — ingest nornir's OWN `src/` tree: chunk every `*.rs` and
    /// time `index_snapshot`. The north-star "populate a real codebase" number
    /// (runs from the repo root under `cargo run --example`; degrades to 0 files
    /// if `src/` is absent).
    pub(super) struct RealEmbedPopulate {
        pub id: &'static str,
    }
    impl Bencher for RealEmbedPopulate {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            use crate::vector::chunk::{ChunkOptions, chunk_file};
            use crate::vector::store::collect_rust_sources;
            let srcs = collect_rust_sources(std::path::Path::new("src"));
            let opts = ChunkOptions::default();
            let mut chunks = Vec::new();
            for (path, text) in &srcs {
                chunks.extend(chunk_file(path, text, &opts));
            }
            let dim = 32usize;
            let dir = tempfile::tempdir()?;
            let wh = IcebergWarehouse::open(dir.path())?;
            let t = std::time::Instant::now();
            if !chunks.is_empty() {
                seed_snapshot(&wh, "nornir", &chunks, dim)?;
            }
            let el = t.elapsed();
            let mut m = serde_json::Map::new();
            m.insert("files".into(), serde_json::json!(srcs.len()));
            m.insert("chunks".into(), serde_json::json!(chunks.len()));
            record(
                &mut m,
                "chunks_per_sec",
                chunks.len() as u64,
                vec![el.as_secs_f64() * 1e6],
                el,
            );
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    /// SEARCH (real) — dense kNN over nornir's own `src/` snapshot: warm the index,
    /// time top-10 queries. The north-star "search a real codebase" number.
    pub(super) struct RealVectorSearch {
        pub id: &'static str,
    }
    impl Bencher for RealVectorSearch {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            use crate::vector::chunk::{ChunkOptions, chunk_file};
            use crate::vector::store::collect_rust_sources;
            let srcs = collect_rust_sources(std::path::Path::new("src"));
            let opts = ChunkOptions::default();
            let mut chunks = Vec::new();
            for (path, text) in &srcs {
                chunks.extend(chunk_file(path, text, &opts));
            }
            let dim = 32usize;
            let dir = tempfile::tempdir()?;
            let wh = IcebergWarehouse::open(dir.path())?;
            let mut m = serde_json::Map::new();
            m.insert("files".into(), serde_json::json!(srcs.len()));
            m.insert("chunks".into(), serde_json::json!(chunks.len()));
            if chunks.is_empty() {
                record(
                    &mut m,
                    "queries_per_sec",
                    0,
                    vec![],
                    std::time::Duration::ZERO,
                );
                return Ok(BenchResult {
                    name: self.id.into(),
                    metrics: m,
                    ..Default::default()
                });
            }
            seed_snapshot(&wh, "nornir", &chunks, dim)?;
            let mp = profile(dim).id();
            let _ = search(&wh, "nornir", Some("sha1"), &mp, &mk_vec(1, dim), 10)?;
            let queries = 128usize;
            let mut per_iter = Vec::with_capacity(queries);
            let t0 = std::time::Instant::now();
            for q in 0..queries as u64 {
                let qv = mk_vec(q.wrapping_mul(31).wrapping_add(7), dim);
                let t = std::time::Instant::now();
                let _ = search(&wh, "nornir", Some("sha1"), &mp, &qv, 10)?;
                per_iter.push(t.elapsed().as_secs_f64() * 1e6);
            }
            let total = t0.elapsed();
            record(&mut m, "queries_per_sec", queries as u64, per_iter, total);
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }

    use crate::vector::store::{Embedder, RepoInput, RepoRef, index_repo, index_repos_batched};

    /// A model-free stand-in embedder for the batched-embed arm: a deterministic,
    /// non-degenerate, L2-normalized `dim`-vector per text. It measures the STEP
    /// STRUCTURE (flattened chunking + one existing-hash read + one embed call +
    /// serial appends) NOT a GPU — the real 4090 batching win rides on top of this
    /// and cannot be benched without weights, so we bench the part that is
    /// deterministic and machine-independent.
    struct BenchEmbedder {
        dim: usize,
    }
    impl Embedder for BenchEmbedder {
        fn profile(&self) -> ModelProfile {
            profile(self.dim)
        }
        fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>> {
            Ok(texts
                .iter()
                .map(|t| {
                    let seed = hash_text(t);
                    let bytes = seed.as_bytes();
                    let mut v = vec![0f32; self.dim];
                    for (k, slot) in v.iter_mut().enumerate() {
                        let b = bytes[k % bytes.len()] as f32;
                        *slot = (b * 0.01 + k as f32 * 0.001).sin() + 0.001 * (k as f32 + 1.0);
                    }
                    let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-9);
                    for x in &mut v {
                        *x /= norm;
                    }
                    v
                })
                .collect())
        }
    }

    /// Synthetic per-member Rust sources for the embed arm: `files` files of unique
    /// content per member so every member's chunks are distinct (real embed work).
    fn embed_member_files(member: usize, files: usize) -> Vec<(String, String)> {
        (0..files)
            .map(|f| {
                let mut body = String::new();
                for i in 0..24 {
                    body.push_str(&format!(
                        "/// doc m{member} f{f} i{i}\npub fn m{member}_f{f}_fn{i}(x: u32) -> u32 {{ x.wrapping_mul({}).wrapping_add({member}) }}\n",
                        i * 7 + 1
                    ));
                }
                (format!("src/m{member}_f{f}.rs"), body)
            })
            .collect()
    }

    /// Populate step-3b EMBED: `members` repos embedded one-at-a-time via the old
    /// per-member `index_repo` loop vs the flattened, GPU-batched
    /// [`index_repos_batched`] the fix uses. Times both into separate fresh
    /// warehouses, asserts identical occurrence + new-vector totals, records the
    /// `speedup`. The win here is the flattened union chunking + ONE existing-hash
    /// read + ONE embed call replacing N of each.
    pub(super) struct PopEmbedMembersBatched {
        pub id: &'static str,
        pub members: usize,
        pub files: usize,
        pub dim: usize,
    }
    impl Bencher for PopEmbedMembersBatched {
        fn id(&self) -> &'static str {
            self.id
        }
        fn run(&self) -> Result<BenchResult> {
            let opts = ChunkOptions::default();
            let emb = BenchEmbedder { dim: self.dim };
            let per_member: Vec<Vec<(String, String)>> = (0..self.members)
                .map(|m| embed_member_files(m, self.files))
                .collect();

            // SEQUENTIAL — old one-repo-at-a-time `index_repo` loop, fresh warehouse.
            let dir_seq = tempfile::tempdir()?;
            let wh_seq = IcebergWarehouse::open(dir_seq.path())?;
            let t = std::time::Instant::now();
            let mut seq_occ = 0usize;
            let mut seq_new = 0usize;
            for (m, files) in per_member.iter().enumerate() {
                let repo_name = format!("repo{m}");
                let r = RepoRef {
                    workspace: "ws",
                    repo: &repo_name,
                    git_sha: "sha1",
                    branch: "main",
                    complete: true,
                };
                let snap = index_repo(&wh_seq, &r, files, &opts, &emb)?;
                seq_occ += snap.occurrences;
                seq_new += snap.new_vectors;
            }
            let seq_ms = t.elapsed().as_secs_f64() * 1000.0;

            // BATCHED — flattened chunk pool + one embed call, fresh warehouse.
            let dir_b = tempfile::tempdir()?;
            let wh_b = IcebergWarehouse::open(dir_b.path())?;
            let inputs: Vec<RepoInput> = per_member
                .iter()
                .enumerate()
                .map(|(m, files)| RepoInput {
                    workspace: "ws".into(),
                    repo: format!("repo{m}"),
                    git_sha: "sha1".into(),
                    branch: "main".into(),
                    complete: true,
                    files: files.clone(),
                })
                .collect();
            let t = std::time::Instant::now();
            let results = index_repos_batched(&wh_b, &inputs, &opts, &emb);
            let bat_ms = t.elapsed().as_secs_f64() * 1000.0;

            let mut bat_occ = 0usize;
            let mut bat_new = 0usize;
            for res in results {
                let snap = res?;
                bat_occ += snap.occurrences;
                bat_new += snap.new_vectors;
            }

            // CORRECTNESS: the batched path indexes the SAME occurrences and embeds
            // the SAME number of new vectors as the serial loop.
            anyhow::ensure!(
                bat_occ == seq_occ,
                "batched embed indexed {bat_occ} occurrences, serial {seq_occ} — not identical"
            );
            anyhow::ensure!(
                bat_new == seq_new,
                "batched embed produced {bat_new} new vectors, serial {seq_new} — dedup diverged"
            );

            let speedup = if bat_ms > 0.0 { seq_ms / bat_ms } else { 0.0 };

            let mut m = serde_json::Map::new();
            m.insert("members".into(), serde_json::json!(self.members));
            m.insert("occurrences".into(), serde_json::json!(bat_occ));
            m.insert("new_vectors".into(), serde_json::json!(bat_new));
            m.insert("seq_ms".into(), serde_json::json!(seq_ms));
            m.insert("batched_ms".into(), serde_json::json!(bat_ms));
            m.insert("speedup".into(), serde_json::json!(speedup));
            m.insert(
                "cores".into(),
                serde_json::json!(
                    std::thread::available_parallelism()
                        .map(|n| n.get())
                        .unwrap_or(1)
                ),
            );
            Ok(BenchResult {
                name: self.id.into(),
                metrics: m,
                ..Default::default()
            })
        }
    }
}

// ═══════════════════════ SEARCH: call-graph (pure) ═════════════════════════

/// SEARCH — call-graph queries (`callers_of` / `call_path` MCP tools): warm a
/// `nodes`-deep chain + fan-in view, then time a battery of deep-BFS + fan-in
/// lookups.
struct SearchCallGraph {
    id: &'static str,
    nodes: usize,
    queries: usize,
}
impl Bencher for SearchCallGraph {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::knowledge::query::KnowledgeView;
        let mut calls = Vec::with_capacity(self.nodes * 2);
        for i in 0..self.nodes {
            calls.extend(mk_call(i, self.nodes));
        }
        let view = KnowledgeView::new(vec![], calls);
        let from = "f0".to_string();
        let to = format!("f{}", self.nodes - 1);
        let _ = view.call_path(&from, &to);
        let _ = view.callers_of("helper");
        let mut per_iter = Vec::with_capacity(self.queries);
        let mut work = 0u64;
        let t0 = std::time::Instant::now();
        for _ in 0..self.queries {
            let t = std::time::Instant::now();
            if let Some(p) = view.call_path(&from, &to) {
                work += p.len() as u64;
            }
            work += view.callers_of("helper").len() as u64;
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
        }
        let total = t0.elapsed();
        let mut m = serde_json::Map::new();
        m.insert("nodes".into(), serde_json::json!(self.nodes));
        m.insert("work".into(), serde_json::json!(work));
        record(
            &mut m,
            "queries_per_sec",
            (self.queries * 2) as u64,
            per_iter,
            total,
        );
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// SEARCH — change-radius traversal: BFS the `call_edges` graph `hops` out from a
/// seed (the "trim to affected set" lever behind Graph-RAG radius + the
/// change-radius MCP query). Pure in-memory over a `nodes` view.
struct SearchCallRadius {
    id: &'static str,
    nodes: usize,
    hops: usize,
}
impl Bencher for SearchCallRadius {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::knowledge::query::KnowledgeView;
        let mut calls = Vec::with_capacity(self.nodes * 2);
        for i in 0..self.nodes {
            calls.extend(mk_call(i, self.nodes));
        }
        let view = KnowledgeView::new(vec![], calls);
        // BFS `hops` from a set of seeds, following callees (forward blast radius).
        let bfs = |seed: &str, hops: usize| -> usize {
            use std::collections::HashSet;
            let mut seen: HashSet<String> = HashSet::new();
            let mut frontier = vec![seed.to_string()];
            seen.insert(seed.to_string());
            for _ in 0..hops {
                let mut next = Vec::new();
                for nname in &frontier {
                    for e in view.callees_of(nname) {
                        let c = e
                            .callee_ident
                            .rsplit("::")
                            .next()
                            .unwrap_or(&e.callee_ident)
                            .to_string();
                        if seen.insert(c.clone()) {
                            next.push(c);
                        }
                    }
                }
                if next.is_empty() {
                    break;
                }
                frontier = next;
            }
            seen.len()
        };
        let _ = bfs("f0", self.hops);
        let queries = 64usize;
        let mut per_iter = Vec::with_capacity(queries);
        let mut reached = 0u64;
        let t0 = std::time::Instant::now();
        for q in 0..queries {
            let seed = format!("f{}", (q * 7) % self.nodes);
            let t = std::time::Instant::now();
            reached += bfs(&seed, self.hops) as u64;
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
        }
        let total = t0.elapsed();
        let mut m = serde_json::Map::new();
        m.insert("nodes".into(), serde_json::json!(self.nodes));
        m.insert("hops".into(), serde_json::json!(self.hops));
        m.insert("reached".into(), serde_json::json!(reached));
        record(&mut m, "queries_per_sec", queries as u64, per_iter, total);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

// ═══════════════════ SEARCH: Ragnar/Eytzinger/STree64 ══════════════════════

/// SEARCH — STree64 exact point lookup vs LinearScan (A/B): `at_exact` over a
/// `keys`-key Ragnar log, timed against the naive linear baseline, with the
/// speedup ratio the keep-or-drop gate reads.
struct SearchRagnarAtExact {
    id: &'static str,
    keys: usize,
}
impl Bencher for SearchRagnarAtExact {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::ragnar::{LinearScan, RagnarLog};
        let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
        let log = RagnarLog::build(rows.clone());
        let lin = LinearScan::build(rows);
        let probes: Vec<i64> = (0..1024u64)
            .map(|i| ((i.wrapping_mul(2_654_435_761) as usize % self.keys) as i64) * 2)
            .collect();
        // Warm.
        for &p in probes.iter().take(16) {
            let _ = log.at_exact(p);
            let _ = lin.at_exact(p);
        }
        let iters = 32usize;
        let mut per_iter = Vec::with_capacity(iters);
        let mut acc = 0u64;
        let t0 = std::time::Instant::now();
        for _ in 0..iters {
            let t = std::time::Instant::now();
            for &p in &probes {
                acc += log.at_exact(p).copied().unwrap_or(0);
            }
            per_iter.push(t.elapsed().as_secs_f64() * 1e6 / probes.len() as f64);
        }
        let fast = t0.elapsed();
        // Baseline pass.
        let tb = std::time::Instant::now();
        let mut accb = 0u64;
        for _ in 0..iters {
            for &p in &probes {
                accb += lin.at_exact(p).copied().unwrap_or(0);
            }
        }
        let slow = tb.elapsed();
        // A/B correctness gate — NOT a debug-only sanity check. `speedup_x` below is
        // only a speedup if both sides answered the same question, and benches run in
        // release where `debug_assert!` is compiled out. Fail the arm instead.
        ensure!(
            acc == accb,
            "{}: STree64 `at_exact` and LinearScan disagree over {} keys \
             ({} probes × {} iters) — checksum {} vs baseline {}; the reported \
             speedup would be measuring a wrong answer",
            self.id,
            self.keys,
            probes.len(),
            iters,
            acc,
            accb
        );
        let n = (iters * probes.len()) as u64;
        let mut m = serde_json::Map::new();
        m.insert("keys".into(), serde_json::json!(self.keys));
        m.insert(
            "baseline_lookups_per_sec".into(),
            serde_json::json!(n as f64 / slow.as_secs_f64().max(1e-9)),
        );
        m.insert(
            "speedup_x".into(),
            serde_json::json!(slow.as_secs_f64() / fast.as_secs_f64().max(1e-9)),
        );
        m.insert("checksum".into(), serde_json::json!(acc));
        m.insert("baseline_checksum".into(), serde_json::json!(accb));
        record(&mut m, "lookups_per_sec", n, per_iter, fast);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// SEARCH — Eytzinger floor (`as_of`, point-in-time) lookup vs LinearScan (A/B).
struct SearchRagnarAsOf {
    id: &'static str,
    keys: usize,
}
impl Bencher for SearchRagnarAsOf {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::ragnar::{LinearScan, RagnarLog};
        let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
        let log = RagnarLog::build(rows.clone());
        let lin = LinearScan::build(rows);
        // Odd probes → floor descent (between stored even keys).
        let probes: Vec<i64> = (0..1024u64)
            .map(|i| ((i.wrapping_mul(40_503) as usize % self.keys) as i64) * 2 + 1)
            .collect();
        for &p in probes.iter().take(16) {
            let _ = log.as_of(p);
            let _ = lin.as_of(p);
        }
        let iters = 32usize;
        let mut per_iter = Vec::with_capacity(iters);
        let mut acc = 0u64;
        let t0 = std::time::Instant::now();
        for _ in 0..iters {
            let t = std::time::Instant::now();
            for &p in &probes {
                acc += log.as_of(p).copied().unwrap_or(0);
            }
            per_iter.push(t.elapsed().as_secs_f64() * 1e6 / probes.len() as f64);
        }
        let fast = t0.elapsed();
        let tb = std::time::Instant::now();
        let mut accb = 0u64;
        for _ in 0..iters {
            for &p in &probes {
                accb += lin.as_of(p).copied().unwrap_or(0);
            }
        }
        let slow = tb.elapsed();
        // A/B correctness gate (see `SearchRagnarAtExact`): release builds compile
        // `debug_assert!` out, so the invariant must be an `ensure!` to be real.
        ensure!(
            acc == accb,
            "{}: Eytzinger `as_of` floor and LinearScan disagree over {} keys \
             ({} probes × {} iters) — checksum {} vs baseline {}; the reported \
             speedup would be measuring a wrong answer",
            self.id,
            self.keys,
            probes.len(),
            iters,
            acc,
            accb
        );
        let n = (iters * probes.len()) as u64;
        let mut m = serde_json::Map::new();
        m.insert("keys".into(), serde_json::json!(self.keys));
        m.insert(
            "baseline_lookups_per_sec".into(),
            serde_json::json!(n as f64 / slow.as_secs_f64().max(1e-9)),
        );
        m.insert(
            "speedup_x".into(),
            serde_json::json!(slow.as_secs_f64() / fast.as_secs_f64().max(1e-9)),
        );
        m.insert("checksum".into(), serde_json::json!(acc));
        m.insert("baseline_checksum".into(), serde_json::json!(accb));
        record(&mut m, "lookups_per_sec", n, per_iter, fast);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// SEARCH — range cut (`boundary_leq`, the as-of replay boundary) vs LinearScan.
struct SearchRagnarBoundary {
    id: &'static str,
    keys: usize,
}
impl Bencher for SearchRagnarBoundary {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::ragnar::{LinearScan, RagnarLog};
        let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
        let log = RagnarLog::build(rows.clone());
        let lin = LinearScan::build(rows);
        let probes: Vec<i64> = (0..1024u64)
            .map(|i| ((i.wrapping_mul(2_246_822_519) as usize % self.keys) as i64) * 2 + 1)
            .collect();
        for &p in probes.iter().take(16) {
            let _ = log.boundary_leq(p);
            let _ = lin.boundary_leq(p);
        }
        let iters = 32usize;
        let mut per_iter = Vec::with_capacity(iters);
        let mut acc = 0usize;
        let t0 = std::time::Instant::now();
        for _ in 0..iters {
            let t = std::time::Instant::now();
            for &p in &probes {
                acc += log.boundary_leq(p);
            }
            per_iter.push(t.elapsed().as_secs_f64() * 1e6 / probes.len() as f64);
        }
        let fast = t0.elapsed();
        let tb = std::time::Instant::now();
        let mut accb = 0usize;
        for _ in 0..iters {
            for &p in &probes {
                accb += lin.boundary_leq(p);
            }
        }
        let slow = tb.elapsed();
        // A/B correctness gate (see `SearchRagnarAtExact`): release builds compile
        // `debug_assert!` out, so the invariant must be an `ensure!` to be real.
        ensure!(
            acc == accb,
            "{}: `boundary_leq` range cut and LinearScan disagree over {} keys \
             ({} probes × {} iters) — checksum {} vs baseline {}; the reported \
             speedup would be measuring a wrong answer",
            self.id,
            self.keys,
            probes.len(),
            iters,
            acc,
            accb
        );
        let n = (iters * probes.len()) as u64;
        let mut m = serde_json::Map::new();
        m.insert("keys".into(), serde_json::json!(self.keys));
        m.insert(
            "baseline_lookups_per_sec".into(),
            serde_json::json!(n as f64 / slow.as_secs_f64().max(1e-9)),
        );
        m.insert(
            "speedup_x".into(),
            serde_json::json!(slow.as_secs_f64() / fast.as_secs_f64().max(1e-9)),
        );
        m.insert("checksum".into(), serde_json::json!(acc));
        m.insert("baseline_checksum".into(), serde_json::json!(accb));
        record(&mut m, "lookups_per_sec", n, per_iter, fast);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

// ═══════════════════════ SEARCH: DataFusion SQL ════════════════════════════

/// SEARCH — raw DataFusion SQL over `git_heat_facts`: a point `WHERE file=` and a
/// `GROUP BY authors_total` aggregate — the shape the `mimir_sql` MCP tool runs.
struct SearchSqlRaw {
    id: &'static str,
    kind: &'static str, // "point" | "agg"
    rows: usize,
}
impl Bencher for SearchSqlRaw {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::iceberg::IcebergWarehouse;
        use crate::warehouse::sql::query;
        let dir = tempfile::tempdir()?;
        let wh = IcebergWarehouse::open(dir.path())?;
        wh.append_git_heat_scan(&mk_heat("repo", self.rows))?;
        let sql = match self.kind {
            "agg" => "SELECT authors_total, COUNT(*) AS n FROM git_heat_facts GROUP BY authors_total"
                .to_string(),
            _ => "SELECT file, commits_total FROM git_heat_facts WHERE file = 'src/m1/f1.rs'"
                .to_string(),
        };
        let _ = query(&wh, &sql);
        let iters = 16usize;
        let mut per_iter = Vec::with_capacity(iters);
        let mut got = 0u64;
        let t0 = std::time::Instant::now();
        for _ in 0..iters {
            let t = std::time::Instant::now();
            if let Ok(b) = query(&wh, &sql) {
                got += b.iter().map(|x| x.num_rows() as u64).sum::<u64>();
            }
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
        }
        let total = t0.elapsed();
        let mut m = serde_json::Map::new();
        m.insert("kind".into(), serde_json::json!(self.kind));
        m.insert("rows".into(), serde_json::json!(self.rows));
        m.insert("result_rows".into(), serde_json::json!(got / iters as u64));
        record(&mut m, "queries_per_sec", iters as u64, per_iter, total);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// Nearest-rank p50 of a latency sample (µs in, µs out); empty ⇒ 0.
fn p50_us(xs: &[f64]) -> f64 {
    percentiles_us(xs.to_vec()).0
}

// ═══════════════════════════ REGISTRATIONS ═════════════════════════════════
//
// Grouped by operation × size. `nornir.mimir.populate.*` and
// `nornir.mimir.search.*` so the mashup / dwarves testmatrix-benches view groups
// them cleanly. Sizes chosen so the whole suite runs serially in a few minutes;
// 10M arms are the cheap in-memory (Ragnar) structures only.

// ── POPULATE: call_edges index build ──
crate::register_bench!(PopCallEdgeIndex {
    id: "nornir.mimir.populate.call_edge_index_1k",
    edges: 1_000,
    iters: 20
});
crate::register_bench!(PopCallEdgeIndex {
    id: "nornir.mimir.populate.call_edge_index_10k",
    edges: 10_000,
    iters: 10
});
crate::register_bench!(PopCallEdgeIndex {
    id: "nornir.mimir.populate.call_edge_index_100k",
    edges: 100_000,
    iters: 5
});
crate::register_bench!(PopCallEdgeIndex {
    id: "nornir.mimir.populate.call_edge_index_1m",
    edges: 1_000_000,
    iters: 3
});

// ── POPULATE: Ragnar/Eytzinger/STree64 build ──
crate::register_bench!(PopRagnarBuild {
    id: "nornir.mimir.populate.ragnar_build_1k",
    keys: 1_000,
    iters: 50
});
crate::register_bench!(PopRagnarBuild {
    id: "nornir.mimir.populate.ragnar_build_10k",
    keys: 10_000,
    iters: 20
});
crate::register_bench!(PopRagnarBuild {
    id: "nornir.mimir.populate.ragnar_build_100k",
    keys: 100_000,
    iters: 10
});
crate::register_bench!(PopRagnarBuild {
    id: "nornir.mimir.populate.ragnar_build_1m",
    keys: 1_000_000,
    iters: 5
});
crate::register_bench!(PopRagnarBuild {
    id: "nornir.mimir.populate.ragnar_build_10m",
    keys: 10_000_000,
    iters: 2
});

// ── POPULATE: LinearScan baseline build ──
crate::register_bench!(PopLinearBuild {
    id: "nornir.mimir.populate.linear_build_1k",
    keys: 1_000,
    iters: 50
});
crate::register_bench!(PopLinearBuild {
    id: "nornir.mimir.populate.linear_build_100k",
    keys: 100_000,
    iters: 10
});
crate::register_bench!(PopLinearBuild {
    id: "nornir.mimir.populate.linear_build_1m",
    keys: 1_000_000,
    iters: 5
});
crate::register_bench!(PopLinearBuild {
    id: "nornir.mimir.populate.linear_build_10m",
    keys: 10_000_000,
    iters: 2
});

// ── POPULATE: Iceberg commit throughput ──
crate::register_bench!(PopIcebergCommit {
    id: "nornir.mimir.populate.iceberg_commit_8",
    commits: 8,
    rows_each: 200
});
crate::register_bench!(PopIcebergCommit {
    id: "nornir.mimir.populate.iceberg_commit_16",
    commits: 16,
    rows_each: 200
});
crate::register_bench!(PopIcebergCommit {
    id: "nornir.mimir.populate.iceberg_commit_32",
    commits: 32,
    rows_each: 200
});
crate::register_bench!(PopIcebergCommit {
    id: "nornir.mimir.populate.iceberg_commit_64",
    commits: 64,
    rows_each: 200
});

// ── POPULATE: commit latency vs table depth — the census-P1 A/B (see
// `PopIcebergCommitDeep`). `_dup` keeps the per-commit duplicate precheck on,
// `_fast` turns it off; `deep_0_*` is the level-pair control. ──
crate::register_bench!(PopIcebergCommitDeep {
    id: "nornir.mimir.populate.iceberg_commit_deep_0_dup",
    seed_commits: 0,
    measure_commits: 8,
    check_dup: true
});
crate::register_bench!(PopIcebergCommitDeep {
    id: "nornir.mimir.populate.iceberg_commit_deep_0_fast",
    seed_commits: 0,
    measure_commits: 8,
    check_dup: false
});
crate::register_bench!(PopIcebergCommitDeep {
    id: "nornir.mimir.populate.iceberg_commit_deep_250_dup",
    seed_commits: 250,
    measure_commits: 8,
    check_dup: true
});
crate::register_bench!(PopIcebergCommitDeep {
    id: "nornir.mimir.populate.iceberg_commit_deep_250_fast",
    seed_commits: 250,
    measure_commits: 8,
    check_dup: false
});
crate::register_bench!(PopIcebergCommitDeep {
    id: "nornir.mimir.populate.iceberg_commit_deep_1k_dup",
    seed_commits: 1000,
    measure_commits: 8,
    check_dup: true
});
crate::register_bench!(PopIcebergCommitDeep {
    id: "nornir.mimir.populate.iceberg_commit_deep_1k_fast",
    seed_commits: 1000,
    measure_commits: 8,
    check_dup: false
});

// ── SEARCH: call-graph queries ──
crate::register_bench!(SearchCallGraph {
    id: "nornir.mimir.search.callgraph_1k",
    nodes: 1_000,
    queries: 500
});
crate::register_bench!(SearchCallGraph {
    id: "nornir.mimir.search.callgraph_10k",
    nodes: 10_000,
    queries: 200
});
crate::register_bench!(SearchCallGraph {
    id: "nornir.mimir.search.callgraph_100k",
    nodes: 100_000,
    queries: 50
});
crate::register_bench!(SearchCallGraph {
    id: "nornir.mimir.search.callgraph_500k",
    nodes: 500_000,
    queries: 20
});

// ── SEARCH: change-radius traversal ──
crate::register_bench!(SearchCallRadius {
    id: "nornir.mimir.search.call_radius_1k",
    nodes: 1_000,
    hops: 4
});
crate::register_bench!(SearchCallRadius {
    id: "nornir.mimir.search.call_radius_10k",
    nodes: 10_000,
    hops: 4
});
crate::register_bench!(SearchCallRadius {
    id: "nornir.mimir.search.call_radius_100k",
    nodes: 100_000,
    hops: 4
});

// ── SEARCH: Ragnar STree64 exact point lookup (+A/B) ──
crate::register_bench!(SearchRagnarAtExact {
    id: "nornir.mimir.search.ragnar_at_exact_1k",
    keys: 1_000
});
crate::register_bench!(SearchRagnarAtExact {
    id: "nornir.mimir.search.ragnar_at_exact_10k",
    keys: 10_000
});
crate::register_bench!(SearchRagnarAtExact {
    id: "nornir.mimir.search.ragnar_at_exact_100k",
    keys: 100_000
});
crate::register_bench!(SearchRagnarAtExact {
    id: "nornir.mimir.search.ragnar_at_exact_1m",
    keys: 1_000_000
});
crate::register_bench!(SearchRagnarAtExact {
    id: "nornir.mimir.search.ragnar_at_exact_10m",
    keys: 10_000_000
});

// ── SEARCH: Eytzinger floor as_of lookup (+A/B) ──
crate::register_bench!(SearchRagnarAsOf {
    id: "nornir.mimir.search.ragnar_as_of_1k",
    keys: 1_000
});
crate::register_bench!(SearchRagnarAsOf {
    id: "nornir.mimir.search.ragnar_as_of_10k",
    keys: 10_000
});
crate::register_bench!(SearchRagnarAsOf {
    id: "nornir.mimir.search.ragnar_as_of_100k",
    keys: 100_000
});
crate::register_bench!(SearchRagnarAsOf {
    id: "nornir.mimir.search.ragnar_as_of_1m",
    keys: 1_000_000
});
crate::register_bench!(SearchRagnarAsOf {
    id: "nornir.mimir.search.ragnar_as_of_10m",
    keys: 10_000_000
});

// ── SEARCH: range cut boundary_leq (+A/B) ──
crate::register_bench!(SearchRagnarBoundary {
    id: "nornir.mimir.search.ragnar_boundary_1k",
    keys: 1_000
});
crate::register_bench!(SearchRagnarBoundary {
    id: "nornir.mimir.search.ragnar_boundary_10k",
    keys: 10_000
});
crate::register_bench!(SearchRagnarBoundary {
    id: "nornir.mimir.search.ragnar_boundary_100k",
    keys: 100_000
});
crate::register_bench!(SearchRagnarBoundary {
    id: "nornir.mimir.search.ragnar_boundary_1m",
    keys: 1_000_000
});
crate::register_bench!(SearchRagnarBoundary {
    id: "nornir.mimir.search.ragnar_boundary_10m",
    keys: 10_000_000
});

// ── SEARCH: raw DataFusion SQL ──
crate::register_bench!(SearchSqlRaw {
    id: "nornir.mimir.search.sql_point_500",
    kind: "point",
    rows: 500
});
crate::register_bench!(SearchSqlRaw {
    id: "nornir.mimir.search.sql_point_5k",
    kind: "point",
    rows: 5_000
});
crate::register_bench!(SearchSqlRaw {
    id: "nornir.mimir.search.sql_agg_500",
    kind: "agg",
    rows: 500
});
crate::register_bench!(SearchSqlRaw {
    id: "nornir.mimir.search.sql_agg_5k",
    kind: "agg",
    rows: 5_000
});

// ── vector / embedding arms (feature-gated) ──
#[cfg(feature = "vector")]
mod vec_registrations {
    use super::vec_benches::*;

    // POPULATE: step-3b embed — per-member `index_repo` loop vs flattened,
    // GPU-batched `index_repos_batched` (the 2026-07-25 fix). Records `speedup`.
    crate::register_bench!(PopEmbedMembersBatched {
        id: "nornir.mimir.populate.embed_members_batched",
        members: 16,
        files: 6,
        dim: 64
    });

    // POPULATE: embeddings + manifest.
    crate::register_bench!(PopEmbeddingIndex {
        id: "nornir.mimir.populate.embeddings_1k",
        chunks: 1_000,
        dim: 32
    });
    crate::register_bench!(PopEmbeddingIndex {
        id: "nornir.mimir.populate.embeddings_5k",
        chunks: 5_000,
        dim: 32
    });
    crate::register_bench!(PopEmbeddingIndex {
        id: "nornir.mimir.populate.embeddings_20k",
        chunks: 20_000,
        dim: 32
    });
    crate::register_bench!(PopEmbeddingIndex {
        id: "nornir.mimir.populate.embeddings_50k",
        chunks: 50_000,
        dim: 32
    });

    // POPULATE: VectorIndex (vann) build across rows × dim.
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_1k_d64",
        rows: 1_000,
        dim: 64
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_1k_d128",
        rows: 1_000,
        dim: 128
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_1k_d256",
        rows: 1_000,
        dim: 256
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_10k_d64",
        rows: 10_000,
        dim: 64
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_10k_d128",
        rows: 10_000,
        dim: 128
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_10k_d256",
        rows: 10_000,
        dim: 256
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_100k_d64",
        rows: 100_000,
        dim: 64
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_100k_d128",
        rows: 100_000,
        dim: 128
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_100k_d256",
        rows: 100_000,
        dim: 256
    });

    // SEARCH: dense kNN single-query across corpus sizes.
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_500",
        rows: 500,
        dim: 128,
        queries: 256,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_1k",
        rows: 1_000,
        dim: 128,
        queries: 256,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_10k",
        rows: 10_000,
        dim: 128,
        queries: 128,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_50k",
        rows: 50_000,
        dim: 128,
        queries: 64,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_100k",
        rows: 100_000,
        dim: 128,
        queries: 32,
        k: 10
    });

    // SEARCH: batched dense kNN across corpus sizes.
    crate::register_bench!(SearchVectorKnnBatch {
        id: "nornir.mimir.search.vec_knn_batch_500",
        rows: 500,
        dim: 128,
        batch: 256,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnBatch {
        id: "nornir.mimir.search.vec_knn_batch_1k",
        rows: 1_000,
        dim: 128,
        batch: 256,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnBatch {
        id: "nornir.mimir.search.vec_knn_batch_10k",
        rows: 10_000,
        dim: 128,
        batch: 256,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnBatch {
        id: "nornir.mimir.search.vec_knn_batch_50k",
        rows: 50_000,
        dim: 128,
        batch: 256,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnBatch {
        id: "nornir.mimir.search.vec_knn_batch_100k",
        rows: 100_000,
        dim: 128,
        batch: 256,
        k: 10
    });

    // SEARCH: HNSW ANN vs the brute-force flat oracle (recall@10 + latency).
    // The Mimir vector cycle: O(log n) approximate search with a recall floor.
    // SEARCH: HNSW recall + latency vs the exact flat oracle, on the corpus
    // geometry the LIVE path actually serves (clustered code embeddings). These
    // carry the real 0.90 floor at the SHIPPED default ef_search (ef=0 → default),
    // so they are red-when-broken without buying recall with latency.
    crate::register_bench!(SearchHnswKnn {
        id: "nornir.mimir.search.vec_hnsw_1k",
        rows: 1_000,
        dim: 128,
        queries: 256,
        k: 10,
        recall_floor: 0.90,
        corpus: HnswCorpus::Realistic,
        ef_search: 0
    });
    crate::register_bench!(SearchHnswKnn {
        id: "nornir.mimir.search.vec_hnsw_10k",
        rows: 10_000,
        dim: 128,
        queries: 128,
        k: 10,
        recall_floor: 0.90,
        corpus: HnswCorpus::Realistic,
        ef_search: 0
    });
    crate::register_bench!(SearchHnswKnn {
        id: "nornir.mimir.search.vec_hnsw_100k",
        rows: 100_000,
        dim: 128,
        queries: 64,
        k: 10,
        recall_floor: 0.90,
        corpus: HnswCorpus::Realistic,
        ef_search: 0
    });

    // SEARCH: the ADVERSARIAL i.i.d.-uniform corpus, kept deliberately. Recall
    // there is NOT free — at the default ef=64 it measures .733 @10k / .266 @100k,
    // which is the curse of dimensionality, not a graph defect (proof: recall
    // converges to exactly 1.0000 as ef grows). So these arms pin the ef the
    // measured curve says is needed for 0.90 and let the latency columns show what
    // it costs (at 100k the HNSW ends up ~10× SLOWER than the flat scan — the
    // honest verdict is that an ANN index does not belong on data of this shape).
    crate::register_bench!(SearchHnswKnn {
        id: "nornir.mimir.search.vec_hnsw_10k_uniform",
        rows: 10_000,
        dim: 128,
        queries: 128,
        k: 10,
        recall_floor: 0.90,
        corpus: HnswCorpus::UniformAdversarial,
        ef_search: 512
    });
    crate::register_bench!(SearchHnswKnn {
        id: "nornir.mimir.search.vec_hnsw_100k_uniform",
        rows: 100_000,
        dim: 128,
        queries: 64,
        k: 10,
        recall_floor: 0.90,
        corpus: HnswCorpus::UniformAdversarial,
        ef_search: 2_048
    });

    // SEARCH: dense kNN varying k at fixed corpus.
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_k1",
        rows: 20_000,
        dim: 128,
        queries: 128,
        k: 1
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_k10",
        rows: 20_000,
        dim: 128,
        queries: 128,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_k50",
        rows: 20_000,
        dim: 128,
        queries: 128,
        k: 50
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_k100",
        rows: 20_000,
        dim: 128,
        queries: 128,
        k: 100
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_k200",
        rows: 20_000,
        dim: 128,
        queries: 128,
        k: 200
    });

    // SEARCH: Graph-RAG context-slicing (radius off/on) across corpus sizes.
    crate::register_bench!(SearchContextSlice {
        id: "nornir.mimir.search.context_slice_1k",
        chunks: 1_000,
        k: 10,
        radius: false
    });
    crate::register_bench!(SearchContextSlice {
        id: "nornir.mimir.search.context_slice_1k_radius",
        chunks: 1_000,
        k: 10,
        radius: true
    });
    crate::register_bench!(SearchContextSlice {
        id: "nornir.mimir.search.context_slice_5k",
        chunks: 5_000,
        k: 10,
        radius: false
    });
    crate::register_bench!(SearchContextSlice {
        id: "nornir.mimir.search.context_slice_5k_radius",
        chunks: 5_000,
        k: 10,
        radius: true
    });
    crate::register_bench!(SearchContextSlice {
        id: "nornir.mimir.search.context_slice_10k",
        chunks: 10_000,
        k: 10,
        radius: false
    });
    crate::register_bench!(SearchContextSlice {
        id: "nornir.mimir.search.context_slice_10k_radius",
        chunks: 10_000,
        k: 10,
        radius: true
    });
    crate::register_bench!(SearchContextSlice {
        id: "nornir.mimir.search.context_slice_20k",
        chunks: 20_000,
        k: 10,
        radius: false
    });

    // SEARCH: RRF hybrid vs dense-only (optimize cycle 1 proof).
    crate::register_bench!(SearchHybridRrf {
        id: "nornir.mimir.search.hybrid_rrf_200",
        docs: 200
    });
    crate::register_bench!(SearchHybridRrf {
        id: "nornir.mimir.search.hybrid_rrf_1k",
        docs: 1_000
    });

    // REAL-project ingest (nornir's own src/) — the north-star populate+search.
    crate::register_bench!(RealEmbedPopulate {
        id: "nornir.mimir.populate.real_src_embed"
    });
    crate::register_bench!(RealVectorSearch {
        id: "nornir.mimir.search.real_src_vec_knn"
    });
}

#[cfg(test)]
mod search_ab_tests {
    use super::*;
    use crate::bench::api::Bencher;

    /// RED-WHEN-BROKEN: the three SEARCH A/B arms publish a `speedup_x` that only
    /// means anything if the fast index and the linear baseline resolve the SAME
    /// answers. That equality is enforced inside the arm by `ensure!`, so an `Err`
    /// from `run()` IS the failure signal — in every profile. (A `debug_assert!`
    /// is compiled out of the release build the benches actually run in, which is
    /// what these arms used to rely on.) Tiny `keys` keeps this a unit test; the
    /// registered arms sweep 1k…10m.
    #[test]
    fn ragnar_ab_arms_agree_with_the_linear_baseline() {
        for keys in [64usize, 512] {
            let exact = SearchRagnarAtExact {
                id: "nornir.mimir.search.ragnar_at_exact_test",
                keys,
            }
            .run()
            .expect("at_exact must agree with LinearScan");
            let as_of = SearchRagnarAsOf {
                id: "nornir.mimir.search.ragnar_as_of_test",
                keys,
            }
            .run()
            .expect("as_of must agree with LinearScan");
            let bound = SearchRagnarBoundary {
                id: "nornir.mimir.search.ragnar_boundary_test",
                keys,
            }
            .run()
            .expect("boundary_leq must agree with LinearScan");
            for out in [&exact, &as_of, &bound] {
                let ck = out.metrics.get("checksum").and_then(|v| v.as_u64());
                let base = out
                    .metrics
                    .get("baseline_checksum")
                    .and_then(|v| v.as_u64());
                assert_eq!(
                    ck, base,
                    "{}: checksums must match: {:?}",
                    out.name, out.metrics
                );
                assert!(
                    out.metrics
                        .get("lookups_per_sec")
                        .and_then(|v| v.as_f64())
                        .unwrap_or(0.0)
                        > 0.0,
                    "{}: lookups_per_sec must be positive: {:?}",
                    out.name,
                    out.metrics
                );
            }
        }
    }
}

// ╔══════════════════════════════════════════════════════════════════════════╗
// ║  APPENDED REGION — the measurement gaps (2026-07-23)                     ║
// ╚══════════════════════════════════════════════════════════════════════════╝
//
// A deliberately SEPARATE, purely ADDITIVE block: nothing above this line is
// reordered, re-wrapped or re-indented, so this merges cleanly beside the two
// other in-flight edits to this file (the `debug_assert` → `ensure!` promotion
// and the served-path `symbol_lookup` / `callers_of` arms).
//
// What it closes, in the order the audit ranked it:
//
//   1. `mimir analyze duplicates` had **zero** arms — the verb that could not
//      complete (15 min CPU, SIGTERM, zero output) and was then rewritten to
//      route candidate generation through `VectorIndex::pairs_above`, shipped
//      with nothing measuring it. Benched at FIVE sizes at the live dim (768) so
//      the complexity CLASS is visible, not one point (see `comparisons_per_sec`).
//   2. The **Ragnar batch sweep** — `as_of_batch::<P>` / `boundary_leq_batch::<P>`
//      landed with no arm at all. Swept over P at two corpus sizes so P is PICKED
//      from evidence rather than guessed.
//   3. `git_heat::scan_repo` — 0 arms, and it is half of `knowledge::scan_all`.
//   4. Tantivy **build** and **search** — 0 arms. (`hybrid_rrf` does not cover
//      this: both of its queries run OUTSIDE its timed loop, which contains only
//      `fuse_bm25_dense`, so it measures the fusion and not the search.)
//   5. Every embedding arm ran at dim 32 while the live model is 768.
//
// **Every arm below carries a floor that runs in RELEASE** (`anyhow::ensure!`,
// never `debug_assert!` — which is compiled out in release and is exactly why 21
// existing arms check nothing). Where the floor is a THROUGHPUT number it is one
// measured on this machine with a stated margin; where no number was measured the
// arm says so in its doc comment and ships with its correctness floor only.

// ─────────────────── shared fixtures for the appended arms ──────────────────

/// Deterministic, well-spread pseudo-random vector — xorshift, uniform in
/// `[-1, 1)`.
///
/// NOT [`mk_vec`]: that one is a smooth `sin` curve, so two different seeds come
/// out strongly correlated. A duplicate detector's fixture needs the opposite —
/// at `dim = 768` the cosine of two independent draws here is ≈ `N(0, 1/√768)`,
/// so the 0.92 threshold sits ~25σ out and **no unplanted pair can clear it by
/// luck**. That is what makes the exact-count floors below safe to assert.
fn gap_rand_vec(seed: u64, dim: usize) -> Vec<f32> {
    let mut s = seed
        .wrapping_mul(0x9E37_79B9_7F4A_7C15)
        .wrapping_add(0x1234_5678)
        | 1;
    (0..dim)
        .map(|_| {
            s ^= s << 13;
            s ^= s >> 7;
            s ^= s << 17;
            ((s >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0) as f32
        })
        .collect()
}

// ══════════════ ANALYZE: `mimir analyze duplicates` (was: 0 arms) ═══════════

/// ANALYZE — the whole `analyze duplicates` verb at the LIVE embedding width.
///
/// Path: [`analyze_duplicates`] →
/// `candidate_pairs` → `VectorIndex::pairs_above` (exact O(n²) SIMD over
/// pre-normalized rows, fanned out on gatling, deliberately **not**
/// ANN-prefiltered because this report's consumer deletes code and a silently
/// dropped pair is worse than a slower scan).
///
/// The arm times the verb from the `SymbolVec` slice inwards — i.e. everything
/// the rewrite touched — on a fixture with `planted` cross-crate duplicate
/// triangles hidden in `symbols - 3·planted` near-orthogonal decoys.
///
/// ## Why several sizes
/// One point cannot show a complexity class. `comparisons_per_sec`
/// (`n(n-1)/2` divided by the wall) is the class-visible column. MEASURED on this
/// box (Threadripper PRO 3975WX, 32 cores, dim 768, contended, best of 3, via a
/// standalone release harness over the same `VectorIndex::pairs_above`):
///
/// | rows  | comparisons | comparisons/s |
/// |-------|-------------|---------------|
/// | 1 000 | 0.50 M      |  9.2 M        |
/// | 2 000 | 2.0 M       |  8.4 M        |
/// | 4 000 | 8.0 M       | 26.9 M        |
/// | 8 000 | 32.0 M      | 43.0 M        |
/// |16 000 | 128.0 M     | 51.2 M        |
/// |32 000 | 512.0 M     | 57.1 M        |
///
/// So the column is **not** flat: it RISES ~6× from 1k to 16k and then plateaus.
/// That is the gatling fan-out saturating — below a few thousand rows the band
/// scan does not earn its threads — and it is exactly the shape a single-point
/// arm would have hidden. Above ~16k rows the curve is flat, which is what makes
/// it extrapolable: the real warehouse shape (79k × 768, 3.1 G comparisons) lands
/// at ≈ 55 s of phase-1 scan. The verb that used to SIGTERM after 15 min of CPU
/// with zero output now has a number.
///
/// `symbols_per_sec` falls ~linearly by construction and is the operator-facing
/// column.
///
/// ## Floors (all `ensure!`, all machine-INDEPENDENT)
/// * every planted triangle must be FOUND — `cluster_count == planted` and
///   `pair_count >= 3·planted` (recall);
/// * nothing else may be reported — `cluster_count == planted` is also the
///   precision floor: a decoy pair clearing 0.92 would raise the count;
/// * `pairs_above` and the naive loop must agree, which the existing unit test
///   `both_candidate_paths_agree` pins; here the count IS the oracle.
///
/// There is no throughput floor on this arm. The numbers above were measured
/// through a standalone release harness (a full release build of nornir was not
/// affordable in this worktree), so they are not yet a number this arm has
/// produced about ITSELF — and a floor transplanted from a different binary is a
/// rumour with a decimal point. The correctness floors above are the
/// red-when-broken part; the throughput floor is deliberately left for the first
/// run of the arm in its own release build.
struct AnalyzeDuplicates {
    id: &'static str,
    symbols: usize,
    dim: usize,
    /// Cross-crate duplicate triangles planted in the corpus (3 members each).
    planted: usize,
    iters: usize,
}

impl Bencher for AnalyzeDuplicates {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::mimir_analyze::{DupThresholds, SymbolVec, analyze_duplicates};
        use std::collections::BTreeSet;

        anyhow::ensure!(
            self.symbols >= self.planted * 3,
            "fixture is impossible: {} symbols cannot hold {} planted triangles",
            self.symbols,
            self.planted
        );
        let dim = self.dim;
        let mk = |name: String, krate: String, sig: String, callees: &[&str], vector: Vec<f32>| {
            SymbolVec {
                id: format!("synthetic::{krate}::{name}"),
                repo: "synthetic".into(),
                crate_name: krate,
                module_path: "m".into(),
                item_kind: "fn".into(),
                item_name: name,
                file: "src/lib.rs".into(),
                line: 1,
                signature: Some(sig),
                callees: callees.iter().map(|s| (*s).to_string()).collect(),
                item_end_line: 0,
                span_lines: 0,
                chunk_hash: String::new(),
                impl_header: None,
                vector,
            }
        };

        let mut syms: Vec<SymbolVec> = Vec::with_capacity(self.symbols);
        // Planted: three copies of one function in three DIFFERENT crates —
        // identical signature + identical callee set (structural 1.0) and a
        // 0.5%-jittered shared vector (cosine ≈ 0.9999). This is the exact shape
        // `cross_scope_only` + the structural gate are built to keep.
        for c in 0..self.planted {
            let base = gap_rand_vec(0xD00D_0000 + c as u64, dim);
            for m in 0..3usize {
                let jit = gap_rand_vec(0xBEEF_0000 + (c * 3 + m) as u64, dim);
                let v: Vec<f32> = base.iter().zip(&jit).map(|(b, j)| b + 0.005 * j).collect();
                syms.push(mk(
                    format!("dup{c}"),
                    format!("dupcrate_{m}"),
                    format!("fn dup{c}(x: u32) -> u32"),
                    &["helper", "log"],
                    v,
                ));
            }
        }
        // Decoys: near-orthogonal, so none of them may pair with anything.
        for i in 0..(self.symbols - self.planted * 3) {
            syms.push(mk(
                format!("noise{i}"),
                format!("noisecrate_{}", i % 11),
                format!("fn noise{i}(a: u{}) -> ()", 8 + (i % 4) * 8),
                &["misc"],
                gap_rand_vec(0x5EED_0000 + i as u64, dim),
            ));
        }

        let thresholds = DupThresholds::default();
        let canonical: BTreeSet<String> = BTreeSet::new();
        let n = syms.len();
        let comparisons = (n as u64) * (n as u64 - 1) / 2;

        let mut per_iter = Vec::with_capacity(self.iters);
        let mut total = std::time::Duration::ZERO;
        let mut clusters = 0usize;
        let mut pairs = 0usize;
        for _ in 0..self.iters.max(1) {
            let t = std::time::Instant::now();
            let rep = analyze_duplicates(&syms, &thresholds, &canonical);
            let el = t.elapsed();
            total += el;
            per_iter.push(el.as_secs_f64() * 1e6);
            clusters = rep.cluster_count;
            pairs = rep.pair_count;
        }

        // ── RED-WHEN-BROKEN (release too — `ensure!`, not `debug_assert!`) ──
        anyhow::ensure!(
            pairs >= 3 * self.planted,
            "RECALL: only {pairs} duplicate pairs survived, but {} planted triangles \
             carry {} edges — the candidate scan is dropping true duplicates",
            self.planted,
            3 * self.planted
        );
        anyhow::ensure!(
            clusters == self.planted,
            "PRECISION/RECALL: {clusters} clusters reported, {} planted. Decoys here are \
             ~25σ from the 0.92 cosine bar, so any extra cluster means the scan is \
             pairing unrelated symbols, and any missing one means it lost a real copy",
            self.planted
        );

        let secs = total.as_secs_f64().max(1e-9) / self.iters.max(1) as f64;
        let mut m = serde_json::Map::new();
        m.insert("symbols".into(), serde_json::json!(n));
        m.insert("dim".into(), serde_json::json!(dim));
        m.insert("planted_clusters".into(), serde_json::json!(self.planted));
        m.insert("clusters".into(), serde_json::json!(clusters));
        m.insert("pairs".into(), serde_json::json!(pairs));
        m.insert("comparisons".into(), serde_json::json!(comparisons));
        // The CLASS-visible column: flat across sizes ⇒ still an exact quadratic
        // scan with constant per-pair cost. It collapses if the per-pair work grows.
        m.insert(
            "comparisons_per_sec".into(),
            serde_json::json!(comparisons as f64 / secs),
        );
        record(&mut m, "symbols_per_sec", n as u64, per_iter, total);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

// ═══════ SEARCH: the Ragnar BATCH sweep (`as_of_batch` / `boundary_leq_batch`) ═══════

/// `P` is a **const generic**, so the sweep needs one arm per width and a match
/// to get from the runtime field to the type-level parameter. Registering the
/// widths is what makes the sweep a bench sweep rather than a one-off script.
macro_rules! ragnar_as_of_batch {
    ($log:expr, $probes:expr, $p:expr) => {
        match $p {
            1 => $log.as_of_batch::<1>($probes),
            4 => $log.as_of_batch::<4>($probes),
            8 => $log.as_of_batch::<8>($probes),
            16 => $log.as_of_batch::<16>($probes),
            20 => $log.as_of_batch::<20>($probes),
            32 => $log.as_of_batch::<32>($probes),
            64 => $log.as_of_batch::<64>($probes),
            other => anyhow::bail!(
                "batch width P={other} is not one of the registered sweep points \
                 (1, 4, 8, 16, 20, 32, 64) — add it to `ragnar_as_of_batch!`"
            ),
        }
    };
}

macro_rules! ragnar_boundary_batch {
    ($log:expr, $probes:expr, $p:expr) => {
        match $p {
            1 => $log.boundary_leq_batch::<1>($probes),
            4 => $log.boundary_leq_batch::<4>($probes),
            8 => $log.boundary_leq_batch::<8>($probes),
            16 => $log.boundary_leq_batch::<16>($probes),
            20 => $log.boundary_leq_batch::<20>($probes),
            32 => $log.boundary_leq_batch::<32>($probes),
            64 => $log.boundary_leq_batch::<64>($probes),
            other => anyhow::bail!(
                "batch width P={other} is not one of the registered sweep points \
                 (1, 4, 8, 16, 20, 32, 64) — add it to `ragnar_boundary_batch!`"
            ),
        }
    };
}

/// The probe set every batch arm uses: 32 768 floor probes (ODD keys, so each one
/// is a genuine predecessor descent between two stored even keys).
///
/// The count is not arbitrary — it is exactly the working set the existing scalar
/// arms (`nornir.mimir.search.ragnar_*`, 1024 probes × 32 iters) issue **one at a
/// time**, so `lookups_per_sec` here is directly comparable to the 8.1 M/s those
/// arms measured at 10M keys.
fn gap_ragnar_probes(keys: usize, queries: usize) -> Vec<i64> {
    (0..queries as u64)
        .map(|i| ((i.wrapping_mul(2_654_435_761) as usize % keys) as i64) * 2 + 1)
        .collect()
}

/// SEARCH — **batched** Eytzinger floor descent (`RagnarLog::as_of_batch::<P>`)
/// against the scalar `as_of` it replaces, swept over the batch width `P`.
///
/// One floor descent serialises on memory: every level of the tree is a dependent
/// load, so the CPU stalls on a miss it cannot overlap with anything. `P`
/// independent descents advanced in lockstep (with a prefetch of every live
/// lane's next line before any of them branch) turn those stalls into
/// parallelism. `P` is a tuning knob, not a constant of nature — this sweep is
/// how it gets PICKED, at both a cache-resident (1M keys) and a
/// memory-bound (10M keys) corpus size.
///
/// ## Floor (`ensure!`, runs in release)
/// The batched answer must be **identical to the scalar descent for every
/// probe**, checked outside the timed region. A faster wrong answer is the only
/// failure mode this optimisation can have, and it is machine-independent — so it
/// is the floor every arm in the sweep carries regardless of what the host does
/// to the throughput numbers.
///
/// `floor_lookups_per_sec` is an additional, MEASURED throughput floor; `0.0`
/// means the arm ships descriptive (no number was measured for that point).
struct SearchRagnarAsOfBatch {
    id: &'static str,
    keys: usize,
    p: usize,
    queries: usize,
    iters: usize,
    floor_lookups_per_sec: f64,
}

impl Bencher for SearchRagnarAsOfBatch {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::ragnar::RagnarLog;
        let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
        let log = RagnarLog::build(rows);
        let probes = gap_ragnar_probes(self.keys, self.queries);

        // ── CORRECTNESS FLOOR — outside the timed region, `ensure!` so it is
        //    still there in the release build the numbers come from. ──
        {
            let batched = ragnar_as_of_batch!(log, &probes, self.p);
            let scalar: Vec<Option<&u64>> = probes.iter().map(|&q| log.as_of(q)).collect();
            anyhow::ensure!(
                batched.len() == scalar.len(),
                "as_of_batch::<{}> returned {} answers for {} queries",
                self.p,
                batched.len(),
                scalar.len()
            );
            let mismatch = batched
                .iter()
                .zip(&scalar)
                .position(|(a, b)| (*a).copied() != (*b).copied());
            anyhow::ensure!(
                mismatch.is_none(),
                "as_of_batch::<{}> disagrees with the scalar floor descent at probe {} \
                 (key {}) — a faster WRONG answer",
                self.p,
                mismatch.unwrap_or(0),
                probes[mismatch.unwrap_or(0)]
            );
        }

        // Warm both paths, then time them separately.
        let _ = ragnar_as_of_batch!(log, &probes, self.p);
        let mut acc = 0u64;
        let mut per_iter = Vec::with_capacity(self.iters);
        let mut fast = std::time::Duration::ZERO;
        for _ in 0..self.iters {
            let t = std::time::Instant::now();
            let out = ragnar_as_of_batch!(log, &probes, self.p);
            let el = t.elapsed();
            fast += el;
            // Consumed AFTER the timer so the sum is not benched, but consumed at
            // all so the call cannot be optimised away.
            acc = acc.wrapping_add(out.iter().filter_map(|v| v.copied()).sum::<u64>());
            per_iter.push(el.as_secs_f64() * 1e6 / self.queries as f64);
        }

        let mut accb = 0u64;
        let mut slow = std::time::Duration::ZERO;
        for _ in 0..self.iters {
            let t = std::time::Instant::now();
            let mut s = 0u64;
            for &q in &probes {
                s = s.wrapping_add(log.as_of(q).copied().unwrap_or(0));
            }
            slow += t.elapsed();
            accb = accb.wrapping_add(s);
        }
        anyhow::ensure!(
            acc == accb,
            "batched and scalar checksums differ ({acc} vs {accb}) — the two paths \
             resolved different rows"
        );

        let n = (self.iters * self.queries) as u64;
        let per_sec = n as f64 / fast.as_secs_f64().max(1e-9);
        let base_per_sec = n as f64 / slow.as_secs_f64().max(1e-9);
        anyhow::ensure!(
            self.floor_lookups_per_sec <= 0.0 || per_sec >= self.floor_lookups_per_sec,
            "as_of_batch::<{}> at {} keys did {:.2} M lookups/s, below the measured floor \
             of {:.2} M/s",
            self.p,
            self.keys,
            per_sec / 1e6,
            self.floor_lookups_per_sec / 1e6
        );

        let mut m = serde_json::Map::new();
        m.insert("keys".into(), serde_json::json!(self.keys));
        m.insert("batch_p".into(), serde_json::json!(self.p));
        m.insert("queries".into(), serde_json::json!(self.queries));
        m.insert(
            "baseline_lookups_per_sec".into(),
            serde_json::json!(base_per_sec),
        );
        m.insert(
            "speedup_x".into(),
            serde_json::json!(per_sec / base_per_sec.max(1e-9)),
        );
        m.insert("checksum".into(), serde_json::json!(acc));
        record(&mut m, "lookups_per_sec", n, per_iter, fast);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// SEARCH — batched replay-boundary cut (`RagnarLog::boundary_leq_batch::<P>`)
/// against the scalar `boundary_leq`. Same descent, different projection (the
/// index+1 cut rather than the value), so it is registered at the un-pipelined
/// control (P=1) and at the plateau the `as_of_batch` sweep PICKED (P=8) rather
/// than at all seven widths — the curve was measured to be the same shape.
///
/// Floor: identical to the scalar cut for every probe (`ensure!`, release).
struct SearchRagnarBoundaryBatch {
    id: &'static str,
    keys: usize,
    p: usize,
    queries: usize,
    iters: usize,
    floor_lookups_per_sec: f64,
}

impl Bencher for SearchRagnarBoundaryBatch {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::warehouse::ragnar::RagnarLog;
        let rows: Vec<(i64, u64)> = (0..self.keys as i64).map(|k| (k * 2, k as u64)).collect();
        let log = RagnarLog::build(rows);
        let probes = gap_ragnar_probes(self.keys, self.queries);

        {
            let batched = ragnar_boundary_batch!(log, &probes, self.p);
            let scalar: Vec<usize> = probes.iter().map(|&q| log.boundary_leq(q)).collect();
            let mismatch = batched.iter().zip(&scalar).position(|(a, b)| a != b);
            anyhow::ensure!(
                batched.len() == scalar.len() && mismatch.is_none(),
                "boundary_leq_batch::<{}> disagrees with the scalar cut at probe {} — \
                 a faster WRONG replay boundary",
                self.p,
                mismatch.unwrap_or(0)
            );
        }

        let _ = ragnar_boundary_batch!(log, &probes, self.p);
        let mut acc = 0usize;
        let mut per_iter = Vec::with_capacity(self.iters);
        let mut fast = std::time::Duration::ZERO;
        for _ in 0..self.iters {
            let t = std::time::Instant::now();
            let out = ragnar_boundary_batch!(log, &probes, self.p);
            let el = t.elapsed();
            fast += el;
            acc = acc.wrapping_add(out.iter().copied().sum::<usize>());
            per_iter.push(el.as_secs_f64() * 1e6 / self.queries as f64);
        }
        let mut accb = 0usize;
        let mut slow = std::time::Duration::ZERO;
        for _ in 0..self.iters {
            let t = std::time::Instant::now();
            let mut s = 0usize;
            for &q in &probes {
                s = s.wrapping_add(log.boundary_leq(q));
            }
            slow += t.elapsed();
            accb = accb.wrapping_add(s);
        }
        anyhow::ensure!(acc == accb, "batched and scalar boundary checksums differ");

        let n = (self.iters * self.queries) as u64;
        let per_sec = n as f64 / fast.as_secs_f64().max(1e-9);
        let base_per_sec = n as f64 / slow.as_secs_f64().max(1e-9);
        anyhow::ensure!(
            self.floor_lookups_per_sec <= 0.0 || per_sec >= self.floor_lookups_per_sec,
            "boundary_leq_batch::<{}> at {} keys did {:.2} M/s, below the measured floor {:.2} M/s",
            self.p,
            self.keys,
            per_sec / 1e6,
            self.floor_lookups_per_sec / 1e6
        );

        let mut m = serde_json::Map::new();
        m.insert("keys".into(), serde_json::json!(self.keys));
        m.insert("batch_p".into(), serde_json::json!(self.p));
        m.insert("queries".into(), serde_json::json!(self.queries));
        m.insert(
            "baseline_lookups_per_sec".into(),
            serde_json::json!(base_per_sec),
        );
        m.insert(
            "speedup_x".into(),
            serde_json::json!(per_sec / base_per_sec.max(1e-9)),
        );
        m.insert("checksum".into(), serde_json::json!(acc as u64));
        record(&mut m, "lookups_per_sec", n, per_iter, fast);
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

// ═════════════ POPULATE: `git_heat::scan_repo` (was: 0 arms) ════════════════

/// Build a deterministic git repo for the git-heat arm: `files` files, one
/// initial commit that adds them all, then `commits` commits that each touch
/// exactly THREE files, with the author rotating over four identities.
///
/// The shape is what makes the arm's floors exact: `scan_repo` diffs every commit
/// against its first parent (root commit ⇒ its whole tree), so the total
/// commit-touches it must aggregate is exactly `files + 3·commits`.
///
/// Uses the `git` CLI rather than `gix` plumbing on purpose — the fixture is
/// UNTIMED setup, and a hand-rolled object writer here would be a second
/// implementation of commit creation with its own bugs. (`tests/support/mcp_seed.rs`
/// shells `git init` for the same reason.) Paths are named explicitly on every
/// `git add`; nothing here ever runs `git add -A`.
fn gap_git_heat_fixture(dir: &std::path::Path, files: usize, commits: usize) -> Result<()> {
    use std::process::Command;
    const AUTHORS: [(&str, &str); 4] = [
        ("Aslaug", "aslaug@nornir.test"),
        ("Bjorn", "bjorn@nornir.test"),
        ("Cerdic", "cerdic@nornir.test"),
        ("Dagny", "dagny@nornir.test"),
    ];
    let git = |author: usize, args: &[&str]| -> Result<()> {
        let (name, email) = AUTHORS[author % AUTHORS.len()];
        let st = Command::new("git")
            .current_dir(dir)
            .args(["-c", &format!("user.name={name}")])
            .args(["-c", &format!("user.email={email}")])
            .args(["-c", "commit.gpgsign=false"])
            .args(["-c", "init.defaultBranch=main"])
            .args(["-c", "gc.auto=0"])
            .args(args)
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .map_err(|e| anyhow::anyhow!("spawning `git {}`: {e}", args.join(" ")))?;
        anyhow::ensure!(
            st.success(),
            "git {} failed in {}",
            args.join(" "),
            dir.display()
        );
        Ok(())
    };

    git(0, &["init", "-q"])?;
    // FLAT layout on purpose. `scan_repo`'s root-commit branch iterates
    // `tree.iter()` and records `entry.filename()` — the TOP-LEVEL entry names,
    // without recursing — so a nested `src/f0000.rs` would have its seed commit
    // credited to the directory entry `src` instead of to the file. Keeping the
    // fixture flat means the arm's exact commit-touch identity is a statement
    // about the rev-walk, not about that quirk. (The quirk itself is real and is
    // reported separately; an arm should not be permanently red for it.)
    let name = |i: usize| format!("f{i:04}.rs");
    for i in 0..files {
        let p = dir.join(name(i));
        std::fs::write(&p, format!("pub fn f{i}() -> u32 {{ {i} }}\n"))?;
    }
    let all: Vec<String> = (0..files).map(name).collect();
    let mut add: Vec<&str> = vec!["add", "--"];
    add.extend(all.iter().map(|s| s.as_str()));
    git(0, &add)?;
    git(0, &["commit", "-q", "-m", "seed"])?;

    for c in 0..commits {
        let touched: Vec<String> = (0..3).map(|k| name((c * 7 + k * 13) % files)).collect();
        for (k, rel) in touched.iter().enumerate() {
            let p = dir.join(rel);
            let body = std::fs::read_to_string(&p).unwrap_or_default();
            std::fs::write(&p, format!("{body}// edit {c}.{k}\n"))?;
        }
        let mut a: Vec<&str> = vec!["add", "--"];
        a.extend(touched.iter().map(|s| s.as_str()));
        git(c, &a)?;
        git(c, &["commit", "-q", "-m", "edit"])?;
    }
    Ok(())
}

/// POPULATE — `knowledge::git_heat::scan_repo`, the gix rev-walk + per-commit
/// first-parent tree diff that produces the per-file activity heat.
///
/// It is **half of `knowledge::scan_all`** and had no arm at all, so nothing
/// noticed its cost: the walk is `O(commits × tree)` by construction (the module
/// says so itself and defers batching), which means it is the half of `scan_all`
/// whose cost tracks HISTORY DEPTH rather than source size. Two sizes are
/// registered at the same file count so that dependence is visible.
///
/// ## Floors (`ensure!`, all exact and machine-independent)
/// * one row per file — a rev-walk that stops early loses rows;
/// * `Σ commits_total == files + 3·commits` — the fixture touches exactly three
///   files per commit after an initial commit that adds them all, so this is the
///   arithmetic identity a correct first-parent diff must satisfy. A diff that
///   silently drops renames/modes, or a walk that misses commits, breaks it;
/// * `commits_30d == commits_total` on every row — the fixture's commits are all
///   "now", so the recency buckets must contain all of them.
struct PopGitHeatScan {
    id: &'static str,
    files: usize,
    commits: usize,
}

impl Bencher for PopGitHeatScan {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        let dir = tempfile::tempdir()?;
        gap_git_heat_fixture(dir.path(), self.files, self.commits)?;

        let ts = chrono::Utc::now();
        // Warm the page cache / object store, then time the real scan.
        let _ =
            crate::knowledge::git_heat::scan_repo(dir.path(), "bench", uuid::Uuid::new_v4(), ts)?;

        let iters = 3usize;
        let mut per_iter = Vec::with_capacity(iters);
        let mut total = std::time::Duration::ZERO;
        let mut scan = None;
        for _ in 0..iters {
            let t = std::time::Instant::now();
            let s = crate::knowledge::git_heat::scan_repo(
                dir.path(),
                "bench",
                uuid::Uuid::new_v4(),
                ts,
            )?;
            let el = t.elapsed();
            total += el;
            per_iter.push(el.as_secs_f64() * 1e6);
            scan = Some(s);
        }
        let scan = scan.expect("at least one timed iteration");

        anyhow::ensure!(
            scan.files.len() == self.files,
            "git-heat produced {} rows for a {}-file repo — the rev-walk lost files",
            scan.files.len(),
            self.files
        );
        let touches: i64 = scan.files.iter().map(|f| f.commits_total).sum();
        let expected = self.files as i64 + 3 * self.commits as i64;
        anyhow::ensure!(
            touches == expected,
            "git-heat aggregated {touches} commit-touches, expected {expected} \
             ({} files seeded + 3 per each of {} commits) — the first-parent diff is \
             dropping or double-counting changes",
            self.files,
            self.commits
        );
        anyhow::ensure!(
            scan.files
                .iter()
                .all(|f| f.commits_30d == f.commits_total && f.authors_total >= 1),
            "every fixture commit is 'now' and authored, so 30d buckets must equal the \
             totals and every file must name at least one author"
        );

        let commits_walked = (self.commits + 1) as u64;
        let mut m = serde_json::Map::new();
        m.insert("files".into(), serde_json::json!(self.files));
        m.insert("commits".into(), serde_json::json!(commits_walked));
        m.insert("rows".into(), serde_json::json!(scan.files.len()));
        m.insert("commit_touches".into(), serde_json::json!(touches));
        m.insert(
            "commits_per_sec".into(),
            serde_json::json!(
                (commits_walked * iters as u64) as f64 / total.as_secs_f64().max(1e-9)
            ),
        );
        record(
            &mut m,
            "rows_per_sec",
            (scan.files.len() * iters) as u64,
            per_iter,
            total,
        );
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

// ═══════════ POPULATE + SEARCH: tantivy BM25 (was: 0 arms) ══════════════════

/// The lexical-search fixture: `docs` small Rust files, each carrying the shared
/// token `nornir` plus a **unique, single-token** marker `qzmarkerNNNNN`.
///
/// The marker is deliberately alphanumeric-only: tantivy's default tokenizer
/// splits on non-alphanumerics, so `qz_marker_00042` would become three terms and
/// the "exactly one hit" floor below would be a statement about the query parser
/// rather than about the index.
fn gap_tantivy_corpus(ws: &std::path::Path, docs: usize) -> Result<Vec<std::path::PathBuf>> {
    let mut paths = Vec::with_capacity(docs);
    for i in 0..docs {
        let f = ws.join(format!("doc_{i:05}.rs"));
        std::fs::write(
            &f,
            format!(
                "//! module {i} of the nornir lexical bench corpus\n\
                 // marker qzmarker{i:05}\n\
                 pub fn work_{i}(input: u32) -> u32 {{\n    \
                 let scaled = input.wrapping_mul({});\n    \
                 scaled ^ {}\n}}\n",
                i as u32 + 7,
                i as u32 * 31
            ),
        )?;
        paths.push(f);
    }
    Ok(paths)
}

/// POPULATE — tantivy index BUILD (`Index::index_files`): the tokenize + posting
/// + commit path behind `nornir index build` and the docs index.
///
/// Floor (`ensure!`): the build must ADD every document — `added == docs`, with
/// zero errors. A writer that silently skipped documents (oversize guard
/// mis-tuned, upsert keying broken) would still "succeed" and still produce a
/// throughput number; this is what makes that case red.
struct PopTantivyBuild {
    id: &'static str,
    docs: usize,
}

impl Bencher for PopTantivyBuild {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::index::{Corpus, Index};
        let ws = tempfile::tempdir()?;
        let paths = gap_tantivy_corpus(ws.path(), self.docs)?;
        let idx_dir = tempfile::tempdir()?;
        let index = Index::open_at(ws.path(), idx_dir.path())?;

        let t = std::time::Instant::now();
        let stats = index.index_files("bench", Corpus::Code, &paths)?;
        let el = t.elapsed();

        anyhow::ensure!(
            stats.added == self.docs && stats.errors == 0,
            "tantivy build added {} of {} documents ({} errors) — the index is not \
             holding the corpus it was handed",
            stats.added,
            self.docs,
            stats.errors
        );

        let mut m = serde_json::Map::new();
        m.insert("docs".into(), serde_json::json!(self.docs));
        m.insert("added".into(), serde_json::json!(stats.added));
        m.insert(
            "skipped_too_large".into(),
            serde_json::json!(stats.skipped_too_large),
        );
        record(
            &mut m,
            "docs_per_sec",
            self.docs as u64,
            vec![el.as_secs_f64() * 1e6],
            el,
        );
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// SEARCH — tantivy BM25 query latency (`Index::search`), the lexical half of the
/// hybrid retrieval the MCP `search` tool serves.
///
/// This is the arm the suite did not have. `nornir.mimir.search.hybrid_rrf_*`
/// looks like it covers it but does not: **both** of its queries (dense and
/// BM25) run OUTSIDE its timed loop, whose body is only `fuse_bm25_dense` — it
/// measures the fusion, and its recall floors are about the fusion too.
///
/// ## Floor (`ensure!`, machine-independent)
/// Every probe queries a marker token planted in exactly ONE document, so the
/// index must return **exactly one hit, and it must be that document**. A
/// tokenizer change, a lost field, or a scoped-filter regression turns this red
/// instead of quietly returning a fast, empty result set — which is the failure a
/// pure latency arm cannot see.
struct SearchTantivyBm25 {
    id: &'static str,
    docs: usize,
    queries: usize,
}

impl Bencher for SearchTantivyBm25 {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        use crate::index::{Corpus, Index};
        let ws = tempfile::tempdir()?;
        let paths = gap_tantivy_corpus(ws.path(), self.docs)?;
        let idx_dir = tempfile::tempdir()?;
        let index = Index::open_at(ws.path(), idx_dir.path())?;
        index.index_files("bench", Corpus::Code, &paths)?;

        // Probe documents spread across the corpus (not the first N).
        let probes: Vec<usize> = (0..self.queries)
            .map(|q| (q * 2_654_435_761) % self.docs)
            .collect();
        for &d in probes.iter().take(4) {
            let _ = index.search(
                &format!("qzmarker{d:05}"),
                Some(Corpus::Code),
                Some("bench"),
                10,
            )?;
        }

        let mut per_iter = Vec::with_capacity(probes.len());
        let mut hits_total = 0usize;
        let t0 = std::time::Instant::now();
        for &d in &probes {
            let q = format!("qzmarker{d:05}");
            let t = std::time::Instant::now();
            let hits = index.search(&q, Some(Corpus::Code), Some("bench"), 10)?;
            per_iter.push(t.elapsed().as_secs_f64() * 1e6);
            anyhow::ensure!(
                hits.len() == 1 && hits[0].path == format!("doc_{d:05}.rs"),
                "BM25 probe `{q}` returned {} hit(s) {:?} — the marker is planted in \
                 exactly one document, so anything else means the index or the query \
                 path lost it",
                hits.len(),
                hits.iter().map(|h| h.path.clone()).collect::<Vec<_>>()
            );
            hits_total += hits.len();
        }
        let total = t0.elapsed();

        // The corpus-wide token must reach the whole corpus — a `limit`-sized
        // page proves the postings list is not truncated to the probe documents.
        let broad = index.search("nornir", Some(Corpus::Code), Some("bench"), 25)?;
        anyhow::ensure!(
            broad.len() == 25.min(self.docs),
            "the shared token `nornir` returned {} of an expected {} hits",
            broad.len(),
            25.min(self.docs)
        );

        let mut m = serde_json::Map::new();
        m.insert("docs".into(), serde_json::json!(self.docs));
        m.insert("hits".into(), serde_json::json!(hits_total));
        record(
            &mut m,
            "queries_per_sec",
            probes.len() as u64,
            per_iter,
            total,
        );
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

// ─────────────── REGISTRATIONS for the appended region ──────────────────────

// ── ANALYZE: `mimir analyze duplicates` at the LIVE embedding width (768) ──
//
// Sizes chosen so the class is readable: `comparisons_per_sec` should be flat
// across the five, while `symbols_per_sec` falls ~linearly. 4k×768 is the size
// the only existing guard (the `analyze_duplicates_completes_at_scale` unit test,
// 60 s ceiling) uses, so the bench brackets it on both sides; the real warehouse
// is 79k×768, which is 100× the pair count of the 8k arm and is left out
// deliberately — a single arm that takes minutes is not a bench, and the flat
// `comparisons_per_sec` column is what extrapolates to it.
crate::register_bench!(AnalyzeDuplicates {
    id: "nornir.mimir.analyze.duplicates_1k_d768",
    symbols: 1_000,
    dim: 768,
    planted: 8,
    iters: 3
});
crate::register_bench!(AnalyzeDuplicates {
    id: "nornir.mimir.analyze.duplicates_2k_d768",
    symbols: 2_000,
    dim: 768,
    planted: 8,
    iters: 3
});
crate::register_bench!(AnalyzeDuplicates {
    id: "nornir.mimir.analyze.duplicates_4k_d768",
    symbols: 4_000,
    dim: 768,
    planted: 8,
    iters: 2
});
crate::register_bench!(AnalyzeDuplicates {
    id: "nornir.mimir.analyze.duplicates_8k_d768",
    symbols: 8_000,
    dim: 768,
    planted: 8,
    iters: 2
});
crate::register_bench!(AnalyzeDuplicates {
    id: "nornir.mimir.analyze.duplicates_16k_d768",
    symbols: 16_000,
    dim: 768,
    planted: 8,
    iters: 1
});

// ── SEARCH: the Ragnar batch-width (P) sweep ──
//
// 1M keys ≈ the index still fits in cache-friendly reach; 10M keys is where the
// dependent-load stall the batch API exists to hide actually dominates.
//
// ## MEASURED — the sweep result the floors below come from
//
// Threadripper PRO 3975WX, 32 cores, 499 GB RAM, `--release`, 32 768 DISTINCT
// probes, best of 7 interleaved reps, machine CONTENDED (load average 9–27
// throughout; several agents compiling). Standalone harness linking the same
// `znippy_zoomies::stree_log::RagnarLog` — a full release build of nornir was not
// affordable on this disk, so the arms below carry that harness's numbers as
// their floors with a 2× margin, not as their expected values.
//
// | P  | 1M keys M/s (best/worst) | 10M keys M/s (best/worst) | 10M ×scalar |
// |----|--------------------------|---------------------------|-------------|
// | -  | scalar 7.12 / 3.94       | scalar 3.20 / 3.09        | 1.00        |
// | 1  | 7.15 / 4.52              | 3.15 / 3.00               | 0.98        |
// | 4  | 14.47 / 11.42            | 6.43 / 6.02               | 2.01        |
// | 8  | **14.59 / 11.87**        | **6.63 / 6.43**           | **2.07**    |
// | 16 | 12.44 / 9.98             | 6.02 / 5.75               | 1.88        |
// | 20 | 11.89 / 9.03             | 5.64 / 5.49               | 1.76        |
// | 32 | 12.55 / 9.42             | 5.84 / 5.35               | 1.82        |
// | 64 | 14.29 / 6.82             | 6.23 / 5.35               | 1.94        |
//
// **The plateau is P = 8, at both sizes** — not ~20. P=4 is within 3 % of it;
// P=16–32 is 9–15 % WORSE than P=8 and P=20 is the worst point of the whole
// sweep above 1. The dip-then-partial-recovery shape (worst at 20, better again
// at 64) is reproducible across all 7 reps at 10M, where best and worst are
// within 3 % of each other, so it is not contention noise. The likely cause is
// register pressure: `floor_index_batch` keeps `[usize; P]` + `[u32; P]` live
// across the descent, which fits the register file at P≤8 and spills beyond it —
// stated as the plausible mechanism, not as a measured one (no PMU counters were
// taken).
//
// P=1 is at or BELOW the scalar descent (0.98–1.00×): the pipeline's prefetch and
// lane bookkeeping cost something and there is nothing to overlap it with. That
// is the control that proves the 2× at P=8 is the overlap and not the rewrite.
//
// ### A caveat about the 8.1 M/s the EXISTING scalar arms report at 10M keys
// It is ~2.2× optimistic. Those arms replay ONE 1024-probe set 32 times, so after
// the first pass the touched tree paths are cache-resident. Measured here on the
// same box: 1024×32 replay → 7.14 M/s at 10M keys; 32 768 distinct probes →
// 3.32 M/s. The batch arms below therefore use distinct probes, and their
// `baseline_lookups_per_sec` column is NOT comparable to the older arms'.
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_1m_p1",
    keys: 1_000_000,
    p: 1,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 2.2e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_1m_p4",
    keys: 1_000_000,
    p: 4,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 5.7e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_1m_p8",
    keys: 1_000_000,
    p: 8,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 5.9e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_1m_p16",
    keys: 1_000_000,
    p: 16,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 4.9e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_1m_p20",
    keys: 1_000_000,
    p: 20,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 4.5e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_1m_p32",
    keys: 1_000_000,
    p: 32,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 4.7e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_1m_p64",
    keys: 1_000_000,
    p: 64,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 3.4e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_10m_p1",
    keys: 10_000_000,
    p: 1,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 1.5e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_10m_p4",
    keys: 10_000_000,
    p: 4,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 3.0e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_10m_p8",
    keys: 10_000_000,
    p: 8,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 3.2e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_10m_p16",
    keys: 10_000_000,
    p: 16,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 2.8e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_10m_p20",
    keys: 10_000_000,
    p: 20,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 2.7e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_10m_p32",
    keys: 10_000_000,
    p: 32,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 2.6e6
});
crate::register_bench!(SearchRagnarAsOfBatch {
    id: "nornir.mimir.search.ragnar_as_of_batch_10m_p64",
    keys: 10_000_000,
    p: 64,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 2.6e6
});

// The other half of the new batch API, at P=1 (the un-pipelined control) and at
// P=8 — the width the sweep above PICKED. Measured the same way and in the same
// run: 1M P=1 7.75/4.60 M/s, P=8 15.85/12.32 M/s; 10M P=1 3.17/3.03 M/s,
// P=8 6.79/6.44 M/s. Note P=1 is 15 % BELOW the scalar `boundary_leq` here, the
// same "pipeline with nothing to overlap" control as above.
crate::register_bench!(SearchRagnarBoundaryBatch {
    id: "nornir.mimir.search.ragnar_boundary_batch_1m_p1",
    keys: 1_000_000,
    p: 1,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 2.3e6
});
crate::register_bench!(SearchRagnarBoundaryBatch {
    id: "nornir.mimir.search.ragnar_boundary_batch_1m_p8",
    keys: 1_000_000,
    p: 8,
    queries: 32_768,
    iters: 16,
    floor_lookups_per_sec: 6.1e6
});
crate::register_bench!(SearchRagnarBoundaryBatch {
    id: "nornir.mimir.search.ragnar_boundary_batch_10m_p1",
    keys: 10_000_000,
    p: 1,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 1.5e6
});
crate::register_bench!(SearchRagnarBoundaryBatch {
    id: "nornir.mimir.search.ragnar_boundary_batch_10m_p8",
    keys: 10_000_000,
    p: 8,
    queries: 32_768,
    iters: 8,
    floor_lookups_per_sec: 3.2e6
});

// ── POPULATE: git-heat (same file count, 4× the history depth) ──
crate::register_bench!(PopGitHeatScan {
    id: "nornir.mimir.populate.git_heat_100c",
    files: 40,
    commits: 100
});
crate::register_bench!(PopGitHeatScan {
    id: "nornir.mimir.populate.git_heat_400c",
    files: 40,
    commits: 400
});

// ═══ POPULATE: per-member step PARALLELIZATION (sequential vs flattened gatling) ═══
//
// These arms measure the 2026-07-25 fix: populate steps that were written as a
// per-member `for` loop (one repo at a time, 31 idle cores) moved into ONE
// flattened znippy-zoomies gatling pool over the members. Each arm times the SAME
// work BOTH ways in one run and records `speedup = seq_ms / par_ms` — the honest
// before/after the fix is about. Correctness is asserted (parallel output MUST
// equal the sequential output); the speedup floor only bites on a genuinely
// multi-core host (`available_parallelism >= 4`) and NEVER during the single-core
// (`_st`) pass (where both passes share one core, so speedup is ~1 by design).

/// Time the step-1c git-heat scan across `members` repos SEQUENTIALLY (the old
/// per-member loop) vs in ONE flattened gatling pool (the fix). git-heat is a real
/// per-repo gix rev-walk + first-parent diff, so `members` independent walks are
/// exactly the kind of work the pool saturates every core with.
struct PopGitHeatMembersParallel {
    id: &'static str,
    members: usize,
    files: usize,
    commits: usize,
}
impl Bencher for PopGitHeatMembersParallel {
    fn id(&self) -> &'static str {
        self.id
    }
    fn run(&self) -> Result<BenchResult> {
        let root = tempfile::tempdir()?;
        let mut repos: Vec<(String, std::path::PathBuf)> = Vec::with_capacity(self.members);
        for m in 0..self.members {
            let d = root.path().join(format!("m{m}"));
            std::fs::create_dir_all(&d)?;
            gap_git_heat_fixture(&d, self.files, self.commits)?;
            repos.push((format!("m{m}"), d));
        }
        let now = chrono::Utc::now();
        let scan = |name: &str, root: &std::path::Path| {
            crate::knowledge::git_heat::scan_repo(root, name, uuid::Uuid::new_v4(), now)
        };

        // Warm the object store / page cache once (parallel) so neither timed pass
        // pays the cold-read tax the other avoids.
        let _warm: Vec<_> = znippy_zoomies::gatling_forkjoin::gatling_map_balanced(
            &repos,
            0,
            1,
            |_| 1u64,
            |_, (n, p)| scan(n, p),
        );

        // SEQUENTIAL — the old one-repo-at-a-time loop.
        let t = std::time::Instant::now();
        let seq: Vec<_> = repos.iter().map(|(n, p)| scan(n, p)).collect();
        let seq_ms = t.elapsed().as_secs_f64() * 1000.0;

        // PARALLEL — the flattened gatling pool the fix uses.
        let t = std::time::Instant::now();
        let par: Vec<_> = znippy_zoomies::gatling_forkjoin::gatling_map_balanced(
            &repos,
            0,
            1,
            |_| 1u64,
            |_, (n, p)| scan(n, p),
        );
        let par_ms = t.elapsed().as_secs_f64() * 1000.0;

        // CORRECTNESS: the pool must produce byte-identical per-member row counts.
        let mut total_rows = 0usize;
        for (s, p) in seq.iter().zip(par.iter()) {
            let s = s
                .as_ref()
                .map_err(|e| anyhow::anyhow!("seq git-heat: {e:#}"))?;
            let p = p
                .as_ref()
                .map_err(|e| anyhow::anyhow!("par git-heat: {e:#}"))?;
            anyhow::ensure!(
                s.files.len() == p.files.len(),
                "parallel git-heat produced {} rows, sequential {} — not identical",
                p.files.len(),
                s.files.len()
            );
            total_rows += p.files.len();
        }

        let speedup = if par_ms > 0.0 { seq_ms / par_ms } else { 0.0 };
        speedup_ok(speedup, self.members)?;

        let mut m = serde_json::Map::new();
        m.insert("members".into(), serde_json::json!(self.members));
        m.insert("rows".into(), serde_json::json!(total_rows));
        m.insert("seq_ms".into(), serde_json::json!(seq_ms));
        m.insert("par_ms".into(), serde_json::json!(par_ms));
        m.insert("speedup".into(), serde_json::json!(speedup));
        m.insert("cores".into(), serde_json::json!(cores_now()));
        Ok(BenchResult {
            name: self.id.into(),
            metrics: m,
            ..Default::default()
        })
    }
}

/// Machine core count for the bench envelope's `cores` metric.
fn cores_now() -> usize {
    std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(1)
}

/// RED-when-broken speedup floor for a parallelization arm — but ONLY where a
/// speedup is actually expected: skip it during the single-core (`_st`) pass (both
/// timed passes share one pinned core, so speedup is ~1 by design) and on hosts
/// with fewer than 4 cores (a 2-core CI can't show a 4× fan). Where it does apply,
/// a step that silently reverted to a serial loop (speedup ≈ 1) turns the arm RED.
fn speedup_ok(speedup: f64, members: usize) -> Result<()> {
    let cores = cores_now();
    if crate::bench::api::pinned_to_single_core() || cores < 4 || members < 2 {
        return Ok(());
    }
    anyhow::ensure!(
        speedup > 1.2,
        "parallelized populate step showed NO speedup (seq/par = {speedup:.2}× on {cores} cores) \
         — the step likely reverted to a serial per-member loop"
    );
    Ok(())
}

// ── POPULATE: per-member step parallelization (seq vs flattened gatling) ──
crate::register_bench!(PopGitHeatMembersParallel {
    id: "nornir.mimir.populate.git_heat_members_par",
    members: 16,
    files: 40,
    commits: 60
});

// ── POPULATE + SEARCH: tantivy BM25 ──
crate::register_bench!(PopTantivyBuild {
    id: "nornir.mimir.populate.tantivy_build_500",
    docs: 500
});
crate::register_bench!(PopTantivyBuild {
    id: "nornir.mimir.populate.tantivy_build_2k",
    docs: 2_000
});
crate::register_bench!(SearchTantivyBm25 {
    id: "nornir.mimir.search.tantivy_bm25_500",
    docs: 500,
    queries: 200
});
crate::register_bench!(SearchTantivyBm25 {
    id: "nornir.mimir.search.tantivy_bm25_2k",
    docs: 2_000,
    queries: 200
});

// ── The 768-dim embedding arms ──
//
// Every embedding arm in the suite above runs at **dim 32** while the live model
// (`jina-v2-base-code`) emits **768**. A 24× narrower row changes which resource
// the kernel is bound by — 32 floats is one cache line, 768 is twelve — so those
// arms cannot be read as statements about the shipped path. These re-register the
// SAME benchers at the live width; the dim-32 ones stay as the narrow-row
// control. Row counts are trimmed where 768 floats × rows would otherwise make
// the fixture the dominant cost.
#[cfg(feature = "vector")]
mod vec_registrations_dim768 {
    use super::vec_benches::*;

    crate::register_bench!(PopEmbeddingIndex {
        id: "nornir.mimir.populate.embeddings_1k_d768",
        chunks: 1_000,
        dim: 768
    });
    crate::register_bench!(PopEmbeddingIndex {
        id: "nornir.mimir.populate.embeddings_5k_d768",
        chunks: 5_000,
        dim: 768
    });
    crate::register_bench!(PopEmbeddingIndex {
        id: "nornir.mimir.populate.embeddings_20k_d768",
        chunks: 20_000,
        dim: 768
    });

    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_1k_d768",
        rows: 1_000,
        dim: 768
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_10k_d768",
        rows: 10_000,
        dim: 768
    });
    crate::register_bench!(PopVannBuild {
        id: "nornir.mimir.populate.vann_build_100k_d768",
        rows: 100_000,
        dim: 768
    });

    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_k10_d768",
        rows: 20_000,
        dim: 768,
        queries: 64,
        k: 10
    });
    crate::register_bench!(SearchVectorKnnSingle {
        id: "nornir.mimir.search.vec_knn_k50_d768",
        rows: 20_000,
        dim: 768,
        queries: 64,
        k: 50
    });
}

#[cfg(test)]
mod gap_arm_tests {
    use super::*;
    use crate::bench::api::Bencher;

    /// RED-WHEN-BROKEN: the duplicate-analysis arm must actually FIND the copies
    /// it planted. This runs the smallest registered shape, so a regression in
    /// `candidate_pairs` / `pairs_above` fails in `cargo test` and not only when
    /// somebody runs the bench suite.
    #[test]
    fn analyze_duplicates_arm_finds_its_planted_clusters() {
        let out = AnalyzeDuplicates {
            id: "nornir.mimir.analyze.duplicates_test",
            symbols: 400,
            dim: 768,
            planted: 4,
            iters: 1,
        }
        .run()
        .expect("the arm's own ensure! floors must hold");
        assert_eq!(
            out.metrics.get("clusters").and_then(|v| v.as_u64()),
            Some(4)
        );
        assert!(
            out.metrics
                .get("pairs")
                .and_then(|v| v.as_u64())
                .unwrap_or(0)
                >= 12
        );
        assert!(
            out.metrics
                .get("comparisons_per_sec")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0)
                > 0.0
        );
    }

    /// The batch floor descent must agree with the scalar one at EVERY registered
    /// width. This is the arm's own `ensure!` run over the whole sweep at a size
    /// small enough for a unit test — so "a faster wrong answer" cannot reach the
    /// bench suite unnoticed.
    #[test]
    fn ragnar_batch_widths_all_agree_with_the_scalar_descent() {
        for p in [1usize, 4, 8, 16, 20, 32, 64] {
            SearchRagnarAsOfBatch {
                id: "nornir.mimir.search.ragnar_as_of_batch_test",
                keys: 50_000,
                p,
                queries: 4_096,
                iters: 1,
                floor_lookups_per_sec: 0.0,
            }
            .run()
            .unwrap_or_else(|e| panic!("as_of_batch::<{p}> arm failed: {e}"));
            SearchRagnarBoundaryBatch {
                id: "nornir.mimir.search.ragnar_boundary_batch_test",
                keys: 50_000,
                p,
                queries: 4_096,
                iters: 1,
                floor_lookups_per_sec: 0.0,
            }
            .run()
            .unwrap_or_else(|e| panic!("boundary_leq_batch::<{p}> arm failed: {e}"));
        }
    }

    /// An unregistered width must ERROR, never silently fall back to some other
    /// `P` — a sweep whose points quietly collapse onto one width measures one
    /// thing and reports seven.
    #[test]
    fn an_unswept_batch_width_is_an_error_not_a_silent_fallback() {
        let err = SearchRagnarAsOfBatch {
            id: "nornir.mimir.search.ragnar_as_of_batch_test",
            keys: 1_000,
            p: 7,
            queries: 64,
            iters: 1,
            floor_lookups_per_sec: 0.0,
        }
        .run()
        .expect_err("P=7 is not a registered sweep point");
        assert!(
            err.to_string()
                .contains("not one of the registered sweep points"),
            "{err}"
        );
    }

    /// RED-WHEN-BROKEN: the git-heat arm's exact commit-touch identity. The
    /// fixture is tiny here; the point is that the arithmetic floor holds against
    /// the real `scan_repo`, not that it is fast.

The file goes on; the console shows all of it.