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

nordisk/nornir

src/vector/hybrid.rs

//! Hybrid ranking: the GPU's dense semantic score combined with the SCIP call
//! graph's exact, resolved relationality.
//!
//! `score = α · dense + β · graph_distance`
//!
//! # Why the graph term is a DISTANCE and not a proximity
//!
//! The instinct is that two symbols close together in the call graph belong
//! together, so proximity should raise the score. For *retrieval* ("show me code
//! near this") that is right. For the question this repo actually asks — *is
//! this a clone family?* — it is exactly backwards, and `mimir_clones` already
//! says why: a clone pair must be *"different signature tokens, different callee
//! neighbourhood"*. Two functions that call one another are collaborators, not
//! re-implementations.
//!
//! And they embed *very* similarly. A thin wrapper `fn save(x) { save_impl(x) }`
//! shares nearly every token with its callee, so it scores a HIGHER cosine than
//! a genuine re-implementation of the same idea living in another crate. Dense
//! similarity alone therefore ranks the least interesting pair in the corpus
//! first. That is the failure the graph term exists to fix, and it is the one
//! [`calibrate`] is measured against.
//!
//! # Why this module has no GPU in it
//!
//! It is a pure fold over `(dense score, hop count)`. The GPU produced the dense
//! half and a warehouse table carries it (`knn_neighbors`); the call graph half
//! comes from `call_edges`, the table eleven lenses already read. Both arrive as
//! DATA. Nothing here opens a device, so every number below is reproducible on a
//! headless runner with no warehouse — which is the same property that makes the
//! lens folds testable, kept deliberately.
//!
//! # What the calibration actually established, and what it did NOT
//!
//! MEASURED on [`calibration_fixture`] (8 true families, 8 wrapper decoys, 16
//! noise pairs), 21 grid points, `HOP_CAP = 4`:
//!
//! ```text
//! alpha = 0.95, beta = 0.05, AP 1.0000 against 0.3371 dense-only (lift 2.97x)
//! curve: (0.00, 0.337) (0.05, 1.000) (0.10, 1.000) ... (1.00, 1.000)
//! ```
//!
//! Read that honestly. It establishes two things and refuses a third:
//!
//! * **Established:** dense similarity alone is badly wrong on this shape —
//!   AP 0.337, because all eight wrappers outrank all eight true families.
//! * **Established:** the graph term fixes it completely, and nearly three times
//!   over. `beta > 0` is not a preference, it is the difference between a
//!   ranking and a wrong ranking.
//! * **NOT established: the value of beta.** Every grid point from 0.05 upward
//!   scores a perfect 1.0, so the sweep reports `beta = 0.05` only because
//!   [`calibrate`] breaks ties toward the smallest beta. The fixture separates
//!   its classes perfectly, so it can pin the SIGN of beta and not its size.
//!
//! Narrowing beta needs a corpus with *graded* overlap — pairs that are somewhat
//! similar and somewhat far — and that means the real `deps` embeddings.
//! Measured 2026-08-25: no such corpus exists on this disk. The only warehouse
//! carrying an `embeddings` table is a 2026-07-23 nordisk backup (123 MB), and
//! `repo = "deps"` has no embeddings at all. So this module ships the calibration
//! HARNESS and the one result the available data supports; running [`calibrate`]
//! against real `knn_neighbors` rows is what pins the number, and the harness is
//! the same either way.
//!
//! Cargo feature: `vector`.

use std::collections::{BTreeSet, HashMap, VecDeque};

/// Hops beyond which two symbols are "unrelated as far as this ranking cares".
///
/// Not a tuning knob so much as a statement about what the call graph can say.
/// Past four hops nearly everything in a crate reaches nearly everything else,
/// so the distance stops discriminating and only costs BFS. Callers that want a
/// different horizon pass it to [`CallGraph::hops`]; this is the default the
/// calibration was run at.
pub const HOP_CAP: u32 = 4;

/// The undirected call graph, built once from `call_edges` and queried many
/// times.
///
/// Undirected on purpose: for "are these two independent?" it does not matter
/// which of a caller/callee pair is which — a wrapper is equally uninteresting
/// read from either end.
#[derive(Debug, Default, Clone)]
pub struct CallGraph {
    adj: HashMap<String, BTreeSet<String>>,
}

impl CallGraph {
    /// Build from `(caller_path, callee_ident)` pairs — the two columns
    /// `call_edges` carries and every lens already reads.
    pub fn from_edges<I, A, B>(edges: I) -> Self
    where
        I: IntoIterator<Item = (A, B)>,
        A: Into<String>,
        B: Into<String>,
    {
        let mut adj: HashMap<String, BTreeSet<String>> = HashMap::new();
        for (a, b) in edges {
            let (a, b) = (a.into(), b.into());
            if a == b {
                continue; // a self-recursive fn is not "near" anything
            }
            adj.entry(a.clone()).or_default().insert(b.clone());
            adj.entry(b).or_default().insert(a);
        }
        Self { adj }
    }

    /// Number of symbols the graph knows. A zero here is a MISSING PRODUCER —
    /// `call_edges` did not land — and callers must say so rather than reporting
    /// that everything is maximally distant.
    pub fn len(&self) -> usize {
        self.adj.len()
    }

    /// `true` when no edges were supplied at all.
    pub fn is_empty(&self) -> bool {
        self.adj.is_empty()
    }

    /// Shortest undirected hop count from `a` to `b`, or `None` when they are
    /// further apart than `cap` (or one of them is not in the graph).
    ///
    /// `None` and `Some(cap)` are deliberately different answers: "not reachable
    /// within the horizon" and "exactly at the horizon" mean the same thing for
    /// the score, but only one of them means the symbol was *seen*.
    pub fn hops(&self, a: &str, b: &str, cap: u32) -> Option<u32> {
        if a == b {
            return Some(0);
        }
        if !self.adj.contains_key(a) || !self.adj.contains_key(b) {
            return None;
        }
        let mut seen: BTreeSet<&str> = BTreeSet::new();
        seen.insert(a);
        let mut q: VecDeque<(&str, u32)> = VecDeque::new();
        q.push_back((a, 0));
        while let Some((cur, d)) = q.pop_front() {
            if d >= cap {
                continue;
            }
            let Some(ns) = self.adj.get(cur) else { continue };
            for n in ns {
                if n == b {
                    return Some(d + 1);
                }
                if seen.insert(n.as_str()) {
                    q.push_back((n.as_str(), d + 1));
                }
            }
        }
        None
    }

    /// The graph term in `0.0..=1.0`: `0.0` for the same symbol, `1.0` for
    /// "unrelated within the horizon". Monotone in the hop count, so a pair that
    /// moves further apart never scores lower.
    pub fn distance(&self, a: &str, b: &str, cap: u32) -> f32 {
        debug_assert!(cap > 0);
        match self.hops(a, b, cap) {
            Some(h) => (h.min(cap) as f32) / (cap as f32),
            None => 1.0,
        }
    }
}

/// One candidate pair, ready to rank.
#[derive(Debug, Clone, PartialEq)]
pub struct Candidate {
    /// Query symbol FQN.
    pub src: String,
    /// Neighbour symbol FQN.
    pub dst: String,
    /// The dense (int8 cosine) score, as `knn_neighbors.cosine` carries it.
    pub dense: f32,
    /// The graph term from [`CallGraph::distance`], in `0.0..=1.0`.
    pub graph: f32,
}

/// `α · dense + β · graph`.
///
/// `dense` is clamped into `0.0..=1.0` first: a cosine can be negative, and a
/// negative dense term would let a large `β` promote a pair that is *dissimilar*
/// and far — a nonsense finding with a plausible score.
pub fn hybrid_score(c: &Candidate, alpha: f32, beta: f32) -> f32 {
    alpha * c.dense.clamp(0.0, 1.0) + beta * c.graph.clamp(0.0, 1.0)
}

/// Rank `cands` by the hybrid score, descending; ties break on `(src, dst)` so
/// the order is deterministic and does not depend on input order.
///
/// The tie-break matters more than it looks: `α = 1, β = 0` on a corpus of
/// near-identical wrappers produces many exact ties, and an unstable order there
/// would make the calibration below measure the sort's mood.
pub fn rank(cands: &[Candidate], alpha: f32, beta: f32) -> Vec<(usize, f32)> {
    let mut v: Vec<(usize, f32)> = cands
        .iter()
        .enumerate()
        .map(|(i, c)| (i, hybrid_score(c, alpha, beta)))
        .collect();
    v.sort_by(|x, y| {
        y.1.total_cmp(&x.1)
            .then_with(|| cands[x.0].src.cmp(&cands[y.0].src))
            .then_with(|| cands[x.0].dst.cmp(&cands[y.0].dst))
    });
    v
}

/// Average precision of a ranking against a labelled truth set.
///
/// `truth[i]` says whether `cands[i]` is a genuine finding. AP rewards putting
/// every true pair above every false one and is insensitive to how many
/// candidates trail behind — which is what a "page one" ranking is judged on.
///
/// Returns `None` when there is nothing to be right about (no positives): a
/// vacuous `1.0` there would let a broken ranker look perfect on empty input.
pub fn average_precision(order: &[(usize, f32)], truth: &[bool]) -> Option<f32> {
    let total = truth.iter().filter(|t| **t).count();
    if total == 0 {
        return None;
    }
    let mut hits = 0usize;
    let mut acc = 0.0f32;
    for (rank0, (idx, _)) in order.iter().enumerate() {
        if truth[*idx] {
            hits += 1;
            acc += hits as f32 / (rank0 + 1) as f32;
        }
    }
    Some(acc / total as f32)
}

/// What a calibration sweep concluded.
#[derive(Debug, Clone, PartialEq)]
pub struct Calibration {
    /// Best `α` found (weights are normalised so `α + β == 1`).
    pub alpha: f32,
    /// Best `β`.
    pub beta: f32,
    /// Average precision at `(α, β)`.
    pub ap: f32,
    /// Average precision at `β = 0` — dense alone, the thing being improved on.
    pub ap_dense_only: f32,
    /// Every `(β, ap)` the sweep visited, in β order. Kept so a caller can see
    /// whether the optimum is a broad plateau or a single lucky point; a peak one
    /// grid step wide is overfitting, not a finding.
    pub curve: Vec<(f32, f32)>,
}

impl Calibration {
    /// How much the graph term actually bought, as a ratio. `1.0` means the
    /// graph term contributed nothing — worth reporting as loudly as a win.
    pub fn lift(&self) -> f32 {
        if self.ap_dense_only <= 0.0 {
            return f32::INFINITY;
        }
        self.ap / self.ap_dense_only
    }
}

/// Sweep `β` over `0.0..=1.0` with `α = 1 − β` and pick the weighting that
/// maximises average precision on a LABELLED candidate set.
///
/// Only the ratio of the two weights changes the ORDER a ranking produces
/// (scaling both scales every score identically), so a one-dimensional sweep on
/// the simplex covers the whole family — a two-dimensional grid over `(α, β)`
/// would re-measure the same rankings many times and report a false precision.
///
/// `steps` grid points are visited. Ties go to the SMALLEST β, so the answer is
/// the least graph-dependent weighting that achieves the best score: if the
/// graph term buys nothing, the calibration says `β = 0` rather than picking a
/// number that merely does no harm.
///
/// Returns `None` when `truth` has no positive labels — there is nothing to
/// calibrate against, and any returned weights would be arbitrary.
pub fn calibrate(cands: &[Candidate], truth: &[bool], steps: usize) -> Option<Calibration> {
    assert_eq!(cands.len(), truth.len(), "one label per candidate");
    let steps = steps.max(2);
    let mut curve = Vec::with_capacity(steps);
    let mut best: Option<(f32, f32)> = None;
    for s in 0..steps {
        let beta = s as f32 / (steps - 1) as f32;
        let alpha = 1.0 - beta;
        let ap = average_precision(&rank(cands, alpha, beta), truth)?;
        curve.push((beta, ap));
        // Strictly-greater keeps the smallest beta on a tie.
        if best.is_none_or(|(_, b_ap)| ap > b_ap) {
            best = Some((beta, ap));
        }
    }
    let (beta, ap) = best?;
    let ap_dense_only = curve.first().map(|c| c.1).unwrap_or(0.0);
    Some(Calibration { alpha: 1.0 - beta, beta, ap, ap_dense_only, curve })
}

/// Attach a graph distance to each dense neighbour, producing rankable
/// candidates.
///
/// `neighbors` is `(src, dst, cosine)` — exactly the three `knn_neighbors`
/// columns that matter here, so the GPU's table plugs straight in.
pub fn candidates<I, A, B>(graph: &CallGraph, neighbors: I, cap: u32) -> Vec<Candidate>
where
    I: IntoIterator<Item = (A, B, f32)>,
    A: Into<String>,
    B: Into<String>,
{
    neighbors
        .into_iter()
        .map(|(a, b, dense)| {
            let (src, dst) = (a.into(), b.into());
            let graph_d = graph.distance(&src, &dst, cap);
            Candidate { src, dst, dense, graph: graph_d }
        })
        .collect()
}

/// A one-line evidence note in the same voice the lenses use.
///
/// An empty call graph is a MISSING PRODUCER — `call_edges` did not land — and
/// with no edges every pair scores `graph = 1.0`, which makes the graph term a
/// constant and the hybrid ranking silently identical to dense-only. That is the
/// exact shape of a green nobody has seen red, so it gets its own sentence.
pub fn evidence_note(graph: &CallGraph, cands: &[Candidate], cal: &Calibration) -> String {
    if graph.is_empty() {
        return format!(
            "hybrid ranking: MISSING FEEDS — call_edges carried NO rows, so every pair scores \
             graph = 1.0 and the graph term is a CONSTANT. This ranking is dense-only whatever \
             beta says; that is a missing producer, not an absence of relationality."
        );
    }
    let spread = {
        let mut lo = f32::INFINITY;
        let mut hi = f32::NEG_INFINITY;
        for c in cands {
            lo = lo.min(c.graph);
            hi = hi.max(c.graph);
        }
        hi - lo
    };
    if spread <= 0.0 {
        return format!(
            "hybrid ranking: the graph term is CONSTANT across all {} candidates ({} symbols in \
             the graph), so beta cannot change the order. Calibrated beta = {:.2} is not \
             evidence the graph helps.",
            cands.len(),
            graph.len(),
            cal.beta
        );
    }
    format!(
        "hybrid ranking over {} candidates and {} graphed symbols: alpha = {:.2}, beta = {:.2}, \
         AP {:.4} against {:.4} dense-only (lift {:.2}x).",
        cands.len(),
        graph.len(),
        cal.alpha,
        cal.beta,
        cal.ap,
        cal.ap_dense_only,
        cal.lift()
    )
}

/// The calibration fixture, as a function rather than a literal so the shape it
/// encodes is stated once and can be re-run at other sizes.
///
/// It models the ONE failure dense similarity has on this corpus:
///
/// * `n_true` genuine clone families — a re-implementation of the same idea in
///   another crate. High cosine (`0.95`), and **no path** through the call
///   graph, because independent re-implementations do not call each other.
/// * `n_decoy` wrapper pairs — `fn save(x) { save_impl(x) }`. Cosine
///   **HIGHER** (`0.97`) than the true clones, because a wrapper shares nearly
///   every token with its callee, and **one hop** apart.
/// * `n_noise` unrelated pairs: low cosine, far apart.
///
/// The decoys outscoring the true findings is not a pessimistic assumption — it
/// is the documented reason `mimir_clones` ANDs its cosine floor with a
/// structural-disagreement test rather than ranking on cosine alone.
pub fn calibration_fixture(
    n_true: usize,
    n_decoy: usize,
    n_noise: usize,
) -> (CallGraph, Vec<(String, String, f32)>, Vec<bool>) {
    let mut edges: Vec<(String, String)> = Vec::new();
    let mut neighbors: Vec<(String, String, f32)> = Vec::new();
    let mut truth: Vec<bool> = Vec::new();

    // Names must NOT encode the label. An earlier version of this fixture called
    // the true pairs `alpha_crate::…`, the decoys `mid_crate::…` and the noise
    // `noise_crate::…`, which sorts every true pair above every false one. At
    // beta = 1 the graph term goes constant, every score ties, and `rank`'s
    // alphabetical tie-break then produced a PERFECT ranking out of the naming
    // alone — so `the_graph_term_fixes_a_ranking…` passed even with
    // `CallGraph::distance` sabotaged to return a constant. The guard was green
    // for a reason that had nothing to do with the graph.
    //
    // Interleaving the classes across one shared crate namespace removes the
    // leak: alphabetical order now alternates true/false/false, which is close to
    // the worst possible ranking, so a tie can never masquerade as a result.
    let n = n_true.max(n_decoy).max(n_noise);
    let slot = |i: usize, class: usize| format!("c{:04}", i * 3 + class);

    for i in 0..n_true {
        let p = slot(i, 0);
        let a = format!("{p}::impl_a::run");
        let b = format!("{p}::impl_b::run");
        // Both are IN the graph -- they must be, or "unreachable" would be
        // indistinguishable from "never scanned" -- but have no path to each
        // other: independent re-implementations do not call one another.
        edges.push((a.clone(), format!("{p}::helper_a")));
        edges.push((b.clone(), format!("{p}::helper_b")));
        neighbors.push((a, b, 0.95));
        truth.push(true);
    }
    for i in 0..n_decoy {
        let p = slot(i, 1);
        let w = format!("{p}::save::run");
        let inner = format!("{p}::save_impl::run");
        edges.push((w.clone(), inner.clone()));
        neighbors.push((w, inner, 0.97));
        truth.push(false);
    }
    for i in 0..n_noise {
        let p = slot(i, 2);
        let a = format!("{p}::thing_a::run");
        let b = format!("{p}::thing_b::run");
        edges.push((a.clone(), format!("{p}::helper_a")));
        edges.push((b.clone(), format!("{p}::helper_b")));
        neighbors.push((a, b, 0.30));
        truth.push(false);
    }
    let _ = n;
    (CallGraph::from_edges(edges), neighbors, truth)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hops_are_undirected_and_capped() {
        let g = CallGraph::from_edges([("a", "b"), ("b", "c"), ("c", "d")]);
        assert_eq!(g.hops("a", "a", HOP_CAP), Some(0));
        assert_eq!(g.hops("a", "b", HOP_CAP), Some(1));
        assert_eq!(g.hops("b", "a", HOP_CAP), Some(1), "undirected");
        assert_eq!(g.hops("a", "d", HOP_CAP), Some(3));
        // The cap must actually cut: at cap 2, `d` is out of reach.
        assert_eq!(g.hops("a", "d", 2), None);
        // ...and a symbol the graph never saw is not merely far, it is absent.
        assert_eq!(g.hops("a", "never_scanned", HOP_CAP), None);
    }

    #[test]
    fn distance_is_zero_for_self_one_for_unreachable_and_monotone_between() {
        let g = CallGraph::from_edges([("a", "b"), ("b", "c"), ("c", "d"), ("d", "e")]);
        assert_eq!(g.distance("a", "a", HOP_CAP), 0.0);
        assert_eq!(g.distance("a", "zzz", HOP_CAP), 1.0);
        let d1 = g.distance("a", "b", HOP_CAP);
        let d2 = g.distance("a", "c", HOP_CAP);
        let d3 = g.distance("a", "d", HOP_CAP);
        assert!(d1 < d2 && d2 < d3, "{d1} {d2} {d3} must increase with hops");
        assert!((0.0..=1.0).contains(&d3));
    }

    /// A negative cosine must not be able to buy rank with a large beta.
    #[test]
    fn a_negative_dense_score_is_clamped_before_it_is_weighted() {
        let c = Candidate { src: "a".into(), dst: "b".into(), dense: -0.9, graph: 1.0 };
        assert_eq!(hybrid_score(&c, 0.5, 0.5), 0.5, "the -0.9 must not subtract");
    }

    #[test]
    fn average_precision_refuses_to_score_an_empty_truth_set() {
        let cands = vec![Candidate { src: "a".into(), dst: "b".into(), dense: 0.9, graph: 0.0 }];
        assert_eq!(average_precision(&rank(&cands, 1.0, 0.0), &[false]), None);
    }

    /// A perfect ranking scores 1.0 and a fully inverted one does not — without
    /// this the AP could be a constant and every calibration below meaningless.
    #[test]
    fn average_precision_separates_a_perfect_ranking_from_an_inverted_one() {
        let order_good = vec![(0usize, 1.0f32), (1, 0.5)];
        let order_bad = vec![(1usize, 1.0f32), (0, 0.5)];
        let truth = [true, false];
        assert_eq!(average_precision(&order_good, &truth), Some(1.0));
        assert_eq!(average_precision(&order_bad, &truth), Some(0.5));
    }

    /// THE MEASUREMENT. Dense alone must rank the wrapper decoys above the true
    /// clone families — and the calibrated beta must fix it.
    ///
    /// This is the RED half made explicit: the `assert!(ap_dense_only < 1.0)`
    /// proves the fixture actually contains the failure, so the improvement
    /// afterwards is an improvement over something and not over nothing.
    #[test]
    fn the_graph_term_fixes_a_ranking_dense_similarity_gets_wrong() {
        let (g, neigh, truth) = calibration_fixture(8, 8, 16);
        let cands = candidates(&g, neigh, HOP_CAP);
        // FIRST: the graph term must actually VARY. Without this the test is
        // blind, and that is not a hypothetical -- it was. With
        // `CallGraph::distance` sabotaged to return a constant, every score at
        // beta = 1 ties and `rank`'s alphabetical tie-break invents an ordering
        // that happens to beat the (genuinely bad) dense-only one, so
        // `ap > ap_dense_only` and `beta > 0` BOTH passed on a dead graph. A
        // constant term cannot change an order; asserting that it is not constant
        // is what makes every assertion below about the GRAPH.
        let distinct: BTreeSet<u32> = cands.iter().map(|c| c.graph.to_bits()).collect();
        assert!(
            distinct.len() > 1,
            "the graph term took ONE value across all {} candidates -- it cannot be changing any \
             ranking, so nothing below is evidence about the call graph",
            cands.len()
        );

        let cal = calibrate(&cands, &truth, 21).expect("calibration");
        // Printed, not just asserted: the calibrated weights are a MEASUREMENT,
        // and a measurement nobody can read is a number somebody will guess again.
        eprintln!("{}", evidence_note(&g, &cands, &cal));
        eprintln!("  curve (beta, AP): {:?}", cal.curve);

        // RED: dense alone is genuinely broken on this shape.
        assert!(
            cal.ap_dense_only < 1.0,
            "the fixture does not contain the failure it claims to (dense-only AP = {}), so \
             nothing below proves anything",
            cal.ap_dense_only
        );
        // GREEN: the calibrated weighting is strictly better.
        assert!(
            cal.ap > cal.ap_dense_only,
            "beta = {} gave AP {} against dense-only {} — the graph term bought NOTHING",
            cal.beta,
            cal.ap,
            cal.ap_dense_only
        );
        assert!(cal.beta > 0.0, "a calibration that lands on beta = 0 has found no signal");
        // A bare `>` would be satisfied by a rounding difference. The graph term
        // must move the ranking by a lot, or it is not worth a second table.
        assert!(
            cal.lift() > 2.0,
            "lift was only {:.2}x (AP {:.4} vs {:.4}) -- too small to be the graph rather than noise",
            cal.lift(),
            cal.ap,
            cal.ap_dense_only
        );
        assert!((cal.alpha + cal.beta - 1.0).abs() < 1e-5, "weights live on the simplex");
    }

    /// An optimum one grid step wide is overfitting. The best beta must sit on a
    /// PLATEAU — its neighbours within the sweep must score the same.
    #[test]
    fn the_calibrated_optimum_is_a_plateau_not_a_spike() {
        let (g, neigh, truth) = calibration_fixture(8, 8, 16);
        let cands = candidates(&g, neigh, HOP_CAP);
        let cal = calibrate(&cands, &truth, 21).unwrap();
        let at_best = cal
            .curve
            .iter()
            .filter(|(_, ap)| (*ap - cal.ap).abs() < 1e-6)
            .count();
        // On THIS fixture the plateau is the entire sweep above zero, because the
        // classes separate perfectly. That is the fixture's honest limitation,
        // recorded here rather than papered over: the assertion below can only
        // ever prove the optimum is not a spike, never that beta is pinned.
        assert!(
            at_best >= 3,
            "only {at_best} of {} grid points reach the best AP {:.4} — that is a spike, and a \
             beta chosen off a spike will not survive a different corpus",
            cal.curve.len(),
            cal.ap
        );
    }

    /// Ties go to the SMALLEST beta: when the graph buys nothing the calibration
    /// must SAY so, not pick a harmless-looking number.
    #[test]
    fn a_graph_that_helps_nothing_calibrates_to_beta_zero() {
        // Every pair unreachable => graph == 1.0 for all => the term is constant.
        let g = CallGraph::from_edges([("x", "y")]);
        let neigh = vec![
            ("p".to_string(), "q".to_string(), 0.9f32),
            ("r".to_string(), "s".to_string(), 0.2f32),
        ];
        let cands = candidates(&g, neigh, HOP_CAP);
        assert!(cands.iter().all(|c| c.graph == 1.0));
        let cal = calibrate(&cands, &[true, false], 21).unwrap();
        assert_eq!(cal.beta, 0.0, "a constant term must not be credited");
        assert!((cal.lift() - 1.0).abs() < 1e-5, "lift must read exactly 1.0x");
    }

    /// The two silent-emptiness shapes must each get their own sentence, and
    /// neither may read like a result.
    #[test]
    fn an_empty_graph_and_a_constant_graph_are_reported_differently() {
        let empty = CallGraph::default();
        let (g, neigh, truth) = calibration_fixture(4, 4, 4);
        let cands = candidates(&g, neigh, HOP_CAP);
        let cal = calibrate(&cands, &truth, 11).unwrap();

        let note_empty = evidence_note(&empty, &cands, &cal);
        assert!(note_empty.contains("MISSING FEEDS"), "{note_empty}");
        assert!(note_empty.contains("missing producer"), "{note_empty}");

        let flat = CallGraph::from_edges([("x", "y")]);
        let flat_cands = candidates(&flat, vec![("p".to_string(), "q".to_string(), 0.9f32)], HOP_CAP);
        let note_flat = evidence_note(&flat, &flat_cands, &cal);
        assert!(note_flat.contains("CONSTANT"), "{note_flat}");
        assert_ne!(note_flat, note_empty, "the two shapes must be distinguishable");

        let note_ok = evidence_note(&g, &cands, &cal);
        assert!(note_ok.contains("lift"), "{note_ok}");
        assert!(!note_ok.contains("MISSING"), "{note_ok}");
    }

    /// Ranking must not depend on the order candidates arrive in.
    #[test]
    fn ranking_is_deterministic_under_input_permutation() {
        let (g, neigh, _) = calibration_fixture(4, 4, 4);
        let cands = candidates(&g, neigh, HOP_CAP);
        let mut rev = cands.clone();
        rev.reverse();
        let a: Vec<(String, String)> = rank(&cands, 0.6, 0.4)
            .iter()
            .map(|(i, _)| (cands[*i].src.clone(), cands[*i].dst.clone()))
            .collect();
        let b: Vec<(String, String)> = rank(&rev, 0.6, 0.4)
            .iter()
            .map(|(i, _)| (rev[*i].src.clone(), rev[*i].dst.clone()))
            .collect();
        assert_eq!(a, b);
    }
}