feat(adr-0180): LocalArena + SemilatticeDelta CRDT substrate (pure-CPU Rust) (#475)
Implements ADR-0180 §4.1 item 1: the Delta-CRDT write-accumulation substrate in core-rs/src/vault.rs. - ArenaEntry: (versor, provenance) — provenance is part of the content key. - LocalArena (§2.1): thread-local, share-nothing, lock-free write cache; snapshot() emits a canonical Delta; non-destructive (flush/GC is the kernel's concern, safe across the §3.2 eventual-consistency window). - SemilatticeDelta trait + Delta (§2.2): join is commutative, associative, idempotent under content-addressed equality (IEEE-754 versor bits + provenance bytes), never arrival order — per the §2.2 amendment landed today. - merge_kernel (§2.2): folds deltas into one content-addressed, deduped, totally ordered set; permutation- and duplicate-invariant — the property §4.3's hash(Sequential)==hash(Concurrent) rides on. Pure-CPU only (§1.5.5): no MLX/UMA handshake, no Python binding — those are downstream (§4.1 item 2; ADR-0181 PR-5). Existing vault recall/reproject paths untouched; zero eval impact. 10 failable property tests in tests/test_arena.rs (mutation-verified: disabling the content sort fails 6 of them loudly, per CLAUDE.md §Schema-Defined Proof Obligations). Also fixes a pre-existing broken doctest in the vault.rs header (indented math block was parsed as a Rust doctest).
This commit is contained in:
parent
a8f9e3685d
commit
3f9edd06da
2 changed files with 355 additions and 1 deletions
|
|
@ -9,7 +9,9 @@
|
||||||
//! The CGA inner product in Cl(4,1) is structurally diagonal with
|
//! The CGA inner product in Cl(4,1) is structurally diagonal with
|
||||||
//! ±1 metric values, so per-versor scoring collapses to
|
//! ±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
|
//! which is 32 multiplies + 32 adds, not the 1024-op full
|
||||||
//! geometric_product the reference scalar path computes. Bit-
|
//! 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;
|
use crate::cga::null_project_raw;
|
||||||
versors.par_iter().map(|v| null_project_raw(v)).collect()
|
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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArenaEntry {
|
||||||
|
pub fn new(versor: [f32; 32], provenance: Vec<u8>) -> 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<ArenaEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ArenaEntry>) -> 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<ArenaEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<u8>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
175
core-rs/tests/test_arena.rs
Normal file
175
core-rs/tests/test_arena.rs
Normal file
|
|
@ -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<u8>)> {
|
||||||
|
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<ArenaEntry>) -> 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);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue