Skip to main content

cortex_memory/
contradictions.rs

1//! First-class contradiction domain objects.
2
3use std::collections::BTreeMap;
4use std::error::Error;
5use std::fmt;
6use std::time::SystemTime;
7
8use cortex_core::ContradictionId;
9
10/// Result type for contradiction operations.
11pub type ContradictionResult<T> = Result<T, ContradictionError>;
12
13/// Errors raised by contradiction domain logic.
14#[derive(Debug, PartialEq, Eq)]
15pub enum ContradictionError {
16    /// Contradiction row was not found.
17    NotFound(ContradictionId),
18    /// Input failed validation.
19    Validation(String),
20}
21
22impl fmt::Display for ContradictionError {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::NotFound(id) => write!(f, "contradiction {id} not found"),
26            Self::Validation(message) => write!(f, "validation failed: {message}"),
27        }
28    }
29}
30
31impl Error for ContradictionError {}
32
33/// Kind of conflict represented by a contradiction object.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ContradictionType {
36    /// Claims cannot both be true under the same scope.
37    HardInconsistency,
38    /// Claims can coexist only under clarified conditions.
39    ConditionalTension,
40    /// One memory may supersede another after review.
41    SupersessionCandidate,
42}
43
44/// Resolution lifecycle for a contradiction.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ContradictionStatus {
47    /// Detected, not yet interpreted.
48    Unresolved,
49    /// Human/system interpretation exists, but the conflict remains open.
50    Interpreted,
51    /// Closed by an explicit resolution.
52    Resolved,
53}
54
55impl ContradictionStatus {
56    /// Whether this status should still block clean canonical treatment.
57    #[must_use]
58    pub const fn is_open(self) -> bool {
59        matches!(self, Self::Unresolved | Self::Interpreted)
60    }
61}
62
63/// First-class contradiction record.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Contradiction {
66    /// Stable contradiction ID.
67    pub id: ContradictionId,
68    /// Left memory/principle/reference.
69    pub left_ref: String,
70    /// Right memory/principle/reference.
71    pub right_ref: String,
72    /// Conflict kind.
73    pub contradiction_type: ContradictionType,
74    /// Current lifecycle status.
75    pub status: ContradictionStatus,
76    /// Interpretation or resolution note.
77    pub interpretation: Option<String>,
78    /// Creation timestamp.
79    pub created_at: SystemTime,
80    /// Last update timestamp.
81    pub updated_at: SystemTime,
82}
83
84impl Contradiction {
85    /// Construct a new unresolved contradiction.
86    pub fn new(
87        id: ContradictionId,
88        left_ref: impl Into<String>,
89        right_ref: impl Into<String>,
90        contradiction_type: ContradictionType,
91        created_at: SystemTime,
92    ) -> ContradictionResult<Self> {
93        let left_ref = left_ref.into();
94        let right_ref = right_ref.into();
95        validate_ref("left_ref", &left_ref)?;
96        validate_ref("right_ref", &right_ref)?;
97
98        Ok(Self {
99            id,
100            left_ref,
101            right_ref,
102            contradiction_type,
103            status: ContradictionStatus::Unresolved,
104            interpretation: None,
105            created_at,
106            updated_at: created_at,
107        })
108    }
109
110    /// Mark this contradiction interpreted but still open.
111    pub fn interpret(
112        &mut self,
113        interpretation: impl Into<String>,
114        updated_at: SystemTime,
115    ) -> ContradictionResult<()> {
116        let interpretation = interpretation.into();
117        validate_note("interpretation", &interpretation)?;
118        self.status = ContradictionStatus::Interpreted;
119        self.interpretation = Some(interpretation);
120        self.updated_at = updated_at;
121        Ok(())
122    }
123
124    /// Mark this contradiction resolved.
125    pub fn resolve(
126        &mut self,
127        resolution: impl Into<String>,
128        updated_at: SystemTime,
129    ) -> ContradictionResult<()> {
130        let resolution = resolution.into();
131        validate_note("resolution", &resolution)?;
132        self.status = ContradictionStatus::Resolved;
133        self.interpretation = Some(resolution);
134        self.updated_at = updated_at;
135        Ok(())
136    }
137
138    /// Whether the contradiction should still be treated as open.
139    #[must_use]
140    pub const fn is_open(&self) -> bool {
141        self.status.is_open()
142    }
143}
144
145/// Pure memory-layer CRUD registry for contradiction objects.
146///
147/// Persistence can wrap this once `cortex-store` exposes a contradiction repo;
148/// the status semantics are intentionally independent of SQLite.
149#[derive(Debug, Default, Clone)]
150pub struct ContradictionRegistry {
151    records: BTreeMap<ContradictionId, Contradiction>,
152}
153
154impl ContradictionRegistry {
155    /// Create or replace a contradiction by ID.
156    pub fn create(&mut self, contradiction: Contradiction) -> Option<Contradiction> {
157        self.records.insert(contradiction.id, contradiction)
158    }
159
160    /// Read a contradiction by ID.
161    #[must_use]
162    pub fn get(&self, id: &ContradictionId) -> Option<&Contradiction> {
163        self.records.get(id)
164    }
165
166    /// Update an existing contradiction in place.
167    pub fn update<F>(&mut self, id: &ContradictionId, update: F) -> ContradictionResult<()>
168    where
169        F: FnOnce(&mut Contradiction) -> ContradictionResult<()>,
170    {
171        let contradiction = self
172            .records
173            .get_mut(id)
174            .ok_or(ContradictionError::NotFound(*id))?;
175        update(contradiction)
176    }
177
178    /// Delete a contradiction by ID.
179    pub fn delete(&mut self, id: &ContradictionId) -> Option<Contradiction> {
180        self.records.remove(id)
181    }
182
183    /// List all records in deterministic ID order.
184    #[must_use]
185    pub fn list(&self) -> Vec<&Contradiction> {
186        self.records.values().collect()
187    }
188
189    /// List unresolved records only.
190    #[must_use]
191    pub fn list_unresolved(&self) -> Vec<&Contradiction> {
192        self.records
193            .values()
194            .filter(|record| record.status == ContradictionStatus::Unresolved)
195            .collect()
196    }
197
198    /// List records that remain open for canonicality/retrieval purposes.
199    #[must_use]
200    pub fn list_open(&self) -> Vec<&Contradiction> {
201        self.records
202            .values()
203            .filter(|record| record.is_open())
204            .collect()
205    }
206
207    /// List resolved records only.
208    #[must_use]
209    pub fn list_resolved(&self) -> Vec<&Contradiction> {
210        self.records
211            .values()
212            .filter(|record| record.status == ContradictionStatus::Resolved)
213            .collect()
214    }
215}
216
217fn validate_ref(field: &str, value: &str) -> ContradictionResult<()> {
218    if value.trim().is_empty() {
219        return Err(ContradictionError::Validation(format!(
220            "{field} must not be empty"
221        )));
222    }
223    Ok(())
224}
225
226fn validate_note(field: &str, value: &str) -> ContradictionResult<()> {
227    if value.trim().is_empty() {
228        return Err(ContradictionError::Validation(format!(
229            "{field} must not be empty"
230        )));
231    }
232    Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use std::time::Duration;
239
240    fn at(seconds: u64) -> SystemTime {
241        SystemTime::UNIX_EPOCH + Duration::from_secs(seconds)
242    }
243
244    fn id(n: u8) -> ContradictionId {
245        format!("con_01ARZ3NDEKTSV4RRFFQ69G5FA{n}").parse().unwrap()
246    }
247
248    fn contradiction(n: u8) -> Contradiction {
249        Contradiction::new(
250            id(n),
251            "mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
252            "mem_01BRZ3NDEKTSV4RRFFQ69G5FAV",
253            ContradictionType::ConditionalTension,
254            at(0),
255        )
256        .unwrap()
257    }
258
259    #[test]
260    fn new_contradiction_starts_unresolved_and_open() {
261        let contradiction = contradiction(1);
262
263        assert_eq!(contradiction.status, ContradictionStatus::Unresolved);
264        assert!(contradiction.is_open());
265        assert!(contradiction.interpretation.is_none());
266    }
267
268    #[test]
269    fn interpreted_contradiction_is_no_longer_unresolved_but_remains_open() {
270        let mut registry = ContradictionRegistry::default();
271        let id = id(2);
272        registry.create(contradiction(2));
273
274        registry
275            .update(&id, |record| {
276                record.interpret("applies under different task scopes", at(5))
277            })
278            .unwrap();
279
280        assert_eq!(registry.list_unresolved().len(), 0);
281        assert_eq!(registry.list_open().len(), 1);
282        assert_eq!(
283            registry.get(&id).unwrap().status,
284            ContradictionStatus::Interpreted
285        );
286    }
287
288    #[test]
289    fn resolved_contradiction_is_closed_and_listed_as_resolved() {
290        let mut registry = ContradictionRegistry::default();
291        let id = id(3);
292        registry.create(contradiction(3));
293
294        registry
295            .update(&id, |record| {
296                record.resolve("newer memory supersedes older", at(6))
297            })
298            .unwrap();
299
300        assert_eq!(registry.list_open().len(), 0);
301        assert_eq!(registry.list_resolved().len(), 1);
302        assert!(!registry.get(&id).unwrap().is_open());
303    }
304
305    #[test]
306    fn registry_crud_round_trip() {
307        let mut registry = ContradictionRegistry::default();
308        let id = id(4);
309
310        assert!(registry.create(contradiction(4)).is_none());
311        assert!(registry.get(&id).is_some());
312        assert_eq!(registry.list().len(), 1);
313        assert!(registry.delete(&id).is_some());
314        assert!(registry.get(&id).is_none());
315    }
316
317    #[test]
318    fn empty_refs_and_notes_fail_validation() {
319        assert!(Contradiction::new(
320            id(5),
321            "",
322            "mem_01BRZ3NDEKTSV4RRFFQ69G5FAV",
323            ContradictionType::HardInconsistency,
324            at(0),
325        )
326        .is_err());
327
328        let mut contradiction = contradiction(6);
329        assert!(contradiction.interpret(" ", at(1)).is_err());
330        assert!(contradiction.resolve("", at(1)).is_err());
331    }
332}