diff --git a/core-rs/src/vault.rs b/core-rs/src/vault.rs index 8444b4e3..14bc74e4 100644 --- a/core-rs/src/vault.rs +++ b/core-rs/src/vault.rs @@ -248,6 +248,31 @@ impl Delta { pub fn is_empty(&self) -> bool { self.entries.is_empty() } + + /// Canonical little-endian serialization of this Delta — the cross-language + /// G1 contract (ADR-0180 / ADR-0196). Byte-for-byte identical to the + /// Python reference `vault.crdt.canonical_bytes`; parity is pinned by + /// `tests/test_crdt_hash_parity.rs`. Layout: + /// + /// ```text + /// u64 entry_count + /// per entry (canonical order): + /// 32 x f32 versor components (IEEE-754, little-endian, 4 bytes each) + /// u64 provenance_length + /// bytes provenance + /// ``` + pub fn canonical_bytes(&self) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&(self.entries.len() as u64).to_le_bytes()); + for entry in &self.entries { + for component in &entry.versor { + out.extend_from_slice(&component.to_le_bytes()); + } + out.extend_from_slice(&(entry.provenance.len() as u64).to_le_bytes()); + out.extend_from_slice(&entry.provenance); + } + out + } } /// The join-semilattice contract for Delta-CRDT state (ADR-0180 §2.2). diff --git a/core-rs/tests/fixtures/crdt_parity_expected.rs b/core-rs/tests/fixtures/crdt_parity_expected.rs new file mode 100644 index 00000000..3594a605 --- /dev/null +++ b/core-rs/tests/fixtures/crdt_parity_expected.rs @@ -0,0 +1,13 @@ +// @generated by tests/fixtures/crdt/_generate.py — DO NOT EDIT. +// Expected canonical_bytes hex per case (mirrors merge_fixtures.json): +// the Python-canonical Delta-CRDT G1 contract (ADR-0180 / ADR-0196) +// the Rust substrate must reproduce byte-for-byte. +// (include!d mid-module, so plain // comments — not //! — are required.) + +pub const EXPECTED_CANONICAL_BYTES_HEX: &[(&str, &str)] = &[ + ("single_entry", "01000000000000000000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"), + ("dedup_within_delta", "01000000000000000000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"), + ("distinct_provenance_retained", "0200000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000006c656674000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000007269676874"), + ("three_delta_merge", "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000071000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000006d000000000000404000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000007a0000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"), + ("signed_zero_distinct", "020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000700000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000070"), +]; diff --git a/core-rs/tests/test_crdt_hash_parity.rs b/core-rs/tests/test_crdt_hash_parity.rs new file mode 100644 index 00000000..cc10dc2c --- /dev/null +++ b/core-rs/tests/test_crdt_hash_parity.rs @@ -0,0 +1,105 @@ +//! ADR-0180 / ADR-0196 gate G1 (slice ZC-0) — Rust↔Python byte-parity. +//! +//! Proves the Rust Delta-CRDT substrate (`core_rs::vault`) produces +//! `canonical_bytes` identical to the Python reference `vault/crdt.py` over the +//! shared golden corpus. Expected hex is generated *from* the Python reference +//! (single source of truth) into `tests/fixtures/crdt_parity_expected.rs`, so +//! the two languages cannot silently disagree. Fails loudly if the Rust +//! serialization, content ordering, or dedup ever diverges from the locked +//! Python contract (CLAUDE.md §Schema-Defined Proof Obligations). + +use core_rs::vault::{merge_kernel, ArenaEntry, Delta}; + +include!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/crdt_parity_expected.rs" +)); + +fn v(idx: usize, val: f32) -> [f32; 32] { + let mut a = [0.0f32; 32]; + a[idx] = val; + a +} + +fn e(idx: usize, val: f32, prov: &str) -> ArenaEntry { + ArenaEntry::new(v(idx, val), prov.as_bytes().to_vec()) +} + +fn delta(entries: Vec) -> Delta { + Delta::from_entries(entries) +} + +fn hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + s.push_str(&format!("{:02x}", b)); + } + s +} + +fn expected(name: &str) -> &'static str { + EXPECTED_CANONICAL_BYTES_HEX + .iter() + .find(|entry| entry.0 == name) + .map(|entry| entry.1) + .unwrap_or_else(|| panic!("no expected fixture for case {name}")) +} + +fn assert_parity(name: &str, deltas: Vec) { + let merged = merge_kernel(&deltas); + assert_eq!(hex(&merged.canonical_bytes()), expected(name), "case {name}"); +} + +#[test] +fn parity_single_entry() { + assert_parity("single_entry", vec![delta(vec![e(0, 1.0, "a")])]); +} + +#[test] +fn parity_dedup_within_delta() { + assert_parity( + "dedup_within_delta", + vec![delta(vec![e(0, 1.0, "a"), e(0, 1.0, "a")])], + ); +} + +#[test] +fn parity_distinct_provenance_retained() { + assert_parity( + "distinct_provenance_retained", + vec![delta(vec![e(5, 2.0, "left"), e(5, 2.0, "right")])], + ); +} + +#[test] +fn parity_three_delta_merge() { + assert_parity( + "three_delta_merge", + vec![ + delta(vec![e(1, 3.0, "z"), e(0, 1.0, "a")]), + delta(vec![e(2, 2.0, "m"), e(0, 1.0, "a")]), + delta(vec![e(10, 5.0, "q")]), + ], + ); +} + +#[test] +fn parity_signed_zero_distinct() { + assert_parity( + "signed_zero_distinct", + vec![delta(vec![e(0, 0.0, "p"), e(0, -0.0, "p")])], + ); +} + +#[test] +fn parity_permutation_invariant_matches_python() { + // Reordering input deltas must not change the merged bytes (and thus the + // Python-pinned hash). Mirrors the Python permutation-invariance test. + let d0 = delta(vec![e(1, 3.0, "z"), e(0, 1.0, "a")]); + let d1 = delta(vec![e(2, 2.0, "m"), e(0, 1.0, "a")]); + let d2 = delta(vec![e(10, 5.0, "q")]); + let forward = hex(&merge_kernel(&[d0.clone(), d1.clone(), d2.clone()]).canonical_bytes()); + let reversed = hex(&merge_kernel(&[d2, d1, d0]).canonical_bytes()); + assert_eq!(forward, reversed); + assert_eq!(forward, expected("three_delta_merge")); +} diff --git a/docs/decisions/ADR-0180-crdt-sharded-vault-concurrency.md b/docs/decisions/ADR-0180-crdt-sharded-vault-concurrency.md index 13180136..dd05a6f6 100644 --- a/docs/decisions/ADR-0180-crdt-sharded-vault-concurrency.md +++ b/docs/decisions/ADR-0180-crdt-sharded-vault-concurrency.md @@ -1,9 +1,9 @@ # ADR-0180: Delta-CRDT Sharded Substrate for Multimodal Concurrency -**Status:** Proposed -**Date:** 2026-05-29 +**Status:** Accepted (2026-05-31) — Delta-CRDT reference contract locked at gate G1 (ADR-0196). See §5. +**Date:** 2026-05-29 (Proposed) · 2026-05-31 (Accepted) **Authors:** Joshua M. Shay, Core R&D Engine -**Domains:** `core-rs/src/vault.rs`, `sensorium/`, `field/` +**Domains:** `core-rs/src/vault.rs`, `vault/crdt.py`, `sensorium/`, `field/` ## 1. Context & Problem Statement @@ -240,3 +240,70 @@ asynchronous Semilattice Joins. 3. **Trace Invariance Proof:** Extend `evals/` to prove that `hash(Sequential_Ingest) == hash(Concurrent_CRDT_Ingest)`. The order of asynchronous merging must not alter the final unified geometric topography. + +## 5. Status: Accepted — G1 reference-contract lock (2026-05-31) + +This ADR is accepted at **gate G1** of the native-substrate adoption ladder +(ADR-0196 / `docs/zig/adoption-gates.md`): the Delta-CRDT substrate now has a +**locked, executable reference contract**. This corresponds to slice **ZC-0** +("contract pinning") in `docs/zig/crdt-substrate/implementation-slices.md`, +whose exit gate is *"Python/Rust reference behavior is locked."* + +### 5.1 The locked contract + +- **Canonical reference:** `vault/crdt.py` — `ArenaEntry`, `Delta`, + `LocalArena`, `merge_kernel`, `canonical_bytes`, `delta_hash`. Pure + content law: no normalization, no versor closure/repair, no field mutation, + no global Vault writes (CLAUDE.md §Normalization Rules / §Core Primitives). +- **Content order** is by the IEEE-754 bit pattern of the 32 versor components, + then the provenance bytes — never arrival order (§2.2 amendment). `+0.0`, + `-0.0`, and distinct NaN payloads are distinct content (bit-addressed). +- **Canonical serialization** (`canonical_bytes`) is the cross-language + contract; `delta_hash` is its SHA-256. Layout (all little-endian): + + ```text + u64 entry_count + per entry (canonical order): + 32 x f32 versor components (IEEE-754, little-endian, 4 bytes each) + u64 provenance_length + bytes provenance + ``` + +### 5.2 Proof obligations (all failable; CLAUDE.md §Schema-Defined Proof Obligations) + +| Obligation | Test | +|---|---| +| C-1 commutativity, C-2 associativity, C-3 idempotence | `tests/test_crdt_semilattice_contract.py` | +| C-4 permutation-invariant merge, C-5 duplicate-delta no-op, kernel == join-fold | `tests/test_crdt_semilattice_contract.py` | +| Content ordering, distinct-provenance retention, signed-zero / NaN bit-addressing | `tests/test_crdt_content_ordering.py` | +| C-7 arena push never mutates the global Vault; snapshot/merge purity | `tests/test_crdt_no_global_write_from_arena.py` | +| Golden corpus: canonical bytes + merge hash regression-lock | `tests/test_crdt_semilattice_contract.py` + `tests/fixtures/crdt/merge_fixtures.json` | +| **Rust ↔ Python byte-parity** | `core-rs/tests/test_crdt_hash_parity.rs` (+ `Delta::canonical_bytes` in `core-rs/src/vault.rs`) | + +The golden corpus is regenerated deterministically from the Python reference by +`tests/fixtures/crdt/_generate.py` (single source of truth; it also emits the +Rust-side expected hex). Mutation-tested: removing dedup breaks C-3/C-5; +ordering by arrival breaks C-1. + +### 5.3 §4 execution-plan status + +1. **`core-rs` LocalArena / SemilatticeDelta** — **done** (`core-rs/src/vault.rs`, + `core-rs/tests/test_arena.rs`), now extended with `canonical_bytes` and + pinned to the Python reference by `test_crdt_hash_parity.rs`. +2. **MLX zero-copy integration** — **deferred / out of scope** per §1.5.5 + (hardware optimization; not required for the substrate contract). +3. **Trace-invariance proof** — the *substrate-level* property is locked: the + merge kernel is permutation-invariant and `delta_hash` is replay-stable + (C-4). The full end-to-end `hash(Sequential_Ingest) == + hash(Concurrent_CRDT_Ingest)` eval over a live concurrent modality pipeline + remains future work, to land when modality ingestion is wired — it rides on + the contract locked here. + +### 5.4 Boundary — what this ADR does NOT authorize + +This ADR locks the **reference contract only**. It does **not** authorize any +Zig implementation. The Zig CRDT prototype (slices **ZC-1 and beyond**) remains +at **gate G2** under ADR-0196 and requires a separate ADR before any Zig code, +backend selector, or runtime wiring. The Rust substrate likewise stays a +pure-CPU, Python-unbound substrate (§1.5.5) until a downstream ADR binds or +promotes it. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 708f2d69..40bf1305 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -235,7 +235,7 @@ ADRs record significant architectural decisions: what was decided, why, what alt | [ADR-0178](ADR-0178-GB3b-referent-accumulation-scope.md) | ADR-0178 GB-3b — referent-aware accumulation chaining (scope) | Proposed (scope only — no code). Sub-phase of | | [ADR-0178](ADR-0178-compositional-structure.md) | Compositional Structure: Comprehension-Guided Multi-Step Derivation (Gap B) | Proposed | | [ADR-0179](ADR-0179-extraction-richness.md) | Extraction Richness: feeding the comprehension composer real quantities | Proposed | -| [ADR-0180](ADR-0180-crdt-sharded-vault-concurrency.md) | Delta-CRDT Sharded Substrate for Multimodal Concurrency | Proposed | +| [ADR-0180](ADR-0180-crdt-sharded-vault-concurrency.md) | Delta-CRDT Sharded Substrate for Multimodal Concurrency | Accepted (2026-05-31 — G1 reference-contract lock) | | [ADR-0181](ADR-0181-audio-compiler-delta-crdt.md) | CORE-native Audio Compiler over the Delta-CRDT Substrate | Proposed | | [ADR-0182](ADR-0182-cross-composer-disagreement-pooling.md) | Cross-composer disagreement pooling: refuse distractor-quantity confusers without a reactive cue rule | Proposed (spec only — no code). Follow-on to | | [ADR-0183](ADR-0183-lawful-audio-lexeme-path.md) | Lawful Audio→Lexeme Path (stub) | Proposed (stub — placeholder to record the fork; not yet a full design) | diff --git a/docs/zig/STATUS.md b/docs/zig/STATUS.md new file mode 100644 index 00000000..ed107b7f --- /dev/null +++ b/docs/zig/STATUS.md @@ -0,0 +1,159 @@ +# Zig Native Substrate — Status & Handoff + +**Last updated:** 2026-05-31 +**Maintainer note:** update this page whenever a gate advances or a slice ships. + +This is the single portable *"where we are / what's next"* page for the Zig +native-substrate work. The doctrine and the definition of each step live in the +sibling docs; **this page records execution state, the immediate next action, +and environment gotchas** so any operator — or a fresh terminal, or another +agent — can resume cold. + +> Cold-start read order: this page → [`README.md`](README.md) (decision +> package) → [`adoption-gates.md`](adoption-gates.md) (G0–G8) → the per-component +> slice docs. + +--- + +## TL;DR + +- CORE is **not** converting to Zig. Zig is a **Ring-1 native-substrate + candidate**, adopted component-by-component through gates **G0–G8**. Python + stays the semantic source of truth; Rust stays the incumbent algebra backend. +- **Ratified:** ADR-0196 (doctrine). **Locked:** the Delta-CRDT reference + contract (ADR-0180 → Accepted, **gate G1**). +- **No Zig code is authorized.** The next Zig step (ZC-1) requires a **new ADR + clearing gate G2**. +- This is **opportunistic Ring-1 work. Ring-2 GSM8K comprehension remains the + live priority** — do not let substrate work displace it. + +--- + +## What shipped + +| PR | Scope | State | +|---|---|---| +| [#509](https://github.com/AssetOverflow/core/pull/509) | ADR-0196 doctrine ratification (`docs/zig/**` + README section) | open | +| [#511](https://github.com/AssetOverflow/core/pull/511) | ADR-0180 → Accepted; Delta-CRDT G1 contract lock (slice ZC-0); this STATUS page | open | + +Merge **#509 first** (narrative coherence + so the sibling links in this page +resolve on `main`). The two PRs are otherwise independent (no file conflict). + +--- + +## Gate status per component + +Gate ladder: `G0 doctrine → G1 reference-locked → G2 prototype-selector → +G3 parity → G4 determinism → G5 mechanical-advantage → G6 ops → G7 supported → +G8 default-by-ADR`. + +### Delta-CRDT substrate (ADR-0180) — highest priority + +| Slice | What | Gate | Status | +|---|---|---|---| +| ZC-0 | Contract pinning: Python reference + golden fixtures + Rust↔Python byte parity | **G1** | ✅ done (#511) | +| ZC-1 | Inert Zig skeleton `core-zig/` (builds + local tests, no runtime wiring) | G2 | ⬜ **needs a new ADR to clear G2 first** | +| ZC-2 | C ABI self-test (create/free/push/snapshot/join/merge/hash, no Python) | — | ⬜ | +| ZC-3 | Python binding behind `CORE_CRDT_BACKEND=zig` (no auto-selection) | — | ⬜ | +| ZC-4 | Parity fixtures: Zig == reference (**reuses the corpus locked in ZC-0**) | G3 | ⬜ | +| ZC-5 | Benchmark + memory profile (must win a *named* mechanical criterion) | G5 | ⬜ | +| ZC-6 | Runtime integration proposal | G6→G7 | ⬜ **new ADR** | +| — | Default backend | G8 | ⬜ **separate ADR** | + +Step detail: [`crdt-substrate/implementation-slices.md`](crdt-substrate/implementation-slices.md). + +### Other components (each starts fresh at its own G0/G1) + +| Component | Doc | Current gate | Blocker / note | +|---|---|---|---| +| Audio compiler (ADR-0181) | [`audio-compiler/`](audio-compiler/README.md) | **G0/G1 (blocked)** | Python audio spec not locked — ADR-0183 lexeme fork is a stub. Lock it (audio's own "ZC-0") before any Zig. | +| Runtime FFI contract | [`runtime-ffi/`](runtime-ffi/README.md) | G0 | Doctrine says land the C-ABI/versioning/ownership contract *before* serious Zig in any component. | +| Batch-recall challenge (`vault_recall_batch`) | [`algebra-kernels/`](algebra-kernels/README.md) | G0 | Optional Rust/Zig contest; only after parity tests. `vault_recall_batch` is Python-canonical today (`algebra/backend.py`). | +| Edge-native ingestion runner | [`core-native-system/`](core-native-system/README.md) | G0 | Future; after the above contracts stabilize. | + +--- + +## Immediate next action + +**Nothing is authorized to start** — this is the deliberate stopping point. + +When the CRDT Zig lane is greenlit, the first step is **an ADR to clear gate G2** +(cite ADR-0196's gate ladder and ADR-0180's locked contract), *then* ZC-1 +(inert `core-zig/` skeleton). The ZC-4 parity fixtures already exist: + +- Python golden corpus: `tests/fixtures/crdt/merge_fixtures.json` +- Rust expected hex: `core-rs/tests/fixtures/crdt_parity_expected.rs` +- Single source of truth (regenerator): `tests/fixtures/crdt/_generate.py` + +A Zig prototype is graded against `canonical_bytes` / `delta_hash` from the +locked reference (below). + +--- + +## The locked reference contract (gate G1) + +- **Python canonical reference:** `vault/crdt.py` — `ArenaEntry`, `Delta`, + `LocalArena`, `merge_kernel`, `canonical_bytes`, `delta_hash`. Pure content + law (content-addressed by IEEE-754 bits then provenance; no normalization, no + versor closure, no global Vault writes). +- **Rust incumbent parity:** `Delta::canonical_bytes` in `core-rs/src/vault.rs`, + pinned byte-identical by `core-rs/tests/test_crdt_hash_parity.rs`. +- **Canonical byte layout** (the cross-language contract; `delta_hash` = its SHA-256): + + ```text + u64 entry_count + per entry (content order): + 32 x f32 versor components (IEEE-754, little-endian, 4 bytes each) + u64 provenance_length + bytes provenance + ``` + +- **Obligation → test map:** ADR-0180 §5.2. + +--- + +## Environment gotchas (these cost time on 2026-05-31) + +1. **Build `core-rs` against Python ≤3.12.** PyO3 0.21 supports max 3.12, but + fresh `uv` worktree venvs resolve to 3.13 and the homebrew system python is + 3.14 (`cargo` then errors: *"configured Python interpreter version (3.14) is + newer than PyO3's maximum supported version (3.12)"*). Use: + + ```bash + PYO3_PYTHON=/opt/homebrew/bin/python3.12 cargo test --test test_crdt_hash_parity --test test_arena -q + ``` + +2. **`public_demo` lane SHA miss is environmental, not a regression.** + `scripts/verify_lane_shas.py` reports `lanes: 7/8` with `✗ public_demo` → + `DemoContractError: showcase exceeded ADR-0099 runtime budget (~46–48s > + 30000 ms)`. This is a **wall-clock** overrun in `core/demos/showcase.py`, + **reproduced identically on clean `main`**. For substrate-only PRs (no + lane-affecting serving code), confirm the 7 content lanes match and the only + miss is `public_demo`'s timeout. Do **not** re-pin or chase it. + +--- + +## How to verify current state + +```bash +# Python contract (default backend) +uv run python -m pytest tests/test_crdt_semilattice_contract.py \ + tests/test_crdt_content_ordering.py \ + tests/test_crdt_no_global_write_from_arena.py -q # expect 21 passed + +# Rust ↔ Python byte parity (needs Py3.12) +cd core-rs && PYO3_PYTHON=/opt/homebrew/bin/python3.12 \ + cargo test --test test_crdt_hash_parity --test test_arena -q # expect 16 passed + +# Regenerate the golden fixtures (Python is the single source of truth) +uv run python tests/fixtures/crdt/_generate.py +``` + +--- + +## Authoritative docs + +- **Doctrine & ratification:** ADR-0196 (`docs/decisions/`), [`README.md`](README.md) +- **Gates:** [`adoption-gates.md`](adoption-gates.md) +- **CRDT slices:** [`crdt-substrate/`](crdt-substrate/README.md) + [`implementation-slices.md`](crdt-substrate/implementation-slices.md) +- **Component contract status / what locked G1:** ADR-0180 §5 diff --git a/tests/fixtures/crdt/_generate.py b/tests/fixtures/crdt/_generate.py new file mode 100644 index 00000000..915b9bf6 --- /dev/null +++ b/tests/fixtures/crdt/_generate.py @@ -0,0 +1,157 @@ +"""Regenerate tests/fixtures/crdt/merge_fixtures.json from the canonical +reference (vault/crdt.py). Run: uv run python tests/fixtures/crdt/_generate.py + +The emitted JSON is the *golden* G1 fixture corpus (ADR-0180 / ADR-0196 gate +G1). Changing the canonical form in vault/crdt.py changes these hashes; the +contract tests pin them, so a golden diff is a reviewed, deliberate act. + +Versor components use only exactly-f32-representable, exactly-JSON-representable +values (integers, signed zeros, halves) so the corpus round-trips with zero +float ambiguity. NaN / signed-zero *bit*-distinctness is exercised in-code in +test_crdt_content_ordering.py, not here. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(_REPO_ROOT)) + +from vault.crdt import ( # noqa: E402 # pyright: ignore[reportMissingImports] + VERSOR_COMPONENTS, + ArenaEntry, + Delta, + canonical_bytes, + delta_hash, + merge_kernel, +) + +_OUT = Path(__file__).resolve().parent / "merge_fixtures.json" +# Rust parity fixture lives under core-rs/tests/fixtures/ (a *subdir* of tests/, +# so cargo does not compile it as a standalone integration-test target). +_RUST_OUT = _REPO_ROOT / "core-rs" / "tests" / "fixtures" / "crdt_parity_expected.rs" + + +def versor(**components: float) -> list[float]: + """Build a 32-float versor; unset indices default to 0.0.""" + v = [0.0] * VERSOR_COMPONENTS + for idx, value in components.items(): + v[int(idx.lstrip("c"))] = value + return v + + +def entry(versor_vals: list[float], provenance: bytes) -> ArenaEntry: + return ArenaEntry.of(versor_vals, provenance) + + +def encode_entry(e: ArenaEntry) -> dict: + return {"versor": list(e.versor), "provenance_hex": e.provenance.hex()} + + +def encode_delta_input(entries: list[ArenaEntry]) -> list[dict]: + return [encode_entry(e) for e in entries] + + +def build_cases() -> list[dict]: + cases: list[dict] = [] + + def add(name: str, description: str, delta_entry_lists: list[list[ArenaEntry]]): + deltas = [Delta.from_entries(es) for es in delta_entry_lists] + merged = merge_kernel(deltas) + cases.append( + { + "name": name, + "description": description, + "input_deltas": [encode_delta_input(es) for es in delta_entry_lists], + "expected_merged_entries": [encode_entry(e) for e in merged.entries], + "expected_canonical_bytes_hex": canonical_bytes(merged).hex(), + "expected_delta_hash": delta_hash(merged), + } + ) + + # 1 — single entry, single delta. + add( + "single_entry", + "One delta, one entry: identity of the canonical form.", + [[entry(versor(c0=1.0), b"a")]], + ) + + # 2 — duplicate byte-identical entries collapse (idempotence at entry level). + add( + "dedup_within_delta", + "Byte-identical entries collapse to one (join idempotence).", + [[entry(versor(c0=1.0), b"a"), entry(versor(c0=1.0), b"a")]], + ) + + # 3 — same versor, distinct provenance: BOTH retained (no over-dedup). + add( + "distinct_provenance_retained", + "Same versor under different provenance are distinct content.", + [[entry(versor(c5=2.0), b"left"), entry(versor(c5=2.0), b"right")]], + ) + + # 4 — three deltas merged; corpus pins the canonical (content-sorted) result. + add( + "three_delta_merge", + "Union of three deltas, content-sorted and deduped.", + [ + [entry(versor(c1=3.0), b"z"), entry(versor(c0=1.0), b"a")], + [entry(versor(c2=2.0), b"m"), entry(versor(c0=1.0), b"a")], + [entry(versor(c10=5.0), b"q")], + ], + ) + + # 5 — signed-zero is distinct content (+0.0 bits != -0.0 bits). + add( + "signed_zero_distinct", + "+0.0 and -0.0 have distinct bit patterns -> distinct entries.", + [[entry(versor(c0=0.0), b"p"), entry(versor(c0=-0.0), b"p")]], + ) + + return cases + + +def write_rust_fixtures(cases: list[dict]) -> None: + """Emit the Rust-side expected canonical_bytes hex so core-rs/tests can + assert byte-parity without a JSON parser dependency. Python stays the single + source of truth.""" + lines = [ + "// @generated by tests/fixtures/crdt/_generate.py — DO NOT EDIT.", + "// Expected canonical_bytes hex per case (mirrors merge_fixtures.json):", + "// the Python-canonical Delta-CRDT G1 contract (ADR-0180 / ADR-0196)", + "// the Rust substrate must reproduce byte-for-byte.", + "// (include!d mid-module, so plain // comments — not //! — are required.)", + "", + "pub const EXPECTED_CANONICAL_BYTES_HEX: &[(&str, &str)] = &[", + ] + for case in cases: + lines.append( + f' ("{case["name"]}", "{case["expected_canonical_bytes_hex"]}"),' + ) + lines.append("];") + _RUST_OUT.parent.mkdir(parents=True, exist_ok=True) + _RUST_OUT.write_text("\n".join(lines) + "\n") + + +def main() -> None: + corpus = { + "_comment": ( + "Golden Delta-CRDT fixture corpus (ADR-0180 / ADR-0196 gate G1). " + "Regenerate with: uv run python tests/fixtures/crdt/_generate.py" + ), + "versor_components": VERSOR_COMPONENTS, + "cases": build_cases(), + } + _OUT.write_text(json.dumps(corpus, indent=2, sort_keys=False) + "\n") + write_rust_fixtures(corpus["cases"]) + print(f"wrote {_OUT} ({len(corpus['cases'])} cases)") + print(f"wrote {_RUST_OUT}") + for case in corpus["cases"]: + print(f" {case['name']:30s} hash={case['expected_delta_hash'][:16]}…") + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/crdt/merge_fixtures.json b/tests/fixtures/crdt/merge_fixtures.json new file mode 100644 index 00000000..2adeabba --- /dev/null +++ b/tests/fixtures/crdt/merge_fixtures.json @@ -0,0 +1,884 @@ +{ + "_comment": "Golden Delta-CRDT fixture corpus (ADR-0180 / ADR-0196 gate G1). Regenerate with: uv run python tests/fixtures/crdt/_generate.py", + "versor_components": 32, + "cases": [ + { + "name": "single_entry", + "description": "One delta, one entry: identity of the canonical form.", + "input_deltas": [ + [ + { + "versor": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "61" + } + ] + ], + "expected_merged_entries": [ + { + "versor": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "61" + } + ], + "expected_canonical_bytes_hex": "01000000000000000000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061", + "expected_delta_hash": "e30f709c0b7b4acd6a8706c800fd78cfb6393a6ef4a5410354f8f5f4879b46a8" + }, + { + "name": "dedup_within_delta", + "description": "Byte-identical entries collapse to one (join idempotence).", + "input_deltas": [ + [ + { + "versor": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "61" + }, + { + "versor": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "61" + } + ] + ], + "expected_merged_entries": [ + { + "versor": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "61" + } + ], + "expected_canonical_bytes_hex": "01000000000000000000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061", + "expected_delta_hash": "e30f709c0b7b4acd6a8706c800fd78cfb6393a6ef4a5410354f8f5f4879b46a8" + }, + { + "name": "distinct_provenance_retained", + "description": "Same versor under different provenance are distinct content.", + "input_deltas": [ + [ + { + "versor": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "6c656674" + }, + { + "versor": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "7269676874" + } + ] + ], + "expected_merged_entries": [ + { + "versor": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "6c656674" + }, + { + "versor": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "7269676874" + } + ], + "expected_canonical_bytes_hex": "0200000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000006c656674000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000007269676874", + "expected_delta_hash": "e58dff3847bdbc0944a7d293ba53ba4b6ea38981795f221717186c87db40b777" + }, + { + "name": "three_delta_merge", + "description": "Union of three deltas, content-sorted and deduped.", + "input_deltas": [ + [ + { + "versor": [ + 0.0, + 3.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "7a" + }, + { + "versor": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "61" + } + ], + [ + { + "versor": [ + 0.0, + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "6d" + }, + { + "versor": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "61" + } + ], + [ + { + "versor": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 5.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "71" + } + ] + ], + "expected_merged_entries": [ + { + "versor": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 5.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "71" + }, + { + "versor": [ + 0.0, + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "6d" + }, + { + "versor": [ + 0.0, + 3.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "7a" + }, + { + "versor": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "61" + } + ], + "expected_canonical_bytes_hex": "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000071000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000006d000000000000404000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000007a0000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061", + "expected_delta_hash": "82894cfb55917b33fd474f697aafbcacf86188bd83addeae999377695a19d81b" + }, + { + "name": "signed_zero_distinct", + "description": "+0.0 and -0.0 have distinct bit patterns -> distinct entries.", + "input_deltas": [ + [ + { + "versor": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "70" + }, + { + "versor": [ + -0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "70" + } + ] + ], + "expected_merged_entries": [ + { + "versor": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "70" + }, + { + "versor": [ + -0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "provenance_hex": "70" + } + ], + "expected_canonical_bytes_hex": "020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000700000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000070", + "expected_delta_hash": "a00e19c6a027b55a5f05e2a4f4b0c5fe82291223d3ebf6dfb639228b9f6bf553" + } + ] +} diff --git a/tests/test_crdt_content_ordering.py b/tests/test_crdt_content_ordering.py new file mode 100644 index 00000000..682a7c73 --- /dev/null +++ b/tests/test_crdt_content_ordering.py @@ -0,0 +1,89 @@ +"""ADR-0180 / ADR-0196 gate G1 (slice ZC-0) — content-addressed ordering. + +The Delta-CRDT canonical order is by *content* — the IEEE-754 bit pattern of +the 32 versor components, then the provenance bytes — never by arrival order +(ADR-0180 §2.2 content-addressed-tiebreak amendment). These tests pin that the +``vault/crdt.py`` reference is byte/bit-addressed, matching the Rust +``content_cmp`` in ``core-rs/src/vault.rs`` (per-component ``f32::to_bits``, +then ``Vec`` provenance compare). +""" + +from __future__ import annotations + +import struct + +from vault.crdt import VERSOR_COMPONENTS, ArenaEntry, Delta + + +def _v(idx: int, val: float) -> list[float]: + v = [0.0] * VERSOR_COMPONENTS + v[idx] = val + return v + + +def _e(idx: int, val: float, prov: bytes) -> ArenaEntry: + return ArenaEntry.of(_v(idx, val), prov) + + +def _f32_from_bits(bits: int) -> float: + return struct.unpack(" distinct content. + assert pos._content_key() != neg._content_key() + assert len(Delta.from_entries([pos, neg])) == 2 + + +def test_nan_is_retained_and_bit_addressed(): + # Two NaNs that differ only in payload bits are distinct content. The + # reference is bit-addressed (struct ' list[float]: + v = [0.0] * _DIM + v[idx] = val + return v + + +def test_arena_push_does_not_write_global_vault(): + store = VaultStore() + assert store.store_count == 0 + + arena = LocalArena() + for i in range(5): + arena.push(_v(i, float(i + 1)), f"p{i}".encode()) + snapshot = arena.snapshot() + merge_kernel([snapshot, arena.snapshot()]) + + # Nothing was published: the Vault is untouched by arena accumulation. + assert store.store_count == 0 + assert len(snapshot) == 5 + + +def test_crdt_module_has_no_global_vault_coupling(): + src = inspect.getsource(crdt) + for forbidden in ("VaultStore", "vault.store", ".store(", "vault_recall", ".recall("): + assert forbidden not in src, ( + f"vault.crdt must not couple to the global Vault; found {forbidden!r}" + ) + + +def test_snapshot_does_not_drain_arena(): + arena = LocalArena() + arena.push(_v(0, 1.0), b"a") + arena.push(_v(1, 2.0), b"b") + before = len(arena) + first = arena.snapshot() + second = arena.snapshot() + assert len(arena) == before == 2 + assert first.entries == second.entries + + +def test_merge_and_from_entries_do_not_mutate_inputs(): + entries = [ArenaEntry.of(_v(0, 1.0), b"a"), ArenaEntry.of(_v(1, 2.0), b"b")] + entries_snapshot = list(entries) + Delta.from_entries(entries) + assert entries == entries_snapshot # from_entries left the input list intact + + delta = Delta.from_entries(entries) + deltas = [delta, delta] + deltas_snapshot = list(deltas) + merge_kernel(deltas) + assert deltas == deltas_snapshot # merge_kernel left the input list intact diff --git a/tests/test_crdt_semilattice_contract.py b/tests/test_crdt_semilattice_contract.py new file mode 100644 index 00000000..6a242312 --- /dev/null +++ b/tests/test_crdt_semilattice_contract.py @@ -0,0 +1,136 @@ +"""ADR-0180 / ADR-0196 gate G1 (slice ZC-0) — Delta-CRDT semilattice contract. + +These are *failable* proof obligations (CLAUDE.md §Schema-Defined Proof +Obligations) for the canonical reference in ``vault/crdt.py``: + + C-1 commutativity join(a, b) == join(b, a) + C-2 associativity (a∘b)∘c == a∘(b∘c) + C-3 idempotence join(a, a) == a + C-4 permutation merge_kernel(perm(deltas)) is order-invariant + C-5 duplicate no-op merge(existing, already_seen) == existing + kernel == fold merge_kernel == reduce(join, …) (the cheap path can + never silently diverge from the semilattice fold) + +plus a golden fixture corpus that regression-locks the canonical byte layout +and merge hash. Each test fails loudly if ``join``/``merge_kernel`` ever orders +by arrival, stops deduplicating, or the canonical serialization drifts. +""" + +from __future__ import annotations + +import functools +import itertools +import json +from pathlib import Path + +import pytest + +from vault.crdt import ( + VERSOR_COMPONENTS, + ArenaEntry, + Delta, + canonical_bytes, + delta_hash, + merge_kernel, +) + +_FIXTURES = Path(__file__).parent / "fixtures" / "crdt" / "merge_fixtures.json" + + +def _v(idx: int, val: float) -> list[float]: + v = [0.0] * VERSOR_COMPONENTS + v[idx] = val + return v + + +def _e(idx: int, val: float, prov: bytes) -> ArenaEntry: + return ArenaEntry.of(_v(idx, val), prov) + + +def _decode_entry(d: dict) -> ArenaEntry: + return ArenaEntry.of(d["versor"], bytes.fromhex(d["provenance_hex"])) + + +def _decode_delta(entry_dicts: list[dict]) -> Delta: + return Delta.from_entries(_decode_entry(e) for e in entry_dicts) + + +# --- the three join-semilattice legs (ADR-0180 §2.2) ----------------------- + + +def test_c1_join_is_commutative(): + a = Delta.from_entries([_e(0, 1.0, b"a")]) + b = Delta.from_entries([_e(1, 2.0, b"b")]) + # Fails if join carried arrival order: a-first vs b-first would differ. + assert a.join(b).entries == b.join(a).entries + assert len(a.join(b)) == 2 # distinct content — not collapsed + + +def test_c2_join_is_associative(): + a = Delta.from_entries([_e(0, 1.0, b"a")]) + b = Delta.from_entries([_e(1, 2.0, b"b")]) + c = Delta.from_entries([_e(2, 3.0, b"c")]) + assert a.join(b).join(c).entries == a.join(b.join(c)).entries + + +def test_c3_join_is_idempotent(): + a = Delta.from_entries([_e(0, 1.0, b"a"), _e(1, 2.0, b"b")]) + # a ∘ a == a — fails if dedup is removed (length would double). + assert a.join(a).entries == a.entries + assert len(a.join(a)) == 2 + + +# --- merge kernel (the load-bearing hash(Sequential) == hash(Concurrent)) --- + + +def test_c4_merge_kernel_is_permutation_invariant(): + d0 = Delta.from_entries([_e(0, 1.0, b"a"), _e(1, 9.0, b"z")]) + d1 = Delta.from_entries([_e(2, 2.0, b"b")]) + d2 = Delta.from_entries([_e(3, 3.0, b"c")]) + reference = merge_kernel([d0, d1, d2]) + ref_hash = delta_hash(reference) + for perm in itertools.permutations([d0, d1, d2]): + merged = merge_kernel(list(perm)) + assert merged.entries == reference.entries + assert delta_hash(merged) == ref_hash + + +def test_c5_duplicate_delta_is_noop(): + d = Delta.from_entries([_e(0, 1.0, b"a"), _e(1, 2.0, b"b")]) + assert merge_kernel([d]).entries == merge_kernel([d, d]).entries + assert delta_hash(merge_kernel([d])) == delta_hash(merge_kernel([d, d, d])) + + +def test_merge_kernel_equals_semilattice_fold(): + deltas = [ + Delta.from_entries([_e(0, 1.0, b"a")]), + Delta.from_entries([_e(1, 2.0, b"b"), _e(0, 1.0, b"a")]), + Delta.from_entries([_e(2, 3.0, b"c")]), + ] + folded = functools.reduce(lambda acc, d: acc.join(d), deltas, Delta()) + assert merge_kernel(deltas).entries == folded.entries + + +# --- golden corpus: regression-lock the canonical form + merge hash --------- + + +def _load_cases() -> list[dict]: + corpus = json.loads(_FIXTURES.read_text()) + assert corpus["versor_components"] == VERSOR_COMPONENTS + return corpus["cases"] + + +@pytest.mark.parametrize("case", _load_cases(), ids=lambda c: c["name"]) +def test_golden_fixture_canonical_form_and_hash(case: dict): + deltas = [_decode_delta(ed) for ed in case["input_deltas"]] + merged = merge_kernel(deltas) + + expected_entries = [_decode_entry(e) for e in case["expected_merged_entries"]] + assert list(merged.entries) == expected_entries + assert canonical_bytes(merged).hex() == case["expected_canonical_bytes_hex"] + assert delta_hash(merged) == case["expected_delta_hash"] + + # Permutation invariance on the real corpus: any arrival order folds to the + # same pinned hash. + for perm in itertools.permutations(deltas): + assert delta_hash(merge_kernel(list(perm))) == case["expected_delta_hash"] diff --git a/vault/crdt.py b/vault/crdt.py new file mode 100644 index 00000000..c4e484d4 --- /dev/null +++ b/vault/crdt.py @@ -0,0 +1,204 @@ +"""vault/crdt.py — canonical Delta-CRDT reference contract (ADR-0180, gate G1). + +This module is the **locked reference contract** for CORE's Delta-CRDT write +substrate. It is the Python-canonical mirror of the Rust incumbent in +``core-rs/src/vault.rs`` (§2.1/§2.2) and the contract any future native backend +(Rust binding, or a Zig prototype under ADR-0196 gate G2) must reproduce +bit-for-bit. See ``docs/zig/crdt-substrate/`` (slice ZC-0) and +``docs/zig/adoption-gates.md`` (G1, G3). + +What this is +------------ +A *thread-local, share-nothing* write cache (``LocalArena``) accumulates +content-addressed writes; a canonical ``Delta`` is the sorted, deduplicated +snapshot; the ``merge_kernel`` folds many deltas into one order-invariant +``Delta``. The join is a semilattice (commutative, associative, idempotent), +so the merged state — and its ``delta_hash`` — cannot depend on the order +deltas arrived in. That is exactly the property ADR-0180 §4.3's +``hash(Sequential_Ingest) == hash(Concurrent_CRDT_Ingest)`` rides on. + +Content law, not cognition +-------------------------- +Ordering is by **content** — the IEEE-754 bit pattern of the 32 versor +components, then the provenance bytes — never by arrival order. This module +performs **no** normalization, no versor closure/repair, no field mutation, and +no global Vault writes (CLAUDE.md §Normalization Rules / §Core Primitives). A +``-0.0`` and a ``+0.0`` have distinct bits and are therefore distinct content, +as a byte-addressed merge requires. + +Canonical serialization (the cross-language contract) +----------------------------------------------------- +``canonical_bytes(delta)`` is the load-bearing artifact; ``delta_hash`` is just +its SHA-256. Layout (all little-endian):: + + u64 entry_count + for each entry in canonical order: + 32 x f32 versor components (IEEE-754, little-endian, 4 bytes each) + u64 provenance_length + bytes provenance + +Each entry is self-delimiting (fixed 128-byte versor + length-prefixed +provenance), so the byte stream is unambiguous. +""" + +from __future__ import annotations + +import hashlib +import struct +from collections.abc import Iterable, Sequence +from dataclasses import dataclass + +__all__ = [ + "VERSOR_COMPONENTS", + "ArenaEntry", + "Delta", + "LocalArena", + "merge_kernel", + "canonical_bytes", + "delta_hash", +] + +# A Cl(4,1) multivector has 2**5 = 32 components (ADR-0180 §2.1; matches the +# ``[f32; 32]`` arena entry in core-rs/src/vault.rs). +VERSOR_COMPONENTS = 32 + + +def _to_f32(x: float) -> float: + """Round a Python float (f64) to the f32 value the Rust ``[f32; 32]`` would + hold. Storing the f32-coerced value makes content identity purely + f32-based, so two inputs that map to the same f32 bits are the same + content — exactly as the Rust substrate sees them.""" + return struct.unpack(" int: + """The unsigned 32-bit IEEE-754 bit pattern of an (already f32-coerced) + component. Mirrors Rust ``f32::to_bits`` used by ``content_cmp``.""" + return struct.unpack(" "ArenaEntry": + components = tuple(versor) + if len(components) != VERSOR_COMPONENTS: + raise ValueError( + f"ArenaEntry versor must have {VERSOR_COMPONENTS} components; " + f"got {len(components)}" + ) + return cls( + versor=tuple(_to_f32(c) for c in components), + provenance=bytes(provenance), + ) + + def _content_key(self) -> tuple[tuple[int, ...], bytes]: + """Total, arrival-independent content order key. + + Compares the 32 components by their IEEE-754 bit patterns (per-component + unsigned-int comparison, matching Rust ``content_cmp``), with the + provenance bytes as the final tiebreak. Python compares the bits tuple + element-wise then the bytes lexicographically — byte-for-byte the Rust + ordering. + """ + return (tuple(_component_bits(c) for c in self.versor), self.provenance) + + +@dataclass(frozen=True) +class Delta: + """A canonical snapshot of newly-ingested entries (ADR-0180 §2.2): always + held in content-addressed order with byte-identical duplicates removed, so + it is a canonical join-semilattice element regardless of insertion order.""" + + entries: tuple[ArenaEntry, ...] = () + + @classmethod + def from_entries(cls, entries: Iterable[ArenaEntry]) -> "Delta": + """Canonicalise an arbitrary entry list: sort by content, drop + byte-identical duplicates. Does not mutate the input.""" + ordered = sorted(entries, key=ArenaEntry._content_key) + deduped: list[ArenaEntry] = [] + last_key: tuple[tuple[int, ...], bytes] | None = None + for entry in ordered: + key = entry._content_key() + if key != last_key: + deduped.append(entry) + last_key = key + return cls(entries=tuple(deduped)) + + def join(self, other: "Delta") -> "Delta": + """Semilattice join: the canonical union of two deltas. Commutative, + associative, idempotent (ADR-0180 §2.2).""" + return Delta.from_entries((*self.entries, *other.entries)) + + def __len__(self) -> int: + return len(self.entries) + + def is_empty(self) -> bool: + return not self.entries + + +class LocalArena: + """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; it does not drain the + arena (flush/GC is the kernel's concern).""" + + def __init__(self) -> None: + self._entries: list[ArenaEntry] = [] + + def push(self, versor: Sequence[float], provenance: bytes = b"") -> None: + """Lock-free local write. Push order is irrelevant: ``snapshot`` + canonicalises into content-addressed order.""" + self._entries.append(ArenaEntry.of(versor, provenance)) + + def __len__(self) -> int: + return len(self._entries) + + def is_empty(self) -> bool: + return not self._entries + + def snapshot(self) -> Delta: + return Delta.from_entries(tuple(self._entries)) + + +def merge_kernel(deltas: Sequence[Delta]) -> Delta: + """Fold a batch of deltas into one content-addressed, deduplicated, totally + ordered ``Delta`` (ADR-0180 §2.2). Invariant under any permutation of + ``deltas`` and under duplicate deltas — the property §4.3's + ``hash(Sequential) == hash(Concurrent)`` rides on.""" + union: list[ArenaEntry] = [] + for delta in deltas: + union.extend(delta.entries) + return Delta.from_entries(union) + + +def canonical_bytes(delta: Delta) -> bytes: + """The canonical little-endian serialization of a ``Delta`` — the + cross-language contract a Rust/Zig backend must reproduce byte-for-byte. + See the module docstring for the layout.""" + out = bytearray() + out += struct.pack(" str: + """SHA-256 (hex) of ``canonical_bytes(delta)`` — the replay-stable merge + key. Equal for any permutation of the deltas that produced ``delta``.""" + return hashlib.sha256(canonical_bytes(delta)).hexdigest()