use std::collections::{BTreeMap, BTreeSet};
use crate::comparison::{
AnchorMap, Bound, ClaimFinding, ClaimResolution, ClaimTier, ClassId, ConstraintSet,
Hypothetical, Judgement, PairSide, Projection, QuarantinePolicy, QuarantineReason,
Reachability, Response, RowUid, ValueBounds, ValueProvenance, admissible_estimate_pair,
admissible_value_pair, compile, compile_human_only, constraining_counts_by_class, determined,
human_rows, 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,
pub demote_agent_evidence: bool,
pub sizing: SizingInputs,
pub claims: ClaimResolution,
pub facet_valued: BTreeSet<String>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct SizingInputs {
pub estimated_costs: BTreeMap<String, f64>,
pub est_evidenced: BTreeSet<String>,
pub estimate_anchored_targets: BTreeSet<String>,
}
#[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,
SizingProbe,
ClaimReprobe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum YieldBasis {
OrderBearingAnswers,
CanonicalResolvingActions,
Calibration,
}
#[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 SizingTarget {
pub id: String,
pub estimate: f64,
pub tier_label: 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,
},
SizingProbe {
subject: Participant,
target: SizingTarget,
},
ClaimReprobe {
subject: String,
tier: ClaimTier,
mean: f64,
low: f64,
high: f64,
distinct: u32,
rows: u32,
},
}
#[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,
pub sizing_residual: usize,
pub sizing_no_calibration_target: bool,
}
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 REASON_SIZING_PROBE: &str = "sizing-probe";
const REASON_CONTESTED_CLAIM: &str = "contested-claim";
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 SIZING_PROBE_ANSWERS: &[&str] = &[
ANSWER_PREFER_A,
ANSWER_PREFER_B,
ANSWER_EQUAL,
ANSWER_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,
multiplier: f64,
cost: 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)
}
struct VerdictSystem<'s, 'a> {
cs: &'s ConstraintSet,
reach: &'s Reachability,
rows: &'s [&'a Judgement],
}
pub(crate) fn pair_side(cs: &ConstraintSet, id: &str, eff_weight: f64) -> PairSide {
let class = cs
.classes
.get(id)
.cloned()
.unwrap_or_else(|| id.to_string());
let bounds = cs.bounds.get(&class).copied().unwrap_or(UNBOUNDED);
let anchor = cs.anchors.get(&class).copied();
PairSide {
class,
eff_weight,
bounds,
anchor,
}
}
fn side_in(cs: &ConstraintSet, item: &PoolItem, cost_other: f64) -> PairSide {
pair_side(cs, &item.id, item.multiplier * cost_other)
}
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 human = inputs.demote_agent_evidence.then(|| {
let vcs = compile_human_only(&inputs.active, &inputs.anchors, QuarantinePolicy::Symmetric);
let vreach = Reachability::build(&vcs);
(vcs, vreach, human_rows(&inputs.active))
});
let verdict = match &human {
Some((vcs, vreach, vrows)) => VerdictSystem {
cs: vcs,
reach: vreach,
rows: vrows,
},
None => VerdictSystem {
cs: &cs,
reach: &reach,
rows: &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(verdict.cs, &pool);
let mut candidates: Vec<Candidate> = Vec::new();
comparison_candidates(
inputs,
&pool,
&verdict,
&relevant,
&rank_map,
depth,
&mut candidates,
);
median_probe_candidates(
inputs,
&pool,
&verdict,
&relevant,
&rank_map,
depth,
&mut candidates,
);
anchor_review_candidates(
inputs,
&cs,
&verdict,
&relevant,
&rank_map,
depth,
&mut candidates,
);
let (sizing_residual, sizing_no_calibration_target) =
sizing_probe_candidates(inputs, &band, &mut candidates);
claim_reprobe_candidates(inputs, &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, &verdict) {
QueueState::Stalled { depth }
} else {
QueueState::Stable { depth }
};
ElicitQueue {
state,
entries,
excluded_value_insensitive,
sizing_residual,
sizing_no_calibration_target,
}
}
#[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 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(),
multiplier: costing.multiplier,
cost: costing.est_cost,
constrained,
agent_only,
bare,
}
}
fn is_masked(projected: Option<&(f64, ValueProvenance)>) -> bool {
matches!(
projected,
Some((_, ValueProvenance::Projected | ValueProvenance::Gauge))
)
}
fn relevant_pairs(cs: &ConstraintSet, 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_in(cs, a, b.cost), side_in(cs, b, a.cost)));
}
}
out
}
fn pool_has_indeterminate(pool: &[PoolItem], verdict: &VerdictSystem<'_, '_>) -> bool {
for (i, a) in pool.iter().enumerate() {
for b in pool.iter().skip(i + 1) {
let (sa, sb) = (
side_in(verdict.cs, a, b.cost),
side_in(verdict.cs, b, a.cost),
);
if !determined(verdict.reach, &sa, &sb).is_determined() {
return true;
}
}
}
false
}
fn comparison_candidates(
inputs: &ElicitInputs<'_>,
pool: &[PoolItem],
verdict: &VerdictSystem<'_, '_>,
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;
}
let (sa, sb) = (
side_in(verdict.cs, a, b.cost),
side_in(verdict.cs, b, a.cost),
);
if determined(verdict.reach, &sa, &sb).is_determined() {
continue; }
if let Some(entry) = build_comparison(
inputs,
verdict,
a,
b,
relevant,
rank_map,
depth,
REASON_FRONTIER_PAIR,
) {
out.push(entry);
}
}
}
}
fn claim_retired(inputs: &ElicitInputs<'_>, id: &str) -> bool {
if inputs.claims.anchored.contains_key(id) {
return true;
}
if inputs.demote_agent_evidence {
return false;
}
inputs.claims.priors.contains_key(id) || inputs.facet_valued.contains(id)
}
fn median_probe_candidates(
inputs: &ElicitInputs<'_>,
pool: &[PoolItem],
verdict: &VerdictSystem<'_, '_>,
relevant: &[(PairSide, PairSide)],
rank_map: &BTreeMap<ClassId, usize>,
depth: usize,
out: &mut Vec<Candidate>,
) {
for u in pool.iter().filter(|p| !p.constrained) {
if claim_retired(inputs, &u.id) {
continue; }
let Some(target) = median_target(inputs, pool, u) else {
continue;
};
if let Some(entry) = build_comparison(
inputs,
verdict,
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<'_>,
verdict: &VerdictSystem<'_, '_>,
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(
verdict.reach,
verdict.rows,
&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,
verdict: &VerdictSystem<'_, '_>,
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(
verdict.reach,
verdict.rows,
&inputs.anchors,
&Hypothetical::AnchorRemoved(&suspect),
relevant,
);
let retired = hypothetical_outcome(
verdict.reach,
verdict.rows,
&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()
}
fn sizing_probe_candidates(
inputs: &ElicitInputs<'_>,
band: &[&FrontierItem],
out: &mut Vec<Candidate>,
) -> (usize, bool) {
let target = sizing_target(inputs, band);
let mut unsized_count = 0_usize;
let mut minted = 0_usize;
let mut eligible_uncalibrated = false;
for item in band {
if admissible_estimate_pair(&item.kind, &item.kind).is_err() {
continue; }
let Some(costing) = inputs.costing.get(&item.id) else {
continue;
};
if !costing.bare_estimate {
continue; }
unsized_count += 1;
if inputs.sizing.est_evidenced.contains(&item.id) {
continue; }
let Some(target) = &target else {
eligible_uncalibrated = true; continue;
};
out.push(sizing_probe(inputs, item, target));
minted += 1;
}
(unsized_count.saturating_sub(minted), eligible_uncalibrated)
}
fn sizing_target(inputs: &ElicitInputs<'_>, band: &[&FrontierItem]) -> Option<SizingTarget> {
let anchored = &inputs.sizing.estimate_anchored_targets;
let costs = &inputs.sizing.estimated_costs;
let in_band: Vec<(&String, f64)> = band
.iter()
.filter(|f| anchored.contains(&f.id))
.filter_map(|f| costs.get_key_value(&f.id).map(|(id, &c)| (id, c)))
.collect();
let tier_fn = |id: &str| -> String {
inputs
.claims
.anchored
.get(id)
.map(|c| match c.tier {
ClaimTier::Pin => "pin".to_string(),
ClaimTier::Human => "human claim".to_string(),
ClaimTier::Agent => "agent claim".to_string(),
ClaimTier::Migrated => "migrated claim".to_string(),
})
.unwrap_or_default()
};
if in_band.is_empty() {
let corpus: Vec<(&String, f64)> = costs
.iter()
.filter(|(id, _)| anchored.contains(*id))
.map(|(id, &c)| (id, c))
.collect();
median_by_cost(corpus, tier_fn)
} else {
median_by_cost(in_band, tier_fn)
}
}
#[expect(
clippy::integer_division,
reason = "median index; integer halving is intended (lower-middle)"
)]
fn median_by_cost(
mut items: Vec<(&String, f64)>,
tier_fn: impl Fn(&str) -> String,
) -> Option<SizingTarget> {
if items.is_empty() {
return None;
}
items.sort_by(|(ia, ca), (ib, cb)| ca.total_cmp(cb).then_with(|| ia.cmp(ib)));
let mid = (items.len() - 1) / 2; items.get(mid).map(|&(id, cost)| SizingTarget {
id: id.clone(),
estimate: cost,
tier_label: tier_fn(id.as_str()),
})
}
fn sizing_probe(
inputs: &ElicitInputs<'_>,
item: &FrontierItem,
target: &SizingTarget,
) -> Candidate {
let mut annotations = Vec::new();
if is_masked(inputs.projection.get(&item.id)) {
annotations.push(MASK_ANNOTATION.to_string());
}
let subject = Participant {
id: item.id.clone(),
annotations,
};
Candidate {
sort_key: format!("siz:{}", item.id),
entry: QueueEntry {
kind: CandidateKind::SizingProbe,
guaranteed_yield: 0,
guaranteed_impact: 0.0,
score: 0.0,
yield_basis: YieldBasis::Calibration,
reasons: vec![Reason {
code: REASON_SIZING_PROBE.to_string(),
text: format!(
"un-sized top-K item — calibrate its cost against {} ({} backing)",
target.id, target.tier_label
),
}],
payload: EntryPayload::SizingProbe {
subject,
target: target.clone(),
},
},
}
}
fn claim_reprobe_candidates(inputs: &ElicitInputs<'_>, out: &mut Vec<Candidate>) {
for finding in &inputs.claims.findings {
if !finding.nominates_reprobe() {
continue; }
let ClaimFinding::Conflict {
item,
tier,
low,
high,
distinct,
rows,
..
} = finding;
let contested = match tier {
ClaimTier::Pin => "contested pin",
_ => "contested human claims",
};
out.push(Candidate {
sort_key: format!("clm:{item}"),
entry: QueueEntry {
kind: CandidateKind::ClaimReprobe,
guaranteed_yield: 0,
guaranteed_impact: 0.0,
score: 0.0,
yield_basis: YieldBasis::Calibration,
reasons: vec![Reason {
code: REASON_CONTESTED_CLAIM.to_string(),
text: format!(
"{contested} on {item} — {distinct} distinct magnitudes \
({low} ‥ {high}) over {rows} rows; resolve by superseding row"
),
}],
payload: EntryPayload::ClaimReprobe {
subject: item.clone(),
tier: *tier,
mean: inputs
.claims
.anchored
.get(item)
.map_or(0.0, |claim| claim.operative),
low: *low,
high: *high,
distinct: *distinct,
rows: *rows,
},
},
});
}
}
#[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: Some(b.to_string()),
response: Some(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: Some("2026-07-12".to_string()),
observed_at: None,
basis: None,
est_lower: None,
est_upper: None,
admission: None,
}
}
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,
demote_agent_evidence: false,
sizing: SizingInputs::default(),
claims: ClaimResolution::default(),
facet_valued: BTreeSet::new(),
}
}
fn sizing(estimated: &[(&str, f64)], evidenced: &[&str]) -> SizingInputs {
let anchored: std::collections::BTreeSet<String> =
estimated.iter().map(|&(id, _)| id.to_string()).collect();
SizingInputs {
estimated_costs: estimated
.iter()
.map(|&(id, c)| (id.to_string(), c))
.collect(),
est_evidenced: evidenced.iter().map(|&s| s.to_string()).collect(),
estimate_anchored_targets: anchored,
}
}
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 assemble_baseline_ignores_anchor_rows_via_the_pairwise_view() {
use crate::comparison::{
AnchorMap, COMPARISON_SCHEMA, COMPARISON_VERSION, ComparisonSession,
FRAME_VALUE_ANCHOR, SessionHeader, StatusMap, VALUE_PROJECTION_PARAMS,
pipeline_from_sessions,
};
let session_of = |judgements: Vec<Judgement>| ComparisonSession {
schema: COMPARISON_SCHEMA.to_string(),
version: COMPARISON_VERSION,
session: SessionHeader {
uid: "s1".to_string(),
date: "2026-07-12".to_string(),
audience: None,
},
judgements,
tombstones: Vec::new(),
};
let anchor = || {
let mut j = jrow("anch", "A", "unused", Response::PreferA, RaterKind::Human);
j.b = None;
j.response = None;
j.form = RowForm::Anchor;
j.frame = FRAME_VALUE_ANCHOR.to_string();
j.magnitude = Some(6.0);
j
};
let pipeline_of = |judgements: Vec<Judgement>| {
pipeline_from_sessions(
&[session_of(judgements)],
&StatusMap::new(),
&AnchorMap::new(),
&VALUE_PROJECTION_PARAMS,
&VALUE_PROJECTION_PARAMS,
&std::collections::BTreeMap::new(),
1.0,
0.65,
)
.expect("pipeline ok")
};
let with_anchor = pipeline_of(vec![win("j0", "A", "C"), win("j1", "B", "D"), anchor()]);
let without_anchor = pipeline_of(vec![win("j0", "A", "C"), win("j1", "B", "D")]);
assert!(
!with_anchor.value_claims.anchored.is_empty(),
"non-vacuous: the claim was captured"
);
fn inputs_of(p: &crate::comparison::Pipeline) -> ElicitInputs<'_> {
mk(
p.active_pairwise().iter().collect(),
&[],
&["A", "B"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0)],
&[],
)
}
let inputs_with = inputs_of(&with_anchor);
let inputs_without = inputs_of(&without_anchor);
assert_eq!(
compile(
&inputs_with.active,
&inputs_with.anchors,
QuarantinePolicy::Symmetric
),
compile(
&inputs_without.active,
&inputs_without.anchors,
QuarantinePolicy::Symmetric
),
"anchor rows never reach the assemble baseline compile"
);
let q_with = assemble(&inputs_with, seq(2));
assert_eq!(q_with, assemble(&inputs_without, seq(2)));
assert_eq!(q_with.entries.len(), 1, "a real candidate rode the proof");
}
#[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 { .. }
| EntryPayload::SizingProbe { .. }
| EntryPayload::ClaimReprobe { .. } => 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");
}
}
#[test]
fn demote_reopens_agent_only_determined_pair() {
let rows = vec![win_agent("g0", "A", "B")];
let refs: Vec<&Judgement> = rows.iter().collect();
let mut inputs = mk(
refs.clone(),
&[],
&["A", "B"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0)],
&[],
);
let off = assemble(&inputs, seq(2));
assert_eq!(
off.state,
QueueState::Stable { depth: 2 },
"knob-off: the agent row retires the pair"
);
assert!(off.entries.is_empty());
inputs.demote_agent_evidence = true;
let on = assemble(&inputs, seq(2));
assert_eq!(
on.state,
QueueState::Candidates,
"knob-on: agent evidence proposes, never retires (INV-2)"
);
assert_eq!(on.entries.len(), 1);
assert_eq!(on.entries[0].kind, CandidateKind::Comparison);
assert!(
on.entries[0].guaranteed_yield >= 1,
"a fresh answer closes the pair in the human system"
);
}
#[test]
fn anchored_pair_stays_determined_both_knob_states() {
let rows = vec![jrow(
"h0",
"A",
"B",
Response::Incomparable,
RaterKind::Human,
)];
let refs: Vec<&Judgement> = rows.iter().collect();
let mut inputs = mk(
refs.clone(),
&[("A", 2.0), ("B", 1.0)],
&["A", "B"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0)],
&[],
);
let off = assemble(&inputs, seq(2));
assert_eq!(off.state, QueueState::Stable { depth: 2 });
assert!(off.entries.is_empty());
inputs.demote_agent_evidence = true;
let on = assemble(&inputs, seq(2));
assert_eq!(
on.state,
QueueState::Stable { depth: 2 },
"knob-on: anchors still determine — human authority"
);
assert!(on.entries.is_empty());
}
#[test]
fn human_determined_pair_survives_knob_on() {
let rows = vec![win("h0", "A", "B"), win_agent("g0", "A", "B")];
let refs: Vec<&Judgement> = rows.iter().collect();
let mut inputs = mk(
refs.clone(),
&[],
&["A", "B"],
&[("A", 1.0, 1.0), ("B", 1.0, 1.0)],
&[],
);
inputs.demote_agent_evidence = true;
let on = assemble(&inputs, seq(2));
assert_eq!(on.state, QueueState::Stable { depth: 2 });
assert!(on.entries.is_empty());
}
use crate::catalog::scan::ScanMode;
use crate::comparison::CostFeed;
use crate::priority::graph::{self, PriorityGraph};
use crate::relation_graph::{self, EntityKey};
fn write_file(root: &std::path::Path, rel: &str, body: &str) {
let path = root.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
fn seed_costed_issue(root: &std::path::Path, id: u32, estimate: &str) {
write_file(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"i\"\ntitle = \"I\"\nkind = \"issue\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[estimate]\n{estimate}\n[value]\nvalue = 5.0\n"
),
);
write_file(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.md"),
"b\n",
);
}
fn write_est_session(root: &std::path::Path) {
write_file(
root,
".doctrine/comparisons/2026-07-11-e1.toml",
"schema = \"doctrine.comparison-session\"\nversion = 2\n\n\
[session]\nuid = \"e1\"\ndate = \"2026-07-11\"\n\n\
[[judgement]]\nuid = \"e1-row\"\nseq = 0\na = \"ISS-002\"\nb = \"ISS-001\"\n\
response = \"prefer-a\"\ndomain = \"estimate\"\nframe = \"more-work\"\n\
form = \"order\"\nrater = \"agent\"\ndate = \"2026-07-11\"\n\n\
# PHASE-06: anchor-form claim row for ISS-001 (bounds 2..6)\n\
[[judgement]]\nuid = \"iss001-anchor\"\nseq = 100\na = \"ISS-001\"\n\
domain = \"estimate\"\nframe = \"cost-anchor\"\n\
form = \"anchor\"\nrater = \"human\"\ndate = \"2026-07-17\"\n\
est_lower = 2.0\nest_upper = 6.0\n",
);
}
fn costing_of(pg: &PriorityGraph, id: u32) -> (f64, f64) {
let cfg = crate::priority::config::PriorityConfig::default();
let (m, c, _) = pg
.item_costing(&EntityKey { prefix: "ISS", id }, &cfg)
.unwrap();
(m, c)
}
#[test]
fn empty_cost_feed_is_identical_graph_build() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
seed_costed_issue(root, 1, "lower = 2.0\nupper = 6.0");
seed_costed_issue(root, 2, "");
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = crate::priority::config::load(root);
let via_load = graph::build_from(&scanned, root).unwrap();
let bare_anchor = scanned
.iter()
.filter(|entity| {
crate::priority::partition::status_class(entity.kind, entity.status.as_deref())
!= crate::priority::partition::StatusClass::Terminal
})
.map(|_| 1.0)
.next()
.unwrap_or(1.0);
let explicit_empty = graph::build_from_with_cfg(
&scanned,
root,
&cfg,
&Projection::new(),
&CostFeed::new(),
&ClaimResolution::default(),
bare_anchor,
&crate::comparison::ClaimResolutionGeneric::<
crate::comparison::EstimatePayload,
>::default(),
)
.unwrap();
assert_eq!(via_load.score, explicit_empty.score, "score map identical");
for id in [1, 2] {
assert_eq!(
costing_of(&via_load, id),
costing_of(&explicit_empty, id),
"ISS-{id:03} costing identical under the empty feed"
);
}
assert_eq!(costing_of(&via_load, 2).1, via_load.cost_ctx.absent);
}
#[test]
fn authored_range_edit_rederives_cost_feed_into_eff_weights() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
seed_costed_issue(root, 1, "lower = 2.0\nupper = 6.0");
seed_costed_issue(root, 2, "");
write_est_session(root);
let before = graph::build(root).unwrap();
let (m1, c1) = costing_of(&before, 2);
assert!((c1 - 4.85).abs() < 1e-9, "fed, not bare-anchored: {c1}");
seed_costed_issue(root, 1, "lower = 2.0\nupper = 10.0");
std::fs::write(
root.join(".doctrine/comparisons/2026-07-11-e1.toml"),
"schema = \"doctrine.comparison-session\"\nversion = 2\n\n\
[session]\nuid = \"e1\"\ndate = \"2026-07-11\"\n\n\
[[judgement]]\nuid = \"e1-row\"\nseq = 0\na = \"ISS-002\"\nb = \"ISS-001\"\n\
response = \"prefer-a\"\ndomain = \"estimate\"\nframe = \"more-work\"\n\
form = \"order\"\nrater = \"agent\"\ndate = \"2026-07-11\"\n\n\
[[judgement]]\nuid = \"iss001-anchor\"\nseq = 100\na = \"ISS-001\"\n\
domain = \"estimate\"\nframe = \"cost-anchor\"\n\
form = \"anchor\"\nrater = \"human\"\ndate = \"2026-07-17\"\n\
est_lower = 2.0\nest_upper = 10.0\n",
)
.unwrap();
let after = graph::build(root).unwrap();
let (m2, c2) = costing_of(&after, 2);
assert!((c2 - 7.45).abs() < 1e-9, "fed cost re-derived: {c2}");
assert!(
(c2 - after.cost_ctx.absent).abs() > 1e-9,
"still projected, not the (also-moved) bare anchor"
);
assert_eq!(m1, m2, "multiplier is cost-free");
assert!((m1 * c1 - m2 * c2).abs() > 1e-9, "eff-weight re-ran");
}
#[test]
fn beta_edit_rederives_cost_feed() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
seed_costed_issue(root, 1, "lower = 2.0\nupper = 6.0");
seed_costed_issue(root, 2, "");
write_est_session(root);
let before = graph::build(root).unwrap();
assert!((costing_of(&before, 2).1 - 4.85).abs() < 1e-9);
write_file(
root,
crate::dtoml::DOCTRINE_TOML,
"[priority.estimate]\nskew = 0.0\n",
);
let after = graph::build(root).unwrap();
assert!(
(costing_of(&after, 2).1 - 2.25).abs() < 1e-9,
"β edit re-derived the fed cost"
);
}
fn unsized_inputs<'a>(
frontier: &[&str],
bare: &[&str],
target_pool: &[(&str, f64)],
evidenced: &[&str],
) -> ElicitInputs<'a> {
let costing: Vec<(&str, f64, f64)> = frontier.iter().map(|&id| (id, 1.0, 5.0)).collect();
let mut inputs = mk(vec![], &[], frontier, &costing, &[]);
for id in bare {
if let Some(c) = inputs.costing.get_mut(*id) {
c.bare_estimate = true;
}
}
inputs.sizing = sizing(target_pool, evidenced);
inputs
}
fn probe_of<'q>(q: &'q ElicitQueue) -> Option<&'q QueueEntry> {
q.entries
.iter()
.find(|e| e.kind == CandidateKind::SizingProbe)
}
#[test]
fn sizing_probe_surfaces_for_unsized_subject_and_gates_candidates() {
let inputs = unsized_inputs(&["S1", "T1"], &["S1"], &[("T1", 2.0)], &[]);
let q = assemble(&inputs, seq(2));
assert_eq!(q.state, QueueState::Candidates, "a probe gates Candidates");
let probe = probe_of(&q).expect("a sizing probe");
assert_eq!(probe.guaranteed_yield, 0);
assert!(
probe.score.total_cmp(&0.0).is_eq(),
"existence admission, score 0"
);
assert_eq!(probe.yield_basis, YieldBasis::Calibration);
match &probe.payload {
EntryPayload::SizingProbe { subject, target } => {
assert_eq!(subject.id, "S1");
assert_eq!(target.id, "T1");
assert!((target.estimate - 2.0).abs() < 1e-9);
}
_ => panic!("expected a sizing-probe payload"),
}
assert_eq!(q.sizing_residual, 0);
assert!(!q.sizing_no_calibration_target);
}
#[test]
fn sizing_probe_incomparable_row_retires_the_probe() {
let inputs = unsized_inputs(&["S1", "T1"], &["S1"], &[("T1", 2.0)], &["S1"]);
let q = assemble(&inputs, seq(2));
assert!(
probe_of(&q).is_none(),
"est-evidenced subject is not re-probed"
);
assert_eq!(
q.sizing_residual, 1,
"declined subject disclosed as residual"
);
}
#[test]
fn sizing_target_is_the_lower_middle_median_authored_estimate() {
let odd = unsized_inputs(
&["S1", "TA", "TB", "TC"],
&["S1"],
&[("TA", 1.0), ("TB", 2.0), ("TC", 3.0)],
&[],
);
let q = assemble(&odd, seq(4));
let EntryPayload::SizingProbe { target, .. } = &probe_of(&q).unwrap().payload else {
panic!("probe payload");
};
assert_eq!(target.id, "TB", "odd median = the middle");
let even = unsized_inputs(
&["S1", "TA", "TB", "TC", "TD"],
&["S1"],
&[("TA", 1.0), ("TB", 2.0), ("TC", 3.0), ("TD", 4.0)],
&[],
);
let q = assemble(&even, seq(5));
let EntryPayload::SizingProbe { target, .. } = &probe_of(&q).unwrap().payload else {
panic!("probe payload");
};
assert_eq!(target.id, "TB", "even count ⇒ lower-cost middle");
let tie = unsized_inputs(
&["S1", "TB", "TA"],
&["S1"],
&[("TA", 2.0), ("TB", 2.0)],
&[],
);
let q = assemble(&tie, seq(3));
let EntryPayload::SizingProbe { target, .. } = &probe_of(&q).unwrap().payload else {
panic!("probe payload");
};
assert_eq!(target.id, "TA", "ties break id-lexicographic");
}
#[test]
fn sizing_target_falls_back_to_all_estimated_then_none() {
let tier1 = unsized_inputs(
&["S1"],
&["S1"],
&[("TA", 1.0), ("TB", 2.0), ("TC", 3.0)],
&[],
);
let q = assemble(&tier1, seq(1));
let EntryPayload::SizingProbe { target, .. } = &probe_of(&q).unwrap().payload else {
panic!("probe payload");
};
assert_eq!(
target.id, "TB",
"tier-1 fallback medians over all estimated"
);
let tier2 = unsized_inputs(&["S1", "S2"], &["S1", "S2"], &[], &[]);
let q = assemble(&tier2, seq(2));
assert!(probe_of(&q).is_none(), "no target ⇒ no probe");
assert!(q.sizing_no_calibration_target, "tier-2 disclosure flag set");
assert_eq!(q.sizing_residual, 2, "both un-sized subjects disclosed");
}
#[test]
fn sizing_probes_sink_below_yield_motivated_and_order_by_id() {
let rows = vec![win("j0", "A", "C"), win("j1", "B", "D")];
let refs: Vec<&Judgement> = rows.iter().collect();
let mut inputs = mk(
refs,
&[],
&["A", "B", "S2", "S1", "T1"],
&[
("A", 1.0, 1.0),
("B", 1.0, 1.0),
("S2", 1.0, 5.0),
("S1", 1.0, 5.0),
("T1", 1.0, 2.0),
],
&[],
);
for id in ["S1", "S2"] {
inputs.costing.get_mut(id).unwrap().bare_estimate = true;
}
inputs.sizing = sizing(&[("T1", 2.0)], &[]);
let q = assemble(&inputs, seq(5));
assert_eq!(
q.entries.first().map(|e| e.kind),
Some(CandidateKind::Comparison),
"the yield-motivated comparison ranks first"
);
let probe_subjects: Vec<&str> = q
.entries
.iter()
.filter_map(|e| match &e.payload {
EntryPayload::SizingProbe { subject, .. } => Some(subject.id.as_str()),
_ => None,
})
.collect();
assert_eq!(
probe_subjects,
vec!["S1", "S2"],
"probes ordered by subject id"
);
let first_probe = q
.entries
.iter()
.position(|e| e.kind == CandidateKind::SizingProbe)
.unwrap();
assert!(
first_probe > 0,
"probes sink below the positive-score entry"
);
}
#[test]
fn sizing_residual_discloses_on_stable_without_gating() {
let mut inputs = unsized_inputs(&["S1"], &["S1"], &[("T1", 2.0)], &["S1"]);
inputs.costing.get_mut("S1").unwrap().multiplier = 0.0;
let q = assemble(&inputs, seq(1));
assert_eq!(
q.state,
QueueState::Stable { depth: 1 },
"residual never gates"
);
assert!(probe_of(&q).is_none());
assert_eq!(
q.sizing_residual, 1,
"unresolved sizing disclosed on Stable"
);
}
#[test]
fn facet_only_target_is_not_valid_probe_target() {
let mut inputs = unsized_inputs(&["S1"], &["S1"], &[("T1", 2.0)], &[]);
inputs.sizing.estimate_anchored_targets.clear();
let q = assemble(&inputs, seq(2));
assert!(probe_of(&q).is_none(), "facet-only target: no probe minted");
assert!(
q.sizing_no_calibration_target,
"facet-only corpus triggers the no-target state detail"
);
assert_eq!(q.sizing_residual, 1, "S1 disclosed as residual");
}
#[test]
fn anchored_claim_target_is_valid_probe_target() {
let inputs = unsized_inputs(&["S1"], &["S1"], &[("T1", 2.0)], &[]);
let q = assemble(&inputs, seq(2));
let probe = probe_of(&q).expect("a sizing probe is minted");
let EntryPayload::SizingProbe { target, .. } = &probe.payload else {
panic!("sizing-probe payload");
};
assert_eq!(target.id, "T1", "anchored-claim target selected");
assert!(!q.sizing_no_calibration_target);
}
#[test]
fn zero_anchored_claims_fires_remedy_detail() {
let inputs = unsized_inputs(&["S1", "S2"], &["S1", "S2"], &[], &[]);
let q = assemble(&inputs, seq(2));
assert!(probe_of(&q).is_none(), "no target ⇒ no probe");
assert!(
q.sizing_no_calibration_target,
"remedy detail fires: no estimated item to calibrate against"
);
}
#[test]
fn unmigrated_facet_counts_as_sized_knob_independently() {
let rows = vec![win("j0", "A", "C"), win("j1", "B", "D")];
let refs: Vec<&Judgement> = rows.iter().collect();
let mut inputs = mk(
refs.clone(),
&[],
&["A", "B", "S1", "T1"],
&[
("A", 1.0, 1.0),
("B", 1.0, 1.0),
("S1", 1.0, 5.0),
("T1", 1.0, 2.0),
],
&[],
);
inputs.costing.get_mut("S1").unwrap().bare_estimate = true;
inputs.sizing = sizing(&[("T1", 2.0)], &[]);
for knob in [false, true] {
inputs.demote_agent_evidence = knob;
let q = assemble(&inputs, seq(4));
let probes_s1: Vec<&str> = q
.entries
.iter()
.filter_map(|e| match &e.payload {
EntryPayload::SizingProbe { subject, .. } if subject.id == "S1" => {
Some(subject.id.as_str())
}
_ => None,
})
.collect();
assert_eq!(probes_s1.len(), 1, "S1 is probed (bare, knob={knob})");
let probes_t1: Vec<&str> = q
.entries
.iter()
.filter_map(|e| match &e.payload {
EntryPayload::SizingProbe { subject, .. } if subject.id == "T1" => {
Some(subject.id.as_str())
}
_ => None,
})
.collect();
assert_eq!(
probes_t1.len(),
0,
"T1 is NOT probed (sized via facet, knob={knob})"
);
}
}
fn write_est_row(root: &std::path::Path, a: &str, b: &str, response: &str) {
write_file(
root,
".doctrine/comparisons/2026-07-11-e1.toml",
&format!(
"schema = \"doctrine.comparison-session\"\nversion = 2\n\n\
[session]\nuid = \"e1\"\ndate = \"2026-07-11\"\n\n\
[[judgement]]\nuid = \"e1-row\"\nseq = 0\na = \"{a}\"\nb = \"{b}\"\n\
response = \"{response}\"\ndomain = \"estimate\"\nframe = \"more-work\"\n\
form = \"order\"\nrater = \"agent\"\ndate = \"2026-07-11\"\n\n\
# PHASE-06: anchor-form claim row for {b} (bounds 2..6)\n\
[[judgement]]\nuid = \"target-anchor\"\nseq = 100\na = \"{b}\"\n\
domain = \"estimate\"\nframe = \"cost-anchor\"\n\
form = \"anchor\"\nrater = \"human\"\ndate = \"2026-07-17\"\n\
est_lower = 2.0\nest_upper = 6.0\n",
),
);
}
#[test]
fn sizing_probe_prefer_answer_projects_subject_non_gauge() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
seed_costed_issue(root, 1, "lower = 2.0\nupper = 6.0"); seed_costed_issue(root, 2, ""); write_est_row(root, "ISS-002", "ISS-001", "prefer-a");
let pipeline = graph::load_comparison_pipeline_for_root(root).unwrap();
let (cost, prov) = pipeline
.estimate
.projection
.get("ISS-002")
.copied()
.expect("subject placed in the est projection");
assert_eq!(
prov,
ValueProvenance::Projected,
"prefer ⇒ Projected placement"
);
assert!(cost > 0.0);
let pg = graph::build(root).unwrap();
let fed = costing_of(&pg, 2).1;
assert!(
(fed - pg.cost_ctx.absent).abs() > 1e-9,
"fed a projected cost, not the bare-anchor fallthrough"
);
}
#[test]
fn sizing_probe_equal_answer_merges_subject_authored_via_class() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
seed_costed_issue(root, 1, "lower = 2.0\nupper = 6.0"); seed_costed_issue(root, 2, "");
write_est_row(root, "ISS-002", "ISS-001", "equal");
let pipeline = graph::load_comparison_pipeline_for_root(root).unwrap();
let (cost, prov) = pipeline
.estimate
.projection
.get("ISS-002")
.copied()
.expect("subject merged into the est projection");
assert_eq!(
prov,
ValueProvenance::Authored,
"equal ⇒ anchored-class membership, provenance Authored"
);
assert!(
(cost - 4.6).abs() < 1e-9,
"the merged member sits at the class anchor cost, not gauge/bare"
);
}
fn anchor_claim(uid: &str, item: &str, magnitude: f64, rater: RaterKind) -> Judgement {
use crate::comparison::{DOMAIN_VALUE, FRAME_VALUE_ANCHOR};
let migrated = matches!(rater, RaterKind::Migrated);
let mut j = jrow(uid, item, "unused", Response::PreferA, rater);
j.b = None;
j.response = None;
j.form = RowForm::Anchor;
j.domain = DOMAIN_VALUE.to_string();
j.frame = FRAME_VALUE_ANCHOR.to_string();
j.magnitude = Some(magnitude);
if migrated {
j.observed_at = j.date.take();
}
j
}
fn claims_from(rows: &[Judgement]) -> ClaimResolution {
let tagged: Vec<(&Judgement, crate::comparison::ResolutionStatus)> = rows
.iter()
.map(|j| (j, crate::comparison::ResolutionStatus::Active))
.collect();
crate::comparison::resolve_claims(&tagged)
}
fn has_median_probe(q: &ElicitQueue, id: &str) -> bool {
q.entries.iter().any(|e| {
e.reasons.iter().any(|r| r.code == "median-probe")
&& matches!(
&e.payload,
EntryPayload::Comparison { a, b, .. } if a.id == id || b.id == id
)
})
}
#[test]
fn prior_valued_item_probe_retired_knob_off_eligible_knob_on() {
let rows = vec![win("j0", "W", "Z")];
let refs: Vec<&Judgement> = rows.iter().collect();
let claim_rows = [anchor_claim("a1", "U", 3.0, RaterKind::Agent)];
let mut 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),
],
);
inputs.claims = claims_from(&claim_rows);
assert!(inputs.claims.priors.contains_key("U"), "prior resolved");
let off = assemble(&inputs, seq(2));
assert!(
!has_median_probe(&off, "U"),
"knob-off: the prior retires the probe (valued for queue purposes)"
);
inputs.demote_agent_evidence = true;
let on = assemble(&inputs, seq(2));
assert!(
has_median_probe(&on, "U"),
"knob-on: rungs 3–5 do not retire elicitation"
);
}
#[test]
fn facet_valued_item_probe_retired_knob_off_eligible_knob_on() {
let rows = vec![win("j0", "W", "Z")];
let refs: Vec<&Judgement> = rows.iter().collect();
let mut 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),
],
);
inputs.facet_valued.insert("U".to_string());
let off = assemble(&inputs, seq(2));
assert!(!has_median_probe(&off, "U"), "knob-off: facet retires");
inputs.demote_agent_evidence = true;
let on = assemble(&inputs, seq(2));
assert!(has_median_probe(&on, "U"), "knob-on: facet stays eligible");
}
#[test]
fn anchored_claim_retires_probe_knob_independently() {
let rows = vec![win("j0", "W", "Z")];
let refs: Vec<&Judgement> = rows.iter().collect();
let claim_rows = [anchor_claim("h1", "U", 6.0, RaterKind::Human)];
let mut 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),
],
);
inputs.claims = claims_from(&claim_rows);
assert!(inputs.claims.anchored.contains_key("U"));
for knob in [false, true] {
inputs.demote_agent_evidence = knob;
let q = assemble(&inputs, seq(2));
assert!(
!has_median_probe(&q, "U"),
"an anchored claim retires the probe (knob = {knob})"
);
}
}
#[test]
fn anchored_conflict_enters_reprobe_queue_knob_independently_agent_never() {
let claim_rows = [
anchor_claim("h1", "U", 4.0, RaterKind::Human),
anchor_claim("h2", "U", 6.0, RaterKind::Human),
anchor_claim("a1", "V", 1.0, RaterKind::Agent),
anchor_claim("a2", "V", 3.0, RaterKind::Agent),
];
let claims = claims_from(&claim_rows);
assert_eq!(claims.findings.len(), 2, "conflicts fire at EVERY tier");
let mut inputs = mk(vec![], &[], &[], &[], &[]);
inputs.claims = claims;
for knob in [false, true] {
inputs.demote_agent_evidence = knob;
let q = assemble(&inputs, seq(2));
let reprobes: Vec<&QueueEntry> = q
.entries
.iter()
.filter(|e| e.kind == CandidateKind::ClaimReprobe)
.collect();
assert_eq!(
reprobes.len(),
1,
"exactly the anchored conflict (knob = {knob})"
);
let EntryPayload::ClaimReprobe {
subject,
tier,
mean,
low,
high,
distinct,
rows,
} = &reprobes[0].payload
else {
panic!("claim-reprobe payload");
};
assert_eq!(subject, "U", "the agent conflict (V) never enters");
assert_eq!(*tier, ClaimTier::Human);
assert_eq!((*mean, *low, *high), (5.0, 4.0, 6.0));
assert_eq!((*distinct, *rows), (2, 2));
assert_eq!(reprobes[0].score, 0.0, "existence-admitted, zero yield");
assert_eq!(reprobes[0].yield_basis, YieldBasis::Calibration);
}
}
#[test]
fn contested_pin_mints_a_reprobe_named_contested_pin() {
let mut p1 = anchor_claim("p1", "U", 3.0, RaterKind::Human);
p1.admission = Some(crate::comparison::AdmissionKind::Pin);
let mut p2 = anchor_claim("p2", "U", 9.0, RaterKind::Human);
p2.admission = Some(crate::comparison::AdmissionKind::Pin);
let mut inputs = mk(vec![], &[], &[], &[], &[]);
inputs.claims = claims_from(&[p1, p2]);
let q = assemble(&inputs, seq(2));
let entry = q
.entries
.iter()
.find(|e| e.kind == CandidateKind::ClaimReprobe)
.expect("the contested pin reprobes");
assert!(
entry
.reasons
.iter()
.any(|r| r.text.contains("contested pin")),
"{:?}",
entry.reasons
);
assert!(matches!(
&entry.payload,
EntryPayload::ClaimReprobe {
tier: ClaimTier::Pin,
..
}
));
}
}