crtx-memory 0.1.1

Memory lifecycle, salience, decay policies, and contradiction objects.
Documentation
//! First-class contradiction domain objects.

use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use std::time::SystemTime;

use cortex_core::ContradictionId;

/// Result type for contradiction operations.
pub type ContradictionResult<T> = Result<T, ContradictionError>;

/// Errors raised by contradiction domain logic.
#[derive(Debug, PartialEq, Eq)]
pub enum ContradictionError {
    /// Contradiction row was not found.
    NotFound(ContradictionId),
    /// Input failed validation.
    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 {}

/// Kind of conflict represented by a contradiction object.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContradictionType {
    /// Claims cannot both be true under the same scope.
    HardInconsistency,
    /// Claims can coexist only under clarified conditions.
    ConditionalTension,
    /// One memory may supersede another after review.
    SupersessionCandidate,
}

/// Resolution lifecycle for a contradiction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContradictionStatus {
    /// Detected, not yet interpreted.
    Unresolved,
    /// Human/system interpretation exists, but the conflict remains open.
    Interpreted,
    /// Closed by an explicit resolution.
    Resolved,
}

impl ContradictionStatus {
    /// Whether this status should still block clean canonical treatment.
    #[must_use]
    pub const fn is_open(self) -> bool {
        matches!(self, Self::Unresolved | Self::Interpreted)
    }
}

/// First-class contradiction record.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Contradiction {
    /// Stable contradiction ID.
    pub id: ContradictionId,
    /// Left memory/principle/reference.
    pub left_ref: String,
    /// Right memory/principle/reference.
    pub right_ref: String,
    /// Conflict kind.
    pub contradiction_type: ContradictionType,
    /// Current lifecycle status.
    pub status: ContradictionStatus,
    /// Interpretation or resolution note.
    pub interpretation: Option<String>,
    /// Creation timestamp.
    pub created_at: SystemTime,
    /// Last update timestamp.
    pub updated_at: SystemTime,
}

impl Contradiction {
    /// Construct a new unresolved 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,
        })
    }

    /// Mark this contradiction interpreted but still open.
    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(())
    }

    /// Mark this contradiction resolved.
    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(())
    }

    /// Whether the contradiction should still be treated as open.
    #[must_use]
    pub const fn is_open(&self) -> bool {
        self.status.is_open()
    }
}

/// Pure memory-layer CRUD registry for contradiction objects.
///
/// Persistence can wrap this once `cortex-store` exposes a contradiction repo;
/// the status semantics are intentionally independent of SQLite.
#[derive(Debug, Default, Clone)]
pub struct ContradictionRegistry {
    records: BTreeMap<ContradictionId, Contradiction>,
}

impl ContradictionRegistry {
    /// Create or replace a contradiction by ID.
    pub fn create(&mut self, contradiction: Contradiction) -> Option<Contradiction> {
        self.records.insert(contradiction.id, contradiction)
    }

    /// Read a contradiction by ID.
    #[must_use]
    pub fn get(&self, id: &ContradictionId) -> Option<&Contradiction> {
        self.records.get(id)
    }

    /// Update an existing contradiction in place.
    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)
    }

    /// Delete a contradiction by ID.
    pub fn delete(&mut self, id: &ContradictionId) -> Option<Contradiction> {
        self.records.remove(id)
    }

    /// List all records in deterministic ID order.
    #[must_use]
    pub fn list(&self) -> Vec<&Contradiction> {
        self.records.values().collect()
    }

    /// List unresolved records only.
    #[must_use]
    pub fn list_unresolved(&self) -> Vec<&Contradiction> {
        self.records
            .values()
            .filter(|record| record.status == ContradictionStatus::Unresolved)
            .collect()
    }

    /// List records that remain open for canonicality/retrieval purposes.
    #[must_use]
    pub fn list_open(&self) -> Vec<&Contradiction> {
        self.records
            .values()
            .filter(|record| record.is_open())
            .collect()
    }

    /// List resolved records only.
    #[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());
    }
}