From 629bc16382b66049544d6e0f048d950f142aaa69 Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 11 Jul 2026 02:07:53 -0700 Subject: [PATCH] fix(vault): isolate DeltaStore frontier from caller aliasing (ADR-0026.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeltaStore.frontier returned the live internal set. A delta built with parents=store.frontier aliased it, so insert's frontier maintenance (add + difference_update) emptied the frontier on every insert and rewrote every stored delta's parents to {} — the CRDT causal chain degenerated into all-roots. Sopher's bridge now copies on its side (sopher ADR-0026.1 §2.1); this is the recommended CORE-side defense in depth: frontier hands out snapshots, and insert copies delta.parents into the event envelope. Regression-tested with the exact aliasing pattern (3-delta chain keeps one head and stable per-delta parents). --- tests/test_delta_store_frontier_isolation.py | 74 ++++++++++++++++++++ vault/delta_store.py | 16 +++-- 2 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 tests/test_delta_store_frontier_isolation.py diff --git a/tests/test_delta_store_frontier_isolation.py b/tests/test_delta_store_frontier_isolation.py new file mode 100644 index 00000000..5615c427 --- /dev/null +++ b/tests/test_delta_store_frontier_isolation.py @@ -0,0 +1,74 @@ +"""DeltaStore frontier isolation (ADR-0026.1 §2.1). + +Regression: ``DeltaStore.frontier`` used to return the live internal set. +A delta built with ``parents=store.frontier`` then aliased that set, so +``insert``'s frontier maintenance (``add`` + ``difference_update``) emptied +the frontier on every insert and rewrote every stored delta's parents to +``{}`` — the CRDT causal chain degenerated into all-roots. Both boundaries +must copy: ``frontier`` hands out snapshots, ``insert`` copies the delta's +parents into the event envelope. +""" + +from __future__ import annotations + +from core.abi import GeometricDelta +from core.epistemic_state import EpistemicState +from vault.delta_store import DeltaStore + + +def _delta(delta_id: str, parents: set[str]) -> GeometricDelta: + versor = [0.0] * 32 + versor[0] = 1.0 + return GeometricDelta( + id=delta_id, + parents=parents, + modality="lh_text", + compiler_id="test_compiler_v1", + semantic={"type": "text_token", "token": 1}, + amr_scope={"level": 0, "region_id": "root", "time_window": [0.0, 0.0]}, + delta_versor=versor, + inverse_ref=None, + provenance={"source": "test", "time": 0.0, "adr_refs": ["ADR-0026"], "hash": delta_id}, + epistemic=EpistemicState.UNVERIFIED_POSSIBLE, + ) + + +def test_frontier_returns_snapshot_not_live_set() -> None: + store = DeltaStore() + snapshot = store.frontier + assert store.insert(_delta("sha256:d1", set()), author="test") + assert snapshot == set(), "pre-insert snapshot must not see later mutations" + assert store.frontier == {"sha256:d1"} + + +def test_insert_from_live_frontier_keeps_causal_chain() -> None: + """The exact aliasing pattern that used to sever the chain: build each + delta's parents directly from ``store.frontier`` and insert in sequence.""" + store = DeltaStore() + ids = ["sha256:d1", "sha256:d2", "sha256:d3"] + deltas = [] + for delta_id in ids: + delta = _delta(delta_id, store.frontier) + deltas.append(delta) + assert store.insert(delta, author="test") + + assert store.frontier == {"sha256:d3"}, "a linear history has exactly one head" + assert deltas[0].parents == set() + assert deltas[1].parents == {"sha256:d1"}, "delta parents must survive insert" + assert deltas[2].parents == {"sha256:d2"} + + event = store.get_event("sha256:d2") + assert event is not None + assert event.parents == {"sha256:d1"} + + +def test_insert_copies_delta_parents_into_event() -> None: + store = DeltaStore() + parents: set[str] = set() + delta = _delta("sha256:root", parents) + assert store.insert(delta, author="test") + + event = store.get_event("sha256:root") + assert event is not None + parents.add("sha256:injected-later") + assert event.parents == set(), "event parents must be isolated from caller mutation" diff --git a/vault/delta_store.py b/vault/delta_store.py index 7efbae96..6e12ca2f 100644 --- a/vault/delta_store.py +++ b/vault/delta_store.py @@ -47,10 +47,13 @@ class DeltaStore: print(f"[REJECT-AND-RETAIN] Delta {delta.id} rejected: {reason}") return False - # Create the CRDT event envelope + # Create the CRDT event envelope. Copy parents: the caller may have + # built the delta from a live frontier set, and the frontier update + # below must never write through into the delta's causal links + # (ADR-0026.1 §2.1). event = DeltaCRDTEvent( id=delta.id, - parents=delta.parents, + parents=set(delta.parents), delta=delta, author=author ) @@ -74,5 +77,10 @@ class DeltaStore: @property def frontier(self) -> Set[str]: - """Get the current causal frontier.""" - return self._frontier + """Get a snapshot of the current causal frontier. + + Returns a copy: handing out the live set would let ``insert``'s own + frontier maintenance mutate any delta built from it, emptying the + frontier and severing every stored delta's parents (ADR-0026.1 §2.1). + """ + return set(self._frontier)