crtx-store 0.1.0

SQLite persistence: migrations, repositories, transactions.
Documentation
//! Store-local authority proof closure verification.
//!
//! This module verifies only proof edges observable in the current SQLite
//! store. It does not claim signed-ledger authority, external anchoring, or
//! policy authority; higher layers must compose this report with those axes.

use cortex_core::{
    EventId, FailingEdge, MemoryId, ProofClosureReport, ProofEdge, ProofEdgeFailure, ProofEdgeKind,
    TemporalAuthorityReport,
};
use rusqlite::{params, OptionalExtension};
use serde_json::Value;

use crate::repo::{EpisodeRepo, EventRepo, MemoryRepo};
use crate::{Pool, StoreResult};

/// Verify store-local proof closure for a memory row.
pub fn verify_memory_proof_closure(
    pool: &Pool,
    memory_id: &MemoryId,
) -> StoreResult<ProofClosureReport> {
    let memories = MemoryRepo::new(pool);
    let Some(memory) = memories.get_by_id(memory_id)? else {
        return Ok(ProofClosureReport::from_edges(
            Vec::new(),
            vec![FailingEdge::missing(
                ProofEdgeKind::LineageClosure,
                memory_id.to_string(),
                "memory row not found",
            )],
        ));
    };

    let mut verified_edges = Vec::new();
    let mut failing_edges = Vec::new();
    let mut source_events =
        string_array_refs(&memory.source_events_json, memory_id, "source_events").unwrap_or_else(
            |edge| {
                failing_edges.push(edge);
                Vec::new()
            },
        );

    for episode_ref in string_array_refs(&memory.source_episodes_json, memory_id, "source_episodes")
        .unwrap_or_else(|edge| {
            failing_edges.push(edge);
            Vec::new()
        })
    {
        verify_episode_lineage(
            pool,
            memory_id,
            &episode_ref,
            &mut source_events,
            &mut verified_edges,
            &mut failing_edges,
        )?;
    }

    if source_events.is_empty() {
        failing_edges.push(FailingEdge::missing(
            ProofEdgeKind::LineageClosure,
            memory_id.to_string(),
            "memory has no source event lineage after episode expansion",
        ));
    }

    for event_ref in source_events {
        verify_event_lineage(
            pool,
            memory_id,
            &event_ref,
            &mut verified_edges,
            &mut failing_edges,
        )?;
    }

    Ok(ProofClosureReport::from_edges(
        verified_edges,
        failing_edges,
    ))
}

/// Convert temporal authority revalidation into a proof closure report.
#[must_use]
pub fn temporal_authority_proof_report(
    target_ref: impl Into<String>,
    report: &TemporalAuthorityReport,
) -> ProofClosureReport {
    let target_ref = target_ref.into();
    if let Some(edge) = report.current_use_failing_edge(target_ref.clone()) {
        return ProofClosureReport::from_edges(Vec::new(), vec![edge]);
    }

    ProofClosureReport::full_chain_verified(vec![ProofEdge::new(
        ProofEdgeKind::AuthorityFold,
        target_ref,
        report.key_id.clone(),
    )
    .with_evidence_ref("temporal_authority")])
}

fn verify_episode_lineage(
    pool: &Pool,
    memory_id: &MemoryId,
    episode_ref: &str,
    source_events: &mut Vec<String>,
    verified_edges: &mut Vec<ProofEdge>,
    failing_edges: &mut Vec<FailingEdge>,
) -> StoreResult<()> {
    let episodes = EpisodeRepo::new(pool);
    let Ok(episode_id) = episode_ref.parse() else {
        failing_edges.push(FailingEdge::broken(
            ProofEdgeKind::LineageClosure,
            memory_id.to_string(),
            episode_ref,
            ProofEdgeFailure::Mismatch,
            "memory source_episodes entry is not a valid episode id",
        ));
        return Ok(());
    };
    let Some(episode) = episodes.get_by_id(&episode_id)? else {
        failing_edges.push(FailingEdge::missing(
            ProofEdgeKind::LineageClosure,
            episode_ref,
            "source episode not found",
        ));
        return Ok(());
    };

    verified_edges.push(
        ProofEdge::new(
            ProofEdgeKind::LineageClosure,
            memory_id.to_string(),
            episode.id.to_string(),
        )
        .with_evidence_ref("memories.source_episodes_json"),
    );

    match string_array_refs(
        &episode.source_events_json,
        memory_id,
        "episode.source_events",
    ) {
        Ok(events) => source_events.extend(events),
        Err(edge) => failing_edges.push(edge),
    }

    Ok(())
}

fn verify_event_lineage(
    pool: &Pool,
    memory_id: &MemoryId,
    event_ref: &str,
    verified_edges: &mut Vec<ProofEdge>,
    failing_edges: &mut Vec<FailingEdge>,
) -> StoreResult<()> {
    let events = EventRepo::new(pool);
    let Ok(event_id) = event_ref.parse::<EventId>() else {
        failing_edges.push(FailingEdge::broken(
            ProofEdgeKind::LineageClosure,
            memory_id.to_string(),
            event_ref,
            ProofEdgeFailure::Mismatch,
            "source event reference is not a valid event id",
        ));
        return Ok(());
    };
    let Some(event) = events.get_by_id(&event_id)? else {
        failing_edges.push(FailingEdge::missing(
            ProofEdgeKind::LineageClosure,
            event_ref,
            "source event not found",
        ));
        return Ok(());
    };

    verified_edges.push(
        ProofEdge::new(
            ProofEdgeKind::LineageClosure,
            memory_id.to_string(),
            event.id.to_string(),
        )
        .with_evidence_ref(event.event_hash.clone()),
    );

    if let Some(prev_hash) = &event.prev_event_hash {
        match event_id_by_hash(pool, prev_hash)? {
            Some(parent_id) => verified_edges.push(
                ProofEdge::new(ProofEdgeKind::HashChain, parent_id, event.id.to_string())
                    .with_evidence_ref(prev_hash.clone()),
            ),
            None => failing_edges.push(FailingEdge::missing(
                ProofEdgeKind::HashChain,
                event.id.to_string(),
                "source event prev_event_hash has no parent event in store",
            )),
        }
    }

    Ok(())
}

fn event_id_by_hash(pool: &Pool, event_hash: &str) -> StoreResult<Option<String>> {
    Ok(pool
        .query_row(
            "SELECT id FROM events WHERE event_hash = ?1;",
            params![event_hash],
            |row| row.get::<_, String>(0),
        )
        .optional()?)
}

fn string_array_refs(
    value: &Value,
    memory_id: &MemoryId,
    field: &'static str,
) -> Result<Vec<String>, FailingEdge> {
    let Some(items) = value.as_array() else {
        return Err(FailingEdge::broken(
            ProofEdgeKind::LineageClosure,
            memory_id.to_string(),
            field,
            ProofEdgeFailure::Mismatch,
            format!("{field} must be a JSON array"),
        ));
    };

    let mut refs = Vec::with_capacity(items.len());
    for item in items {
        let Some(text) = item.as_str() else {
            return Err(FailingEdge::broken(
                ProofEdgeKind::LineageClosure,
                memory_id.to_string(),
                field,
                ProofEdgeFailure::Mismatch,
                format!("{field} must contain string refs"),
            ));
        };
        refs.push(text.to_string());
    }
    Ok(refs)
}