minerva 0.2.0

Causal ordering for distributed systems
//! Declaration-scheduled stratum assembly as a typed machine (S272).
//!
//! The S270 boundary-schedule principle: a delivered [`Declaration`] pins
//! its candidate's fold input (the cut), winner fixation makes it the
//! boundary's, and staging ahead of fixation is a speculation with a
//! full-rebuild fallback, so boundary latency becomes a schedule, not an
//! algorithm. The schedule itself (when to fold, how much per tick, which
//! candidate to back in a contested window) is the caller's and stays so;
//! what this type owns is the speculation's *safety*: the candidate
//! identity, the cut-boundedness of every folded entry, and the
//! finish-time proof that the staged input is exactly the adopted
//! winner's, so the laws the consumer guide stated as caller convention
//! (work begins only after a declaration pins a cut, a provisional
//! candidate may lose, the adopted address and cut must exactly match the
//! staged candidate, a mismatch demands the rebuild) are refusals here,
//! never reimplemented at each consumer (the alma stage-3 request,
//! ruling R-56).
//!
//! The machine never chooses: it owns no threads, clocks, I/O, or
//! scheduling policy. Progress is bounded by the caller's own absorb
//! calls (the chunk the caller feeds *is* the budget), cancellation is
//! dropping the value, and a losing candidate surfaces as a typed miss
//! whose cure (the full rebuild) stays in the caller's hands, where the
//! current pairs live.

extern crate alloc;

use crate::metis::{Cut, DotSet, Dotted, Metatheses, Rhapsody, VersionVector};

use super::shadow::{EpochStratum, EpochStratumError};
use super::{Adopted, Declaration, EpochAddress};
use crate::metis::dot::RawDot;

/// One absorb door's verdict: which side of the pinned cut the entry
/// fell on.
///
/// Not an error; a window entry is lawful traffic that simply belongs to
/// the shadow replay after adoption, and the door reports the
/// classification instead of silently dropping or poisoning the staged
/// input.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[must_use = "a window entry was not folded; it belongs to the shadow replay after adoption"]
pub enum EpochPreparationFold {
    /// Every declared event dot and every dot of the delta's structural
    /// footprint sits at or below the pinned cut: the entry is part of
    /// the candidate's fold input and was merged into the staged pairs.
    Staged,
    /// The entry belongs to the open window, not the staged fold input:
    /// a declared event dot lies above the pinned cut, the delta's
    /// structural footprint reaches above it (a mislabeled entry, caught
    /// by the defense scan), or the entry declared no event dots and is
    /// unclassifiable (an undotted retraction is protocol-invisible, so
    /// no cut can witness which side it fell on; it stays window-side,
    /// where the shadow lawfully absorbs it). Nothing was folded.
    Window {
        /// The first uncovered event dot, or the first above-cut
        /// footprint `(station, high water)`, or the non-dot `(0, 0)`
        /// for an entry that declared no events. A raw coordinate
        /// ([`RawDot`]): two of its three shapes are not identities.
        dot: RawDot,
    },
}

/// The preparation's refusals.
///
/// At [`begin`](EpochPreparation::begin), a base that already carries
/// state above the declared cut; at
/// [`finish`](EpochPreparation::finish), the staged candidate losing, the
/// cut drifting, or the staged pairs failing stratum validation. Every
/// miss's cure is the same and stays caller-side: fall back to the full
/// rebuild from the pairs the caller still owns.
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum EpochPreparationMiss {
    /// The seeded base pairs already carry an identity above the declared
    /// cut, so no amount of covered folding could produce the candidate's
    /// stratum: the staging would be poisoned from its first byte, and
    /// the door refuses at the seam instead of at the boundary.
    #[error("the seeded base already reaches {dot}, above the declared cut")]
    UnpinnedBase {
        /// The first `(station, high water)` above the declared cut: a
        /// per-station reading ([`RawDot`]), not an identity.
        dot: RawDot,
    },
    /// Adoption fixed a different winner than the staged candidate: the
    /// speculation lost, exactly as the schedule principle allows. The
    /// staged pairs are folded at the losing cut and are not the fold
    /// input the winner demands.
    #[error("adoption fixed {adopted:?}, not the staged {staged:?}")]
    DifferentWinner {
        /// The candidate this preparation staged.
        staged: EpochAddress,
        /// The winner adoption actually fixed.
        adopted: EpochAddress,
    },
    /// The adopted witness names the staged address but a different cut.
    /// One machine derives both from the same declaration, so this is
    /// cross-machine or cross-document confusion, surfaced with the first
    /// disagreeing station rather than folded into a wrong-input stratum
    /// refusal downstream.
    #[error("the adopted cut disagrees with the staged cut at station {station}")]
    DriftedCut {
        /// The first station whose two cut counters disagree.
        station: u32,
        /// The staged declaration's counter for that station.
        staged: u64,
        /// The adopted witness's counter for that station.
        adopted: u64,
    },
    /// The staged pairs failed structural validation at the adopted cut
    /// (the [`EpochStratum::new`] refusals): the staging was fed
    /// something other than the generation's covered log.
    #[error("the staged pairs are not the adopted stratum: {0}")]
    Stratum(#[source] EpochStratumError),
}

/// Declaration-scheduled stratum assembly: the deterministic core of the
/// S270 adoption recipe, owned by a type instead of a caller convention.
///
/// [`begin`](Self::begin) pins the candidate (address and cut) from a
/// witnessed [`Declaration`] and seeds the staged pairs, the generation
/// base for a peer, the dry-run's already-folded pairs for the declarer
/// (which is why the declarer pays the fold once, not twice). Each
/// [`absorb_text`](Self::absorb_text) /
/// [`absorb_moves`](Self::absorb_moves) folds one covered log entry,
/// classified by the entry's own declared event dots against the pinned
/// cut (with structural footprint scans as defense), so the staged input
/// can never silently grow past the candidate's boundary and an
/// unclassifiable entry never stages. [`finish`](Self::finish)
/// consumes the staging against the adoption witness: the address and cut
/// must match exactly, and the staged pairs then pass the full
/// [`EpochStratum::new`] validation.
///
/// # What finish proves, and what stays yours
///
/// The proof is candidate identity plus the stratum's structural laws (cut
/// coverage, boundedness, foldability). It is *not* a content audit. Coverage
/// is a receipt property of the context. Take a base whose context covers the
/// cut while its store is missing covered content: it passes the door and
/// re-founds without that content. The full rebuild would do the same, if the
/// caller's log were equally incomplete. Feeding the generation's actual
/// covered log (base plus every covered entry, each exactly once in
/// survivor-law terms) is the caller's obligation under both paths; what this
/// type removes is the *classification and identity* conventions, not the
/// log's honesty. Agreement with the full rebuild, byte for byte, holds
/// exactly when that obligation is met (the S270 instrument pins it with
/// structural columns), and a future content commitment strong enough for
/// `finish` to verify is the joint lineage-digest territory (horizons axis
/// 13), not this type today.
///
/// In a contested window, which candidate to stage (or whether to stage
/// several) is caller policy: one `EpochPreparation` stages one
/// candidate, and a lost race is the typed
/// [`DifferentWinner`](EpochPreparationMiss::DifferentWinner) miss.
#[derive(Debug)]
pub struct EpochPreparation {
    address: EpochAddress,
    cut: Cut,
    text: Dotted<Rhapsody>,
    moves: Dotted<Metatheses>,
}

impl EpochPreparation {
    /// Pins `declaration`'s candidate and seeds the staged pairs.
    ///
    /// The base is the caller's generation opening pair (the prior
    /// consignment, or empty pairs for a first generation), or the
    /// declarer's dry-run products. Validation here is the cheap
    /// per-station high-water scans, over both contexts and the text
    /// plane's recording skeleton (whose loci ride whole even when
    /// invisible); the full structural proof is
    /// [`finish`](Self::finish)'s, through the one stratum door, so the
    /// boundary never pays it twice.
    ///
    /// # Errors
    ///
    /// [`EpochPreparationMiss::UnpinnedBase`] when either base pair
    /// already reaches above the declared cut.
    pub fn begin(
        declaration: &Declaration,
        text: Dotted<Rhapsody>,
        moves: Dotted<Metatheses>,
    ) -> Result<Self, EpochPreparationMiss> {
        let cut = declaration.cut();
        if let Some(dot) = beyond(cut.as_vector(), text.context())
            .or_else(|| beyond(cut.as_vector(), text.store().woven()))
            .or_else(|| beyond(cut.as_vector(), moves.context()))
        {
            return Err(EpochPreparationMiss::UnpinnedBase { dot });
        }
        Ok(Self {
            address: declaration.address(),
            cut: cut.clone(),
            text,
            moves,
        })
    }

    /// The staged candidate's address.
    #[must_use]
    pub const fn address(&self) -> EpochAddress {
        self.address
    }

    /// The pinned candidate cut: the fold input's boundary, fixed the
    /// moment the declaration was witnessed.
    #[must_use]
    pub const fn cut(&self) -> &Cut {
        &self.cut
    }

    /// Folds one text log entry if the pinned cut covers it.
    ///
    /// `events` is the entry's own event dots, the annotation an event
    /// log carries anyway, and it is the classifier: an entry is window
    /// traffic exactly when any of its events lies above the pinned cut.
    /// The delta's bytes cannot decide this alone, because a deletion is
    /// a bare context delta whose footprint is entirely the *retracted*
    /// (pre-cut) dots: a post-cut deletion of a pre-cut identity is
    /// byte-identical to a pre-cut one, and folding it here would sweep
    /// an identity the at-cut stratum must retain. That is also why an
    /// entry declaring *no* events is unclassifiable and stays
    /// window-side: an undotted retraction (a raw `Composer::retract`
    /// with no reserved marker dot) is invisible to every cut, which is
    /// the dotted-deletes half of the one-event-space adoption duty the
    /// consumer guide records (R-53).
    ///
    /// Two structural scans back the declared events as defense. The delta's
    /// causal context and its recording skeleton must also sit below the cut,
    /// so a mislabeled entry cannot smuggle above-cut state. Both are scanned,
    /// because a fragment's skeleton ships whole while its context need only
    /// cover visible support (the S190 `novel_to` law). A covered entry merges
    /// into the staged pair; a window entry is left to the caller (it replays
    /// through the shadow's batch doors after adoption). The caller's chunk is
    /// the budget: progress is exactly the entries the caller feeds, on
    /// whatever tick it owns.
    pub fn absorb_text(
        &mut self,
        events: &[RawDot],
        delta: &Dotted<Rhapsody>,
    ) -> EpochPreparationFold {
        let vector = self.cut.as_vector();
        if let Some(dot) = Self::window_events(vector, events)
            .or_else(|| beyond(vector, delta.context()))
            .or_else(|| beyond(vector, delta.store().woven()))
        {
            return EpochPreparationFold::Window { dot };
        }
        self.text.merge_from(delta);
        EpochPreparationFold::Staged
    }

    /// Folds one movement log entry under the same contract as
    /// [`absorb_text`](Self::absorb_text). The declared events classify. The
    /// context scan defends: the movement record's support is exactly its
    /// testimony dots, which the covered pair's context always carries. An
    /// entry declaring no events stays window-side, because an undotted
    /// movement retraction is as protocol-invisible as a text one.
    pub fn absorb_moves(
        &mut self,
        events: &[RawDot],
        delta: &Dotted<Metatheses>,
    ) -> EpochPreparationFold {
        let vector = self.cut.as_vector();
        if let Some(dot) =
            Self::window_events(vector, events).or_else(|| beyond(vector, delta.context()))
        {
            return EpochPreparationFold::Window { dot };
        }
        self.moves.merge_from(delta);
        EpochPreparationFold::Staged
    }

    /// The event-dot classifier: the first declared event the pinned cut
    /// does not cover or whose index is the non-dot zero, or `(0, 0)`
    /// when no events were declared at all (an unclassifiable entry
    /// never stages).
    fn window_events(cut: &VersionVector, events: &[RawDot]) -> Option<RawDot> {
        if events.is_empty() {
            return Some(RawDot::new(0, 0));
        }
        // The non-dot `index == 0` declares no event at all, so it is as
        // unclassifiable as the empty slice and never stages; a
        // mislabeled entry cannot smuggle a retraction past the cut by
        // naming a dot that names nothing.
        events
            .iter()
            .copied()
            .find(|raw| raw.counter == 0 || raw.counter > cut.get(raw.station))
    }

    /// Consumes the staging against the adoption witness: exact address
    /// and cut match, then the full stratum validation over the staged
    /// pairs. On success the caller holds a validated stratum over the
    /// staged input, ready for
    /// [`into_transition`](EpochStratum::into_transition), and identical
    /// to the full rebuild's exactly when the staged base and entries
    /// were the generation's covered log (the caller obligation the type
    /// docs state; the rebuild carries the same one). On any miss the
    /// cure is the full rebuild from the pairs the caller still owns.
    ///
    /// # Errors
    ///
    /// [`EpochPreparationMiss::DifferentWinner`] when adoption fixed
    /// another candidate, [`EpochPreparationMiss::DriftedCut`] when the
    /// same address arrives with a disagreeing cut (cross-machine
    /// confusion), and [`EpochPreparationMiss::Stratum`] when the staged
    /// pairs fail the [`EpochStratum::new`] proof.
    pub fn finish(self, adopted: &Adopted) -> Result<EpochStratum, EpochPreparationMiss> {
        if adopted.address() != self.address {
            return Err(EpochPreparationMiss::DifferentWinner {
                staged: self.address,
                adopted: adopted.address(),
            });
        }
        let staged = self.cut.as_vector();
        let winner = adopted.cut().as_vector();
        let drifted = staged
            .into_iter()
            .chain(winner)
            .map(|(station, _)| station)
            .find(|&station| staged.get(station) != winner.get(station));
        if let Some(station) = drifted {
            return Err(EpochPreparationMiss::DriftedCut {
                station,
                staged: staged.get(station),
                adopted: winner.get(station),
            });
        }
        EpochStratum::new(adopted, self.text, self.moves).map_err(EpochPreparationMiss::Stratum)
    }
}

/// The first `(station, high water)` in `context` that `cut` does not
/// cover, read structurally off the per-station high waters (a boundary
/// check must not pay per historical dot; the consignment door's own
/// rule).
fn beyond(cut: &VersionVector, context: &DotSet) -> Option<RawDot> {
    context
        .high_water()
        .into_iter()
        .find(|&(station, high)| high > cut.get(station))
        .map(RawDot::from)
}