Skip to main content

cortex_store/
proof.rs

1//! Store-local authority proof closure verification.
2//!
3//! This module verifies only proof edges observable in the current SQLite
4//! store. It does not claim signed-ledger authority, external anchoring, or
5//! policy authority; higher layers must compose this report with those axes.
6
7use 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
17/// Verify store-local proof closure for a memory row.
18pub 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/// Convert temporal authority revalidation into a proof closure report.
85#[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}