1use cortex_core::{
8 EventId, FailingEdge, MemoryId, ProofClosureReport, ProofEdge, ProofEdgeFailure, ProofEdgeKind,
9 TemporalAuthorityReport,
10};
11use rusqlite::{params, OptionalExtension};
12use serde_json::Value;
13
14use crate::repo::{EpisodeRepo, EventRepo, MemoryRepo};
15use crate::{Pool, StoreResult};
16
17pub fn verify_memory_proof_closure(
19 pool: &Pool,
20 memory_id: &MemoryId,
21) -> StoreResult<ProofClosureReport> {
22 let memories = MemoryRepo::new(pool);
23 let Some(memory) = memories.get_by_id(memory_id)? else {
24 return Ok(ProofClosureReport::from_edges(
25 Vec::new(),
26 vec![FailingEdge::missing(
27 ProofEdgeKind::LineageClosure,
28 memory_id.to_string(),
29 "memory row not found",
30 )],
31 ));
32 };
33
34 if memory.memory_type == "operator_note" {
38 return Ok(ProofClosureReport::from_edges(
39 vec![cortex_core::ProofEdge::new(
40 ProofEdgeKind::LineageClosure,
41 memory_id.to_string(),
42 "operator_note",
43 )],
44 Vec::new(),
45 ));
46 }
47
48 let mut verified_edges = Vec::new();
49 let mut failing_edges = Vec::new();
50 let mut source_events =
51 string_array_refs(&memory.source_events_json, memory_id, "source_events").unwrap_or_else(
52 |edge| {
53 failing_edges.push(edge);
54 Vec::new()
55 },
56 );
57
58 for episode_ref in string_array_refs(&memory.source_episodes_json, memory_id, "source_episodes")
59 .unwrap_or_else(|edge| {
60 failing_edges.push(edge);
61 Vec::new()
62 })
63 {
64 verify_episode_lineage(
65 pool,
66 memory_id,
67 &episode_ref,
68 &mut source_events,
69 &mut verified_edges,
70 &mut failing_edges,
71 )?;
72 }
73
74 if source_events.is_empty() {
75 failing_edges.push(FailingEdge::missing(
76 ProofEdgeKind::LineageClosure,
77 memory_id.to_string(),
78 "memory has no source event lineage after episode expansion",
79 ));
80 }
81
82 for event_ref in source_events {
83 verify_event_lineage(
84 pool,
85 memory_id,
86 &event_ref,
87 &mut verified_edges,
88 &mut failing_edges,
89 )?;
90 }
91
92 Ok(ProofClosureReport::from_edges(
93 verified_edges,
94 failing_edges,
95 ))
96}
97
98#[must_use]
100pub fn temporal_authority_proof_report(
101 target_ref: impl Into<String>,
102 report: &TemporalAuthorityReport,
103) -> ProofClosureReport {
104 let target_ref = target_ref.into();
105 if let Some(edge) = report.current_use_failing_edge(target_ref.clone()) {
106 return ProofClosureReport::from_edges(Vec::new(), vec![edge]);
107 }
108
109 ProofClosureReport::full_chain_verified(vec![ProofEdge::new(
110 ProofEdgeKind::AuthorityFold,
111 target_ref,
112 report.key_id.clone(),
113 )
114 .with_evidence_ref("temporal_authority")])
115}
116
117fn verify_episode_lineage(
118 pool: &Pool,
119 memory_id: &MemoryId,
120 episode_ref: &str,
121 source_events: &mut Vec<String>,
122 verified_edges: &mut Vec<ProofEdge>,
123 failing_edges: &mut Vec<FailingEdge>,
124) -> StoreResult<()> {
125 let episodes = EpisodeRepo::new(pool);
126 let Ok(episode_id) = episode_ref.parse() else {
127 failing_edges.push(FailingEdge::broken(
128 ProofEdgeKind::LineageClosure,
129 memory_id.to_string(),
130 episode_ref,
131 ProofEdgeFailure::Mismatch,
132 "memory source_episodes entry is not a valid episode id",
133 ));
134 return Ok(());
135 };
136 let Some(episode) = episodes.get_by_id(&episode_id)? else {
137 failing_edges.push(FailingEdge::missing(
138 ProofEdgeKind::LineageClosure,
139 episode_ref,
140 "source episode not found",
141 ));
142 return Ok(());
143 };
144
145 verified_edges.push(
146 ProofEdge::new(
147 ProofEdgeKind::LineageClosure,
148 memory_id.to_string(),
149 episode.id.to_string(),
150 )
151 .with_evidence_ref("memories.source_episodes_json"),
152 );
153
154 match string_array_refs(
155 &episode.source_events_json,
156 memory_id,
157 "episode.source_events",
158 ) {
159 Ok(events) => source_events.extend(events),
160 Err(edge) => failing_edges.push(edge),
161 }
162
163 Ok(())
164}
165
166fn verify_event_lineage(
167 pool: &Pool,
168 memory_id: &MemoryId,
169 event_ref: &str,
170 verified_edges: &mut Vec<ProofEdge>,
171 failing_edges: &mut Vec<FailingEdge>,
172) -> StoreResult<()> {
173 let events = EventRepo::new(pool);
174 let Ok(event_id) = event_ref.parse::<EventId>() else {
175 failing_edges.push(FailingEdge::broken(
176 ProofEdgeKind::LineageClosure,
177 memory_id.to_string(),
178 event_ref,
179 ProofEdgeFailure::Mismatch,
180 "source event reference is not a valid event id",
181 ));
182 return Ok(());
183 };
184 let Some(event) = events.get_by_id(&event_id)? else {
185 failing_edges.push(FailingEdge::missing(
186 ProofEdgeKind::LineageClosure,
187 event_ref,
188 "source event not found",
189 ));
190 return Ok(());
191 };
192
193 verified_edges.push(
194 ProofEdge::new(
195 ProofEdgeKind::LineageClosure,
196 memory_id.to_string(),
197 event.id.to_string(),
198 )
199 .with_evidence_ref(event.event_hash.clone()),
200 );
201
202 if let Some(prev_hash) = &event.prev_event_hash {
203 match event_id_by_hash(pool, prev_hash)? {
204 Some(parent_id) => verified_edges.push(
205 ProofEdge::new(ProofEdgeKind::HashChain, parent_id, event.id.to_string())
206 .with_evidence_ref(prev_hash.clone()),
207 ),
208 None => failing_edges.push(FailingEdge::missing(
209 ProofEdgeKind::HashChain,
210 event.id.to_string(),
211 "source event prev_event_hash has no parent event in store",
212 )),
213 }
214 }
215
216 Ok(())
217}
218
219fn event_id_by_hash(pool: &Pool, event_hash: &str) -> StoreResult<Option<String>> {
220 Ok(pool
221 .query_row(
222 "SELECT id FROM events WHERE event_hash = ?1;",
223 params![event_hash],
224 |row| row.get::<_, String>(0),
225 )
226 .optional()?)
227}
228
229fn string_array_refs(
230 value: &Value,
231 memory_id: &MemoryId,
232 field: &'static str,
233) -> Result<Vec<String>, FailingEdge> {
234 let Some(items) = value.as_array() else {
235 return Err(FailingEdge::broken(
236 ProofEdgeKind::LineageClosure,
237 memory_id.to_string(),
238 field,
239 ProofEdgeFailure::Mismatch,
240 format!("{field} must be a JSON array"),
241 ));
242 };
243
244 let mut refs = Vec::with_capacity(items.len());
245 for item in items {
246 let Some(text) = item.as_str() else {
247 return Err(FailingEdge::broken(
248 ProofEdgeKind::LineageClosure,
249 memory_id.to_string(),
250 field,
251 ProofEdgeFailure::Mismatch,
252 format!("{field} must contain string refs"),
253 ));
254 };
255 refs.push(text.to_string());
256 }
257 Ok(refs)
258}