# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-provenance-complete-deletion-in-agent-memory-stores-when-retention-tombstones-outlive-their-source-records.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-provenance-complete-deletion-in-agent-memory-stores-when-retention-tombstones-outlive-their-source-records.warrant.json
# Markdown SHA-256:   a0293309f662a8a8804c93a12f21eca3043c3d04021aa7cb7cf50d667077b8ab
# Code AST SHA-256:   0c97dfdda3b247a9c61ced229fc5ff4aa7233cb6b4fecf096a0d5eb4a575b3d5
# Verified Classes:   ProvenanceClosureError, Record, Tombstone, MemoryStore
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass, field


class ProvenanceClosureError(RuntimeError):
    """Raised when a forget would prune inbound provenance edges without
    materializing their closure into the tombstone, breaking the invariant
    that every tombstone remains reachable from any live or tombstoned
    derived record via an unlearning traversal."""


@dataclass
class Record:
    rid: str
    content_hash: str
    provenance: list[str] = field(default_factory=list)  # inbound parent ids
    tombstoned: bool = False


@dataclass
class Tombstone:
    content_hash: str
    closure: set[str] = field(default_factory=set)  # parent content hashes


class MemoryStore:
    def __init__(self) -> None:
        self.records: dict[str, Record] = {}
        self.tombstones: dict[str, Tombstone] = {}

    def write(self, rid: str, content_hash: str, provenance: list[str] | None = None) -> None:
        self.records[rid] = Record(rid, content_hash, list(provenance or []))

    def _inbound(self, rid: str) -> list[Record]:
        return [r for r in self.records.values() if rid in r.provenance]

    def forget(self, rid: str, mode: str = "hard") -> None:
        rec = self.records.get(rid)
        if rec is None:
            raise ProvenanceClosureError(f"forget({rid!r}): no such record")

        inbound = self._inbound(rid)

        # The invariant: before pruning any inbound edge, the closure of the
        # forgotten content must be materialized into its tombstone, otherwise
        # a later unlearning traversal from a derived record cannot reach it.
        tomb = self.tombstones.setdefault(rec.content_hash, Tombstone(rec.content_hash))
        for child in inbound:
            if child.content_hash not in tomb.closure:
                tomb.closure.add(child.content_hash)

        if mode == "hard":
            # Only now is it safe to drop the record and prune inbound edges.
            for child in inbound:
                child.provenance = [p for p in child.provenance if p != rid]
            del self.records[rid]
        else:
            rec.tombstoned = True

        # Post-condition guard: every inbound edge we are about to sever must
        # already be represented in the tombstone closure.
        for child in inbound:
            if child.content_hash not in tomb.closure:
                raise ProvenanceClosureError(
                    f"forget({rid!r}): child {child.rid!r} would lose closure"
                )

    def unlearn_derivations(self, content_hash: str) -> set[str]:
        tomb = self.tombstones.get(content_hash)
        if tomb is None:
            return set()
        return set(tomb.closure)


def assert_unlearning_monotone(store: MemoryStore, rid: str) -> None:
    """The derivation set for a forgotten content must not shrink across a
    forget. A shrink is the signature of a pruned-edge closure break."""
    before = store.unlearn_derivations(store.records[rid].content_hash)
    store.forget(rid, mode="hard")
    after = store.unlearn_derivations(store.records.get(rid, Record(rid, "")).content_hash)
    if not before.issubset(after):
        raise ProvenanceClosureError(
            f"unlearning set shrank for {rid!r}: {before} -> {after}"
        )
