Merge pull request #511 from AssetOverflow/feat/adr-0180-crdt-contract-lock
This commit is contained in:
commit
77feadaab1
12 changed files with 1914 additions and 4 deletions
|
|
@ -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).
|
||||
|
|
|
|||
13
core-rs/tests/fixtures/crdt_parity_expected.rs
vendored
Normal file
13
core-rs/tests/fixtures/crdt_parity_expected.rs
vendored
Normal 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"),
|
||||
];
|
||||
105
core-rs/tests/test_crdt_hash_parity.rs
Normal file
105
core-rs/tests/test_crdt_hash_parity.rs
Normal 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"));
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
159
docs/zig/STATUS.md
Normal 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) (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
|
||||
157
tests/fixtures/crdt/_generate.py
vendored
Normal file
157
tests/fixtures/crdt/_generate.py
vendored
Normal 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
884
tests/fixtures/crdt/merge_fixtures.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
89
tests/test_crdt_content_ordering.py
Normal file
89
tests/test_crdt_content_ordering.py
Normal 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)
|
||||
71
tests/test_crdt_no_global_write_from_arena.py
Normal file
71
tests/test_crdt_no_global_write_from_arena.py
Normal 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
|
||||
136
tests/test_crdt_semilattice_contract.py
Normal file
136
tests/test_crdt_semilattice_contract.py
Normal 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 (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"]
|
||||
204
vault/crdt.py
Normal file
204
vault/crdt.py
Normal 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()
|
||||
Loading…
Reference in a new issue