Merge pull request #511 from AssetOverflow/feat/adr-0180-crdt-contract-lock

This commit is contained in:
Shay 2026-05-31 20:30:26 -07:00 committed by GitHub
commit 77feadaab1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1914 additions and 4 deletions

View file

@ -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<u8> {
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).

View file

@ -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"),
];

View file

@ -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<ArenaEntry>) -> 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<Delta>) {
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"));
}

View file

@ -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.

View file

@ -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) |

159
docs/zig/STATUS.md Normal file
View file

@ -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) (G0G8) → 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 **G0G8**. 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 (~4648s >
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

157
tests/fixtures/crdt/_generate.py vendored Normal file
View file

@ -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()

884
tests/fixtures/crdt/merge_fixtures.json vendored Normal file
View file

@ -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"
}
]
}

View file

@ -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<u8>`` 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("<f", struct.pack("<I", bits))[0]
def test_canonical_order_independent_of_insertion_order():
e_a = _e(0, 1.0, b"a")
e_b = _e(5, 2.0, b"b")
e_c = _e(2, 3.0, b"c")
d1 = Delta.from_entries([e_a, e_b, e_c])
d2 = Delta.from_entries([e_c, e_a, e_b])
d3 = Delta.from_entries([e_b, e_c, e_a])
assert d1.entries == d2.entries == d3.entries
# The canonical order is exactly the content-key sort order.
assert list(d1.entries) == sorted([e_a, e_b, e_c], key=ArenaEntry._content_key)
def test_distinct_provenance_is_retained():
versor = _v(5, 2.0)
d = Delta.from_entries(
[ArenaEntry.of(versor, b"left"), ArenaEntry.of(versor, b"right")]
)
# Fails if dedup keys on the versor alone (which would drop state).
assert len(d) == 2
def test_byte_identical_entries_collapse():
d = Delta.from_entries([_e(0, 1.0, b"x"), _e(0, 1.0, b"x")])
assert len(d) == 1
def test_signed_zero_is_distinct_content():
pos = ArenaEntry.of(_v(0, 0.0), b"p")
neg = ArenaEntry.of(_v(0, -0.0), b"p")
# +0.0 bits (0x00000000) != -0.0 bits (0x80000000) -> 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 '<f' round-trips the f32 payload,
# matching numpy's f32 cast and Rust's to_bits on all supported
# little-endian targets).
nan1 = _f32_from_bits(0x7FC00000)
nan2 = _f32_from_bits(0x7FC00001)
e1 = ArenaEntry.of(_v(0, nan1), b"n")
e2 = ArenaEntry.of(_v(0, nan2), b"n")
assert e1._content_key() != e2._content_key()
assert len(Delta.from_entries([e1, e2])) == 2
# A NaN is retained as content — never dropped or normalized to a number.
assert len(Delta.from_entries([ArenaEntry.of(_v(3, nan1), b"")])) == 1
def test_content_order_follows_component_bit_order():
# Component 0 sorts by its u32 bit pattern: 1.0 (0x3f800000) < 2.0
# (0x40000000); a negative value's sign bit makes it sort *after* positives
# under unsigned bit comparison (matching Rust to_bits ordering).
lo = _e(0, 1.0, b"")
hi = _e(0, 2.0, b"")
neg = _e(0, -1.0, b"") # 0xbf800000 — high bit set, sorts last
ordered = Delta.from_entries([hi, neg, lo]).entries
assert ordered == (lo, hi, neg)

View file

@ -0,0 +1,71 @@
"""ADR-0180 / ADR-0196 gate G1 (slice ZC-0) — C-7: arena writes are local.
A ``LocalArena`` is a thread-local, share-nothing write cache. Pushing to it
must never mutate the global Vault; only an explicit merge *publication* (out of
scope for the reference contract ADR-0180 §1.5.5) may alter Vault-visible
state. These tests fail loudly if the CRDT reference ever grows a global-write
side effect, and pin the purity/immutability of snapshot and merge.
"""
from __future__ import annotations
import inspect
import vault.crdt as crdt
from vault.crdt import ArenaEntry, Delta, LocalArena, merge_kernel
from vault.store import VaultStore
_DIM = crdt.VERSOR_COMPONENTS
def _v(idx: int, val: float) -> 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

View file

@ -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 (ab)c == a(bc)
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"]

204
vault/crdt.py Normal file
View file

@ -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("<f", struct.pack("<f", float(x)))[0]
def _component_bits(x: float) -> 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("<I", struct.pack("<f", x))[0]
@dataclass(frozen=True)
class ArenaEntry:
"""One content-addressed write: a Cl(4,1) versor plus opaque provenance.
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, ADR-0180 §2.2).
"""
versor: tuple[float, ...]
provenance: bytes
@classmethod
def of(cls, versor: Sequence[float], provenance: bytes = b"") -> "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("<Q", len(delta.entries))
for entry in delta.entries:
for component in entry.versor:
out += struct.pack("<f", component)
out += struct.pack("<Q", len(entry.provenance))
out += entry.provenance
return bytes(out)
def delta_hash(delta: Delta) -> 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()