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    // operator_note memories are self-attesting — the operator's direct act
35    // of writing is the proof. Return FullChainVerified immediately without
36    // requiring event lineage.
37    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/// Convert temporal authority revalidation into a proof closure report.
99#[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}