minerva 0.2.0

Causal ordering for distributed systems
//! The day-one narrative's compiled truth (S290, ruling R-72).
//!
//! `docs/day-one.adoc` walks a newcomer through their first replicated
//! document, and this file is the story's code, held honest in both
//! directions by machine: it is an *integration* test, a separate crate
//! importing `minerva::` exactly as any consumer will, so it compiles
//! only while the narrated surface is genuinely public; and the sync
//! test at the bottom embeds the narrative and asserts every quoted
//! listing appears verbatim in this file, so an edit to either side that
//! forgets the other fails the gate. The acts are numbered to match the
//! document.
//!
//! The cast: two stations, 1 ("Ada") and 2 ("Bea"), each holding a
//! `Composer` over a `Rhapsody` causal pair, its own deterministic
//! clock, and its own copy of the caller-side glyph plane (minerva
//! stores element *identities*; what an element means is deliberately
//! yours, the R-12 boundary).

use std::collections::BTreeMap;

use minerva::kairos::{Clock, TickCounter};
use minerva::metis::{Anchor, Composer, Dot, DotSet, Locus, Retired, Rhapsody};

/// One participant: a composer over the causal pair, a clock, and this
/// author's own copy of the caller-side glyph plane. The plane is
/// per-author on purpose: payloads are yours to carry, so the example
/// must carry them, not smuggle them through shared memory.
struct Author {
    composer: Composer<Rhapsody>,
    clock: Clock<TickCounter>,
    glyphs: BTreeMap<Dot, char>,
}

impl Author {
    fn new(station: u32) -> Self {
        Self {
            composer: Composer::new(station),
            clock: Clock::with_default_config(TickCounter::new(), station)
                .expect("a fresh station id is valid"),
            glyphs: BTreeMap::new(),
        }
    }

    /// Appends one glyph at the end of this author's current reading and
    /// records it on this author's own glyph plane.
    fn append(&mut self, glyph: char) -> Dot {
        let visible = self.composer.state().store().order();
        let anchor = visible
            .last()
            .map_or(Anchor::Origin, |&dot| Anchor::After(dot.into()));
        let rank = self.clock.now(0u16);
        let (dot, _delta) = self.composer.compose(|assigned| {
            let mut rhapsody = Rhapsody::new();
            assert!(rhapsody.weave(assigned, Locus { anchor, rank }));
            (rhapsody, DotSet::new())
        });
        let _ = self.glyphs.insert(dot, glyph);
        dot
    }

    /// Observed-remove: the element stays addressable (its identity is
    /// not erased), it just stops being visible.
    fn erase(&mut self, dot: Dot) {
        let mut superseded = DotSet::new();
        assert!(superseded.insert(dot));
        let _ = self.composer.retract(superseded);
    }

    /// Pulls exactly what `other` owes this author and absorbs it, then
    /// carries the payloads for the same dots: the whole anti-entropy
    /// round. The payload half is the caller's transport made explicit;
    /// dot-keyed payloads are immutable and uniquely keyed, so the
    /// exchange is a plain union with nothing to reconcile.
    fn pull_from(&mut self, other: &Self, other_station: u32) {
        let mine = self.composer.state().context().clone();
        let owed = other.composer.owed_to(&mine);
        let _ = self.composer.absorb(other_station, &owed);
        for (&dot, &glyph) in &other.glyphs {
            let _ = self.glyphs.entry(dot).or_insert(glyph);
        }
    }

    /// This author's current reading, through her own glyph plane.
    fn reading(&self) -> String {
        self.composer
            .state()
            .store()
            .order()
            .into_iter()
            .map(|dot| self.glyphs[&dot])
            .collect()
    }
}

#[test]
fn test_day_one_two_authors_one_document() {
    let mut ada = Author::new(1);
    let mut bea = Author::new(2);

    // Act 1: Ada types "hi" alone. Each keystroke mints a dot (her
    // station id plus her next counter) and places it after the previous
    // one.
    let h = ada.append('h');
    let i = ada.append('i');
    assert_eq!(ada.reading(), "hi");

    // Act 2: Bea joins by pulling what Ada owes her. One round of
    // owed-and-absorb (plus the payloads it references) and she reads
    // the same document from her own state alone.
    bea.pull_from(&ada, 1);
    assert_eq!(bea.reading(), "hi");

    // Act 3: both edit at the same place, concurrently, with no
    // coordination: Ada types 'a' after the 'i' while Bea types '!'
    // after the same 'i'. Neither knows about the other yet.
    let a = ada.append('a');
    let bang = bea.append('!');

    // Act 4: they exchange. Both edits survive (nothing is lost to a
    // "last writer"), and both replicas read the SAME interleaving:
    // the two new elements are siblings at one anchor, ordered by the
    // fixed public sibling rule, never by arrival order.
    ada.pull_from(&bea, 2);
    bea.pull_from(&ada, 1);
    assert_eq!(
        ada.composer.state().store().order(),
        bea.composer.state().store().order(),
        "one document, whatever order the network chose",
    );
    let converged = ada.reading();
    assert_eq!(converged.len(), 4);
    assert!(
        converged.starts_with("hi"),
        "the shared prefix holds: {converged}"
    );
    assert_eq!(bea.reading(), converged);

    // Byte identity, not just logical equality: the wire frame is the
    // representation-independence witness.
    assert_eq!(
        ada.composer.state().store().to_bytes(),
        bea.composer.state().store().to_bytes(),
    );

    // Act 5: Ada decides her 'a' was a typo and erases it; the removal
    // gossips like any other change. An erase is an observed-remove of
    // that one identity: Bea's concurrent '!' is untouchable by it. The
    // erased element is now an order tombstone: invisible, but still in
    // the skeleton (in general a tombstone with anchored descendants
    // holds their place, and some replica may not have heard yet; this
    // one is a childless leaf, the simplest case, and still waits for
    // the witness below).
    ada.erase(a);
    bea.pull_from(&ada, 1);
    assert_eq!(ada.reading(), "hi!");
    assert_eq!(bea.reading(), "hi!");
    assert_eq!(ada.composer.state().store().skeleton_len(), 4);

    // Act 6: garbage collection is a claims-gated fold, never a guess.
    // The `Retired` claim is stronger than "invisible right now": every
    // replica has applied the removal AND no racing write can still
    // anchor through the dot. This sequential walk can prove that by
    // inspection (both authors settled, nothing in flight, no writes
    // between this scan and both condenses below). A real fleet cannot
    // inspect: it gathers acknowledgements through the fixed-roster
    // `Retirement` tracker, counting each only once the acker's cut is
    // delivered (the receiver's cut-gating duty), and then either
    // applies the excision fleet-wide before writes resume, or adds the
    // oath-plane writer discipline, which is what licenses per-replica
    // condense on local evidence (rulings R-41 and R-42). The excision
    // is then real and observable: one locus leaves the skeleton at
    // each replica, and the reading never moves.
    let invisible_everywhere = [&ada, &bea]
        .iter()
        .all(|author| !author.composer.state().store().is_visible(a));
    assert!(invisible_everywhere);
    let mut retired = DotSet::new();
    assert!(retired.insert(a));
    let retired = Retired::trust(retired);

    for author in [&mut ada, &mut bea] {
        let before = author.composer.state().store().order();
        let excised = author.composer.condense(&retired);
        assert_eq!(excised, 1, "exactly the witnessed tombstone is excised");
        let store = author.composer.state().store();
        assert!(store.locus(a).is_none(), "the locus is gone");
        assert_eq!(store.skeleton_len(), 3);
        assert_eq!(store.order(), before, "condense never changes the reading");
    }
    assert_eq!(ada.reading(), "hi!");
    assert_eq!(
        ada.composer.state().store().to_bytes(),
        bea.composer.state().store().to_bytes(),
        "after the same honest excision, byte-identical again",
    );

    // Epilogue: the reading is a pure function of what was delivered.
    // Replaying the same history in any order would land here again; the
    // in-crate suites hold that law under millions of adversarial
    // schedules, which is why this file gets to be short.
    let final_order: Vec<Dot> = ada.composer.state().store().order();
    assert_eq!(final_order, vec![h, i, bang]);
}

/// The narrative-sync half of the drift gate: every `[source,rust]`
/// listing in `docs/day-one.adoc` must appear verbatim (modulo leading
/// indentation) as a contiguous run of this file, so a listing edited on
/// either side without the other fails here, not in a reader's hands.
#[test]
fn test_the_narrative_listings_are_verbatim_from_this_file() {
    let narrative = include_str!("../docs/day-one.adoc");
    let source = include_str!("day_one.rs");
    let source_lines: Vec<&str> = source.lines().map(str::trim).collect();

    let mut blocks = 0usize;
    let mut lines = narrative.lines();
    while let Some(line) = lines.next() {
        if line.trim() != "[source,rust]" {
            continue;
        }
        assert_eq!(
            lines.next().map(str::trim),
            Some("----"),
            "a source block opens with its fence",
        );
        let block: Vec<&str> = lines
            .by_ref()
            .take_while(|candidate| candidate.trim() != "----")
            .map(str::trim)
            .collect();
        assert!(!block.is_empty(), "a narrative listing is never empty");
        blocks += 1;
        let quoted = source_lines
            .windows(block.len())
            .any(|window| window == block.as_slice());
        assert!(
            quoted,
            "narrative listing {blocks} is not a verbatim run of tests/day_one.rs:\n{}",
            block.join("\n"),
        );
    }
    assert_eq!(blocks, 6, "the narrative quotes six listings");
}