use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use std::time::SystemTime;
use cortex_core::ContradictionId;
pub type ContradictionResult<T> = Result<T, ContradictionError>;
#[derive(Debug, PartialEq, Eq)]
pub enum ContradictionError {
NotFound(ContradictionId),
Validation(String),
}
impl fmt::Display for ContradictionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound(id) => write!(f, "contradiction {id} not found"),
Self::Validation(message) => write!(f, "validation failed: {message}"),
}
}
}
impl Error for ContradictionError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContradictionType {
HardInconsistency,
ConditionalTension,
SupersessionCandidate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContradictionStatus {
Unresolved,
Interpreted,
Resolved,
}
impl ContradictionStatus {
#[must_use]
pub const fn is_open(self) -> bool {
matches!(self, Self::Unresolved | Self::Interpreted)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Contradiction {
pub id: ContradictionId,
pub left_ref: String,
pub right_ref: String,
pub contradiction_type: ContradictionType,
pub status: ContradictionStatus,
pub interpretation: Option<String>,
pub created_at: SystemTime,
pub updated_at: SystemTime,
}
impl Contradiction {
pub fn new(
id: ContradictionId,
left_ref: impl Into<String>,
right_ref: impl Into<String>,
contradiction_type: ContradictionType,
created_at: SystemTime,
) -> ContradictionResult<Self> {
let left_ref = left_ref.into();
let right_ref = right_ref.into();
validate_ref("left_ref", &left_ref)?;
validate_ref("right_ref", &right_ref)?;
Ok(Self {
id,
left_ref,
right_ref,
contradiction_type,
status: ContradictionStatus::Unresolved,
interpretation: None,
created_at,
updated_at: created_at,
})
}
pub fn interpret(
&mut self,
interpretation: impl Into<String>,
updated_at: SystemTime,
) -> ContradictionResult<()> {
let interpretation = interpretation.into();
validate_note("interpretation", &interpretation)?;
self.status = ContradictionStatus::Interpreted;
self.interpretation = Some(interpretation);
self.updated_at = updated_at;
Ok(())
}
pub fn resolve(
&mut self,
resolution: impl Into<String>,
updated_at: SystemTime,
) -> ContradictionResult<()> {
let resolution = resolution.into();
validate_note("resolution", &resolution)?;
self.status = ContradictionStatus::Resolved;
self.interpretation = Some(resolution);
self.updated_at = updated_at;
Ok(())
}
#[must_use]
pub const fn is_open(&self) -> bool {
self.status.is_open()
}
}
#[derive(Debug, Default, Clone)]
pub struct ContradictionRegistry {
records: BTreeMap<ContradictionId, Contradiction>,
}
impl ContradictionRegistry {
pub fn create(&mut self, contradiction: Contradiction) -> Option<Contradiction> {
self.records.insert(contradiction.id, contradiction)
}
#[must_use]
pub fn get(&self, id: &ContradictionId) -> Option<&Contradiction> {
self.records.get(id)
}
pub fn update<F>(&mut self, id: &ContradictionId, update: F) -> ContradictionResult<()>
where
F: FnOnce(&mut Contradiction) -> ContradictionResult<()>,
{
let contradiction = self
.records
.get_mut(id)
.ok_or(ContradictionError::NotFound(*id))?;
update(contradiction)
}
pub fn delete(&mut self, id: &ContradictionId) -> Option<Contradiction> {
self.records.remove(id)
}
#[must_use]
pub fn list(&self) -> Vec<&Contradiction> {
self.records.values().collect()
}
#[must_use]
pub fn list_unresolved(&self) -> Vec<&Contradiction> {
self.records
.values()
.filter(|record| record.status == ContradictionStatus::Unresolved)
.collect()
}
#[must_use]
pub fn list_open(&self) -> Vec<&Contradiction> {
self.records
.values()
.filter(|record| record.is_open())
.collect()
}
#[must_use]
pub fn list_resolved(&self) -> Vec<&Contradiction> {
self.records
.values()
.filter(|record| record.status == ContradictionStatus::Resolved)
.collect()
}
}
fn validate_ref(field: &str, value: &str) -> ContradictionResult<()> {
if value.trim().is_empty() {
return Err(ContradictionError::Validation(format!(
"{field} must not be empty"
)));
}
Ok(())
}
fn validate_note(field: &str, value: &str) -> ContradictionResult<()> {
if value.trim().is_empty() {
return Err(ContradictionError::Validation(format!(
"{field} must not be empty"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn at(seconds: u64) -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(seconds)
}
fn id(n: u8) -> ContradictionId {
format!("con_01ARZ3NDEKTSV4RRFFQ69G5FA{n}").parse().unwrap()
}
fn contradiction(n: u8) -> Contradiction {
Contradiction::new(
id(n),
"mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"mem_01BRZ3NDEKTSV4RRFFQ69G5FAV",
ContradictionType::ConditionalTension,
at(0),
)
.unwrap()
}
#[test]
fn new_contradiction_starts_unresolved_and_open() {
let contradiction = contradiction(1);
assert_eq!(contradiction.status, ContradictionStatus::Unresolved);
assert!(contradiction.is_open());
assert!(contradiction.interpretation.is_none());
}
#[test]
fn interpreted_contradiction_is_no_longer_unresolved_but_remains_open() {
let mut registry = ContradictionRegistry::default();
let id = id(2);
registry.create(contradiction(2));
registry
.update(&id, |record| {
record.interpret("applies under different task scopes", at(5))
})
.unwrap();
assert_eq!(registry.list_unresolved().len(), 0);
assert_eq!(registry.list_open().len(), 1);
assert_eq!(
registry.get(&id).unwrap().status,
ContradictionStatus::Interpreted
);
}
#[test]
fn resolved_contradiction_is_closed_and_listed_as_resolved() {
let mut registry = ContradictionRegistry::default();
let id = id(3);
registry.create(contradiction(3));
registry
.update(&id, |record| {
record.resolve("newer memory supersedes older", at(6))
})
.unwrap();
assert_eq!(registry.list_open().len(), 0);
assert_eq!(registry.list_resolved().len(), 1);
assert!(!registry.get(&id).unwrap().is_open());
}
#[test]
fn registry_crud_round_trip() {
let mut registry = ContradictionRegistry::default();
let id = id(4);
assert!(registry.create(contradiction(4)).is_none());
assert!(registry.get(&id).is_some());
assert_eq!(registry.list().len(), 1);
assert!(registry.delete(&id).is_some());
assert!(registry.get(&id).is_none());
}
#[test]
fn empty_refs_and_notes_fail_validation() {
assert!(Contradiction::new(
id(5),
"",
"mem_01BRZ3NDEKTSV4RRFFQ69G5FAV",
ContradictionType::HardInconsistency,
at(0),
)
.is_err());
let mut contradiction = contradiction(6);
assert!(contradiction.interpret(" ", at(1)).is_err());
assert!(contradiction.resolve("", at(1)).is_err());
}
}