soma-som-core 0.1.0

Universal soma(som) structural primitives — Quad / Tree / Ring / Genesis / Fingerprint / TemporalLedger / CrossingRecord
Documentation
// SPDX-License-Identifier: LGPL-3.0-only
#![allow(missing_docs)]

//! RingStore: the L0 persistence trait for ring state.
//!
//! ## Core / extension separation
//!
//! The `RingStore` trait defines the abstract persistence contract for ring
//! state. It is the **L0 layer** in the three-layer persistence model
//! (three-domain persistence model):
//!
//! - **L0**: Trait definition (this module, in `soma-som-core`)
//! - **L1**: Local backend implementation (e.g., `RedbRingStore` in `soma-persist`)
//! - **L2**: Replicated storage (future)
//!
//! ## Boundary Crossing Model
//!
//! OU is the single write boundary. The orchestrator calls write methods
//! after each cycle completes. The web layer (and other extensions) call
//! read methods for observation. The trait enforces this asymmetry by
//! grouping methods into write and read surfaces.
//!
//! ## Scope
//!
//! `RingStore` covers **ring data only** (domain 1 of the three-domain model):
//! quads, crossing records, ledger entries, perspectival chains,
//! and genesis metadata. Management data (users, roles) is an application-tier
//! concern (the application assigns to DIRECTOR). Audit data is an extension-layer concern, not ring-internal.

use crate::crossing::CrossingRecord;
use crate::ledger::LedgerEntry;
use crate::perspectival::PerspectivalEntry;
use crate::quad::Quad;
use crate::ring::Ring;
use crate::types::{Layer, UnitId};

/// Typed backend-error categories for `RingStoreError::Backend`.
///
/// Universal across persistence backends — local k-v store, replicated log,
/// CRDT-merged store, etc. Backends classify their native errors into these
/// categories before surfacing through `RingStore`. Callers can match on the
/// category to drive recovery logic without depending on any backend crate.
///
/// - `Storage` covers any I/O / medium / schema failure (disk, network, table, page).
/// - `Concurrency` covers any atomicity / locking / replication / consensus failure.
/// - `Other` is the uncategorized escape hatch.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RingStoreBackendError {
    #[error("storage error: {0}")]
    Storage(String),
    #[error("concurrency error: {0}")]
    Concurrency(String),
    #[error("backend error: {0}")]
    Other(String),
}

/// Backend-agnostic error type for `RingStore` operations.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RingStoreError {
    #[error("not found: {context}")]
    NotFound { context: String },

    #[error("integrity error: {context}")]
    Integrity { context: String },

    #[error("serialization error: {0}")]
    Serialization(String),

    #[error("backend error: {0}")]
    Backend(RingStoreBackendError),
}

pub type RingStoreResult<T> = Result<T, RingStoreError>;

/// The L0 persistence trait for ring state.
///
/// Implementations provide the storage backend (L1) for ring data.
/// The orchestrator writes through this trait after each cycle;
/// the web layer and other extensions read through it for observation.
pub trait RingStore: Send + Sync {
    fn initialize(&self) -> RingStoreResult<()>;
    fn compact(&self) -> RingStoreResult<bool>;

    // ── Write surface (orchestrator → OU boundary) ──
    fn persist_genesis(
        &self,
        ring: &Ring,
        crossing_records: &[CrossingRecord],
        ledger_entry: &LedgerEntry,
        seed: &[u8],
        boundary_verifying_key: &[u8; 32],
    ) -> RingStoreResult<()>;

    fn persist_cycle(
        &self,
        ring: &Ring,
        cycle_index: u64,
        crossing_records: &[CrossingRecord],
        ledger_entry: &LedgerEntry,
    ) -> RingStoreResult<()>;

    fn persist_perspectival(
        &self,
        cycle_index: u64,
        entries: &[(UnitId, PerspectivalEntry)],
        cross_verification: &[u8; 32],
    ) -> RingStoreResult<()>;

    // ── Read surface (observation boundary) ──
    fn latest_cycle(&self) -> RingStoreResult<Option<u64>>;
    fn has_genesis(&self) -> RingStoreResult<bool>;
    fn get_som_quad(&self, cycle_index: u64, unit: UnitId, layer: Layer) -> RingStoreResult<Quad>;
    fn get_soma_quad(&self, cycle_index: u64, unit: UnitId) -> RingStoreResult<Quad>;
    fn get_crossing_records(&self, cycle_index: u64) -> RingStoreResult<Vec<CrossingRecord>>;
    fn get_ledger_entry(&self, cycle_index: u64) -> RingStoreResult<LedgerEntry>;
    fn get_all_ledger_entries(&self) -> RingStoreResult<Vec<LedgerEntry>>;
    fn get_genesis_seed(&self) -> RingStoreResult<Vec<u8>>;
    fn get_boundary_key(&self) -> RingStoreResult<[u8; 32]>;
    fn get_perspectival_entry(
        &self,
        cycle_index: u64,
        unit: UnitId,
    ) -> RingStoreResult<PerspectivalEntry>;
    fn get_perspectival_entries(
        &self,
        cycle_index: u64,
    ) -> RingStoreResult<Vec<(UnitId, PerspectivalEntry)>>;
    fn get_unit_chain(&self, unit: UnitId) -> RingStoreResult<Vec<PerspectivalEntry>>;
    fn get_cross_verification(&self, cycle_index: u64) -> RingStoreResult<[u8; 32]>;
    fn get_all_cross_verifications(&self) -> RingStoreResult<Vec<(u64, [u8; 32])>>;
}

// inline: exercises module-private items via super::*
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ring_store_is_object_safe() {
        fn _assert_object_safe(_: &dyn RingStore) {}
    }

    #[test]
    fn error_variants() {
        let e = RingStoreError::NotFound {
            context: "test".into(),
        };
        assert!(e.to_string().contains("not found"));
        let e = RingStoreError::Integrity {
            context: "bad".into(),
        };
        assert!(e.to_string().contains("integrity"));
        let e = RingStoreError::Backend(RingStoreBackendError::Storage("disk full".into()));
        assert!(e.to_string().contains("storage error"));
        let e = RingStoreError::Backend(RingStoreBackendError::Concurrency("lock held".into()));
        assert!(e.to_string().contains("concurrency error"));
    }

    #[test]
    fn backend_error_pattern_match_drives_recovery() {
        // Adopter-value demonstration: pattern-match on the category to choose
        // a recovery action without depending on the backend crate.
        fn classify(e: &RingStoreError) -> &'static str {
            match e {
                RingStoreError::Backend(RingStoreBackendError::Storage(_)) => "retry-after-io",
                RingStoreError::Backend(RingStoreBackendError::Concurrency(_)) => "retry-tx",
                RingStoreError::Backend(RingStoreBackendError::Other(_)) => "escalate",
                _ => "other",
            }
        }
        assert_eq!(
            classify(&RingStoreError::Backend(RingStoreBackendError::Storage("x".into()))),
            "retry-after-io"
        );
        assert_eq!(
            classify(&RingStoreError::Backend(RingStoreBackendError::Concurrency("x".into()))),
            "retry-tx"
        );
    }
}