fix(vault): isolate DeltaStore frontier from caller aliasing (ADR-0026.1)
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).
This commit is contained in:
parent
84eb4f8ecf
commit
629bc16382
2 changed files with 86 additions and 4 deletions
74
tests/test_delta_store_frontier_isolation.py
Normal file
74
tests/test_delta_store_frontier_isolation.py
Normal file
|
|
@ -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"
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue