minerva 0.2.0

Causal ordering for distributed systems
//! The event identity: one station's one-based event coordinate.
//!
//! A [`Dot`] carries the crate's one identity law on the type: the counter
//! is one-based, and `0` is not a dot. Every canonical codec already
//! refuses the non-dot counter at each identity seat, so the type's domain
//! equals the decoded domain and no decoder changes acceptance (ruling
//! R-91; PRD 0028 R6). Station validity is deliberately not this type's
//! law: a station id is caller-configured, the crate orders and stores
//! station zero like any other id, and the nonzero-station rule is the
//! consumer's authenticated-boundary law. Alma's model 2 was refined at
//! R-91. Refusal ownership stays at the door that owns the law.
//!
//! A [`RawDot`] is the zero-capable sibling: a coordinate as a payload
//! seat spells it (a movement target, a locus anchor, a preparation
//! label). Reads over raw coordinates are total; a raw coordinate that
//! names no event dangles lawfully (the S117 anchor law) and is never a
//! wire refusal. [`Dot::try_from`] is the one crossing between the two.
//! The payload seats speak [`RawDot`] since S346: anchors, verges,
//! movement targets, the visual-insert caret, the preparation window
//! label, and every refusal-evidence field that can lawfully carry a
//! non-dot. Evidence fields that carry a proven identity spell [`Dot`].
//!
//! Ordering is derived on `(station, counter)` in declaration order, so a
//! [`Dot`] orders exactly as the tuple it replaces: every maintained
//! ascending collection, every canonical encode order, and the sibling
//! tiebreak (PRD 0017 R4) are byte-for-byte unchanged.

use core::num::NonZeroU64;

use thiserror::Error;

/// A validated event identity: a station and its one-based event counter.
///
/// Constructible only with a nonzero counter, so "`0` is not a dot" is
/// carried by the type rather than re-checked at every door. The station
/// keeps the full `u32` domain: a station id is caller-configured, so the
/// nonzero-station rule belongs to the consumer's authenticated boundary,
/// not to this type (ruling R-91).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Dot {
    station: u32,
    counter: NonZeroU64,
}

impl Dot {
    /// Builds an identity from an already-proven counter.
    #[must_use]
    pub const fn new(station: u32, counter: NonZeroU64) -> Self {
        Self { station, counter }
    }

    /// Builds an identity from raw parts.
    ///
    /// # Errors
    ///
    /// [`InvalidDot`] when `counter` is the non-dot zero.
    pub const fn from_parts(station: u32, counter: u64) -> Result<Self, InvalidDot> {
        match NonZeroU64::new(counter) {
            Some(counter) => Ok(Self { station, counter }),
            None => Err(InvalidDot { station }),
        }
    }

    /// The minting station's caller-configured id.
    #[must_use]
    pub const fn station(self) -> u32 {
        self.station
    }

    /// The one-based event counter.
    #[must_use]
    pub const fn counter(self) -> u64 {
        self.counter.get()
    }

    /// The counter with its nonzero proof retained.
    #[must_use]
    pub const fn counter_nonzero(self) -> NonZeroU64 {
        self.counter
    }

    /// The identity as a raw coordinate: total, and one-way --- the
    /// nonzero proof is surrendered.
    #[must_use]
    pub const fn raw(self) -> RawDot {
        RawDot {
            station: self.station,
            counter: self.counter.get(),
        }
    }
}

impl core::fmt::Display for Dot {
    /// Prints the coordinate pair: `(station, counter)`.
    ///
    /// The spelling equals the replaced tuple's `Debug` output, so a
    /// diagnostic that names a dot reads the same before and after the
    /// re-typing (S346).
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "({}, {})", self.station, self.counter)
    }
}

impl TryFrom<(u32, u64)> for Dot {
    type Error = InvalidDot;

    fn try_from((station, counter): (u32, u64)) -> Result<Self, InvalidDot> {
        Self::from_parts(station, counter)
    }
}

impl TryFrom<RawDot> for Dot {
    type Error = InvalidDot;

    fn try_from(raw: RawDot) -> Result<Self, InvalidDot> {
        Self::from_parts(raw.station, raw.counter)
    }
}

impl From<Dot> for (u32, u64) {
    fn from(dot: Dot) -> Self {
        (dot.station, dot.counter.get())
    }
}

impl From<Dot> for RawDot {
    fn from(dot: Dot) -> Self {
        dot.raw()
    }
}

/// A raw event coordinate: the zero-capable spelling a payload seat keeps.
///
/// Not an identity claim. A raw coordinate may name the non-dot counter
/// zero or a dot no store holds; consumers read it totally (an unknown
/// coordinate resolves to nothing, never to a refusal). Ordering matches
/// [`Dot`]'s on the shared domain, so a payload-keyed collection iterates
/// exactly as its tuple-keyed predecessor.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RawDot {
    /// The named station id.
    pub station: u32,
    /// The named counter, zero-capable.
    pub counter: u64,
}

impl RawDot {
    /// Builds a raw coordinate.
    #[must_use]
    pub const fn new(station: u32, counter: u64) -> Self {
        Self { station, counter }
    }
}

impl core::fmt::Display for RawDot {
    /// Prints the coordinate pair: `(station, counter)`, the replaced
    /// tuple's `Debug` spelling (see [`Dot`]'s `Display`).
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "({}, {})", self.station, self.counter)
    }
}

impl From<(u32, u64)> for RawDot {
    fn from((station, counter): (u32, u64)) -> Self {
        Self { station, counter }
    }
}

impl From<RawDot> for (u32, u64) {
    fn from(raw: RawDot) -> Self {
        (raw.station, raw.counter)
    }
}

/// A coordinate offered as an identity names the non-dot counter zero.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
#[error("station {station} names the non-dot counter zero")]
pub struct InvalidDot {
    /// The station attached to the refused coordinate.
    pub station: u32,
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::vec::Vec;

    extern crate alloc;

    #[test]
    fn the_non_dot_zero_is_unrepresentable_and_the_station_domain_is_whole() {
        assert_eq!(Dot::from_parts(3, 0), Err(InvalidDot { station: 3 }));
        assert_eq!(Dot::try_from((0, 0)), Err(InvalidDot { station: 0 }));
        let zero_station = Dot::from_parts(0, 1).unwrap();
        assert_eq!(zero_station.station(), 0);
        assert_eq!(zero_station.counter(), 1);
    }

    #[test]
    fn ordering_matches_the_tuple_it_replaces() {
        let tuples = [(0u32, 1u64), (0, 2), (1, 1), (1, 7), (2, 1)];
        let dots: Vec<Dot> = tuples
            .iter()
            .map(|&pair| Dot::try_from(pair).unwrap())
            .collect();
        let mut sorted = dots.clone();
        sorted.sort_unstable();
        assert_eq!(dots, sorted);
        let raws: Vec<RawDot> = dots.iter().map(|&dot| dot.raw()).collect();
        let mut raw_sorted = raws.clone();
        raw_sorted.sort_unstable();
        assert_eq!(raws, raw_sorted);
    }

    #[test]
    fn the_crossing_round_trips_and_the_surrender_is_total() {
        let dot = Dot::from_parts(7, 11).unwrap();
        assert_eq!(Dot::try_from(dot.raw()), Ok(dot));
        assert_eq!(<(u32, u64)>::from(dot), (7, 11));
        let dangling = RawDot::new(7, 0);
        assert_eq!(Dot::try_from(dangling), Err(InvalidDot { station: 7 }));
        assert_eq!(<(u32, u64)>::from(dangling), (7, 0));
    }
}