minerva 0.2.0

Causal ordering for distributed systems
extern crate alloc;

use alloc::collections::BTreeMap;

use super::{DotSet, DotStore, Dotted, UnknownStation, VersionVector};

mod received;

pub use received::Received;

/// A per-peer causal-context tracker.
///
/// For each member of a caller-declared roster it holds the [`DotSet`] this
/// replica can vouch that peer has seen: the *gossip-targeting* half of the
/// imagined knowledge matrix (one carrier down: contexts instead of vectors).
///
/// The dual of [`Stability`](crate::metis::Stability): that folds each
/// member's *delivered cut* and reads their meet ("what may I forget"), this
/// folds each member's *seen context* and reads the per-peer residual ("what
/// do you provably lack"). Same roster-fixed, bottom-defaulting shape over
/// the two carriers. Every peer has a row from birth, at bottom.
///
/// # The row is a lower bound, and that direction is the safety story
///
/// A row grows only by union under evidence and never regresses, so it can
/// only *under*-estimate what the peer holds. The asymmetry is the whole
/// safety story:
///
/// - Under-claiming makes [`owed`](Self::owed) a superset delta, re-shipping
///   dots the peer already has. A liveness cost only, absorbed idempotently
///   by the causal merge. Normal operation is exactly this.
/// - Over-claiming makes `owed` omit dots the peer genuinely lacks, stalling
///   convergence silently. Unsafe, and undetectable here.
///
/// # Evidence discipline, made constructional
///
/// [`note`](Self::note) therefore takes a [`Received`] rather than a bare
/// context, and the only unaudited way to obtain one is
/// [`Composer::absorb`](crate::metis::Composer::absorb), which mints it from
/// a delta the peer actually shipped. A peer cannot ship what it has not
/// seen. A context you merely *hope* the peer has, or one you sent *to* it,
/// does not typecheck. [`Received::trust`] is the single audited door, named
/// for the burden it carries.
///
/// The tracker cannot audit a `trust` claim, exactly as
/// [`Stability`](crate::metis::Stability) cannot audit a cut claim. What it
/// can do is make the accidental over-claims unrepresentable and let the
/// default stale-row mistake fail safe.
///
/// # A context is whole-world evidence
///
/// A note for one peer may carry dots minted by *any* station, since a
/// context records everything that peer has seen from every source. The row
/// accumulates its whole causal past, not a per-peer fiber.
///
/// # The roster is the meaning
///
/// Fixed at construction, off-roster peers refused ([`UnknownStation`]),
/// exactly as [`Stability`](crate::metis::Stability)'s is: an unheralded row
/// would silently widen or narrow the family [`common`](Self::common) meets
/// over. Membership change means a new tracker.
///
/// # Totality
///
/// [`note`](Self::note) folds by union, so it is idempotent, commutative,
/// and monotone: any permutation and duplication of a fixed multiset of
/// notes yields the identical tracker.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Purview {
    /// Per roster peer, the union of every context noted for it; bottom until
    /// the first note. Every roster peer has a row from birth.
    rows: BTreeMap<u32, DotSet>,
}

impl Purview {
    /// Creates a tracker over a fixed roster of `peer` ids, every row at bottom
    /// (nothing vouched until evidence arrives). Duplicate ids collapse.
    ///
    /// The roster is the deployment fact, exactly as
    /// [`Stability::new`](crate::metis::Stability::new)'s is; see
    /// [`common`](Self::common) for why an empty roster answers bottom rather
    /// than the vacuous top.
    #[must_use]
    pub fn new(roster: impl IntoIterator<Item = u32>) -> Self {
        Self {
            rows: roster
                .into_iter()
                .map(|peer| (peer, DotSet::new()))
                .collect(),
        }
    }

    /// Folds received `evidence` into the row of the peer it names, by union.
    ///
    /// The peer and the context both come *out of* the [`Received`] witness, so
    /// there is no way to raise one peer's row by another's evidence, and no way
    /// to raise any row by a context the tracker cannot prove the peer held: the
    /// two hazards this method used to leave to a prose rule are now
    /// unrepresentable. The row grows by [`merge`](DotSet::merge) and never
    /// regresses: stale or reordered evidence is absorbed, so the row stays a
    /// monotone lower bound on what the peer has seen. A note may carry dots from
    /// any station (a context is whole-world evidence, not a per-peer fiber).
    ///
    /// # The evidence carries its own proof
    ///
    /// The only unaudited way to build a [`Received`] is
    /// [`Composer::absorb`](crate::metis::Composer::absorb), which mints it from the
    /// context of a delta the peer actually shipped. The one deliberate escape,
    /// [`Received::trust`], is the caller's word for a full-state exchange the
    /// receive path cannot witness; only a `trust` over-claim can still stall
    /// convergence (the unsafe direction), and its name is the warning. See the
    /// type-level evidence discipline.
    ///
    /// A bare [`DotSet`] paired with a peer id no longer notes: the raw path
    /// that used to over-claim by accident does not typecheck.
    ///
    /// ```compile_fail
    /// use minerva::metis::{Dot, DotSet, Purview};
    ///
    /// let mut purview = Purview::new([1u32]);
    /// let mut seen = DotSet::new();
    /// let _ = seen.insert(Dot::from_parts(1, 1).unwrap());
    /// // `note` takes a `&Received`, not a `(u32, &DotSet)`: this does not
    /// // compile, so a hoped-for context can never be noted by accident.
    /// let _ = purview.note(1, &seen);
    /// ```
    ///
    /// # Errors
    ///
    /// Refuses a peer not on the roster, returning [`UnknownStation`] and
    /// leaving the tracker unchanged: an unheralded row is a membership change,
    /// the caller's policy.
    pub fn note(&mut self, evidence: &Received) -> Result<(), UnknownStation> {
        let peer = evidence.from();
        let vouched = self
            .rows
            .get_mut(&peer)
            .ok_or(UnknownStation { station: peer })?;
        *vouched = vouched.merge(evidence.context());
        Ok(())
    }

    /// The row for `peer`: the context this replica can vouch that peer has
    /// seen. [`None`] off-roster.
    #[must_use]
    pub fn of(&self, peer: u32) -> Option<&DotSet> {
        self.rows.get(&peer)
    }

    /// The delta `state` owes `peer`: exactly what `state` holds that the row
    /// says `peer` provably lacks, via
    /// [`Dotted::delta_for`](Dotted::delta_for) against the row. [`None`]
    /// off-roster.
    ///
    /// Because every row rises only by [`note`](Self::note)d [`Received`]
    /// evidence, the row is a lower bound and this over-ships, never
    /// under-ships: a lagging row re-sends dots the peer already holds (a
    /// harmless liveness cost), which the peer's merge absorbs idempotently.
    /// Merging the returned delta into the peer's true state reaches exactly
    /// what merging the full `state` would. That guarantee is now conditional on
    /// exactly one thing: no [`Received::trust`] over-claim was fed to a
    /// [`note`](Self::note). Evidence from [`Composer::absorb`](crate::metis::Composer::absorb)
    /// is a lower bound by construction, so a caller who never reaches for
    /// `trust` gets the over-ship guarantee unconditionally.
    #[must_use]
    pub fn owed<S: DotStore>(&self, state: &Dotted<S>, peer: u32) -> Option<Dotted<S>> {
        self.rows.get(&peer).map(|row| state.delta_for(row))
    }

    /// The intersection of every row: the dots *every* roster peer provably has
    /// seen, the n-ary [`intersect`](DotSet::intersect) fold of the rows.
    ///
    /// Meet-shaped, so any fold order gives the same answer, and any row still
    /// at bottom (an unreported peer) pins the result at bottom: nothing is
    /// common knowledge until every peer has vouched, the
    /// [`Stability`](crate::metis::Stability) watermark's unreported-member
    /// behavior one carrier down.
    ///
    /// For an *empty* roster the mathematical meet is the lattice top ("everyone
    /// has everything", vacuously): a fabrication a meet over a caller-declared
    /// family must never invent, so the degenerate answer is BOTTOM (the crate's
    /// standing empty-family rule). `O(roster x dots)`, computed on read.
    #[must_use]
    pub fn common(&self) -> DotSet {
        let mut rows = self.rows.values();
        let Some(first) = rows.next() else {
            return DotSet::new();
        };
        rows.fold(first.clone(), |acc, row| acc.intersect(row))
    }

    /// The [`floor`](DotSet::floor) of [`common`](Self::common): a genuine
    /// gap-free cut of collective knowledge.
    ///
    /// [`common`](Self::common) may run past holes (a peer received a dot out of
    /// order); its floor is the greatest gap-free prefix every peer provably
    /// holds, the honest bridge to
    /// [`Stability::report`](crate::metis::Stability::report)-shaped reads (a cut
    /// claim can never over-claim). For the empty roster this is the empty
    /// vector.
    #[must_use]
    pub fn common_floor(&self) -> VersionVector {
        self.common().floor()
    }

    /// Iterates the declared roster in ascending peer order.
    pub fn roster(&self) -> impl Iterator<Item = u32> + '_ {
        self.rows.keys().copied()
    }
}