//! The CRDT convergence harness (QUICKSTART, Agent F): *"apply same envelope //! set in N random orders, assert byte-identical materialized state."* This is //! v0 acceptance criterion 1 (Convergence) and criterion 5 (Reduction //! determinism) — the determinism heart of Chapter 6. //! //! It drives the **real** [`epiphany_ops`] crate (Agent C has shipped). There //! are two levels of convergence, and this harness asserts both: //! //! * **Real-Score convergence** ([`assert_graph_convergence`], //! [`run_graph_convergence`]) is the headline criterion 1: an edit session is //! reduced onto a real base [`epiphany_core::Score`] via //! [`OperationSet::reduce_onto`], and the entire materialized graph (the //! `Score` *and* its bookkeeping state) must be identical across delivery //! orders, pass `check_invariants`, and genuinely mutate the score. //! * **Reducer-bookkeeping convergence** ([`assert_convergence`], //! [`run_convergence`], [`run_two_staff_convergence`]) compares the canonical //! *bookkeeping projection* — [`OperationSet::reduce`] → //! `MaterializedState::canonical_bytes` — across delivery orders. This is the //! Chapter 6 §6.3 ledger (effects, conflicts, anomalies, tombstones, //! spellings, pending), not the full musical graph; it is retained as the //! byte-canonical determinism gate and the basis of criterion 5. //! //! Because the canonical reduction order //! ([`epiphany_ops::canonical_reduction_order`]) is a function of the operation //! *set* and not of delivery order, every delivery permutation must materialize //! identically at both levels. The negative control in the tests proves the //! harness is not vacuous: a reducer that consumed *arrival* order instead would //! diverge, and this harness would catch it. use epiphany_core::{check_invariants, OperationId, Score, StaffInstanceId, VoiceId}; use epiphany_ops::{ canonical_reduction_order, GraphMaterialization, OperationEnvelope, OperationSet, }; use crate::rng::Rng; /// Agent C's authoritative reduction-determinism gate (10,000 randomized sets; /// criteria 1 and 5 at scale), re-exported as the suite's single entry point. pub use epiphany_ops::fuzz::run_reduction_determinism_fuzz as ops_reduction_determinism_fuzz; /// Accepts `envelopes` in the given order into a fresh [`OperationSet`] and /// returns the canonical materialized bytes. fn materialize_in_order(envelopes: &[OperationEnvelope]) -> Vec { let mut set = OperationSet::new(); set.accept_all(envelopes.iter().cloned()); set.reduce().canonical_bytes() } /// The canonical reduction order (as `OperationId`s) of the accepted singles. fn reduction_order_of(envelopes: &[OperationEnvelope]) -> Vec { let mut set = OperationSet::new(); set.accept_all(envelopes.iter().cloned()); let singles = set.single_envelopes(); canonical_reduction_order(&singles) .into_iter() .map(|e| e.id) .collect() } /// Asserts that `envelopes` materialize to **byte-identical** state under /// `orders` independent random delivery permutations (acceptance criterion 1). /// Panics, with the diverging permutation index, on the first mismatch. pub fn assert_convergence(envelopes: &[OperationEnvelope], orders: usize, rng: &mut Rng) { let reference = materialize_in_order(envelopes); for k in 0..orders { let perm = rng.permutation(envelopes.len()); let shuffled: Vec = perm.iter().map(|&i| envelopes[i].clone()).collect(); let got = materialize_in_order(&shuffled); assert_eq!( reference, got, "delivery permutation #{k} changed the materialized state \ (reduction is not order-independent)" ); } } /// Asserts the stronger §6.7 property used by acceptance criterion 5: both the /// **materialized state** and the **canonical reduction order itself** are /// identical across `orders` delivery permutations. Returns that canonical order. pub fn assert_reduction_determinism( envelopes: &[OperationEnvelope], orders: usize, rng: &mut Rng, ) -> Vec { let reference_order = reduction_order_of(envelopes); let reference_state = materialize_in_order(envelopes); for k in 0..orders { let perm = rng.permutation(envelopes.len()); let shuffled: Vec = perm.iter().map(|&i| envelopes[i].clone()).collect(); assert_eq!( reference_order, reduction_order_of(&shuffled), "delivery permutation #{k} changed the canonical reduction order" ); assert_eq!( reference_state, materialize_in_order(&shuffled), "delivery permutation #{k} changed the materialized state" ); } reference_order } /// Asserts the histories actually honor the **causal-first** order, not merely /// permutation invariance (spec §"Identifiers": every causal predecessor's stamp /// must be strictly less). For each operation `B` and each operation `A` in /// `B`'s causal context that is present in the set, asserts `A`'s stamp is /// strictly less than `B`'s *and* `A` precedes `B` in the canonical reduction /// order. This proves the order is causal-first, which sorting-by-HLC delivers /// only when histories are authored conformantly. pub fn assert_causal_order_respected(envelopes: &[OperationEnvelope]) { let mut set = OperationSet::new(); set.accept_all(envelopes.iter().cloned()); let singles = set.single_envelopes(); let ordered = canonical_reduction_order(&singles); let pos: std::collections::BTreeMap = ordered.iter().enumerate().map(|(i, e)| (e.id, i)).collect(); for b in &ordered { for a in &ordered { if a.id != b.id && b.causal_context.covers(a.id) { assert!( a.stamp.reduction_tuple() < b.stamp.reduction_tuple(), "authoring rule violated: causal predecessor {:?} stamp is not strictly \ less than successor {:?}", a.id, b.id ); assert!( pos[&a.id] < pos[&b.id], "causal predecessor {:?} does not precede successor {:?} in the reduction order", a.id, b.id ); } } } } /// A self-contained driver: generate `n_ops` envelopes from `seed`, assert the /// histories honor causal order, and assert convergence across `orders` /// permutations. Deterministic. pub fn run_convergence(n_ops: usize, orders: usize, seed: u64) { let mut rng = Rng::new(seed); let envelopes = crate::generators::operation_envelopes(&mut rng, n_ops, 3, 6, 6); assert_causal_order_respected(&envelopes); assert_convergence(&envelopes, orders, &mut rng); } /// The v0 criterion-1 scenario: overlapping edits to a two-staff score by two /// replicas. Asserts both staves are actually populated in the materialized /// result, that the histories honor causal order, and that they converge to /// byte-identical materialized state across `orders` delivery permutations. pub fn run_two_staff_convergence(orders: usize, seed: u64) { let mut rng = Rng::new(seed); let envelopes = crate::generators::two_staff_edit_session(&mut rng); assert_causal_order_respected(&envelopes); crate::generators::assert_two_staff_populated(&envelopes); assert_convergence(&envelopes, orders, &mut rng); } /// **The authoritative criterion-1/5 gate.** Generates `sets` randomized /// *conformant* operation sets (the testkit's generator honors the HLC authoring /// rule) and asserts, for each: (a) causal-first order is genuinely respected /// ([`assert_causal_order_respected`]), and (b) reduction is byte-identical and /// order-identical across `orders` delivery permutations /// ([`assert_reduction_determinism`]). /// /// This is the testkit's own gate and the one the suite treats as authoritative /// for causal-order correctness. Agent C's re-exported /// [`ops_reduction_determinism_fuzz`] is run in addition; its baseline histories /// now use the same causal HLC authoring rule, while its explicit anomaly /// injections continue to exercise quarantine behavior. pub fn run_authoritative_reduction_gate(sets: usize, orders: usize, seed: u64) { let mut rng = Rng::new(seed); for _ in 0..sets { let n = 1 + rng.below(30) as usize; let envelopes = crate::generators::operation_envelopes(&mut rng, n, 3, 8, 8); assert_causal_order_respected(&envelopes); assert_reduction_determinism(&envelopes, orders, &mut rng); } } // === Real-Score convergence (acceptance criterion 1, graph level). ========== /// Reduces `envelopes` onto `base` in the given delivery order via the real /// [`OperationSet::reduce_onto`], returning the full graph materialization (the /// `epiphany_core::Score` together with its canonical bookkeeping state). fn materialize_onto_in_order( base: &Score, envelopes: &[OperationEnvelope], ) -> GraphMaterialization { let mut set = OperationSet::new(); set.accept_all(envelopes.iter().cloned()); set.reduce_onto(base) } /// The number of events the given voice carries in `score`, or `None` if the /// voice is absent. fn voice_event_count(score: &Score, voice: VoiceId) -> Option { score .voices() .find_map(|(_, _, v)| (v.id == voice).then_some(v.events.len())) } /// **Real-Score convergence (acceptance criterion 1).** Reduces the same /// operation set onto `base` under `orders` independent delivery permutations /// and asserts the entire [`GraphMaterialization`] — the real /// [`epiphany_core::Score`] *and* its bookkeeping state — is identical every /// time. Also asserts the materialized score satisfies every Chapter 5 graph /// invariant ([`check_invariants`]) and that the session is non-vacuous: the /// score actually changed and each targeted voice grew. pub fn assert_graph_convergence( base: &Score, envelopes: &[OperationEnvelope], targets: &[(StaffInstanceId, VoiceId)], orders: usize, rng: &mut Rng, ) { let reference = materialize_onto_in_order(base, envelopes); // The materialized real Score is structurally valid. let violations = check_invariants(&reference.score); assert!( violations.is_empty(), "materialized score violates graph invariants: {violations:?}" ); // Non-vacuity: the session genuinely mutated the score, and each targeted // voice grew (the generator inserts only at fresh positions, so no insert is // lost to promotion). assert!( reference.score != *base, "the edit session did not change the base score (vacuous convergence test)" ); for &(_, voice) in targets { let before = voice_event_count(base, voice).expect("target voice exists in base"); let after = voice_event_count(&reference.score, voice).expect("target voice survives reduction"); assert!( after > before, "target voice {voice:?} did not grow under reduction ({before} -> {after})" ); } for k in 0..orders { let perm = rng.permutation(envelopes.len()); let shuffled: Vec = perm.iter().map(|&i| envelopes[i].clone()).collect(); let got = materialize_onto_in_order(base, &shuffled); assert_eq!( reference, got, "delivery permutation #{k} changed the materialized Score \ (graph reduction is not order-independent)" ); } } /// Selects a `valid_score` base with at least two voices (scanning successive /// seeds), so the graph convergence gate genuinely edits two staves. fn two_voice_base(seed: u64) -> Score { let mut s = seed; for _ in 0..64 { let score = epiphany_core::generators::valid_score(s); if score.voices().count() >= 2 { return score; } s = s.wrapping_mul(0x9E37_79B9).wrapping_add(1); } epiphany_core::generators::valid_score(seed) } /// **The self-contained real-Score convergence driver (criterion 1).** Builds a /// two-voice base score, authors a real ~50-bar edit session against its actual /// voices ([`crate::generators::graph_edit_session`]), and asserts graph-level /// convergence across `orders` delivery permutations. This is the graph /// counterpart of [`run_two_staff_convergence`] (its reducer-bookkeeping twin). pub fn run_graph_convergence(orders: usize, seed: u64) { let base = two_voice_base(seed); let mut rng = Rng::new(seed ^ 0x67A0_6FAC_E0B0_B0B0); let (targets, envelopes) = crate::generators::graph_edit_session(&base, &mut rng); assert_graph_convergence(&base, &envelopes, &targets, orders, &mut rng); } /// Builds a two-voice base, authors a real ~50-bar edit session, reduces it onto /// the base via [`OperationSet::reduce_onto`], and returns the materialized real /// [`Score`] together with the causal frontier it covers. Used by the /// full-Score serialization gate (criterion 4, whole-graph tier). pub fn materialized_score(seed: u64) -> (Score, Vec) { let base = two_voice_base(seed); let mut rng = Rng::new(seed ^ 0x5C0E_5E51_A11A_B1E5); let (_targets, envelopes) = crate::generators::graph_edit_session(&base, &mut rng); let materialization = materialize_onto_in_order(&base, &envelopes); let frontier = crate::generators::frontier_bytes(&envelopes); (materialization.score, frontier) } #[cfg(test)] mod tests { use super::*; use epiphany_core::{PitchId, PitchSpelling, ReplicaId, WallClockTime}; use epiphany_ops::{ valuegen, AuthorId, CausalContext, HybridLogicalClock, OperationKind, OperationPayload, OperationStamp, RespellPitchOp, }; use std::collections::BTreeMap; #[test] fn small_sets_converge() { for seed in 0..200u64 { run_convergence(16, 8, seed.wrapping_mul(0x9E37_79B9)); } } #[test] fn two_staff_session_converges() { for seed in 0..16u64 { run_two_staff_convergence(8, seed.wrapping_mul(0x9E37_79B9).wrapping_add(1)); } } #[test] fn graph_sessions_converge_on_the_real_score() { for seed in 0..8u64 { run_graph_convergence(4, seed.wrapping_mul(0x9E37_79B9).wrapping_add(7)); } } #[test] fn empty_and_singleton_sets_are_trivially_stable() { let mut rng = Rng::new(1); assert_convergence(&[], 4, &mut rng); let one = crate::generators::operation_envelopes(&mut rng, 1, 2, 6, 6); assert_convergence(&one, 4, &mut rng); } // --- Negative control: prove the harness is not vacuous. --------------- // // Two concurrent RespellPitch operations on the *same* pitch with different // spellings. A reducer that consumed *arrival* order would land on whichever // arrived last — so two delivery orders diverge. The real (canonical) reducer // lands on the same spelling regardless. The convergence harness compares the // canonical result, so it would FAIL for the arrival-order reducer; this test // demonstrates the discriminating power directly. fn respell(replica: u64, counter: u64, phys: i64, spelling: u8) -> OperationEnvelope { let id = OperationId::new(ReplicaId(replica), counter); OperationEnvelope { id, author: AuthorId(replica as u128), stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(phys), 0), id), // No causal link → the two operations are concurrent. causal_context: CausalContext::new(), transaction: None, payload: OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp { pitch: PitchId::new(ReplicaId(0x0B7E_C700), 0), spelling: valuegen::spelling(spelling), })), } } /// A deliberately broken, order-*dependent* reducer: last spelling wins by /// arrival order. Stands in for the bug the harness must catch. fn naive_arrival_order_spelling(envs: &[OperationEnvelope]) -> Option { let mut last: BTreeMap = BTreeMap::new(); for e in envs { if let OperationPayload::Primitive(OperationKind::RespellPitch(op)) = &e.payload { last.insert(op.pitch, op.spelling.clone()); } } last.values().next().cloned() } #[test] fn negative_control_arrival_order_reducer_would_diverge() { let a = respell(1, 0, 10, 0xAA); let b = respell(2, 0, 20, 0xBB); // Arrival order A,B vs B,A: the broken arrival-order reducer diverges... let naive_ab = naive_arrival_order_spelling(&[a.clone(), b.clone()]); let naive_ba = naive_arrival_order_spelling(&[b.clone(), a.clone()]); assert_ne!( naive_ab, naive_ba, "negative control is mis-constructed: an arrival-order reducer must diverge here" ); // ...while the real canonical reducer converges (the harness passes). let canon_ab = materialize_in_order(&[a.clone(), b.clone()]); let canon_ba = materialize_in_order(&[b, a]); assert_eq!( canon_ab, canon_ba, "the canonical reducer must converge regardless of arrival order" ); } }