use std::collections::{BTreeMap, BTreeSet};
use crate::comparison::{
AnchorMap, Bound, ClassId, ConstraintSet, Hypothetical, Judgement, PairSide, Projection,
QuarantinePolicy, QuarantineReason, Reachability, Response, RowUid, ValueBounds,
ValueProvenance, admissible_value_pair, compile, constraining_counts_by_class, determined,
hypothetical_outcome, synthetic_answer_row,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DecisionContext {
Sequencing { depth: usize },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FrontierItem {
pub id: String,
pub kind: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct ItemCosting {
pub multiplier: f64,
pub est_cost: f64,
pub bare_estimate: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct ElicitInputs<'a> {
pub active: Vec<&'a Judgement>,
pub anchors: AnchorMap,
pub frontier: Vec<FrontierItem>,
pub costing: BTreeMap<String, ItemCosting>,
pub projection: Projection,
pub rank_decay: f64,
pub confirm_boost: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum QueueState {
Candidates,
Stalled { depth: usize },
Stable { depth: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CandidateKind {
Comparison,
AnchorReview,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum YieldBasis {
OrderBearingAnswers,
CanonicalResolvingActions,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Reason {
pub code: String,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Participant {
pub id: String,
pub annotations: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AnchorSubject {
pub id: String,
pub anchor: Option<f64>,
pub conflict_pairs: Vec<(String, String)>,
pub quarantined_rows: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AskSpec {
pub answers: Vec<&'static str>,
pub yield_by_answer: BTreeMap<String, i64>,
pub yield_note: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum EntryPayload {
Comparison {
a: Participant,
b: Participant,
ask: AskSpec,
},
AnchorReview {
subject: AnchorSubject,
ask: AskSpec,
},
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct QueueEntry {
pub kind: CandidateKind,
pub guaranteed_yield: i64,
pub guaranteed_impact: f64,
pub score: f64,
pub yield_basis: YieldBasis,
pub reasons: Vec<Reason>,
pub payload: EntryPayload,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ElicitQueue {
pub state: QueueState,
pub entries: Vec<QueueEntry>,
pub excluded_value_insensitive: usize,
}
const REASON_FRONTIER_PAIR: &str = "indeterminate-frontier-pair";
const REASON_MEDIAN_PROBE: &str = "median-probe";
const REASON_AGENT_ONLY: &str = "agent-only-calibration";
const REASON_STALE_ANCHOR: &str = "stale-anchor-suspect";
const ANSWER_PREFER_A: &str = "prefer-a";
const ANSWER_PREFER_B: &str = "prefer-b";
const ANSWER_EQUAL: &str = "equal";
const ANSWER_INCOMPARABLE: &str = "incomparable";
pub(crate) const ANSWER_REVISE_ANCHOR: &str = "revise-anchor";
pub(crate) const ANSWER_UPHOLD_ANCHOR: &str = "uphold-anchor";
const MASK_ANNOTATION: &str = "projection masked by bare estimate";
const ANCHOR_YIELD_NOTE: &str = "revise-anchor yield assumes a RESOLVING revision (conflict \
removed); a still-conflicting value yields nothing and re-surfaces this candidate next \
refresh. uphold-anchor models retiring the COMPLETE cited closure — real yield may exceed it";
const UNBOUNDED: ValueBounds = ValueBounds {
lower: Bound::Unbounded,
upper: Bound::Unbounded,
};
#[derive(Debug, Clone)]
struct PoolItem {
id: String,
kind: String,
class: ClassId,
multiplier: f64,
cost: f64,
bounds: ValueBounds,
anchor: Option<f64>,
constrained: bool,
agent_only: bool,
bare: bool,
}
fn is_zero(w: f64) -> bool {
w.abs().total_cmp(&0.0).is_eq()
}
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "frontier ranks are tiny counts, far from f64 precision limits"
)]
fn rank_weight(r: usize, decay: f64) -> f64 {
1.0 / (1.0 + decay * (r as f64))
}
fn class_rank(rank_map: &BTreeMap<ClassId, usize>, class: &ClassId, depth: usize) -> usize {
rank_map.get(class).copied().unwrap_or(depth)
}
fn side_vs(item: &PoolItem, cost_other: f64) -> PairSide {
PairSide {
class: item.class.clone(),
eff_weight: item.multiplier * cost_other,
bounds: item.bounds,
anchor: item.anchor,
}
}
struct AnswerEval {
yield_delta: i64,
newly: Vec<(ClassId, ClassId)>,
}
fn answer_impact(
newly: &[(ClassId, ClassId)],
rank_map: &BTreeMap<ClassId, usize>,
depth: usize,
decay: f64,
) -> f64 {
newly
.iter()
.map(|(ca, cb)| {
let r = class_rank(rank_map, ca, depth).min(class_rank(rank_map, cb, depth));
rank_weight(r, decay)
})
.sum()
}
fn reduce_answers(
evals: &[AnswerEval],
rank_map: &BTreeMap<ClassId, usize>,
depth: usize,
decay: f64,
) -> Option<(i64, f64)> {
let gy = evals.iter().map(|e| e.yield_delta).min()?;
let gi = evals
.iter()
.filter(|e| e.yield_delta == gy)
.map(|e| answer_impact(&e.newly, rank_map, depth, decay))
.reduce(|a, b| if a.total_cmp(&b).is_le() { a } else { b })?;
Some((gy, gi))
}
struct Candidate {
sort_key: String,
entry: QueueEntry,
}
pub(crate) fn assemble(inputs: &ElicitInputs<'_>, ctx: DecisionContext) -> ElicitQueue {
let DecisionContext::Sequencing { depth } = ctx;
let cs = compile(&inputs.active, &inputs.anchors, QuarantinePolicy::Symmetric);
let reach = Reachability::build(&cs);
let counts = constraining_counts_by_class(&cs, &inputs.active);
let band: Vec<&FrontierItem> = inputs.frontier.iter().take(depth).collect();
let rank_map = build_rank_map(&band, &cs);
let mut pool: Vec<PoolItem> = Vec::new();
let mut value_bearing = 0_usize;
for item in &band {
if admissible_value_pair(&item.kind, &item.kind).is_err() {
continue; }
let Some(costing) = inputs.costing.get(&item.id) else {
continue; };
value_bearing += 1;
if is_zero(costing.multiplier) {
continue; }
pool.push(resolve_item(
item,
costing,
&cs,
&counts,
&inputs.projection,
));
}
let n_pool = pool.len();
let excluded_value_insensitive = pairs(value_bearing).saturating_sub(pairs(n_pool));
let relevant = relevant_pairs(&pool);
let mut candidates: Vec<Candidate> = Vec::new();
comparison_candidates(
inputs,
&pool,
&reach,
&relevant,
&rank_map,
depth,
&mut candidates,
);
median_probe_candidates(
inputs,
&pool,
&reach,
&relevant,
&rank_map,
depth,
&mut candidates,
);
anchor_review_candidates(
inputs,
&cs,
&reach,
&relevant,
&rank_map,
depth,
&mut candidates,
);
candidates.sort_by(|a, b| {
b.entry
.score
.total_cmp(&a.entry.score)
.then_with(|| a.sort_key.cmp(&b.sort_key))
});
let entries: Vec<QueueEntry> = candidates.into_iter().map(|c| c.entry).collect();
let state = if !entries.is_empty() {
QueueState::Candidates
} else if pool_has_indeterminate(&pool, &reach) {
QueueState::Stalled { depth }
} else {
QueueState::Stable { depth }
};
ElicitQueue {
state,
entries,
excluded_value_insensitive,
}
}
#[expect(clippy::integer_division, reason = "exact: n·(n−1) is even")]
fn pairs(n: usize) -> usize {
n.saturating_mul(n.saturating_sub(1)) / 2
}
fn build_rank_map(band: &[&FrontierItem], cs: &ConstraintSet) -> BTreeMap<ClassId, usize> {
let mut out: BTreeMap<ClassId, usize> = BTreeMap::new();
for (r, item) in band.iter().enumerate() {
let class = cs
.classes
.get(&item.id)
.cloned()
.unwrap_or_else(|| item.id.clone());
out.entry(class)
.and_modify(|best| *best = (*best).min(r))
.or_insert(r);
}
out
}
fn resolve_item(
item: &FrontierItem,
costing: &ItemCosting,
cs: &ConstraintSet,
counts: &BTreeMap<ClassId, crate::comparison::RaterCounts>,
projection: &Projection,
) -> PoolItem {
let class = cs
.classes
.get(&item.id)
.cloned()
.unwrap_or_else(|| item.id.clone());
let bounds = cs.bounds.get(&class).copied().unwrap_or(UNBOUNDED);
let anchor = cs.anchors.get(&class).copied();
let class_counts = counts.get(&class).copied().unwrap_or_default();
let constrained = class_counts.total() > 0 || anchor.is_some();
let agent_only = class_counts.human == 0 && class_counts.agent >= 1;
let bare = costing.bare_estimate && is_masked(projection.get(&item.id));
PoolItem {
id: item.id.clone(),
kind: item.kind.clone(),
class,
multiplier: costing.multiplier,
cost: costing.est_cost,
bounds,
anchor,
constrained,
agent_only,
bare,
}
}
fn is_masked(projected: Option<&(f64, ValueProvenance)>) -> bool {
matches!(
projected,
Some((_, ValueProvenance::Projected | ValueProvenance::Gauge))
)
}
fn relevant_pairs(pool: &[PoolItem]) -> Vec<(PairSide, PairSide)> {
let mut out = Vec::new();
for (i, a) in pool.iter().enumerate() {
for b in pool.iter().skip(i + 1) {
out.push((side_vs(a, b.cost), side_vs(b, a.cost)));
}
}
out
}
fn pool_has_indeterminate(pool: &[PoolItem], reach: &Reachability) -> bool {
for (i, a) in pool.iter().enumerate() {
for b in pool.iter().skip(i + 1) {
if !determined(reach, &side_vs(a, b.cost), &side_vs(b, a.cost)).is_determined() {
return true;
}
}
}
false
}
fn comparison_candidates(
inputs: &ElicitInputs<'_>,
pool: &[PoolItem],
reach: &Reachability,
relevant: &[(PairSide, PairSide)],
rank_map: &BTreeMap<ClassId, usize>,
depth: usize,
out: &mut Vec<Candidate>,
) {
for (i, a) in pool.iter().enumerate() {
for b in pool.iter().skip(i + 1) {
if !a.constrained || !b.constrained {
continue; }
if admissible_value_pair(&a.kind, &b.kind).is_err() {
continue;
}
if determined(reach, &side_vs(a, b.cost), &side_vs(b, a.cost)).is_determined() {
continue; }
if let Some(entry) = build_comparison(
inputs,
reach,
a,
b,
relevant,
rank_map,
depth,
REASON_FRONTIER_PAIR,
) {
out.push(entry);
}
}
}
}
fn median_probe_candidates(
inputs: &ElicitInputs<'_>,
pool: &[PoolItem],
reach: &Reachability,
relevant: &[(PairSide, PairSide)],
rank_map: &BTreeMap<ClassId, usize>,
depth: usize,
out: &mut Vec<Candidate>,
) {
for u in pool.iter().filter(|p| !p.constrained) {
let Some(target) = median_target(inputs, pool, u) else {
continue;
};
if let Some(entry) = build_comparison(
inputs,
reach,
u,
target,
relevant,
rank_map,
depth,
REASON_MEDIAN_PROBE,
) {
out.push(entry);
}
}
}
#[expect(
clippy::integer_division,
reason = "median index; integer halving is intended"
)]
fn median_target<'p>(
inputs: &ElicitInputs<'_>,
pool: &'p [PoolItem],
u: &PoolItem,
) -> Option<&'p PoolItem> {
let mut comparable: Vec<(&PoolItem, f64)> = pool
.iter()
.filter(|p| p.id != u.id)
.filter(|p| admissible_value_pair(&u.kind, &p.kind).is_ok())
.filter_map(|p| inputs.projection.get(&p.id).map(|&(v, _)| (p, v)))
.collect();
if comparable.is_empty() {
return None;
}
comparable.sort_by(|(pa, va), (pb, vb)| va.total_cmp(vb).then_with(|| pa.id.cmp(&pb.id)));
let mid = comparable.len() / 2;
let median = comparable.get(mid).map_or(0.0, |&(_, v)| v);
comparable
.into_iter()
.min_by(|(pa, va), (pb, vb)| {
(va - median)
.abs()
.total_cmp(&(vb - median).abs())
.then_with(|| pa.id.cmp(&pb.id))
})
.map(|(p, _)| p)
}
#[expect(
clippy::too_many_arguments,
reason = "yield inputs + impact-band context (rank_map, depth) fanned to a private helper"
)]
fn build_comparison(
inputs: &ElicitInputs<'_>,
reach: &Reachability,
a: &PoolItem,
b: &PoolItem,
relevant: &[(PairSide, PairSide)],
rank_map: &BTreeMap<ClassId, usize>,
depth: usize,
reason_code: &str,
) -> Option<Candidate> {
let order_bearing = [
(ANSWER_PREFER_A, Response::PreferA),
(ANSWER_PREFER_B, Response::PreferB),
(ANSWER_EQUAL, Response::Equal),
];
let mut evals: Vec<AnswerEval> = Vec::new();
let mut yield_by_answer: BTreeMap<String, i64> = BTreeMap::new();
for (token, response) in order_bearing {
let row = synthetic_answer_row(&a.id, &b.id, response);
let outcome = hypothetical_outcome(
reach,
&inputs.active,
&inputs.anchors,
&Hypothetical::Answer(Box::new(row)),
relevant,
);
yield_by_answer.insert(token.to_string(), outcome.yield_delta());
evals.push(AnswerEval {
yield_delta: outcome.yield_delta(),
newly: outcome.newly_determined,
});
}
yield_by_answer.insert(ANSWER_INCOMPARABLE.to_string(), 0);
let (gy, gi) = reduce_answers(&evals, rank_map, depth, inputs.rank_decay)?;
if gy <= 0 {
return None; }
let boost = if a.agent_only && b.agent_only {
inputs.confirm_boost
} else {
1.0
};
let score = i64_as_f64(gy) * gi * boost;
let mut reasons = vec![Reason {
code: reason_code.to_string(),
text: comparison_reason_text(reason_code),
}];
if boost.total_cmp(&1.0).is_gt() {
reasons.push(Reason {
code: REASON_AGENT_ONLY.to_string(),
text: "both items currently calibrated only by agent evidence".to_string(),
});
}
let ask = AskSpec {
answers: vec![
ANSWER_PREFER_A,
ANSWER_PREFER_B,
ANSWER_EQUAL,
ANSWER_INCOMPARABLE,
],
yield_by_answer,
yield_note: None,
};
let payload = EntryPayload::Comparison {
a: participant(a),
b: participant(b),
ask,
};
let (lo, hi) = if a.id <= b.id {
(&a.id, &b.id)
} else {
(&b.id, &a.id)
};
Some(Candidate {
sort_key: format!("cmp:{lo}:{hi}"),
entry: QueueEntry {
kind: CandidateKind::Comparison,
guaranteed_yield: gy,
guaranteed_impact: gi,
score,
yield_basis: YieldBasis::OrderBearingAnswers,
reasons,
payload,
},
})
}
fn comparison_reason_text(code: &str) -> String {
if code == REASON_MEDIAN_PROBE {
"un-constrained item — calibrate against the projected median of its comparable set"
.to_string()
} else {
"an indeterminate value_dim order between two top-K frontier items".to_string()
}
}
fn participant(item: &PoolItem) -> Participant {
let mut annotations = Vec::new();
if item.bare {
annotations.push(MASK_ANNOTATION.to_string());
}
Participant {
id: item.id.clone(),
annotations,
}
}
fn anchor_review_candidates(
inputs: &ElicitInputs<'_>,
cs: &ConstraintSet,
reach: &Reachability,
relevant: &[(PairSide, PairSide)],
rank_map: &BTreeMap<ClassId, usize>,
depth: usize,
out: &mut Vec<Candidate>,
) {
for suspect in suspect_anchors(cs, &inputs.anchors) {
let rows = rows_citing(cs, &suspect);
let removed = hypothetical_outcome(
reach,
&inputs.active,
&inputs.anchors,
&Hypothetical::AnchorRemoved(&suspect),
relevant,
);
let retired = hypothetical_outcome(
reach,
&inputs.active,
&inputs.anchors,
&Hypothetical::RowsRetired(&rows),
relevant,
);
let revise_yield = removed.yield_delta();
let uphold_yield = retired.yield_delta();
let evals = [
AnswerEval {
yield_delta: revise_yield,
newly: removed.newly_determined,
},
AnswerEval {
yield_delta: uphold_yield,
newly: retired.newly_determined,
},
];
let Some((gy, gi)) = reduce_answers(&evals, rank_map, depth, inputs.rank_decay) else {
continue;
};
let score = i64_as_f64(gy).max(0.0) * gi;
let mut yield_by_answer = BTreeMap::new();
yield_by_answer.insert(ANSWER_REVISE_ANCHOR.to_string(), revise_yield);
yield_by_answer.insert(ANSWER_UPHOLD_ANCHOR.to_string(), uphold_yield);
let subject = AnchorSubject {
id: suspect.clone(),
anchor: inputs.anchors.get(&suspect).copied(),
conflict_pairs: conflict_pairs_for(cs, &suspect),
quarantined_rows: rows.iter().cloned().collect(),
};
let ask = AskSpec {
answers: vec![ANSWER_REVISE_ANCHOR, ANSWER_UPHOLD_ANCHOR],
yield_by_answer,
yield_note: Some(ANCHOR_YIELD_NOTE.to_string()),
};
out.push(Candidate {
sort_key: format!("anc:{suspect}"),
entry: QueueEntry {
kind: CandidateKind::AnchorReview,
guaranteed_yield: gy,
guaranteed_impact: gi,
score,
yield_basis: YieldBasis::CanonicalResolvingActions,
reasons: vec![Reason {
code: REASON_STALE_ANCHOR.to_string(),
text: format!("anchor on {suspect} sits on a quarantined conflict path"),
}],
payload: EntryPayload::AnchorReview { subject, ask },
},
});
}
}
fn suspect_anchors(cs: &ConstraintSet, anchors: &AnchorMap) -> Vec<String> {
let mut out: BTreeSet<String> = BTreeSet::new();
for reason in cs.quarantined.values() {
if let QuarantineReason::AnchorConflict { pairs } = reason {
for (x, y) in pairs {
for token in [x, y] {
if let Some(entity) = resolve_anchored(token, anchors, cs) {
out.insert(entity);
}
}
}
}
}
out.into_iter().collect()
}
fn resolve_anchored(token: &str, anchors: &AnchorMap, cs: &ConstraintSet) -> Option<String> {
if anchors.contains_key(token) {
return Some(token.to_string());
}
cs.classes
.iter()
.find(|(entity, class)| class.as_str() == token && anchors.contains_key(*entity))
.map(|(entity, _)| entity.clone())
}
fn rows_citing(cs: &ConstraintSet, suspect: &str) -> BTreeSet<RowUid> {
let mut out = BTreeSet::new();
for (uid, reason) in &cs.quarantined {
if let QuarantineReason::AnchorConflict { pairs } = reason
&& pairs
.iter()
.any(|(x, y)| cites(x, suspect, cs) || cites(y, suspect, cs))
{
out.insert(uid.clone());
}
}
out
}
fn cites(token: &str, suspect: &str, cs: &ConstraintSet) -> bool {
token == suspect || cs.classes.get(suspect).is_some_and(|c| c.as_str() == token)
}
fn conflict_pairs_for(cs: &ConstraintSet, suspect: &str) -> Vec<(String, String)> {
let mut out: BTreeSet<(String, String)> = BTreeSet::new();
for reason in cs.quarantined.values() {
if let QuarantineReason::AnchorConflict { pairs } = reason {
for (x, y) in pairs {
if cites(x, suspect, cs) || cites(y, suspect, cs) {
out.insert((x.clone(), y.clone()));
}
}
}
}
out.into_iter().collect()
}
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "guaranteed yields are small determinacy counts, exact in f64"
)]
fn i64_as_f64(v: i64) -> f64 {
v as f64
}
#[cfg(test)]
mod tests {
use super::*;
use crate::comparison::{DOMAIN_VALUE, FRAME_EQUAL_EFFORT, RaterKind, RowForm};
fn jrow(uid: &str, a: &str, b: &str, response: Response, rater: RaterKind) -> Judgement {
Judgement {
uid: uid.to_string(),
seq: 0,
a: a.to_string(),
b: b.to_string(),
response,
domain: DOMAIN_VALUE.to_string(),
frame: FRAME_EQUAL_EFFORT.to_string(),
form: RowForm::Order,
magnitude: None,
supersedes: None,
lens: None,
rater,
by: None,
note: None,
date: "2026-07-12".to_string(),
}
}
fn win(uid: &str, w: &str, l: &str) -> Judgement {
jrow(uid, w, l, Response::PreferA, RaterKind::Human)
}
fn win_agent(uid: &str, w: &str, l: &str) -> Judgement {
jrow(uid, w, l, Response::PreferA, RaterKind::Agent)
}
fn mk<'a>(
active: Vec<&'a Judgement>,
anchors: &[(&str, f64)],
frontier: &[&str],
costing: &[(&str, f64, f64)],
projection: &[(&str, f64, ValueProvenance)],
) -> ElicitInputs<'a> {
ElicitInputs {
active,
anchors: anchors.iter().map(|&(e, v)| (e.to_string(), v)).collect(),
frontier: frontier
.iter()
.map(|&id| FrontierItem {
id: id.to_string(),
kind: "IMP".to_string(),
})
.collect(),
costing: costing
.iter()
.map(|&(id, m, c)| {
(
id.to_string(),
ItemCosting {
multiplier: m,
est_cost: c,
bare_estimate: false,
},
)
})
.collect(),
projection: projection
.iter()
.map(|&(id, v, p)| (id.to_string(), (v, p)))
.collect(),
rank_decay: 1.0,
confirm_boost: 1.5,
}
}
fn seq(depth: usize) -> DecisionContext {
DecisionContext::Sequencing { depth }
}
#[test]
fn indeterminate_constrained_pair_is_a_comparison_candidate() {
let rows = vec![win("j0", "A", "C"), win("j1", "B", "D")];
let refs: Vec<&Judgement> = rows.iter().collect();
let inputs = mk(
refs.clone(),
&[],
&["A", "B"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0)],
&[],
);
let q = assemble(&inputs, seq(2));
assert_eq!(q.state, QueueState::Candidates);
assert_eq!(q.entries.len(), 1);
assert_eq!(q.entries[0].kind, CandidateKind::Comparison);
assert!(q.entries[0].guaranteed_yield >= 1);
}
#[test]
fn all_determined_pool_no_suspects_is_stable() {
let rows = vec![win("j0", "A", "C"), win("j1", "B", "C")];
let refs: Vec<&Judgement> = rows.iter().collect();
let inputs = mk(
refs.clone(),
&[("A", 5.0), ("B", 3.0), ("C", 0.0)],
&["A", "B"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0)],
&[],
);
let q = assemble(&inputs, seq(2));
assert_eq!(q.state, QueueState::Stable { depth: 2 });
assert!(q.entries.is_empty());
}
#[test]
fn zero_yield_bridge_drops_admission_and_stalls() {
let rows = vec![
win("j0", "T", "A"),
win("j1", "A", "L"),
win("j2", "T", "B"),
win("j3", "B", "L"),
];
let refs: Vec<&Judgement> = rows.iter().collect();
let inputs = mk(
refs.clone(),
&[("T", 5.0), ("L", -5.0)],
&["A", "B"],
&[("A", 1.0, 1.0), ("B", 1.0, 2.0)],
&[],
);
let q = assemble(&inputs, seq(2));
assert!(q.entries.is_empty(), "zero-yield candidate not admitted");
assert_eq!(q.state, QueueState::Stalled { depth: 2 });
}
#[test]
fn confirm_boost_agent_only_outranks_human_touched() {
let agent_rows = vec![win_agent("j0", "A", "C"), win_agent("j1", "B", "D")];
let human_rows = vec![win("j0", "A", "C"), win("j1", "B", "D")];
let a_refs: Vec<&Judgement> = agent_rows.iter().collect();
let h_refs: Vec<&Judgement> = human_rows.iter().collect();
let cost = [("A", 1.0, 1.0), ("B", 1.0, 1.0)];
let qa = assemble(&mk(a_refs.clone(), &[], &["A", "B"], &cost, &[]), seq(2));
let qh = assemble(&mk(h_refs.clone(), &[], &["A", "B"], &cost, &[]), seq(2));
let sa = qa.entries[0].score;
let sh = qh.entries[0].score;
assert!(sa > sh, "agent-only outranks human-touched");
assert!(
(sa - sh * 1.5).abs() < 1e-9,
"score scales by confirm_boost"
);
assert!(
qa.entries[0]
.reasons
.iter()
.any(|r| r.code == "agent-only-calibration"),
"agent case discloses the boost reason"
);
assert!(
qh.entries[0]
.reasons
.iter()
.all(|r| r.code != "agent-only-calibration"),
"human case claims no boost"
);
}
#[test]
fn guaranteed_impact_is_min_over_argmin_yield_answers() {
let rank_map: BTreeMap<ClassId, usize> = [("A", 0usize), ("B", 1), ("C", 2)]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
let evals = vec![
AnswerEval {
yield_delta: 1,
newly: vec![("A".to_string(), "B".to_string())],
},
AnswerEval {
yield_delta: 1,
newly: vec![("B".to_string(), "C".to_string())],
},
AnswerEval {
yield_delta: 2,
newly: vec![
("A".to_string(), "B".to_string()),
("B".to_string(), "C".to_string()),
],
},
];
let (gy, gi) = reduce_answers(&evals, &rank_map, 3, 1.0).unwrap();
assert_eq!(gy, 1);
assert!((gi - 0.5).abs() < 1e-9, "min over argmin-yield answers");
}
#[test]
fn rank_weight_decays_monotonically() {
assert!((rank_weight(0, 1.0) - 1.0).abs() < 1e-9);
assert!(rank_weight(0, 1.0) > rank_weight(1, 1.0));
assert!(rank_weight(1, 1.0) > rank_weight(2, 1.0));
}
#[test]
fn value_insensitive_zero_multiplier_excluded_and_counted() {
let rows = vec![
win("j0", "A", "P"),
win("j1", "B", "Q"),
win("j2", "Z", "R"),
];
let refs: Vec<&Judgement> = rows.iter().collect();
let inputs = mk(
refs.clone(),
&[],
&["A", "B", "Z"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0), ("Z", 0.0, 1.0)],
&[],
);
let q = assemble(&inputs, seq(3));
assert_eq!(q.excluded_value_insensitive, 2, "pairs (A,Z),(B,Z) dropped");
assert_eq!(q.entries.len(), 1);
for e in &q.entries {
if let EntryPayload::Comparison { a, b, .. } = &e.payload {
assert!(a.id != "Z" && b.id != "Z", "Z never surfaces");
}
}
}
#[test]
fn entries_sorted_by_score_then_id() {
let rows = vec![win("j0", "A", "B"), win("j1", "B", "C")];
let refs: Vec<&Judgement> = rows.iter().collect();
let inputs = mk(
refs.clone(),
&[("A", 1.0), ("C", 3.0)],
&["A", "B", "C"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0), ("C", 1.0, 1.0)],
&[],
);
let q = assemble(&inputs, seq(3));
for w in q.entries.windows(2) {
assert!(w[0].score >= w[1].score - 1e-12);
}
let subjects: Vec<&str> = q
.entries
.iter()
.filter_map(|e| match &e.payload {
EntryPayload::AnchorReview { subject, .. } => Some(subject.id.as_str()),
EntryPayload::Comparison { .. } => None,
})
.collect();
assert_eq!(subjects, vec!["A", "C"], "id-lexicographic tiebreak");
}
#[test]
fn anchor_review_min_over_resolving_uphold_below_removal() {
let rows = vec![win("j0", "A", "B"), win("j1", "B", "C")];
let refs: Vec<&Judgement> = rows.iter().collect();
let inputs = mk(
refs.clone(),
&[("A", 1.0), ("C", 3.0)],
&["A", "B", "C"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0), ("C", 1.0, 1.0)],
&[],
);
let q = assemble(&inputs, seq(3));
let subject_a = q
.entries
.iter()
.find(|e| matches!(&e.payload, EntryPayload::AnchorReview { subject, .. } if subject.id == "A"))
.expect("an anchor-review candidate for suspect A");
assert_eq!(subject_a.kind, CandidateKind::AnchorReview);
assert_eq!(subject_a.yield_basis, YieldBasis::CanonicalResolvingActions);
assert_eq!(
subject_a.guaranteed_yield, -1,
"min over resolving = uphold"
);
if let EntryPayload::AnchorReview { ask, .. } = &subject_a.payload {
assert_eq!(ask.yield_by_answer.get("revise-anchor"), Some(&2));
assert_eq!(ask.yield_by_answer.get("uphold-anchor"), Some(&-1));
assert!(ask.yield_note.is_some(), "conditional-yield disclosure");
} else {
panic!("expected anchor-review payload");
}
}
#[test]
fn anchor_review_not_k_gated_and_keeps_determined_pool_as_candidates() {
let rows = vec![
win("j0", "P", "Pl"),
win("j1", "Q", "Ql"),
win("j2", "R", "S"),
];
let refs: Vec<&Judgement> = rows.iter().collect();
let inputs = mk(
refs.clone(),
&[("P", 5.0), ("Q", 3.0), ("R", 1.0), ("S", 3.0)],
&["P", "Q"],
&[("P", 1.0, 1.0), ("Q", 1.0, 1.0)],
&[],
);
let q = assemble(&inputs, seq(2));
assert_eq!(q.state, QueueState::Candidates, "suspect keeps Candidates");
assert!(
q.entries.iter().any(
|e| matches!(&e.payload, EntryPayload::AnchorReview { subject, .. }
if subject.id == "R" || subject.id == "S")
),
"suspect outside top-K still admits (not K-gated)"
);
}
#[test]
fn median_probe_surfaces_for_unconstrained_item() {
let rows = vec![win("j0", "W", "Z")];
let refs: Vec<&Judgement> = rows.iter().collect();
let inputs = mk(
refs.clone(),
&[("Z", 0.0)],
&["U", "W"],
&[("U", 1.0, 1.0), ("W", 1.0, 1.0)],
&[
("U", 2.0, ValueProvenance::Projected),
("W", 3.0, ValueProvenance::Projected),
],
);
let q = assemble(&inputs, seq(2));
let probe = q
.entries
.iter()
.find(|e| e.reasons.iter().any(|r| r.code == "median-probe"))
.expect("a median-probe candidate");
assert_eq!(probe.kind, CandidateKind::Comparison);
assert!(probe.guaranteed_yield > 0);
if let EntryPayload::Comparison { a, b, .. } = &probe.payload {
let ids = [a.id.as_str(), b.id.as_str()];
assert!(ids.contains(&"U") && ids.contains(&"W"));
} else {
panic!("expected comparison payload");
}
}
}