use std::collections::BTreeMap;
use super::compile::AnchorMap;
use super::resolve::ResolutionStatus;
use super::{AdmissionKind, DOMAIN_VALUE, Judgement, RaterKind, RowForm};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum ClaimTier {
Migrated,
Agent,
Human,
Pin,
}
impl ClaimTier {
pub(crate) fn is_anchored(self) -> bool {
matches!(self, ClaimTier::Pin | ClaimTier::Human)
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ResolvedClaim {
pub value: f64,
pub tier: ClaimTier,
pub conflict: Option<ClaimConflict>,
pub rows: u32,
pub attribution: Option<ClaimAttribution>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct ClaimAttribution {
pub by: Option<String>,
pub date: Option<String>,
pub observed_at: Option<String>,
pub basis: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ClaimConflict {
pub low: f64,
pub high: f64,
pub distinct: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ClaimFinding {
Conflict {
domain: String,
item: String,
tier: ClaimTier,
low: f64,
high: f64,
distinct: u32,
rows: u32,
},
}
impl ClaimFinding {
pub(crate) fn nominates_reprobe(&self) -> bool {
let ClaimFinding::Conflict { tier, .. } = self;
tier.is_anchored()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct ClaimResolution {
pub anchored: BTreeMap<String, ResolvedClaim>,
pub priors: BTreeMap<String, ResolvedClaim>,
pub lensed: BTreeMap<(String, String), ResolvedClaim>,
pub findings: Vec<ClaimFinding>,
}
impl ClaimResolution {
pub(crate) fn anchor_map(&self) -> AnchorMap {
self.anchored
.iter()
.map(|(item, claim)| (item.clone(), claim.value))
.collect()
}
}
struct ClaimRow {
tier: ClaimTier,
magnitude: f64,
attribution: ClaimAttribution,
}
pub(crate) fn resolve_claims(rows: &[(&Judgement, ResolutionStatus)]) -> ClaimResolution {
let mut unlensed: BTreeMap<String, Vec<ClaimRow>> = BTreeMap::new();
let mut lensed_groups: BTreeMap<(String, String), Vec<ClaimRow>> = BTreeMap::new();
for (j, status) in rows {
if j.domain != DOMAIN_VALUE
|| !matches!(j.form, RowForm::Anchor)
|| !matches!(
status,
ResolutionStatus::Active | ResolutionStatus::InertLens
)
{
continue;
}
let Some(magnitude) = j.magnitude else {
continue;
};
let row = ClaimRow {
tier: tier_of(j),
magnitude,
attribution: ClaimAttribution {
by: j.by.clone(),
date: j.date.clone(),
observed_at: j.observed_at.clone(),
basis: j.basis.clone(),
},
};
match &j.lens {
Some(lens) => lensed_groups
.entry((lens.clone(), j.a.clone()))
.or_default()
.push(row),
None => unlensed.entry(j.a.clone()).or_default().push(row),
}
}
let mut out = ClaimResolution::default();
for (item, group) in unlensed {
let claim = resolve_group(&group);
if let Some(conflict) = &claim.conflict {
out.findings.push(ClaimFinding::Conflict {
domain: DOMAIN_VALUE.to_string(),
item: item.clone(),
tier: claim.tier,
low: conflict.low,
high: conflict.high,
distinct: conflict.distinct,
rows: claim.rows,
});
}
if claim.tier.is_anchored() {
out.anchored.insert(item, claim);
} else {
out.priors.insert(item, claim);
}
}
for (key, group) in lensed_groups {
out.lensed.insert(key, resolve_group(&group));
}
out
}
fn tier_of(j: &Judgement) -> ClaimTier {
match (&j.admission, &j.rater) {
(Some(AdmissionKind::Pin), _) => ClaimTier::Pin,
(None, RaterKind::Human) => ClaimTier::Human,
(None, RaterKind::Agent) => ClaimTier::Agent,
(None, RaterKind::Migrated) => ClaimTier::Migrated,
}
}
fn resolve_group(group: &[ClaimRow]) -> ResolvedClaim {
let winning = group
.iter()
.map(|r| r.tier)
.max()
.unwrap_or(ClaimTier::Migrated); let winning_rows: Vec<&ClaimRow> = group.iter().filter(|r| r.tier == winning).collect();
let attribution = match winning_rows.as_slice() {
[only] => Some(only.attribution.clone()),
_ => None,
};
let mut magnitudes: Vec<f64> = winning_rows.iter().map(|r| r.magnitude).collect();
magnitudes.sort_by(f64::total_cmp);
let rows = u32::try_from(magnitudes.len()).unwrap_or(u32::MAX);
let mut distinct = 0_u32;
let mut prev: Option<f64> = None;
for &m in &magnitudes {
if prev.is_none_or(|p| p.total_cmp(&m).is_ne()) {
distinct += 1;
}
prev = Some(m);
}
let (value, conflict) = match (magnitudes.first(), magnitudes.last()) {
(Some(&low), Some(&high)) if distinct > 1 => {
let mean = magnitudes.iter().sum::<f64>() / f64::from(rows);
(
mean,
Some(ClaimConflict {
low,
high,
distinct,
}),
)
}
(Some(&only), _) => (only, None),
_ => (0.0, None), };
ResolvedClaim {
value,
tier: winning,
conflict,
rows,
attribution,
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::{ClaimFinding, ClaimResolution, ClaimTier, resolve_claims};
use crate::comparison::resolve::{ResolutionStatus, StatusMap, resolve};
use crate::comparison::{
AdmissionKind, COMPARISON_SCHEMA, COMPARISON_VERSION, ComparisonSession,
FRAME_VALUE_ANCHOR, Judgement, RaterKind, RowForm, SessionHeader,
};
fn anchor(uid: &str, item: &str, magnitude: f64, rater: RaterKind) -> Judgement {
let migrated = matches!(rater, RaterKind::Migrated);
Judgement {
uid: uid.to_string(),
seq: 0,
a: item.to_string(),
b: None,
response: None,
domain: crate::comparison::DOMAIN_VALUE.to_string(),
frame: FRAME_VALUE_ANCHOR.to_string(),
form: RowForm::Anchor,
magnitude: Some(magnitude),
supersedes: None,
lens: None,
rater,
by: None,
note: None,
date: (!migrated).then(|| "2026-07-16".to_string()),
observed_at: migrated.then(|| "2026-07-16".to_string()),
basis: None,
admission: None,
}
}
fn pin(uid: &str, item: &str, magnitude: f64) -> Judgement {
let mut j = anchor(uid, item, magnitude, RaterKind::Human);
j.admission = Some(AdmissionKind::Pin);
j
}
fn lensed(uid: &str, item: &str, magnitude: f64, lens: &str) -> Judgement {
let mut j = anchor(uid, item, magnitude, RaterKind::Human);
j.lens = Some(lens.to_string());
j
}
fn active(rows: &[Judgement]) -> Vec<(&Judgement, ResolutionStatus)> {
rows.iter()
.map(|j| {
let status = if j.lens.is_some() {
ResolutionStatus::InertLens
} else {
ResolutionStatus::Active
};
(j, status)
})
.collect()
}
fn session(uid: &str, judgements: Vec<Judgement>) -> ComparisonSession {
ComparisonSession {
schema: COMPARISON_SCHEMA.to_string(),
version: COMPARISON_VERSION,
session: SessionHeader {
uid: uid.to_string(),
date: "2026-07-16".to_string(),
audience: None,
},
judgements,
tombstones: Vec::new(),
}
}
fn conflict_of<'a>(claims: &'a ClaimResolution, item: &str) -> &'a ClaimFinding {
claims
.findings
.iter()
.find(|f| {
let ClaimFinding::Conflict { item: i, .. } = f;
i == item
})
.expect("conflict finding present")
}
#[test]
fn pin_outranks_all_tiers_under_derived_ord() {
assert!(ClaimTier::Migrated < ClaimTier::Agent);
assert!(ClaimTier::Agent < ClaimTier::Human);
assert!(ClaimTier::Human < ClaimTier::Pin);
let all = [
ClaimTier::Pin,
ClaimTier::Migrated,
ClaimTier::Human,
ClaimTier::Agent,
];
assert_eq!(all.iter().max(), Some(&ClaimTier::Pin));
}
#[test]
fn resolution_is_invariant_under_row_permutation() {
let rows = [
anchor("h1", "SL-100", 5.0, RaterKind::Human),
anchor("h2", "SL-100", 5.0, RaterKind::Human),
anchor("h3", "SL-100", 7.0, RaterKind::Human),
anchor("a1", "SL-200", 2.0, RaterKind::Agent),
anchor("m1", "SL-300", 1.0, RaterKind::Migrated),
lensed("l1", "SL-100", 9.0, "user-value"),
];
let forward = active(&rows);
let baseline = resolve_claims(&forward);
let mut reversed = forward.clone();
reversed.reverse();
assert_eq!(resolve_claims(&reversed), baseline);
let mut rotated = forward.clone();
rotated.rotate_left(3);
assert_eq!(resolve_claims(&rotated), baseline);
}
#[test]
fn corroborating_rows_resolve_without_conflict() {
let rows = [
anchor("h1", "SL-100", 5.0, RaterKind::Human),
anchor("h2", "SL-100", 5.0, RaterKind::Human),
anchor("h3", "SL-100", 5.0, RaterKind::Human),
];
let claims = resolve_claims(&active(&rows));
let claim = &claims.anchored["SL-100"];
assert_eq!(claim.value, 5.0);
assert_eq!(claim.tier, ClaimTier::Human);
assert_eq!(claim.conflict, None);
assert_eq!(claim.rows, 3);
assert!(claims.findings.is_empty());
}
#[test]
fn conflict_takes_the_multiset_mean_with_interval_and_distinct() {
let rows = [
anchor("h1", "SL-100", 5.0, RaterKind::Human),
anchor("h2", "SL-100", 5.0, RaterKind::Human),
anchor("h3", "SL-100", 7.0, RaterKind::Human),
];
let claims = resolve_claims(&active(&rows));
let claim = &claims.anchored["SL-100"];
assert_eq!(claim.value, 17.0 / 3.0, "multiset mean, not deduped");
let conflict = claim.conflict.as_ref().expect("conflict present");
assert_eq!((conflict.low, conflict.high), (5.0, 7.0));
assert_eq!(conflict.distinct, 2);
assert_eq!(claim.rows, 3);
assert_eq!(
conflict_of(&claims, "SL-100"),
&ClaimFinding::Conflict {
domain: "value".to_string(),
item: "SL-100".to_string(),
tier: ClaimTier::Human,
low: 5.0,
high: 7.0,
distinct: 2,
rows: 3,
}
);
}
#[test]
fn conflicting_pins_raise_a_contested_pin_finding() {
let rows = [pin("p1", "SL-100", 3.0), pin("p2", "SL-100", 9.0)];
let claims = resolve_claims(&active(&rows));
let claim = &claims.anchored["SL-100"];
assert_eq!(claim.tier, ClaimTier::Pin);
assert_eq!(claim.value, 6.0);
let finding = conflict_of(&claims, "SL-100");
assert!(
matches!(
finding,
ClaimFinding::Conflict {
tier: ClaimTier::Pin,
..
}
),
"contested pin carries its tier: {finding:?}"
);
assert!(finding.nominates_reprobe());
}
#[test]
fn reprobe_nomination_is_anchored_tiers_only() {
let rows = [
anchor("h1", "SL-100", 4.0, RaterKind::Human),
anchor("h2", "SL-100", 6.0, RaterKind::Human),
anchor("a1", "SL-200", 1.0, RaterKind::Agent),
anchor("a2", "SL-200", 3.0, RaterKind::Agent),
anchor("m1", "SL-300", 1.0, RaterKind::Migrated),
anchor("m2", "SL-300", 5.0, RaterKind::Migrated),
];
let claims = resolve_claims(&active(&rows));
assert!(conflict_of(&claims, "SL-100").nominates_reprobe());
assert!(!conflict_of(&claims, "SL-200").nominates_reprobe());
assert!(!conflict_of(&claims, "SL-300").nominates_reprobe());
assert_eq!(claims.findings.len(), 3, "conflicts fire at EVERY tier");
}
#[test]
fn lower_tiers_contribute_nothing_to_a_won_item() {
let rows = [
anchor("h1", "SL-100", 6.0, RaterKind::Human),
anchor("a1", "SL-100", 1.0, RaterKind::Agent),
anchor("a2", "SL-100", 99.0, RaterKind::Agent),
anchor("m1", "SL-100", 42.0, RaterKind::Migrated),
];
let claims = resolve_claims(&active(&rows));
let claim = &claims.anchored["SL-100"];
assert_eq!(claim.value, 6.0);
assert_eq!(claim.conflict, None, "agent disagreement is not a bound");
assert_eq!(claim.rows, 1, "winning-tier rows only");
assert!(!claims.priors.contains_key("SL-100"), "one route per item");
assert!(claims.findings.is_empty());
}
#[test]
fn agent_and_migrated_claims_route_to_priors() {
let rows = [
anchor("a1", "SL-100", 2.0, RaterKind::Agent),
anchor("m1", "SL-200", 3.0, RaterKind::Migrated),
];
let claims = resolve_claims(&active(&rows));
assert!(claims.anchored.is_empty());
assert_eq!(claims.priors["SL-100"].tier, ClaimTier::Agent);
assert_eq!(claims.priors["SL-200"].tier, ClaimTier::Migrated);
}
#[test]
fn cross_session_same_tier_claims_conflict_never_latest_wins() {
let s1 = session("s1", vec![anchor("p", "SL-100", 4.0, RaterKind::Human)]);
let s2 = session("s2", vec![anchor("q", "SL-100", 8.0, RaterKind::Human)]);
let sessions = [s1, s2];
let res = resolve(&sessions, &StatusMap::new()).expect("resolve ok");
let claims = resolve_claims(&res.rows);
let claim = &claims.anchored["SL-100"];
assert_eq!(claim.value, 6.0, "mean of both sessions' claims");
assert!(claim.conflict.is_some(), "concurrency surfaces, no winner");
assert_eq!(claim.rows, 2);
}
#[test]
fn identical_refire_changes_no_value_and_raises_no_conflict() {
let once = [session(
"s1",
vec![anchor("p", "SL-100", 4.0, RaterKind::Human)],
)];
let twice = [
session("s1", vec![anchor("p", "SL-100", 4.0, RaterKind::Human)]),
session("s2", vec![anchor("q", "SL-100", 4.0, RaterKind::Human)]),
];
let res_once = resolve(&once, &StatusMap::new()).expect("resolve ok");
let res_twice = resolve(&twice, &StatusMap::new()).expect("resolve ok");
let one = resolve_claims(&res_once.rows);
let two = resolve_claims(&res_twice.rows);
assert_eq!(one.anchored["SL-100"].value, two.anchored["SL-100"].value);
assert_eq!(two.anchored["SL-100"].conflict, None);
assert_eq!(two.anchored["SL-100"].rows, 2, "corroboration counted");
assert!(two.findings.is_empty());
}
#[test]
fn lens_isolation_holds_non_vacuously_in_both_directions() {
let mixed = [
session(
"s1",
vec![
anchor("h1", "SL-100", 5.0, RaterKind::Human),
anchor("a1", "SL-200", 2.0, RaterKind::Agent),
lensed("l1", "SL-100", 9.0, "user-value"),
lensed("l3", "SL-300", 1.0, "ops-value"),
],
),
session("s2", vec![lensed("l2", "SL-100", 3.0, "user-value")]),
];
let res = resolve(&mixed, &StatusMap::new()).expect("resolve ok");
for uid in ["l1", "l2", "l3"] {
let (_, status) = res
.rows
.iter()
.find(|(j, _)| j.uid == uid)
.expect("row present");
assert_eq!(status, &ResolutionStatus::InertLens);
}
let with_lensed = resolve_claims(&res.rows);
assert!(!with_lensed.lensed.is_empty(), "direction 1: non-empty");
let key = ("user-value".to_string(), "SL-100".to_string());
let lensed_claim = &with_lensed.lensed[&key];
assert!(lensed_claim.conflict.is_some(), "resolved identically");
assert_eq!(lensed_claim.value, 6.0);
let unlensed_only = [
session(
"s1",
vec![
anchor("h1", "SL-100", 5.0, RaterKind::Human),
anchor("a1", "SL-200", 2.0, RaterKind::Agent),
],
),
session("s2", vec![]),
];
let res2 = resolve(&unlensed_only, &StatusMap::new()).expect("resolve ok");
let without_lensed = resolve_claims(&res2.rows);
assert_eq!(
with_lensed.anchored, without_lensed.anchored,
"direction 2: lensed rows never touch the unlensed partitions"
);
assert_eq!(with_lensed.priors, without_lensed.priors);
}
#[test]
fn lensed_conflicts_do_not_enter_the_finding_stream() {
let rows = [
lensed("l1", "SL-100", 1.0, "user-value"),
lensed("l2", "SL-100", 9.0, "user-value"),
];
let claims = resolve_claims(&active(&rows));
let key = ("user-value".to_string(), "SL-100".to_string());
assert!(claims.lensed[&key].conflict.is_some(), "queryable in place");
assert!(claims.findings.is_empty());
}
#[test]
fn anchor_map_never_launders_agent_or_migrated_claims() {
let minted = |tier: usize, item: &str, uid: &str| -> Judgement {
match tier {
0 => anchor(uid, item, 1.0, RaterKind::Migrated),
1 => anchor(uid, item, 2.0, RaterKind::Agent),
2 => anchor(uid, item, 3.0, RaterKind::Human),
_ => pin(uid, item, 4.0),
}
};
for mask in 0..16_u32.pow(3) {
let mut rows: Vec<Judgement> = Vec::new();
for (i, item) in ["SL-100", "SL-200", "SL-300"].iter().enumerate() {
let tiers = (mask / 16_u32.pow(u32::try_from(i).unwrap())) % 16;
for tier in 0..4 {
if tiers & (1 << tier) != 0 {
rows.push(minted(tier, item, &format!("j{i}t{tier}")));
}
}
}
let claims = resolve_claims(&active(&rows));
let map = claims.anchor_map();
let anchored_values: BTreeMap<String, f64> = claims
.anchored
.iter()
.map(|(k, c)| (k.clone(), c.value))
.collect();
assert_eq!(map, anchored_values, "anchor_map ≡ anchored, mask {mask}");
for claim in claims.anchored.values() {
assert!(claim.tier.is_anchored(), "anchored is Pin/Human only");
}
for (item, claim) in &claims.priors {
assert!(!claim.tier.is_anchored(), "priors are Agent/Migrated");
assert!(!map.contains_key(item), "a prior never enters the map");
}
}
}
#[test]
fn non_live_rows_carry_no_claim() {
let rows = [
anchor("dead", "SL-100", 9.0, RaterKind::Human),
anchor("live", "SL-100", 5.0, RaterKind::Human),
anchor("tomb", "SL-200", 3.0, RaterKind::Human),
];
let tagged: Vec<(&Judgement, ResolutionStatus)> = vec![
(
&rows[0],
ResolutionStatus::Superseded {
by: "live".to_string(),
},
),
(&rows[1], ResolutionStatus::Active),
(&rows[2], ResolutionStatus::Tombstoned),
];
let claims = resolve_claims(&tagged);
let claim = &claims.anchored["SL-100"];
assert_eq!((claim.value, claim.rows), (5.0, 1));
assert_eq!(claim.conflict, None, "the superseded row is gone");
assert!(!claims.anchored.contains_key("SL-200"));
}
#[test]
fn pairwise_rows_never_enter_the_claims_pass() {
let mut order = anchor("ord", "SL-100", 5.0, RaterKind::Human);
order.form = RowForm::Order;
order.b = Some("SL-200".to_string());
order.response = Some(crate::comparison::Response::PreferA);
order.frame = crate::comparison::FRAME_EQUAL_EFFORT.to_string();
let rows = [order];
let claims = resolve_claims(&active(&rows));
assert_eq!(claims, ClaimResolution::default());
}
}