1use crate::claims::conflicting_ids;
11use crate::ledger::MemoryStore;
12use crate::trace::{PrivacyClass, Trace, TraceStatus};
13use serde::{Deserialize, Serialize};
14use std::collections::HashSet;
15
16const MAX_CHAIN: usize = 64;
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct TraceLineage {
22 pub trace: Trace,
23 pub predecessors: Vec<Trace>,
25 pub successor: Option<Trace>,
27 pub conflicts: Vec<String>,
30}
31
32impl MemoryStore {
33 pub fn provenance_chain(&self, id: &str) -> Option<TraceLineage> {
36 let entry = self.get(id)?;
37 let trace = entry.trace.clone();
38 let active = entry.status == TraceStatus::Active;
39
40 let mut predecessors = Vec::new();
41 let mut seen = HashSet::new();
42 seen.insert(trace.id.clone());
43 let mut cursor = trace.supersedes.clone();
44 while let Some(old_id) = cursor {
45 if predecessors.len() >= MAX_CHAIN || !seen.insert(old_id.clone()) {
46 break;
47 }
48 let Some(old) = self.get(&old_id) else {
49 break;
50 };
51 cursor = old.trace.supersedes.clone();
52 predecessors.push(old.trace.clone());
53 }
54
55 let allow_sensitive = self.policy().allow_sensitive_persistence;
59 let successor = self
60 .all()
61 .filter(|e| e.status != TraceStatus::Purged)
62 .filter(|e| allow_sensitive || e.trace.privacy != PrivacyClass::Sensitive)
63 .filter(|e| e.trace.supersedes.as_deref() == Some(id))
64 .map(|e| e.trace.clone())
65 .next();
66
67 let conflicts = if active {
68 conflicting_ids(&trace, self.active_traces())
69 } else {
70 Vec::new()
71 };
72
73 Some(TraceLineage {
74 trace,
75 predecessors,
76 successor,
77 conflicts,
78 })
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::policy::KernelPolicy;
86 use crate::trace::{Trace, TraceKind};
87 use std::ops::{Deref, DerefMut};
88 use std::path::PathBuf;
89 use std::sync::atomic::{AtomicU64, Ordering};
90 use std::time::{SystemTime, UNIX_EPOCH};
91
92 static COUNTER: AtomicU64 = AtomicU64::new(0);
93
94 struct TestStore {
99 dir: PathBuf,
100 store: MemoryStore,
101 }
102
103 impl TestStore {
104 fn open() -> Self {
105 let nonce = SystemTime::now()
106 .duration_since(UNIX_EPOCH)
107 .unwrap()
108 .as_nanos();
109 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
110 let dir = std::env::temp_dir().join(format!(
111 "hikmah-why-test-{}-{nonce}-{n}",
112 std::process::id()
113 ));
114 std::fs::create_dir_all(&dir).expect("mkdir");
115 let store =
116 MemoryStore::open(dir.join("memory.jsonl"), KernelPolicy::default()).expect("open");
117 TestStore { dir, store }
118 }
119 }
120
121 impl Drop for TestStore {
122 fn drop(&mut self) {
123 let _ = std::fs::remove_dir_all(&self.dir);
124 }
125 }
126
127 impl Deref for TestStore {
128 type Target = MemoryStore;
129 fn deref(&self) -> &MemoryStore {
130 &self.store
131 }
132 }
133
134 impl DerefMut for TestStore {
135 fn deref_mut(&mut self) -> &mut MemoryStore {
136 &mut self.store
137 }
138 }
139
140 fn open_store() -> TestStore {
141 TestStore::open()
142 }
143
144 fn add(store: &mut MemoryStore, content: &str) -> Trace {
145 store
146 .remember(Trace::new(TraceKind::Belief, content, "human:a"))
147 .expect("remember")
148 .0
149 }
150
151 #[test]
152 fn chain_follows_supersession_predecessors() {
153 let mut store = open_store();
154 let old = add(&mut store, "old belief");
155 let mut corrected = Trace::new(TraceKind::Belief, "corrected belief", "human:a");
156 corrected.supersedes = Some(old.id.clone());
157 let new = store.remember(corrected).expect("remember").0;
158
159 let chain = store.provenance_chain(&new.id).expect("chain");
160 assert_eq!(chain.trace.id, new.id);
161 assert_eq!(chain.predecessors.len(), 1);
162 assert_eq!(chain.predecessors[0].id, old.id);
163 assert!(chain.successor.is_none());
164 assert!(chain.conflicts.is_empty());
165 }
166
167 #[test]
168 fn chain_reports_the_successor() {
169 let mut store = open_store();
170 let old = add(&mut store, "old belief");
171 let mut corrected = Trace::new(TraceKind::Belief, "corrected belief", "human:a");
172 corrected.supersedes = Some(old.id.clone());
173 store.remember(corrected).expect("remember");
174
175 let chain = store.provenance_chain(&old.id).expect("chain");
176 assert!(chain.predecessors.is_empty());
177 let successor = chain.successor.expect("successor");
178 assert_eq!(successor.supersedes.as_deref(), Some(old.id.as_str()));
179 }
180
181 #[test]
182 fn chain_lists_unresolved_conflicts() {
183 let mut store = open_store();
184 let mut a = Trace::new(TraceKind::Belief, "the sky is green", "human:a");
185 a.claim_key = Some("sky-color".into());
186 a.claim_value = Some("green".into());
187 let a = store.remember(a).expect("remember").0;
188 let mut b = Trace::new(TraceKind::Belief, "the sky is blue", "human:b");
189 b.claim_key = Some("sky-color".into());
190 b.claim_value = Some("blue".into());
191 store.remember(b).expect("remember");
192
193 let chain = store.provenance_chain(&a.id).expect("chain");
194 assert_eq!(chain.conflicts.len(), 1);
195 }
196
197 #[test]
198 fn chain_is_none_for_unknown_id() {
199 let store = open_store();
200 assert!(store.provenance_chain("tr_missing").is_none());
201 }
202}