core/vault/crdt.py
Shay 92ea9ee6f5 feat(vault): lock Delta-CRDT reference contract (ADR-0180 -> Accepted, gate G1)
Establishes the canonical Delta-CRDT reference contract so a future native
(Rust/Zig) backend is gate-G1-eligible under ADR-0196 — the ZC-0 'contract
pinning' slice. No Zig code; ZC-1+ remains gated at G2.

- vault/crdt.py: canonical Python reference (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.
- ZC-0 contract tests (semilattice C-1..C-5; content ordering / signed-zero /
  NaN bit-addressing; C-7 no-global-write) — all failable (mutation-checked:
  no-dedup breaks C3/C5, arrival-order breaks C1).
- Golden fixture corpus (tests/fixtures/crdt/) regenerated deterministically
  from the reference; single source of truth also emits the Rust expected hex.
- core-rs: Delta::canonical_bytes + test_crdt_hash_parity.rs proving Rust
  produces byte-identical canonical_bytes to the Python reference.
- ADR-0180 -> Accepted: locked contract, byte layout, obligation map, and the
  explicit boundary that no Zig is authorized.

Verification: ZC-0 21 passed, Rust arena+parity 16 passed, architectural
invariants 40 passed, smoke 67 passed. Serving frozen: 7/8 lane SHAs match;
the public_demo miss is a pre-existing wall-clock budget overrun (ADR-0099,
~46-48s > 30s) reproduced identically on clean main — environmental.
2026-05-31 16:25:21 -07:00

204 lines
7.9 KiB
Python

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