minerva 0.2.0

Causal ordering for distributed systems
//! The mark store: [`Scholion`] and [`Scholia`] (PRD 0021, stage D of the
//! structured document horizon).
//!
//! A *scholion* is one mark over a sequence: a span said in identities (two
//! sided [`Verge`] boundaries) carrying a caller-opaque tag. *Scholia* is
//! the store of them: dot-tagged marks under the survivor law, the
//! annotation instance of the open [`DotStore`](crate::metis::DotStore)
//! axis. The names are the classical ones and the division of labor rides
//! in them: scholia are the marginal annotations keyed to spans of a text,
//! living beside it without altering it, their interpretive content held
//! elsewhere (the commentary). So here: the store carries *where* a mark
//! sits (identity boundaries) and *what the caller said* (the opaque tag);
//! what a tag means, which tags nest, and how marks render stay the
//! caller's forever (ruling R-23 redraws the rich-text-schema refusal at
//! exactly this line: mark mechanics in, mark semantics out).
//!
//! The hazard this closes by construction is positional drift: a
//! position-anchored span ends up decorating the wrong words after
//! concurrent inserts, while an identity-anchored span cannot move,
//! because a dot never moves and a deleted element still holds its order
//! slot as a tombstone. The projection onto the current document order,
//! with its honest verdicts (covered, empty, inverted, dangling), is the
//! sequence's own read: [`Rhapsody::extent`](crate::metis::Rhapsody::extent).
//!
//! # The recipes
//!
//! A `Scholia<T>` behaves exactly like the payload store it carries
//! (`DotFun<Scholion<T>>`): the merge is the survivor law with content
//! following the dot, so every register recipe applies verbatim.
//!
//! * *Mint a shared mark*: `composer.compose(|dot| (Scholia::singleton(dot,
//!   scholion), DotSet::new()))`; the mark converges everywhere the delta
//!   reaches, and concurrent marks are siblings (all readable, none
//!   chosen).
//! * *Edit a mark* (move an end, restyle a tag): a register supersede, one
//!   covered delta re-minting the whole scholion over the old dot
//!   ([`Composer::compose_super`](crate::metis::Composer::compose_super)
//!   with the superseded set naming the old dot). Ends stay immutable per
//!   dot, so a half-moved span is unrepresentable.
//! * *Remove a mark*: `composer.retract` of the mark's dot. A concurrent
//!   edit of the same mark survives as its own dot (observed-remove, the
//!   axis's standing semantics).
//! * *Per-participant marks* (cursors, selections): key by participant,
//!   `DotMap<Participant, Scholia<T>>`, the cursor-register recipe with a
//!   span payload.

extern crate alloc;

use super::rhapsody::Verge;
use super::{Dot, DotFun, DotFunIter, DotSet};

mod store;

/// One mark: a span said in identities, carrying a caller-opaque tag.
///
/// The two [`Verge`] ends fix the span's boundaries in the sequence's
/// identity space, sides chosen at mint (the expansion rule: see the
/// [`Verge`] and [`extent`](crate::metis::Rhapsody::extent) docs). The tag
/// is whatever the caller's mark model needs (a style token, a comment id,
/// a proposal reference); minerva never reads it, compares it only for
/// value equality, and attaches no meaning to it.
///
/// A scholion is immutable under its dot (content follows the dot, axis
/// law 3): editing a mark is a register supersede that re-mints the whole
/// scholion, so its `start`, `end`, and `tag` always changed together or
/// not at all.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Scholion<T> {
    /// The span's start boundary.
    pub start: Verge,
    /// The span's end boundary.
    pub end: Verge,
    /// The caller's tag, opaque here: meaning is never the store's.
    pub tag: T,
}

/// The mark store: dot-tagged [`Scholion`]s under the survivor law, the
/// annotation instance of the open [`DotStore`](crate::metis::DotStore)
/// axis (PRD 0021).
///
/// Structurally this is the payload store applied to spans
/// (`DotFun<Scholion<T>>`), and its merge semantics are exactly that store's:
/// no new merge law, no policy, concurrent marks and concurrent edits of one
/// mark surface as siblings. The named type adds two things: the mark
/// vocabulary itself, and the anchor read
/// ([`anchor_dots`](Self::anchor_dots)). The anchor read is the set of
/// sequence dots the live marks are pinned to. A retention policy must treat
/// that set as load-bearing. A `condense` that excises a tombstone a mark
/// anchors leaves that mark [`Dangling`](crate::metis::Extent::Dangling). The
/// verdict is honest, and the pinning policy is the caller's (PRD 0021 R5).
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Scholia<T> {
    /// The marks, one scholion per dot.
    marks: DotFun<Scholion<T>>,
}

// Manual, as on the payload store itself: bottom needs no `T: Default`.
impl<T> Default for Scholia<T> {
    fn default() -> Self {
        Self {
            marks: DotFun::default(),
        }
    }
}

impl<'a, T: Clone> IntoIterator for &'a Scholia<T> {
    type Item = (Dot, &'a Scholion<T>);
    type IntoIter = DotFunIter<'a, Scholion<T>>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<T: Clone> Scholia<T> {
    /// Creates the empty store: no marks, the lattice bottom.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            marks: DotFun::new(),
        }
    }

    /// A one-mark store, the delta building block: `scholion` under `dot`,
    /// nothing else.
    #[must_use]
    pub fn singleton(dot: Dot, scholion: Scholion<T>) -> Self {
        Self {
            marks: DotFun::singleton(dot, scholion),
        }
    }

    /// Records a mark: `scholion` under `dot`, returning whether the store
    /// changed. The refusal is the payload store's ([`DotFun::insert`]): an
    /// already-carried dot is a no-op.
    #[must_use = "the write is refused (returns false) for an already-carried dot"]
    pub fn insert(&mut self, dot: Dot, scholion: Scholion<T>) -> bool {
        self.marks.insert(dot, scholion)
    }

    /// The mark under `dot`, if the store carries it.
    #[must_use]
    pub fn get(&self, dot: Dot) -> Option<&Scholion<T>> {
        self.marks.get(dot)
    }

    /// Iterates the `(dot, &scholion)` entries in ascending dot order.
    #[must_use]
    pub fn iter(&self) -> DotFunIter<'_, Scholion<T>> {
        self.marks.iter()
    }

    /// The current marks, ascending by dot: every surviving scholion.
    pub fn marks(&self) -> impl Iterator<Item = &Scholion<T>> + '_ {
        self.marks.values()
    }

    /// The supersede-set of a mark edit: exactly the dots this store
    /// currently carries, as a [`DotSet`] (the register discipline,
    /// [`DotFun::observed`]). A caller editing *one* mark supersedes that
    /// mark's dot alone; this whole-store read serves the
    /// single-live-mark shapes (one selection, one active suggestion).
    #[must_use]
    pub fn observed(&self) -> DotSet {
        self.marks.observed()
    }

    /// The sequence dots the live marks are pinned to, ascending, each
    /// once: every dot named by a surviving scholion's verges.
    ///
    /// This is the retention interlock's read (PRD 0021 R5): a sequence
    /// `condense` may excise a sterile order tombstone, and a mark
    /// anchored to it degrades to [`Dangling`](crate::metis::Extent::Dangling)
    /// there. A caller whose marks must outlive its retention cadence
    /// folds this set into its condense policy (fertility is the
    /// skeleton's own notion; pinning is the caller's), or accepts the
    /// surfaced degrade.
    #[must_use]
    pub fn anchor_dots(&self) -> DotSet {
        let mut anchors = DotSet::new();
        for scholion in self.marks.values() {
            for verge in [scholion.start, scholion.end] {
                // The payload-to-identity funnel (R-91): a verge names a raw
                // coordinate, and one that names no dot drops silently, as it
                // always did at the have-set door.
                if let Some(anchor) = verge.dot().and_then(|raw| Dot::try_from(raw).ok()) {
                    let _ = anchors.insert(anchor);
                }
            }
        }
        anchors
    }

    /// The number of marks the store carries (surviving writes).
    #[must_use]
    pub fn len(&self) -> usize {
        self.marks.len()
    }

    /// Whether the store carries no marks at all (the lattice bottom).
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.marks.is_empty()
    }
}