use std::cmp::Ordering;
use std::collections::BTreeSet;
pub const DEFAULT_FORCED_CONTRADICTION_CAP: usize = 8;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GuardedMemory {
pub memory_id: String,
pub trust_milli: i64,
pub freshness_epoch: i64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SuppressionBasis {
HigherTrust,
Fresher,
DeterministicTieBreak,
}
impl SuppressionBasis {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::HigherTrust => "higher_trust",
Self::Fresher => "fresher",
Self::DeterministicTieBreak => "deterministic_tie_break",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ContradictionSuppression {
pub kept_memory_id: String,
pub suppressed_memory_id: String,
pub basis: SuppressionBasis,
}
#[must_use]
pub fn decide_contradiction_survivor(
left: &GuardedMemory,
right: &GuardedMemory,
) -> ContradictionSuppression {
let (keep, suppress, basis) = match left.trust_milli.cmp(&right.trust_milli) {
Ordering::Greater => (left, right, SuppressionBasis::HigherTrust),
Ordering::Less => (right, left, SuppressionBasis::HigherTrust),
Ordering::Equal => match left.freshness_epoch.cmp(&right.freshness_epoch) {
Ordering::Greater => (left, right, SuppressionBasis::Fresher),
Ordering::Less => (right, left, SuppressionBasis::Fresher),
Ordering::Equal => {
if left.memory_id <= right.memory_id {
(left, right, SuppressionBasis::DeterministicTieBreak)
} else {
(right, left, SuppressionBasis::DeterministicTieBreak)
}
}
},
};
ContradictionSuppression {
kept_memory_id: keep.memory_id.clone(),
suppressed_memory_id: suppress.memory_id.clone(),
basis,
}
}
fn canonical_pair(a: &str, b: &str) -> Option<(String, String)> {
let a = a.trim();
let b = b.trim();
if a.is_empty() || b.is_empty() || a == b {
return None;
}
if a <= b {
Some((a.to_string(), b.to_string()))
} else {
Some((b.to_string(), a.to_string()))
}
}
#[must_use]
pub fn unresolved_contradiction_pairs(
detected: &[(String, String)],
resolved: &[(String, String)],
) -> Vec<(String, String)> {
let resolved_set: BTreeSet<(String, String)> = resolved
.iter()
.filter_map(|(a, b)| canonical_pair(a, b))
.collect();
let mut unresolved: BTreeSet<(String, String)> = BTreeSet::new();
for (a, b) in detected {
if let Some(pair) = canonical_pair(a, b)
&& !resolved_set.contains(&pair)
{
unresolved.insert(pair);
}
}
unresolved.into_iter().collect()
}
#[must_use]
pub fn is_in_unresolved_contradiction(memory_id: &str, unresolved: &[(String, String)]) -> bool {
let id = memory_id.trim();
!id.is_empty() && unresolved.iter().any(|(a, b)| a == id || b == id)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ForcedContradictionView {
pub shown: Vec<String>,
pub total: usize,
}
#[must_use]
pub fn forced_contradiction_view(members: &[GuardedMemory], cap: usize) -> ForcedContradictionView {
let mut ranked: Vec<&GuardedMemory> = members.iter().collect();
ranked.sort_by(|a, b| {
b.trust_milli
.cmp(&a.trust_milli)
.then(b.freshness_epoch.cmp(&a.freshness_epoch))
.then(a.memory_id.cmp(&b.memory_id))
});
let total = ranked.len();
let shown = ranked
.into_iter()
.take(cap)
.map(|memory| memory.memory_id.clone())
.collect();
ForcedContradictionView { shown, total }
}
#[cfg(test)]
mod tests {
use super::{
DEFAULT_FORCED_CONTRADICTION_CAP, GuardedMemory, SuppressionBasis,
decide_contradiction_survivor, forced_contradiction_view, is_in_unresolved_contradiction,
unresolved_contradiction_pairs,
};
fn mem(id: &str, trust_milli: i64, freshness_epoch: i64) -> GuardedMemory {
GuardedMemory {
memory_id: id.to_string(),
trust_milli,
freshness_epoch,
}
}
#[test]
fn survivor_prefers_higher_trust_then_fresher_then_id() {
let d = decide_contradiction_survivor(&mem("a", 900, 1), &mem("b", 100, 999));
assert_eq!(d.kept_memory_id, "a");
assert_eq!(d.suppressed_memory_id, "b");
assert_eq!(d.basis, SuppressionBasis::HigherTrust);
let d = decide_contradiction_survivor(&mem("a", 500, 10), &mem("b", 500, 20));
assert_eq!(d.kept_memory_id, "b");
assert_eq!(d.basis, SuppressionBasis::Fresher);
let d = decide_contradiction_survivor(&mem("z", 500, 10), &mem("a", 500, 10));
assert_eq!(d.kept_memory_id, "a");
assert_eq!(d.basis, SuppressionBasis::DeterministicTieBreak);
}
#[test]
fn survivor_decision_is_symmetric_in_argument_order() {
let forward = decide_contradiction_survivor(&mem("a", 500, 20), &mem("b", 700, 10));
let reversed = decide_contradiction_survivor(&mem("b", 700, 10), &mem("a", 500, 20));
assert_eq!(
forward, reversed,
"the survivor must not depend on arg order"
);
assert_eq!(forward.kept_memory_id, "b");
}
#[test]
fn unresolved_set_is_detected_minus_resolved() {
let detected = vec![
("mem_a".to_string(), "mem_b".to_string()),
("mem_c".to_string(), "mem_d".to_string()),
("mem_b".to_string(), "mem_a".to_string()),
];
let resolved = vec![("mem_b".to_string(), "mem_a".to_string())];
let unresolved = unresolved_contradiction_pairs(&detected, &resolved);
assert_eq!(
unresolved,
vec![("mem_c".to_string(), "mem_d".to_string())],
"a->b is resolved and dedups; only c-d remains unresolved"
);
assert!(is_in_unresolved_contradiction("mem_c", &unresolved));
assert!(!is_in_unresolved_contradiction("mem_a", &unresolved));
assert!(!is_in_unresolved_contradiction("", &unresolved));
}
#[test]
fn forced_view_ranks_caps_and_reports_total_no_silent_drop() {
let members = vec![mem("low", 100, 1), mem("high", 900, 1), mem("mid", 500, 1)];
let view = forced_contradiction_view(&members, 2);
assert_eq!(
view.total, 3,
"total must reflect all members despite the cap"
);
assert_eq!(view.shown, vec!["high".to_string(), "mid".to_string()]);
let full = forced_contradiction_view(&members, DEFAULT_FORCED_CONTRADICTION_CAP);
assert_eq!(full.shown.len(), 3);
}
}