1use std::collections::BTreeMap;
4use std::error::Error;
5use std::fmt;
6use std::time::SystemTime;
7
8use cortex_core::ContradictionId;
9
10pub type ContradictionResult<T> = Result<T, ContradictionError>;
12
13#[derive(Debug, PartialEq, Eq)]
15pub enum ContradictionError {
16 NotFound(ContradictionId),
18 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ContradictionType {
36 HardInconsistency,
38 ConditionalTension,
40 SupersessionCandidate,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ContradictionStatus {
47 Unresolved,
49 Interpreted,
51 Resolved,
53}
54
55impl ContradictionStatus {
56 #[must_use]
58 pub const fn is_open(self) -> bool {
59 matches!(self, Self::Unresolved | Self::Interpreted)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Contradiction {
66 pub id: ContradictionId,
68 pub left_ref: String,
70 pub right_ref: String,
72 pub contradiction_type: ContradictionType,
74 pub status: ContradictionStatus,
76 pub interpretation: Option<String>,
78 pub created_at: SystemTime,
80 pub updated_at: SystemTime,
82}
83
84impl Contradiction {
85 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 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 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 #[must_use]
140 pub const fn is_open(&self) -> bool {
141 self.status.is_open()
142 }
143}
144
145#[derive(Debug, Default, Clone)]
150pub struct ContradictionRegistry {
151 records: BTreeMap<ContradictionId, Contradiction>,
152}
153
154impl ContradictionRegistry {
155 pub fn create(&mut self, contradiction: Contradiction) -> Option<Contradiction> {
157 self.records.insert(contradiction.id, contradiction)
158 }
159
160 #[must_use]
162 pub fn get(&self, id: &ContradictionId) -> Option<&Contradiction> {
163 self.records.get(id)
164 }
165
166 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 pub fn delete(&mut self, id: &ContradictionId) -> Option<Contradiction> {
180 self.records.remove(id)
181 }
182
183 #[must_use]
185 pub fn list(&self) -> Vec<&Contradiction> {
186 self.records.values().collect()
187 }
188
189 #[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 #[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 #[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}