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