diff --git a/core-rs/src/vault.rs b/core-rs/src/vault.rs index f095ee4f..8444b4e3 100644 --- a/core-rs/src/vault.rs +++ b/core-rs/src/vault.rs @@ -9,7 +9,9 @@ //! The CGA inner product in Cl(4,1) is structurally diagonal with //! ±1 metric values, so per-versor scoring collapses to //! -//! sum_i metric[i] * v[i] * q[i] +//! ```text +//! sum_i metric[i] * v[i] * q[i] +//! ``` //! //! which is 32 multiplies + 32 adds, not the 1024-op full //! geometric_product the reference scalar path computes. Bit- @@ -150,3 +152,180 @@ pub fn vault_reproject_parallel(versors: &[[f32; 32]]) -> Vec<[f32; 32]> { use crate::cga::null_project_raw; versors.par_iter().map(|v| null_project_raw(v)).collect() } + +// =========================================================================== +// Delta-CRDT substrate (ADR-0180 §2.1 / §2.2) +// =========================================================================== +// +// `LocalArena` is the thread-local, share-nothing write cache each modality +// adapter accumulates into (ADR-0180 §2.1). It never touches global state, so +// it needs no locks: concurrency safety comes from each thread owning its own +// arena, not from synchronisation. +// +// `SemilatticeDelta` is the join-semilattice contract (ADR-0180 §2.2): the +// merge of deltas is commutative, associative, and idempotent. The Merge +// Kernel folds deltas into a single content-addressed, deduplicated, totally +// ordered set — the order-invariant state the Vault consumes. Ordering is by +// content (the versor's IEEE-754 bit pattern + provenance bytes), never +// arrival order, per the §2.2 content-addressed-tiebreak amendment +// (2026-05-29). That is precisely what makes +// `hash(Sequential_Ingest) == hash(Concurrent_CRDT_Ingest)` (§4.3) reachable: +// the merged state cannot depend on the order deltas arrived in. +// +// This is the pure-CPU Rust substrate (§1.5.5); no MLX/UMA handshake and no +// Python binding land here — those are downstream (ADR-0180 §4.1 item 2, and +// ADR-0181 PR-5 respectively). + +use std::cmp::Ordering; + +/// One write accumulated into an arena: a Cl(4,1) versor plus opaque +/// provenance bytes. Provenance is part of the content key, so two writes of +/// the same versor under different provenance are distinct semilattice +/// elements (both retained); two byte-identical writes collapse (the +/// idempotence leg of the join semilattice, ADR-0180 §2.2). +#[derive(Clone, Debug)] +pub struct ArenaEntry { + pub versor: [f32; 32], + pub provenance: Vec, +} + +impl ArenaEntry { + pub fn new(versor: [f32; 32], provenance: Vec) -> Self { + Self { versor, provenance } + } +} + +/// Total, arrival-independent content order over arena entries. +/// +/// f32 has no total `Ord` (NaN is unordered), so we compare the raw IEEE-754 +/// bit patterns — which is exactly what "content-addressed" means here: +/// byte-identical versors sort together and deduplicate, and `-0.0`/`+0.0` +/// (distinct bytes) are treated as distinct content, as a byte-addressed merge +/// requires. Ties on the versor fall through to the provenance bytes, giving a +/// total order (ADR-0180 §2.2 amendment, mirroring ADR-0181 §2.2's merge key). +fn content_cmp(a: &ArenaEntry, b: &ArenaEntry) -> Ordering { + for i in 0..32 { + let o = a.versor[i].to_bits().cmp(&b.versor[i].to_bits()); + if o != Ordering::Equal { + return o; + } + } + a.provenance.cmp(&b.provenance) +} + +#[inline] +fn content_eq(a: &ArenaEntry, b: &ArenaEntry) -> bool { + content_cmp(a, b) == Ordering::Equal +} + +/// A snapshot of newly-ingested entries (ADR-0180 §2.2). A `Delta` is always +/// held in content-addressed order with byte-identical duplicates removed, so +/// it is a canonical join-semilattice element regardless of the order its +/// entries were inserted in. +#[derive(Clone, Debug, Default)] +pub struct Delta { + entries: Vec, +} + +impl Delta { + /// Canonicalise an arbitrary entry list into a Delta: sort by content, + /// drop byte-identical duplicates. `sort_by` is stable, but the dedup key + /// is the *whole* content, so stability is immaterial to the result. + pub fn from_entries(mut entries: Vec) -> Self { + entries.sort_by(content_cmp); + entries.dedup_by(|a, b| content_eq(a, b)); + Self { entries } + } + + pub fn entries(&self) -> &[ArenaEntry] { + &self.entries + } + + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +/// The join-semilattice contract for Delta-CRDT state (ADR-0180 §2.2). +/// +/// Implementors must satisfy, with content-addressed equality (`content_cmp`): +/// * commutativity `a.join(b) == b.join(a)` +/// * associativity `a.join(b).join(c) == a.join(b.join(c))` +/// * idempotence `a.join(a) == a` +/// +/// These are exercised as failable tests in `tests/test_arena.rs`; if `join` +/// ever orders by arrival instead of content, or stops deduplicating, those +/// tests fail loudly (CLAUDE.md §Schema-Defined Proof Obligations). +pub trait SemilatticeDelta: Sized { + fn join(&self, other: &Self) -> Self; +} + +impl SemilatticeDelta for Delta { + fn join(&self, other: &Self) -> Self { + let mut merged = + Vec::with_capacity(self.entries.len() + other.entries.len()); + merged.extend_from_slice(&self.entries); + merged.extend_from_slice(&other.entries); + Delta::from_entries(merged) + } +} + +/// Thread-local, share-nothing write cache for one modality adapter +/// (ADR-0180 §2.1). Adapters push entries here lock-free; nothing is ever +/// written to global state from an arena. `snapshot` emits the order-invariant +/// `Delta` the Merge Kernel folds. +#[derive(Clone, Debug, Default)] +pub struct LocalArena { + entries: Vec, +} + +impl LocalArena { + pub fn new() -> Self { + Self { + entries: Vec::new(), + } + } + + /// Lock-free local write. Push order is irrelevant: `snapshot` + /// canonicalises into content-addressed order. + pub fn push(&mut self, versor: [f32; 32], provenance: Vec) { + self.entries.push(ArenaEntry::new(versor, provenance)); + } + + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Emit a canonical Delta of everything accumulated so far. Does not drain + /// the arena — flush/GC policy is the Merge Kernel's concern, not the + /// arena's. + pub fn snapshot(&self) -> Delta { + Delta::from_entries(self.entries.clone()) + } +} + +/// The Merge Kernel (ADR-0180 §2.2): fold a batch of deltas into a single +/// content-addressed, deduplicated, totally ordered entry set. The result is +/// invariant under any permutation of `deltas` (commutativity + associativity) +/// and under duplicate deltas (idempotence) — the property §4.3's +/// `hash(Sequential) == hash(Concurrent)` proof rides on. +/// +/// Implemented as a single canonicalisation of the union rather than a +/// `fold(join)` chain; `tests/test_arena.rs` pins that the two are +/// content-equal, so the cheap path can never silently diverge from the +/// semilattice fold it stands in for. +pub fn merge_kernel(deltas: &[Delta]) -> Delta { + let mut all = Vec::new(); + for d in deltas { + all.extend_from_slice(&d.entries); + } + Delta::from_entries(all) +} diff --git a/core-rs/tests/test_arena.rs b/core-rs/tests/test_arena.rs new file mode 100644 index 00000000..42e00b22 --- /dev/null +++ b/core-rs/tests/test_arena.rs @@ -0,0 +1,175 @@ +//! Delta-CRDT substrate proof obligations (ADR-0180 §2.1 / §2.2). +//! +//! Each test is written to FAIL LOUDLY under the specific violation it names +//! (CLAUDE.md §Schema-Defined Proof Obligations): a schema/trait is only a +//! proven property when a test would break if the property were silently +//! removed. The map: +//! +//! * commutativity / associativity / idempotence — the three join-semilattice +//! legs ADR-0180 §2.2 claims `Delta::join` satisfies. +//! * permutation-invariant `merge_kernel` — the load-bearing property §4.3's +//! `hash(Sequential) == hash(Concurrent)` rides on. Fails if `from_entries` +//! stops sorting (i.e. orders by arrival). +//! * distinct-provenance retention — guards the dedup from collapsing on the +//! versor alone, which would silently drop epistemic state (a wrong=0-style +//! data-loss hazard at the merge layer). + +use core_rs::vault::{merge_kernel, ArenaEntry, Delta, LocalArena, SemilatticeDelta}; + +/// Deterministic distinct versor per seed. +fn versor(seed: u8) -> [f32; 32] { + let mut v = [0f32; 32]; + for (i, slot) in v.iter_mut().enumerate() { + *slot = (seed as f32) * 0.5 + (i as f32) * 0.125; + } + v +} + +fn entry(seed: u8, prov: &str) -> ArenaEntry { + ArenaEntry::new(versor(seed), prov.as_bytes().to_vec()) +} + +/// Canonical, comparable view of a Delta: (versor bits, provenance) per entry, +/// in the Delta's own order. Two Deltas are content-equal iff these match, +/// including order — so this also pins that ordering is content-addressed. +fn keys(d: &Delta) -> Vec<([u32; 32], Vec)> { + d.entries() + .iter() + .map(|e| { + let mut bits = [0u32; 32]; + for (i, b) in bits.iter_mut().enumerate() { + *b = e.versor[i].to_bits(); + } + (bits, e.provenance.clone()) + }) + .collect() +} + +fn delta(entries: Vec) -> Delta { + Delta::from_entries(entries) +} + +// --- the three join-semilattice legs (ADR-0180 §2.2) ---------------------- + +#[test] +fn join_is_commutative() { + let a = delta(vec![entry(1, "p1"), entry(3, "p3")]); + let b = delta(vec![entry(2, "p2"), entry(3, "p3")]); + // Fails if join carries arrival order: a-first vs b-first would differ. + assert_eq!(keys(&a.join(&b)), keys(&b.join(&a))); +} + +#[test] +fn join_is_associative() { + let a = delta(vec![entry(1, "p1")]); + let b = delta(vec![entry(2, "p2")]); + let c = delta(vec![entry(3, "p3")]); + assert_eq!(keys(&a.join(&b).join(&c)), keys(&a.join(&b.join(&c)))); +} + +#[test] +fn join_is_idempotent() { + let a = delta(vec![entry(1, "p1"), entry(2, "p2")]); + // a ∘ a == a — fails if dedup is removed (length would double). + let joined = a.join(&a); + assert_eq!(keys(&joined), keys(&a)); + assert_eq!(joined.len(), 2); +} + +// --- the load-bearing property for §4.3 ----------------------------------- + +#[test] +fn merge_kernel_is_permutation_invariant() { + let d0 = delta(vec![entry(5, "a"), entry(1, "b")]); + let d1 = delta(vec![entry(3, "c")]); + let d2 = delta(vec![entry(9, "d"), entry(2, "e"), entry(7, "f")]); + + let forward = merge_kernel(&[d0.clone(), d1.clone(), d2.clone()]); + let reversed = merge_kernel(&[d2.clone(), d1.clone(), d0.clone()]); + let shuffled = merge_kernel(&[d1.clone(), d0.clone(), d2.clone()]); + + // hash(Sequential) == hash(Concurrent): merged state is independent of the + // order deltas arrived in. Fails the instant `from_entries` orders by + // arrival instead of content. + assert_eq!(keys(&forward), keys(&reversed)); + assert_eq!(keys(&forward), keys(&shuffled)); +} + +#[test] +fn merge_kernel_dedups_duplicate_deltas() { + let d = delta(vec![entry(4, "x"), entry(6, "y")]); + // Re-ingesting the same delta is a no-op (idempotence at the kernel). + let once = merge_kernel(&[d.clone()]); + let twice = merge_kernel(&[d.clone(), d.clone()]); + assert_eq!(keys(&once), keys(&twice)); + assert_eq!(twice.len(), 2); +} + +#[test] +fn merge_kernel_equals_semilattice_fold() { + let deltas = [ + delta(vec![entry(8, "a"), entry(2, "b")]), + delta(vec![entry(5, "c")]), + delta(vec![entry(2, "b"), entry(9, "d")]), // overlaps the first delta + ]; + let folded = deltas + .iter() + .fold(Delta::default(), |acc, d| acc.join(d)); + // The cheap union-then-canonicalise path must equal the explicit + // semilattice fold, or the kernel has silently diverged from the trait. + assert_eq!(keys(&merge_kernel(&deltas)), keys(&folded)); +} + +// --- data-loss / over-dedup guard ----------------------------------------- + +#[test] +fn distinct_provenance_is_not_collapsed() { + // Same versor, different provenance => two distinct semilattice elements. + // Fails if dedup keys on the versor alone (which would drop state). + let same_versor = vec![entry(7, "alpha"), entry(7, "beta")]; + let merged = delta(same_versor); + assert_eq!(merged.len(), 2); + + // Byte-identical content (same versor + same provenance) => collapses. + let identical = vec![entry(7, "alpha"), entry(7, "alpha")]; + assert_eq!(delta(identical).len(), 1); +} + +#[test] +fn merge_result_is_content_sorted() { + let d = merge_kernel(&[delta(vec![entry(9, "z"), entry(1, "a"), entry(5, "m")])]); + let ks = keys(&d); + let mut sorted = ks.clone(); + sorted.sort(); + assert_eq!(ks, sorted, "merge output must be in content-addressed order"); +} + +// --- LocalArena (ADR-0180 §2.1) ------------------------------------------- + +#[test] +fn arena_snapshot_independent_of_push_order() { + let mut a = LocalArena::new(); + a.push(versor(3), b"p3".to_vec()); + a.push(versor(1), b"p1".to_vec()); + a.push(versor(2), b"p2".to_vec()); + + let mut b = LocalArena::new(); + b.push(versor(2), b"p2".to_vec()); + b.push(versor(3), b"p3".to_vec()); + b.push(versor(1), b"p1".to_vec()); + + // Two arenas fed the same writes in different orders snapshot to the same + // canonical Delta (ADR-0180 §2.1: push order is irrelevant). + assert_eq!(keys(&a.snapshot()), keys(&b.snapshot())); +} + +#[test] +fn arena_snapshot_does_not_drain() { + let mut a = LocalArena::new(); + a.push(versor(1), b"p1".to_vec()); + let _ = a.snapshot(); + // Flush/GC is the Merge Kernel's concern, not the arena's; snapshot must + // be non-destructive so a delayed merge (the §3.2 window) cannot lose it. + assert_eq!(a.len(), 1); + assert_eq!(a.snapshot().len(), 1); +}