use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiscordKind {
Conflict,
ConflictingConfidence,
MissingOverlap,
TranslationFail,
EvidenceGap,
ReplicationFail,
ProvenanceFragile,
StatusDivergent,
MethodMismatch,
}
impl DiscordKind {
pub const ALL: &'static [DiscordKind] = &[
DiscordKind::Conflict,
DiscordKind::ConflictingConfidence,
DiscordKind::MissingOverlap,
DiscordKind::TranslationFail,
DiscordKind::EvidenceGap,
DiscordKind::ReplicationFail,
DiscordKind::ProvenanceFragile,
DiscordKind::StatusDivergent,
DiscordKind::MethodMismatch,
];
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Conflict => "conflict",
Self::ConflictingConfidence => "conflicting_confidence",
Self::MissingOverlap => "missing_overlap",
Self::TranslationFail => "translation_fail",
Self::EvidenceGap => "evidence_gap",
Self::ReplicationFail => "replication_fail",
Self::ProvenanceFragile => "provenance_fragile",
Self::StatusDivergent => "status_divergent",
Self::MethodMismatch => "method_mismatch",
}
}
}
impl std::fmt::Display for DiscordKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct DiscordSet {
kinds: BTreeSet<DiscordKind>,
}
impl DiscordSet {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn singleton(kind: DiscordKind) -> Self {
let mut s = Self::default();
s.kinds.insert(kind);
s
}
pub fn from_kinds(kinds: impl IntoIterator<Item = DiscordKind>) -> Self {
Self {
kinds: kinds.into_iter().collect(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.kinds.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.kinds.len()
}
#[must_use]
pub fn contains(&self, kind: DiscordKind) -> bool {
self.kinds.contains(&kind)
}
pub fn insert(&mut self, kind: DiscordKind) -> bool {
self.kinds.insert(kind)
}
pub fn join(&self, other: &Self) -> Self {
Self {
kinds: self.kinds.union(&other.kinds).copied().collect(),
}
}
pub fn meet(&self, other: &Self) -> Self {
Self {
kinds: self.kinds.intersection(&other.kinds).copied().collect(),
}
}
#[must_use]
pub fn is_subset(&self, other: &Self) -> bool {
self.kinds.is_subset(&other.kinds)
}
pub fn iter(&self) -> impl Iterator<Item = DiscordKind> + '_ {
self.kinds.iter().copied()
}
}
impl std::fmt::Display for DiscordSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.kinds.is_empty() {
return write!(f, "{{}}");
}
write!(f, "{{")?;
let mut first = true;
for kind in &self.kinds {
if !first {
write!(f, ", ")?;
}
first = false;
write!(f, "{kind}")?;
}
write!(f, "}}")
}
}
pub type ContextId = String;
pub trait ContextRefinement {
fn refines(&self, c_prime: &str, c: &str) -> bool;
}
pub trait Detector {
fn kind(&self) -> DiscordKind;
fn fires(&self, context: &str) -> bool;
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiscordAssignment {
by_context: BTreeMap<ContextId, DiscordSet>,
}
impl DiscordAssignment {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
pub fn build_from_detectors<D: Detector>(detectors: &[D], contexts: &[ContextId]) -> Self {
let mut by_context = BTreeMap::new();
for context in contexts {
let mut set = DiscordSet::default();
for detector in detectors {
if detector.fires(context) {
set.insert(detector.kind());
}
}
by_context.insert(context.clone(), set);
}
Self { by_context }
}
pub fn set(&mut self, context: impl Into<ContextId>, kinds: DiscordSet) {
self.by_context.insert(context.into(), kinds);
}
pub fn get(&self, context: &str) -> DiscordSet {
self.by_context.get(context).cloned().unwrap_or_default()
}
pub fn iter(&self) -> impl Iterator<Item = (&ContextId, &DiscordSet)> {
self.by_context.iter()
}
pub fn frontier_support(&self) -> FrontierSupport {
let contexts = self
.by_context
.iter()
.filter(|(_, set)| !set.is_empty())
.map(|(c, _)| c.clone())
.collect();
FrontierSupport { contexts }
}
pub fn context_count(&self) -> usize {
self.by_context.len()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontierSupport {
contexts: BTreeSet<ContextId>,
}
impl FrontierSupport {
pub fn contains(&self, context: &str) -> bool {
self.contexts.contains(context)
}
pub fn len(&self) -> usize {
self.contexts.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.contexts.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &ContextId> {
self.contexts.iter()
}
pub fn is_upward_closed<R: ContextRefinement>(
&self,
refinement: &R,
universe: &[ContextId],
) -> bool {
for c_prime in &self.contexts {
for c in universe {
if c_prime != c && refinement.refines(c_prime, c) && !self.contexts.contains(c) {
return false;
}
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
struct PosetRefinement {
refines_map: BTreeMap<ContextId, BTreeSet<ContextId>>,
}
impl PosetRefinement {
fn new(direct_edges: &[(&str, &str)], universe: &[&str]) -> Self {
let mut refines_map: BTreeMap<ContextId, BTreeSet<ContextId>> = BTreeMap::new();
for c in universe {
refines_map
.entry(c.to_string())
.or_default()
.insert(c.to_string());
}
for (cp, c) in direct_edges {
refines_map
.entry(cp.to_string())
.or_default()
.insert(c.to_string());
}
let all: Vec<ContextId> = universe.iter().map(|s| s.to_string()).collect();
for k in &all {
for i in &all {
for j in &all {
if refines_map.get(i).is_some_and(|s| s.contains(k))
&& refines_map.get(k).is_some_and(|s| s.contains(j))
{
refines_map.entry(i.clone()).or_default().insert(j.clone());
}
}
}
}
Self { refines_map }
}
}
impl ContextRefinement for PosetRefinement {
fn refines(&self, c_prime: &str, c: &str) -> bool {
self.refines_map
.get(c_prime)
.is_some_and(|set| set.contains(c))
}
}
struct FixedDetector {
kind: DiscordKind,
fires_on: BTreeSet<String>,
}
impl FixedDetector {
fn new(kind: DiscordKind, fires_on: &[&str]) -> Self {
Self {
kind,
fires_on: fires_on.iter().map(|s| (*s).to_string()).collect(),
}
}
}
impl Detector for FixedDetector {
fn kind(&self) -> DiscordKind {
self.kind
}
fn fires(&self, context: &str) -> bool {
self.fires_on.contains(context)
}
}
fn ctxs(names: &[&str]) -> Vec<ContextId> {
names.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn discord_kinds_round_trip_serde() {
for kind in DiscordKind::ALL {
let json = serde_json::to_string(kind).unwrap();
let back: DiscordKind = serde_json::from_str(&json).unwrap();
assert_eq!(*kind, back);
}
}
#[test]
fn discord_set_join_is_union() {
let a = DiscordSet::from_kinds([DiscordKind::Conflict, DiscordKind::EvidenceGap]);
let b = DiscordSet::from_kinds([DiscordKind::EvidenceGap, DiscordKind::MethodMismatch]);
let joined = a.join(&b);
assert_eq!(joined.len(), 3);
assert!(joined.contains(DiscordKind::Conflict));
assert!(joined.contains(DiscordKind::EvidenceGap));
assert!(joined.contains(DiscordKind::MethodMismatch));
}
#[test]
fn discord_set_meet_is_intersection() {
let a = DiscordSet::from_kinds([DiscordKind::Conflict, DiscordKind::EvidenceGap]);
let b = DiscordSet::from_kinds([DiscordKind::EvidenceGap, DiscordKind::MethodMismatch]);
let met = a.meet(&b);
assert_eq!(met.len(), 1);
assert!(met.contains(DiscordKind::EvidenceGap));
}
#[test]
fn empty_assignment_has_empty_support() {
let assignment = DiscordAssignment::empty();
assert!(assignment.frontier_support().is_empty());
}
#[test]
fn theorem_4_monotone_detector_yields_upward_closed_support() {
let universe = ctxs(&["c1", "c2", "c3"]);
let refinement = PosetRefinement::new(&[("c1", "c2"), ("c2", "c3")], &["c1", "c2", "c3"]);
let monotone_conflict = FixedDetector::new(DiscordKind::Conflict, &["c1", "c2", "c3"]);
let assignment = DiscordAssignment::build_from_detectors(&[monotone_conflict], &universe);
let support = assignment.frontier_support();
assert!(support.is_upward_closed(&refinement, &universe));
assert!(support.contains("c1"));
assert!(support.contains("c2"));
assert!(support.contains("c3"));
}
#[test]
fn theorem_4_violation_when_detector_is_not_monotone() {
let universe = ctxs(&["c1", "c2", "c3"]);
let refinement = PosetRefinement::new(&[("c1", "c2"), ("c2", "c3")], &["c1", "c2", "c3"]);
let buggy = FixedDetector::new(DiscordKind::Conflict, &["c1"]);
let assignment = DiscordAssignment::build_from_detectors(&[buggy], &universe);
let support = assignment.frontier_support();
assert!(!support.is_upward_closed(&refinement, &universe));
assert!(support.contains("c1"));
assert!(!support.contains("c2"));
assert!(!support.contains("c3"));
}
#[test]
fn theorem_4_holds_for_pointwise_union_of_monotone_detectors() {
let universe = ctxs(&["c1", "c2", "c3", "c4"]);
let refinement = PosetRefinement::new(
&[("c1", "c2"), ("c2", "c4"), ("c3", "c4")],
&["c1", "c2", "c3", "c4"],
);
let det1 = FixedDetector::new(DiscordKind::Conflict, &["c1", "c2", "c4"]);
let det2 = FixedDetector::new(DiscordKind::EvidenceGap, &["c3", "c4"]);
let assignment = DiscordAssignment::build_from_detectors(&[det1, det2], &universe);
let support = assignment.frontier_support();
assert!(support.is_upward_closed(&refinement, &universe));
assert_eq!(support.len(), 4);
}
#[test]
fn frontier_support_contains_only_nonempty_contexts() {
let universe = ctxs(&["c1", "c2", "c3"]);
let det = FixedDetector::new(DiscordKind::Conflict, &["c1", "c3"]);
let assignment = DiscordAssignment::build_from_detectors(&[det], &universe);
let support = assignment.frontier_support();
assert!(support.contains("c1"));
assert!(!support.contains("c2"));
assert!(support.contains("c3"));
assert_eq!(support.len(), 2);
}
#[test]
fn manual_assignment_set_and_get() {
let mut a = DiscordAssignment::empty();
a.set("c1", DiscordSet::singleton(DiscordKind::ReplicationFail));
assert_eq!(a.get("c1").len(), 1);
assert!(a.get("c1").contains(DiscordKind::ReplicationFail));
assert!(a.get("c2").is_empty());
}
#[test]
fn poset_refinement_is_reflexive() {
let r = PosetRefinement::new(&[("c1", "c2")], &["c1", "c2"]);
assert!(r.refines("c1", "c1"));
assert!(r.refines("c2", "c2"));
}
#[test]
fn poset_refinement_is_transitive() {
let r = PosetRefinement::new(&[("a", "b"), ("b", "c")], &["a", "b", "c"]);
assert!(r.refines("a", "b"));
assert!(r.refines("b", "c"));
assert!(r.refines("a", "c"));
}
#[test]
fn discord_kind_display() {
assert_eq!(DiscordKind::Conflict.to_string(), "conflict");
assert_eq!(
DiscordKind::ConflictingConfidence.to_string(),
"conflicting_confidence"
);
assert_eq!(DiscordKind::MethodMismatch.to_string(), "method_mismatch");
}
#[test]
fn assignment_serde_round_trip() {
let mut a = DiscordAssignment::empty();
a.set(
"c1",
DiscordSet::from_kinds([DiscordKind::Conflict, DiscordKind::EvidenceGap]),
);
let json = serde_json::to_string(&a).unwrap();
let back: DiscordAssignment = serde_json::from_str(&json).unwrap();
assert_eq!(back, a);
}
}