use std::collections::{BTreeMap, BTreeSet};
use super::{Judgement, RaterKind, Response};
pub(crate) type RowUid = String;
pub(crate) type ClassId = String;
pub(crate) type AnchorMap = BTreeMap<String, f64>;
pub(crate) type EdgeMap = BTreeMap<(ClassId, ClassId), BTreeSet<RowUid>>;
#[derive(Debug, Clone, Copy)]
pub(crate) struct PairRow<'a> {
pub uid: &'a str,
pub a: &'a str,
pub b: &'a str,
pub response: Response,
}
impl<'a> PairRow<'a> {
pub(crate) fn of(j: &'a Judgement) -> Option<Self> {
match (j.b.as_deref(), j.response) {
(Some(b), Some(response)) => Some(PairRow {
uid: &j.uid,
a: &j.a,
b,
response,
}),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Bound {
Unbounded,
Open(f64),
Closed(f64),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct ValueBounds {
pub lower: Bound,
pub upper: Bound,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum QuarantineReason {
PreferenceCycle { classes: Vec<ClassId> },
AnchorConflict { pairs: Vec<(String, String)> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CompilationStatus {
Constraining,
NoConstraint,
Quarantined(QuarantineReason),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum QuarantinePolicy {
Symmetric,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ConstraintSet {
pub classes: BTreeMap<String, ClassId>,
pub edges: EdgeMap,
pub anchors: BTreeMap<ClassId, f64>,
pub quarantined: BTreeMap<RowUid, QuarantineReason>,
pub bounds: BTreeMap<ClassId, ValueBounds>,
}
impl ConstraintSet {
pub(crate) fn status_of(&self, judgement: &Judgement) -> CompilationStatus {
if let Some(reason) = self.quarantined.get(&judgement.uid) {
return CompilationStatus::Quarantined(reason.clone());
}
if matches!(judgement.response, Some(Response::Incomparable)) {
return CompilationStatus::NoConstraint;
}
CompilationStatus::Constraining
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct RaterCounts {
pub human: u32,
pub agent: u32,
}
impl RaterCounts {
fn bump(&mut self, rater: &RaterKind) {
match rater {
RaterKind::Human => self.human += 1,
RaterKind::Agent => self.agent += 1,
RaterKind::Migrated => {}
}
}
pub(crate) fn total(self) -> u32 {
self.human + self.agent
}
}
pub(crate) fn constraining_counts_by_class(
cs: &ConstraintSet,
active: &[&Judgement],
) -> BTreeMap<ClassId, RaterCounts> {
let mut out: BTreeMap<ClassId, RaterCounts> = BTreeMap::new();
for &j in active {
if !matches!(cs.status_of(j), CompilationStatus::Constraining) {
continue;
}
let Some(pair) = PairRow::of(j) else {
continue;
};
let classes: BTreeSet<&ClassId> = [cs.classes.get(pair.a), cs.classes.get(pair.b)]
.into_iter()
.flatten()
.collect();
for class in classes {
out.entry(class.clone()).or_default().bump(&j.rater);
}
}
out
}
pub(crate) fn human_rows<'a>(active: &[&'a Judgement]) -> Vec<&'a Judgement> {
active
.iter()
.copied()
.filter(|j| matches!(j.rater, RaterKind::Human))
.collect()
}
pub(crate) fn compile_human_only(
active: &[&Judgement],
anchors: &AnchorMap,
policy: QuarantinePolicy,
) -> ConstraintSet {
compile(&human_rows(active), anchors, policy)
}
pub(crate) fn compile(
active: &[&Judgement],
anchors: &AnchorMap,
policy: QuarantinePolicy,
) -> ConstraintSet {
let QuarantinePolicy::Symmetric = policy;
let pairs: Vec<PairRow<'_>> = active.iter().filter_map(|j| PairRow::of(j)).collect();
let equal_rows: Vec<&PairRow<'_>> = pairs
.iter()
.filter(|p| matches!(p.response, Response::Equal))
.collect();
let strict_rows: Vec<(&PairRow<'_>, &str, &str)> = pairs
.iter()
.filter_map(|p| match p.response {
Response::PreferA => Some((p, p.a, p.b)),
Response::PreferB => Some((p, p.b, p.a)),
_ => None,
})
.collect();
let mut quarantined = c2_quarantine(&equal_rows, anchors);
let surviving_equals: Vec<&PairRow<'_>> = equal_rows
.iter()
.copied()
.filter(|p| !quarantined.contains_key(p.uid))
.collect();
let entities: BTreeSet<&str> = pairs.iter().flat_map(|p| [p.a, p.b]).collect();
let classes = build_classes(&entities, &surviving_equals);
let class_anchors = class_anchor_values(&classes, anchors);
let mut edges: EdgeMap = BTreeMap::new();
for (p, winner, loser) in &strict_rows {
let (Some(w), Some(l)) = (classes.get(*winner), classes.get(*loser)) else {
continue; };
edges
.entry((w.clone(), l.clone()))
.or_default()
.insert(p.uid.to_string());
}
c3_quarantine(&mut edges, &mut quarantined);
c4_quarantine(&mut edges, &class_anchors, &mut quarantined);
let all_classes: BTreeSet<ClassId> = classes.values().cloned().collect();
let bounds = compute_bounds(&all_classes, &edges, &class_anchors);
let set = ConstraintSet {
classes,
edges,
anchors: class_anchors,
quarantined,
bounds,
};
debug_assert!(
is_feasible(&set.edges, &set.anchors),
"C5: compile produced an infeasible ConstraintSet"
);
set
}
fn c2_quarantine(
equal_rows: &[&PairRow<'_>],
anchors: &AnchorMap,
) -> BTreeMap<RowUid, QuarantineReason> {
let entities: BTreeSet<&str> = equal_rows.iter().flat_map(|p| [p.a, p.b]).collect();
let mut dsu = DisjointSet::new(entities.iter().copied());
let mut adj: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
let mut rows_by_edge: BTreeMap<(String, String), BTreeSet<RowUid>> = BTreeMap::new();
for p in equal_rows {
dsu.union(p.a, p.b);
adj.entry(p.a).or_default().insert(p.b);
adj.entry(p.b).or_default().insert(p.a);
rows_by_edge
.entry(vertex_pair(p.a, p.b))
.or_default()
.insert(p.uid.to_string());
}
let mut members: BTreeMap<String, Vec<&str>> = BTreeMap::new();
for &e in &entities {
members.entry(dsu.find(e)).or_default().push(e);
}
let mut pairs_by_row: BTreeMap<RowUid, BTreeSet<(String, String)>> = BTreeMap::new();
for group in members.values() {
let anchored: Vec<(&str, f64)> = group
.iter()
.filter_map(|&e| anchors.get(e).map(|&v| (e, v)))
.collect();
for (i, &(x, value_x)) in anchored.iter().enumerate() {
for &(y, value_y) in anchored.iter().skip(i + 1) {
if value_x.total_cmp(&value_y).is_eq() {
continue; }
for edge in edges_on_simple_paths(&adj, x, y) {
let Some(uids) = rows_by_edge.get(&edge) else {
continue;
};
for uid in uids {
pairs_by_row
.entry(uid.clone())
.or_default()
.insert((x.to_string(), y.to_string()));
}
}
}
}
}
pairs_by_row
.into_iter()
.map(|(uid, pairs)| {
(
uid,
QuarantineReason::AnchorConflict {
pairs: pairs.into_iter().collect(),
},
)
})
.collect()
}
fn edges_on_simple_paths<'a>(
adj: &BTreeMap<&'a str, BTreeSet<&'a str>>,
from: &'a str,
to: &str,
) -> BTreeSet<(String, String)> {
let mut out = BTreeSet::new();
let mut on_path: Vec<&str> = vec![from];
dfs_simple_paths(adj, from, to, &mut on_path, &mut out);
out
}
fn dfs_simple_paths<'a>(
adj: &BTreeMap<&'a str, BTreeSet<&'a str>>,
cur: &'a str,
to: &str,
on_path: &mut Vec<&'a str>,
out: &mut BTreeSet<(String, String)>,
) {
if cur == to {
for step in on_path.windows(2) {
if let [a, b] = step {
out.insert(vertex_pair(a, b));
}
}
return;
}
let Some(next) = adj.get(cur) else {
return;
};
for &n in next {
if on_path.contains(&n) {
continue;
}
on_path.push(n);
dfs_simple_paths(adj, n, to, on_path, out);
on_path.pop();
}
}
fn vertex_pair(a: &str, b: &str) -> (String, String) {
if a <= b {
(a.to_string(), b.to_string())
} else {
(b.to_string(), a.to_string())
}
}
#[derive(Debug)]
struct DisjointSet {
parent: BTreeMap<String, String>,
}
impl DisjointSet {
fn new<'a>(entities: impl Iterator<Item = &'a str>) -> Self {
Self {
parent: entities.map(|e| (e.to_string(), e.to_string())).collect(),
}
}
fn find(&self, entity: &str) -> String {
let mut cur = entity;
while let Some(parent) = self.parent.get(cur) {
if parent.as_str() == cur {
break;
}
cur = parent;
}
cur.to_string()
}
fn union(&mut self, a: &str, b: &str) {
let root_a = self.find(a);
let root_b = self.find(b);
if root_a == root_b {
return;
}
let (small, large) = if root_a < root_b {
(root_a, root_b)
} else {
(root_b, root_a)
};
self.parent.insert(large, small);
}
}
fn build_classes(
entities: &BTreeSet<&str>,
surviving_equals: &[&PairRow<'_>],
) -> BTreeMap<String, ClassId> {
let mut dsu = DisjointSet::new(entities.iter().copied());
for p in surviving_equals {
dsu.union(p.a, p.b);
}
entities
.iter()
.map(|&e| (e.to_string(), dsu.find(e)))
.collect()
}
fn class_anchor_values(
classes: &BTreeMap<String, ClassId>,
anchors: &AnchorMap,
) -> BTreeMap<ClassId, f64> {
let mut out: BTreeMap<ClassId, f64> = BTreeMap::new();
for (entity, &value) in anchors {
let Some(class) = classes.get(entity) else {
continue; };
let slot = out.entry(class.clone()).or_insert(value);
debug_assert!(
slot.total_cmp(&value).is_eq(),
"post-C2 invariant broken: class `{class}` holds two distinct anchor values"
);
}
out
}
fn c3_quarantine(edges: &mut EdgeMap, quarantined: &mut BTreeMap<RowUid, QuarantineReason>) {
let nodes: BTreeSet<ClassId> = edges
.keys()
.flat_map(|(u, v)| [u.clone(), v.clone()])
.collect();
let adj = adjacency(edges);
let component = kosaraju(&nodes, &adj);
let mut scc_members: BTreeMap<usize, Vec<ClassId>> = BTreeMap::new();
for (node, &id) in &component {
scc_members.entry(id).or_default().push(node.clone());
}
let cyclic: Vec<(ClassId, ClassId)> = edges
.keys()
.filter(|(u, v)| component.get(u) == component.get(v))
.cloned()
.collect();
for key in cyclic {
let Some(rows) = edges.remove(&key) else {
continue;
};
let Some(id) = component.get(&key.0) else {
continue; };
let classes = scc_members.get(id).cloned().unwrap_or_default();
for uid in rows {
quarantined.insert(
uid,
QuarantineReason::PreferenceCycle {
classes: classes.clone(),
},
);
}
}
}
fn kosaraju(
nodes: &BTreeSet<ClassId>,
adj: &BTreeMap<ClassId, BTreeSet<ClassId>>,
) -> BTreeMap<ClassId, usize> {
let mut seen: BTreeSet<&str> = BTreeSet::new();
let mut finish_order: Vec<&str> = Vec::new();
for n in nodes {
dfs_finish_order(n, adj, &mut seen, &mut finish_order);
}
let reversed = transpose(adj);
let mut component: BTreeMap<ClassId, usize> = BTreeMap::new();
let mut next_id = 0_usize;
for &n in finish_order.iter().rev() {
if !component.contains_key(n) {
dfs_assign(n, &reversed, &mut component, next_id);
next_id += 1;
}
}
component
}
fn dfs_finish_order<'a>(
v: &'a str,
adj: &'a BTreeMap<ClassId, BTreeSet<ClassId>>,
seen: &mut BTreeSet<&'a str>,
finish_order: &mut Vec<&'a str>,
) {
if !seen.insert(v) {
return;
}
if let Some(next) = adj.get(v) {
for w in next {
dfs_finish_order(w, adj, seen, finish_order);
}
}
finish_order.push(v);
}
fn dfs_assign(
v: &str,
adj: &BTreeMap<ClassId, BTreeSet<ClassId>>,
component: &mut BTreeMap<ClassId, usize>,
id: usize,
) {
if component.contains_key(v) {
return;
}
component.insert(v.to_string(), id);
if let Some(next) = adj.get(v) {
for w in next {
dfs_assign(w, adj, component, id);
}
}
}
type ViolationMap = BTreeMap<(ClassId, ClassId), BTreeSet<(ClassId, ClassId)>>;
fn anchor_violations_path_form(
edges: &EdgeMap,
class_anchors: &BTreeMap<ClassId, f64>,
) -> ViolationMap {
let adj = adjacency(edges);
let reversed = transpose(&adj);
let mut out: ViolationMap = BTreeMap::new();
for (x, anchor_x) in class_anchors {
let forward = reach_including(&adj, x);
for (y, anchor_y) in class_anchors {
if x == y || anchor_x.total_cmp(anchor_y).is_gt() || !forward.contains(y) {
continue;
}
let reverse = reach_including(&reversed, y);
for (u, v) in edges.keys() {
if forward.contains(u) && reverse.contains(v) {
out.entry((u.clone(), v.clone()))
.or_default()
.insert((x.clone(), y.clone()));
}
}
}
}
out
}
fn anchor_violations_two_pass(
edges: &EdgeMap,
class_anchors: &BTreeMap<ClassId, f64>,
) -> BTreeSet<(ClassId, ClassId)> {
let adj = adjacency(edges);
let reversed = transpose(&adj);
let nodes: BTreeSet<ClassId> = edges
.keys()
.flat_map(|(u, v)| [u.clone(), v.clone()])
.collect();
let mut ceiling: BTreeMap<ClassId, f64> = BTreeMap::new();
let mut floor: BTreeMap<ClassId, f64> = BTreeMap::new();
for n in &nodes {
let upstream = reach_including(&reversed, n)
.into_iter()
.filter_map(|c| class_anchors.get(&c).copied());
if let Some(m) = total_min(upstream) {
ceiling.insert(n.clone(), m);
}
let downstream = reach_including(&adj, n)
.into_iter()
.filter_map(|c| class_anchors.get(&c).copied());
if let Some(m) = total_max(downstream) {
floor.insert(n.clone(), m);
}
}
edges
.keys()
.filter(|(u, v)| {
matches!(
(ceiling.get(u), floor.get(v)),
(Some(c), Some(f)) if c.total_cmp(f).is_le()
)
})
.cloned()
.collect()
}
fn c4_quarantine(
edges: &mut EdgeMap,
class_anchors: &BTreeMap<ClassId, f64>,
quarantined: &mut BTreeMap<RowUid, QuarantineReason>,
) {
let by_pair = anchor_violations_path_form(edges, class_anchors);
let crossing = anchor_violations_two_pass(edges, class_anchors);
debug_assert_eq!(
by_pair.keys().cloned().collect::<BTreeSet<_>>(),
crossing,
"C4: normative path form and floor/ceiling two-pass disagree"
);
for key in crossing {
let Some(rows) = edges.remove(&key) else {
continue;
};
let pairs: Vec<(String, String)> = by_pair
.get(&key)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default();
for uid in rows {
quarantined.insert(
uid,
QuarantineReason::AnchorConflict {
pairs: pairs.clone(),
},
);
}
}
debug_assert!(
anchor_violations_two_pass(edges, class_anchors).is_empty(),
"C4: a violation survived the single quarantine pass"
);
}
fn adjacency(edges: &EdgeMap) -> BTreeMap<ClassId, BTreeSet<ClassId>> {
let mut adj: BTreeMap<ClassId, BTreeSet<ClassId>> = BTreeMap::new();
for (winner, loser) in edges.keys() {
adj.entry(winner.clone()).or_default().insert(loser.clone());
}
adj
}
fn transpose(adj: &BTreeMap<ClassId, BTreeSet<ClassId>>) -> BTreeMap<ClassId, BTreeSet<ClassId>> {
let mut out: BTreeMap<ClassId, BTreeSet<ClassId>> = BTreeMap::new();
for (from, tos) in adj {
for to in tos {
out.entry(to.clone()).or_default().insert(from.clone());
}
}
out
}
fn reach_including(adj: &BTreeMap<ClassId, BTreeSet<ClassId>>, start: &str) -> BTreeSet<ClassId> {
let mut out: BTreeSet<ClassId> = BTreeSet::new();
out.insert(start.to_string());
let mut stack: Vec<String> = vec![start.to_string()];
while let Some(cur) = stack.pop() {
let Some(next) = adj.get(&cur) else {
continue;
};
for n in next {
if out.insert(n.clone()) {
stack.push(n.clone());
}
}
}
out
}
fn total_min(values: impl Iterator<Item = f64>) -> Option<f64> {
values.reduce(|a, b| if a.total_cmp(&b).is_le() { a } else { b })
}
fn total_max(values: impl Iterator<Item = f64>) -> Option<f64> {
values.reduce(|a, b| if a.total_cmp(&b).is_ge() { a } else { b })
}
fn compute_bounds(
all_classes: &BTreeSet<ClassId>,
edges: &EdgeMap,
class_anchors: &BTreeMap<ClassId, f64>,
) -> BTreeMap<ClassId, ValueBounds> {
let adj = adjacency(edges);
let reversed = transpose(&adj);
let mut out: BTreeMap<ClassId, ValueBounds> = BTreeMap::new();
for class in all_classes {
let bounds = if let Some(&a) = class_anchors.get(class) {
ValueBounds {
lower: Bound::Closed(a),
upper: Bound::Closed(a),
}
} else {
let below = total_max(
reach_including(&adj, class)
.into_iter()
.filter_map(|c| class_anchors.get(&c).copied()),
);
let above = total_min(
reach_including(&reversed, class)
.into_iter()
.filter_map(|c| class_anchors.get(&c).copied()),
);
ValueBounds {
lower: below.map_or(Bound::Unbounded, Bound::Open),
upper: above.map_or(Bound::Unbounded, Bound::Open),
}
};
out.insert(class.clone(), bounds);
}
out
}
fn is_feasible(edges: &EdgeMap, class_anchors: &BTreeMap<ClassId, f64>) -> bool {
let nodes: BTreeSet<ClassId> = edges
.keys()
.flat_map(|(u, v)| [u.clone(), v.clone()])
.collect();
let adj = adjacency(edges);
let component = kosaraju(&nodes, &adj);
let acyclic = edges
.keys()
.all(|(u, v)| component.get(u) != component.get(v));
acyclic && anchor_violations_two_pass(edges, class_anchors).is_empty()
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, BTreeSet};
use super::{
AnchorMap, Bound, CompilationStatus, ConstraintSet, EdgeMap, QuarantinePolicy,
QuarantineReason, ValueBounds, anchor_violations_path_form, anchor_violations_two_pass,
compile, compile_human_only,
};
use crate::comparison::{
DOMAIN_VALUE, FRAME_EQUAL_EFFORT, Judgement, RaterKind, Response, RowForm,
};
fn row(uid: &str, a: &str, b: &str, response: Response) -> 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: RaterKind::Human,
by: None,
note: None,
date: Some("2026-07-11".to_string()),
observed_at: None,
basis: None,
est_lower: None,
est_upper: None,
admission: None,
}
}
fn prefer(uid: &str, winner: &str, loser: &str) -> Judgement {
row(uid, winner, loser, Response::PreferA)
}
fn equal(uid: &str, a: &str, b: &str) -> Judgement {
row(uid, a, b, Response::Equal)
}
fn anchors(pairs: &[(&str, f64)]) -> AnchorMap {
pairs.iter().map(|&(e, v)| (e.to_string(), v)).collect()
}
fn compiled(rows: &[Judgement], anchors: &AnchorMap) -> ConstraintSet {
let refs: Vec<&Judgement> = rows.iter().collect();
compile(&refs, anchors, QuarantinePolicy::Symmetric)
}
fn edge_map(edges: &[(&str, &str, &str)]) -> EdgeMap {
let mut out: EdgeMap = EdgeMap::new();
for &(w, l, uid) in edges {
out.entry((w.to_string(), l.to_string()))
.or_default()
.insert(uid.to_string());
}
out
}
fn cycle(classes: &[&str]) -> QuarantineReason {
QuarantineReason::PreferenceCycle {
classes: classes.iter().map(|c| c.to_string()).collect(),
}
}
fn conflict(pairs: &[(&str, &str)]) -> QuarantineReason {
QuarantineReason::AnchorConflict {
pairs: pairs
.iter()
.map(|&(x, y)| (x.to_string(), y.to_string()))
.collect(),
}
}
fn edge_keys(cs: &ConstraintSet) -> Vec<(String, String)> {
cs.edges.keys().cloned().collect()
}
fn class_of<'a>(cs: &'a ConstraintSet, entity: &str) -> &'a str {
cs.classes.get(entity).expect("entity has a class")
}
#[test]
fn c1_prefer_a_builds_strict_edge_winner_over_loser() {
let rows = [prefer("j1", "A", "B")];
let cs = compiled(&rows, &anchors(&[]));
assert_eq!(
edge_keys(&cs),
vec![("A".to_string(), "B".to_string())],
"one edge A > B"
);
assert_eq!(class_of(&cs, "A"), "A");
assert_eq!(class_of(&cs, "B"), "B");
assert!(cs.quarantined.is_empty());
}
#[test]
fn c1_prefer_b_orients_the_edge_to_the_second_entity() {
let rows = [row("j1", "A", "B", Response::PreferB)];
let cs = compiled(&rows, &anchors(&[]));
assert_eq!(edge_keys(&cs), vec![("B".to_string(), "A".to_string())]);
}
#[test]
fn c1_equal_merges_classes_and_class_id_is_smallest_member() {
let rows = [equal("e1", "B", "A"), equal("e2", "B", "C")];
let cs = compiled(&rows, &anchors(&[]));
assert_eq!(class_of(&cs, "A"), "A");
assert_eq!(class_of(&cs, "B"), "A");
assert_eq!(class_of(&cs, "C"), "A");
}
#[test]
fn c1_incomparable_is_no_constraint_and_builds_no_edge() {
let rows = [row("j1", "A", "B", Response::Incomparable)];
let cs = compiled(&rows, &anchors(&[]));
assert!(cs.edges.is_empty());
assert!(cs.quarantined.is_empty());
assert_eq!(class_of(&cs, "A"), "A");
assert_eq!(
cs.status_of(&rows[0]),
CompilationStatus::NoConstraint,
"incomparable is excluded from constraining-judgement counts"
);
}
#[test]
fn c1_anchor_lands_on_the_merged_class() {
let rows = [equal("e1", "A", "B")];
let cs = compiled(&rows, &anchors(&[("B", 3.0)]));
assert_eq!(cs.anchors.get("A"), Some(&3.0));
assert_eq!(cs.anchors.len(), 1);
}
#[test]
fn c1_anchor_without_row_evidence_gets_no_class() {
let rows = [prefer("j1", "A", "B")];
let cs = compiled(&rows, &anchors(&[("Z", 9.0)]));
assert!(!cs.classes.contains_key("Z"));
assert!(cs.anchors.is_empty());
}
#[test]
fn c1_magnitude_stays_uncompiled_pure_order_semantics() {
let mut ratio = row("j1", "A", "B", Response::PreferA);
ratio.form = RowForm::Ratio;
ratio.magnitude = Some(2.5);
let rows = [ratio];
let cs = compiled(&rows, &anchors(&[]));
assert_eq!(edge_keys(&cs), vec![("A".to_string(), "B".to_string())]);
assert!(cs.anchors.is_empty());
assert_eq!(
cs.bounds.get("A"),
Some(&ValueBounds {
lower: Bound::Unbounded,
upper: Bound::Unbounded,
})
);
}
#[test]
fn c3_obligation_1_triangle_quarantined_external_edge_retained() {
let rows = [
prefer("j1", "A", "B"),
prefer("j2", "B", "C"),
prefer("j3", "C", "A"),
prefer("j4", "A", "D"),
];
let cs = compiled(&rows, &anchors(&[]));
let reason = cycle(&["A", "B", "C"]);
assert_eq!(cs.quarantined.get("j1"), Some(&reason));
assert_eq!(cs.quarantined.get("j2"), Some(&reason));
assert_eq!(cs.quarantined.get("j3"), Some(&reason));
assert_eq!(cs.quarantined.len(), 3);
assert_eq!(
edge_keys(&cs),
vec![("A".to_string(), "D".to_string())],
"the external member-level edge A > D is untouched"
);
assert_eq!(cs.status_of(&rows[3]), CompilationStatus::Constraining);
assert_eq!(
cs.bounds.get("D"),
Some(&ValueBounds {
lower: Bound::Unbounded,
upper: Bound::Unbounded,
}),
"no anchors anywhere: D is unconstrained in value"
);
}
#[test]
fn c3_two_cycle_quarantines_both_rows() {
let rows = [prefer("j1", "A", "B"), prefer("j2", "B", "A")];
let cs = compiled(&rows, &anchors(&[]));
let reason = cycle(&["A", "B"]);
assert_eq!(cs.quarantined.get("j1"), Some(&reason));
assert_eq!(cs.quarantined.get("j2"), Some(&reason));
assert!(cs.edges.is_empty());
}
#[test]
fn c3_runs_before_c4_cycle_edge_also_in_anchor_conflict() {
let rows = [
prefer("j1", "A", "B"),
prefer("j2", "B", "A"),
prefer("j3", "B", "C"),
];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("C", 2.0)]));
let reason = cycle(&["A", "B"]);
assert_eq!(cs.quarantined.get("j1"), Some(&reason));
assert_eq!(cs.quarantined.get("j2"), Some(&reason));
assert!(!cs.quarantined.contains_key("j3"));
assert_eq!(edge_keys(&cs), vec![("B".to_string(), "C".to_string())]);
}
#[test]
fn c3_strict_edge_within_an_equality_class_is_a_self_loop_cycle() {
let rows = [equal("e1", "A", "B"), prefer("j1", "A", "B")];
let cs = compiled(&rows, &anchors(&[]));
assert_eq!(cs.quarantined.get("j1"), Some(&cycle(&["A"])));
assert!(!cs.quarantined.contains_key("e1"));
assert!(cs.edges.is_empty());
}
#[test]
fn c4_single_row_conflict_quarantined() {
let rows = [prefer("j1", "A", "B")];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("B", 2.0)]));
assert_eq!(cs.quarantined.get("j1"), Some(&conflict(&[("A", "B")])));
assert!(cs.edges.is_empty());
}
#[test]
fn c4_chain_conflict_quarantines_every_edge_on_the_path() {
let rows = [prefer("j1", "A", "B"), prefer("j2", "B", "C")];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("C", 3.0)]));
let reason = conflict(&[("A", "C")]);
assert_eq!(cs.quarantined.get("j1"), Some(&reason));
assert_eq!(cs.quarantined.get("j2"), Some(&reason));
assert!(cs.edges.is_empty());
}
#[test]
fn c4_parallel_paths_between_one_pair_quarantine_all_paths() {
let rows = [
prefer("j1", "A", "B"),
prefer("j2", "B", "D"),
prefer("j3", "A", "C"),
prefer("j4", "C", "D"),
];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("D", 2.0)]));
let reason = conflict(&[("A", "D")]);
for uid in ["j1", "j2", "j3", "j4"] {
assert_eq!(cs.quarantined.get(uid), Some(&reason), "row {uid}");
}
assert!(cs.edges.is_empty());
}
#[test]
fn c4_one_edge_serving_several_conflicts_lists_every_pair() {
let rows = [
prefer("j1", "A", "M"),
prefer("j2", "B", "M"),
prefer("j3", "M", "C"),
];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("B", 1.5), ("C", 5.0)]));
assert_eq!(cs.quarantined.get("j1"), Some(&conflict(&[("A", "C")])));
assert_eq!(cs.quarantined.get("j2"), Some(&conflict(&[("B", "C")])));
assert_eq!(
cs.quarantined.get("j3"),
Some(&conflict(&[("A", "C"), ("B", "C")]))
);
assert!(cs.edges.is_empty());
}
#[test]
fn c4_equal_anchor_values_across_distinct_classes_still_violate() {
let rows = [prefer("j1", "A", "B")];
let cs = compiled(&rows, &anchors(&[("A", 2.0), ("B", 2.0)]));
assert_eq!(cs.quarantined.get("j1"), Some(&conflict(&[("A", "B")])));
}
#[test]
fn c4_consistent_anchored_chain_is_retained() {
let rows = [prefer("j1", "A", "B"), prefer("j2", "B", "C")];
let cs = compiled(&rows, &anchors(&[("A", 5.0), ("C", 1.0)]));
assert!(cs.quarantined.is_empty());
assert_eq!(cs.edges.len(), 2);
}
#[test]
fn c4_path_form_matches_floor_ceiling_on_branching_goldens() {
let diamond = edge_map(&[
("A", "B", "j1"),
("B", "D", "j2"),
("A", "C", "j3"),
("C", "D", "j4"),
]);
let diamond_anchors = anchors(&[("A", 1.0), ("D", 2.0)]);
let shared = edge_map(&[("A", "M", "j1"), ("B", "M", "j2"), ("M", "C", "j3")]);
let shared_anchors = anchors(&[("A", 1.0), ("B", 1.5), ("C", 5.0)]);
let mixed = edge_map(&[
("X", "P", "j1"),
("P", "Y", "j2"),
("P", "Q", "j3"),
("Q", "Z", "j4"),
]);
let mixed_anchors = anchors(&[("X", 1.0), ("Y", 5.0), ("Z", 0.5)]);
for (edges, anchor_map) in [
(&diamond, &diamond_anchors),
(&shared, &shared_anchors),
(&mixed, &mixed_anchors),
] {
let path_form: BTreeSet<(String, String)> =
anchor_violations_path_form(edges, anchor_map)
.into_keys()
.collect();
let two_pass = anchor_violations_two_pass(edges, anchor_map);
assert_eq!(path_form, two_pass, "violation computations must agree");
}
let mixed_violations = anchor_violations_two_pass(&mixed, &mixed_anchors);
let expected: BTreeSet<(String, String)> = [
("X".to_string(), "P".to_string()),
("P".to_string(), "Y".to_string()),
]
.into_iter()
.collect();
assert_eq!(mixed_violations, expected);
}
#[test]
fn c4_single_pass_leaves_no_residual_violation() {
let rows = [
prefer("j1", "A", "M"),
prefer("j2", "B", "M"),
prefer("j3", "M", "C"),
prefer("j4", "C", "D"),
];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("B", 1.5), ("C", 5.0)]));
assert!(anchor_violations_two_pass(&cs.edges, &cs.anchors).is_empty());
}
#[test]
fn c2_equality_path_between_differently_anchored_members_quarantined() {
let rows = [
equal("e1", "A", "M"),
equal("e2", "M", "B"),
equal("e3", "M", "W"),
];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("B", 2.0)]));
let reason = conflict(&[("A", "B")]);
assert_eq!(cs.quarantined.get("e1"), Some(&reason));
assert_eq!(cs.quarantined.get("e2"), Some(&reason));
assert!(!cs.quarantined.contains_key("e3"));
assert_eq!(class_of(&cs, "A"), "A");
assert_eq!(class_of(&cs, "B"), "B");
assert_eq!(class_of(&cs, "M"), "M");
assert_eq!(class_of(&cs, "W"), "M");
assert_eq!(cs.anchors.get("A"), Some(&1.0));
assert_eq!(cs.anchors.get("B"), Some(&2.0));
}
#[test]
fn c2_same_anchor_value_merge_is_retained() {
let rows = [equal("e1", "A", "B")];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("B", 1.0)]));
assert!(cs.quarantined.is_empty());
assert_eq!(class_of(&cs, "B"), "A");
assert_eq!(cs.anchors.get("A"), Some(&1.0));
}
#[test]
fn c2_parallel_equality_paths_all_quarantined() {
let rows = [
equal("e1", "A", "M"),
equal("e2", "M", "B"),
equal("e3", "A", "N"),
equal("e4", "N", "B"),
];
let cs = compiled(&rows, &anchors(&[("A", 1.0), ("B", 2.0)]));
let reason = conflict(&[("A", "B")]);
for uid in ["e1", "e2", "e3", "e4"] {
assert_eq!(cs.quarantined.get(uid), Some(&reason), "row {uid}");
}
for entity in ["A", "B", "M", "N"] {
assert_eq!(class_of(&cs, entity), entity, "classes rebuilt apart");
}
}
fn independently_feasible(cs: &ConstraintSet) -> bool {
let nodes: BTreeSet<&str> = cs
.edges
.keys()
.flat_map(|(u, v)| [u.as_str(), v.as_str()])
.collect();
let mut indegree: BTreeMap<&str, usize> = nodes.iter().map(|&n| (n, 0)).collect();
for (_, v) in cs.edges.keys() {
if let Some(d) = indegree.get_mut(v.as_str()) {
*d += 1;
}
}
let mut ready: Vec<&str> = indegree
.iter()
.filter(|&(_, &d)| d == 0)
.map(|(&n, _)| n)
.collect();
let mut placed = 0_usize;
while let Some(n) = ready.pop() {
placed += 1;
for (u, v) in cs.edges.keys() {
if u.as_str() == n
&& let Some(d) = indegree.get_mut(v.as_str())
{
*d -= 1;
if *d == 0 {
ready.push(v.as_str());
}
}
}
}
if placed != nodes.len() {
return false; }
for (x, anchor_x) in &cs.anchors {
let mut seen: BTreeSet<&str> = BTreeSet::new();
let mut stack = vec![x.as_str()];
while let Some(cur) = stack.pop() {
for (u, v) in cs.edges.keys() {
if u.as_str() == cur && seen.insert(v.as_str()) {
stack.push(v.as_str());
}
}
}
for y in seen {
if let Some(anchor_y) = cs.anchors.get(y)
&& !anchor_x.total_cmp(anchor_y).is_gt()
{
return false; }
}
}
true
}
#[test]
fn c5_feasibility_property_over_enumerated_ledgers() {
const ENTITY_PAIRS: [(&str, &str); 6] = [
("A", "B"),
("A", "C"),
("A", "D"),
("B", "C"),
("B", "D"),
("C", "D"),
];
let anchor_configs = [
anchors(&[]),
anchors(&[("A", 1.0), ("D", 2.0)]),
anchors(&[("A", 2.0), ("C", 2.0), ("B", 5.0)]),
];
for cfg in &anchor_configs {
for mask in 0..4_u32.pow(6) {
let mut rows = Vec::new();
for (i, (a, b)) in ENTITY_PAIRS.iter().enumerate() {
let uid = format!("j{i}");
match (mask / 4_u32.pow(i as u32)) % 4 {
1 => rows.push(row(&uid, a, b, Response::PreferA)),
2 => rows.push(row(&uid, a, b, Response::PreferB)),
3 => rows.push(row(&uid, a, b, Response::Equal)),
_ => {}
}
}
let cs = compiled(&rows, cfg);
assert!(
independently_feasible(&cs),
"infeasible ConstraintSet for mask {mask} cfg {cfg:?}"
);
let mut per_class: BTreeMap<&String, f64> = BTreeMap::new();
for (entity, value) in cfg {
let Some(class) = cs.classes.get(entity) else {
continue;
};
if let Some(prev) = per_class.insert(class, *value) {
assert!(
prev.total_cmp(value).is_eq(),
"class with two anchor values for mask {mask}"
);
}
}
}
}
}
#[test]
fn c6_lower_is_max_anchor_below_upper_is_min_anchor_above() {
let rows = [
prefer("j1", "A", "M"),
prefer("j2", "B", "M"),
prefer("j3", "M", "D"),
prefer("j4", "M", "E"),
];
let cs = compiled(
&rows,
&anchors(&[("A", 10.0), ("B", 6.0), ("D", 1.0), ("E", 3.0)]),
);
assert!(cs.quarantined.is_empty());
assert_eq!(
cs.bounds.get("M"),
Some(&ValueBounds {
lower: Bound::Open(3.0),
upper: Bound::Open(6.0),
})
);
}
#[test]
fn c6_closed_only_via_anchor_on_the_class_itself() {
let rows = [prefer("j1", "A", "B")];
let cs = compiled(&rows, &anchors(&[("A", 7.0)]));
assert_eq!(
cs.bounds.get("A"),
Some(&ValueBounds {
lower: Bound::Closed(7.0),
upper: Bound::Closed(7.0),
})
);
assert_eq!(
cs.bounds.get("B"),
Some(&ValueBounds {
lower: Bound::Unbounded,
upper: Bound::Open(7.0),
})
);
}
#[test]
fn c6_isolated_unanchored_class_is_unbounded_both_sides() {
let rows = [row("j1", "A", "B", Response::Incomparable)];
let cs = compiled(&rows, &anchors(&[]));
let unbounded = ValueBounds {
lower: Bound::Unbounded,
upper: Bound::Unbounded,
};
assert_eq!(cs.bounds.get("A"), Some(&unbounded));
assert_eq!(cs.bounds.get("B"), Some(&unbounded));
}
#[test]
fn c8_status_of_assigns_all_three_compilation_statuses() {
let rows = [
prefer("j1", "A", "B"),
row("j2", "C", "D", Response::Incomparable),
prefer("j3", "X", "Y"),
prefer("j4", "Y", "X"),
];
let cs = compiled(&rows, &anchors(&[]));
assert_eq!(cs.status_of(&rows[0]), CompilationStatus::Constraining);
assert_eq!(cs.status_of(&rows[1]), CompilationStatus::NoConstraint);
assert!(matches!(
cs.status_of(&rows[2]),
CompilationStatus::Quarantined(QuarantineReason::PreferenceCycle { .. })
));
}
#[test]
fn compile_is_deterministic_across_input_order() {
let rows = [
prefer("j1", "A", "B"),
prefer("j2", "B", "C"),
equal("e1", "C", "D"),
row("j3", "A", "D", Response::Incomparable),
];
let anchor_map = anchors(&[("D", 4.0)]);
let forward: Vec<&Judgement> = rows.iter().collect();
let backward: Vec<&Judgement> = rows.iter().rev().collect();
let cs1 = compile(&forward, &anchor_map, QuarantinePolicy::Symmetric);
let cs2 = compile(&backward, &anchor_map, QuarantinePolicy::Symmetric);
assert_eq!(cs1, cs2);
}
fn agent_prefer(uid: &str, winner: &str, loser: &str) -> Judgement {
Judgement {
rater: RaterKind::Agent,
..prefer(uid, winner, loser)
}
}
#[test]
fn human_only_retains_human_row_from_agent_involved_cycle() {
let rows = [
prefer("h1", "A", "B"),
agent_prefer("g1", "B", "C"),
agent_prefer("g2", "C", "A"),
];
let refs: Vec<&Judgement> = rows.iter().collect();
let anchor_map = anchors(&[]);
let full = compile(&refs, &anchor_map, QuarantinePolicy::Symmetric);
assert!(
matches!(
full.status_of(&rows[0]),
CompilationStatus::Quarantined(QuarantineReason::PreferenceCycle { .. })
),
"full system: the human leg rides the cycle quarantine"
);
let human = compile_human_only(&refs, &anchor_map, QuarantinePolicy::Symmetric);
assert_eq!(
human.status_of(&rows[0]),
CompilationStatus::Constraining,
"human-only system: no cycle, the row constrains"
);
assert_eq!(edge_keys(&human), vec![("A".to_string(), "B".to_string())]);
assert!(human.quarantined.is_empty());
assert!(!human.classes.contains_key("C"));
}
#[test]
fn human_only_runs_its_own_quarantine_passes() {
let rows = [
prefer("h1", "X", "Y"),
prefer("h2", "Y", "X"),
agent_prefer("g1", "X", "Z"),
];
let refs: Vec<&Judgement> = rows.iter().collect();
let human = compile_human_only(&refs, &anchors(&[]), QuarantinePolicy::Symmetric);
assert!(
matches!(
human.status_of(&rows[0]),
CompilationStatus::Quarantined(QuarantineReason::PreferenceCycle { .. })
),
"human-only 2-cycle quarantined by the subset's own C3"
);
}
}