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);
}
}