# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-forget-before-it-can-read-the-retention-scope-divergence-at-the-read-decay-boundary.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-forget-before-it-can-read-the-retention-scope-divergence-at-the-read-decay-boundary.warrant.json
# Markdown SHA-256:   c4b66e0485301852a4978d54f7d06f36e6a74671f120affd844808f100c2aca2
# Code AST SHA-256:   03b6a009dfec87901e588826b622cceca8ef3b01c2a6170aa574abda7167521e
# Verified Classes:   ResurrectionError, Tombstone, MemoryStore
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Optional


class ResurrectionError(RuntimeError):
    """Raised when a write would resurrect a key suppressed by a broader-scope tombstone."""
    pass


@dataclass
class Tombstone:
    key: str
    scope: str
    at: float


@dataclass
class MemoryStore:
    # scope -> key -> (value, owner_scope, written_at)
    data: dict = field(default_factory=dict)
    tombstones: list = field(default_factory=list)
    scope_parents: dict = field(default_factory=dict)  # child -> parent

    def _ancestors(self, scope: str):
        seen, cur = [], scope
        while cur is not None:
            seen.append(cur)
            cur = self.scope_parents.get(cur)
        return seen

    def _suppressing_tombstone(self, key: str, scope: str) -> Optional[Tombstone]:
        # Suppression inherits DOWNWARD: a tombstone at any ancestor scope
        # (or the scope itself) dominates reads/writes at this scope.
        for anc in self._ancestors(scope):
            for t in self.tombstones:
                if t.key == key and t.scope == anc:
                    return t
        return None

    def read(self, key: str, scope: str, now: float, decay_half_life: float) -> dict:
        tomb = self._suppressing_tombstone(key, scope)
        if tomb is not None:
            return {"state": "suppressed", "suppressed_by": tomb.scope}
        rec = self.data.get(scope, {}).get(key)
        if rec is None:
            return {"state": "absent"}
        value, owner, written_at = rec
        # Decay evaluated against the OWNING scope's clock, not the reader's.
        age = now - written_at
        if age > decay_half_life:
            return {"state": "decayed", "owner": owner}
        return {"state": "live", "value": value, "owner": owner}

    def write(self, key: str, value, scope: str, now: float) -> None:
        tomb = self._suppressing_tombstone(key, scope)
        if tomb is not None:
            # A write on a suppressed key is a resurrection attempt.
            raise ResurrectionError(
                f"key {key!r} suppressed by tombstone at scope {tomb.scope!r}; "
                f"write at scope {scope!r} rejected"
            )
        self.data.setdefault(scope, {})[key] = (value, scope, now)

    def forget(self, key: str, scope: str, now: float) -> None:
        # Forget is a scoped tombstone, never a row delete; audit stays intact.
        self.tombstones.append(Tombstone(key=key, scope=scope, at=now))


# Demonstration of the boundary failure this record documents.
store = MemoryStore(scope_parents={"session": "agent", "agent": "tenant"})
store.write("user_secret", "s3cr3t", scope="tenant", now=0.0)
store.forget("user_secret", scope="agent", now=1.0)

r = store.read("user_secret", scope="session", now=2.0, decay_half_life=1e9)
assert r["state"] == "suppressed" and r["suppressed_by"] == "agent"

try:
    store.write("user_secret", "s3cr3t", scope="session", now=3.0)
except ResurrectionError as e:
    print("blocked:", e)
