minerva 0.2.0

Causal ordering for distributed systems
use thiserror::Error;

use crate::kairos::DecodeError as KairosDecodeError;
use crate::metis::DecodeError as VectorDecodeError;
use crate::metis::dot::Dot;

/// Caller-owned allocation limits for an epoch-ledger snapshot decode.
///
/// Each ceiling is stated in the collection's own unit. Together they bound
/// every materialized map and set: round slots are the roster product, window
/// adoption rounds are the roster by candidate product, and each carried
/// vector has its own entry ceiling. Zero is valid for every collection except
/// the snapshot's encoded horizon, whose nonzero invariant is structural.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EpochLedgerDecodeBudget {
    roster: usize,
    lineage: usize,
    candidates: usize,
    protocol_dots: usize,
    vector_entries: usize,
}

impl EpochLedgerDecodeBudget {
    /// Builds a complete decode budget.
    #[must_use]
    pub const fn new(
        max_roster: usize,
        max_lineage: usize,
        max_candidates: usize,
        max_protocol_dots: usize,
        max_vector_entries: usize,
    ) -> Self {
        Self {
            roster: max_roster,
            lineage: max_lineage,
            candidates: max_candidates,
            protocol_dots: max_protocol_dots,
            vector_entries: max_vector_entries,
        }
    }

    pub(super) const fn max_roster(self) -> usize {
        self.roster
    }

    pub(super) const fn max_lineage(self) -> usize {
        self.lineage
    }

    pub(super) const fn max_candidates(self) -> usize {
        self.candidates
    }

    pub(super) const fn max_protocol_dots(self) -> usize {
        self.protocol_dots
    }

    pub(super) const fn max_vector_entries(self) -> usize {
        self.vector_entries
    }
}

/// Why a validated local epoch checkpoint cannot seed the current machine.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum EpochLedgerRehydrateError {
    /// The checkpoint's recorded roster differs from the caller's current
    /// roster.
    #[error("epoch checkpoint roster does not match the configured roster")]
    RosterMismatch,
    /// The checkpoint's recognizer horizon differs from the configured horizon.
    #[error("epoch checkpoint horizon {checkpoint} does not match configured horizon {configured}")]
    HorizonMismatch {
        /// The horizon persisted in the checkpoint.
        checkpoint: u64,
        /// The caller's configured horizon.
        configured: u64,
    },
}

/// Decode failure for an [`EpochLedgerSnapshot`](super::EpochLedgerSnapshot).
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum EpochLedgerDecodeError {
    /// The leading version byte is not understood.
    #[error("unknown epoch-ledger snapshot version: {0:#04x}")]
    UnknownVersion(u8),
    /// The input ended before the declared frame or carried a trailing suffix.
    #[error(
        "unexpected epoch-ledger frame length at byte {offset}: need {needed}, have {remaining}"
    )]
    UnexpectedLength {
        /// Byte offset of the failed read.
        offset: usize,
        /// Bytes required by that read.
        needed: usize,
        /// Bytes remaining in the input.
        remaining: usize,
    },
    /// The encoded generation was zero.
    #[error("epoch-ledger generation zero is invalid")]
    ZeroGeneration,
    /// The encoded retention horizon was zero.
    #[error("epoch-ledger horizon zero is invalid")]
    ZeroHorizon,
    /// A declared collection count exceeds the caller's budget.
    #[error("epoch-ledger {collection} count {count} exceeds budget {budget}")]
    TooMany {
        /// Stable collection name for diagnostics.
        collection: &'static str,
        /// Count declared by the frame.
        count: u64,
        /// Maximum count licensed by the caller.
        budget: u64,
    },
    /// Ordered station ids were duplicated or out of order.
    #[error("non-canonical epoch-ledger roster: station {found} does not ascend past {previous}")]
    NonAscendingRoster {
        /// Previous station id.
        previous: u32,
        /// Offending station id.
        found: u32,
    },
    /// Ordered dots were duplicated or out of order.
    #[error("non-canonical epoch-ledger {collection}: dot {found} does not ascend past {previous}")]
    NonAscendingDots {
        /// Stable collection name for diagnostics.
        collection: &'static str,
        /// Previous dot.
        previous: Dot,
        /// Offending dot.
        found: Dot,
    },
    /// A dot carried the non-dot counter zero.
    #[error("non-canonical epoch-ledger {collection}: station {station} has counter zero")]
    ZeroDot {
        /// Stable collection name for diagnostics.
        collection: &'static str,
        /// Station attached to the malformed dot.
        station: u32,
    },
    /// A one-byte option or state tag was outside its closed vocabulary.
    #[error("invalid epoch-ledger {field} tag: {tag:#04x}")]
    BadTag {
        /// Stable field name for diagnostics.
        field: &'static str,
        /// Unrecognized tag.
        tag: u8,
    },
    /// A nested declaration rank failed to decode.
    #[error("epoch-ledger declaration rank: {0}")]
    Rank(#[source] KairosDecodeError),
    /// A nested version-vector frame failed to decode.
    #[error("epoch-ledger {field} vector: {source}")]
    Vector {
        /// Stable field name for diagnostics.
        field: &'static str,
        /// Nested codec refusal.
        #[source]
        source: VectorDecodeError,
    },
    /// A nested vector exceeds the per-vector entry budget.
    #[error("epoch-ledger {field} vector has {count} entries, past budget {budget}")]
    TooManyVectorEntries {
        /// Stable field name for diagnostics.
        field: &'static str,
        /// Entry count declared by the nested frame.
        count: u64,
        /// Maximum entries licensed for one vector.
        budget: u64,
    },
    /// Cross-field state could not have been produced by [`Epochs`](crate::metis::Epochs).
    #[error("invalid epoch-ledger state: {0}")]
    InvalidState(&'static str),
}