# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-of-a-scoped-memory-tool-silently-resurrect-forgotten-data-when-a-ttl-expires-during-an-in-flight-write.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-of-a-scoped-memory-tool-silently-resurrect-forgotten-data-when-a-ttl-expires-during-an-in-flight-write.warrant.json
# Markdown SHA-256:   e33619193d46414278802554ddd8234fa0d24bd82a4a0dc9957287a4c4a6f947
# Code AST SHA-256:   1b1731c93d55009b7ff5edb2ea5f553f38118bfafee10fe778a928032db4882f
# Verified Classes:   MemoryResurrectionError, MemoryExpiredError, MemoryStore
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class MemoryResurrectionError(RuntimeError):
    """Raised when a write would resurrect a forgotten (tombstoned) identity."""
    pass


class MemoryExpiredError(RuntimeError):
    """Raised when a write targets an entry that expired mid-flight (retryable)."""
    pass


class MemoryStore:
    def __init__(self):
        # key -> {"value": ..., "expires_at": float|None, "tombstone_gen": int}
        self._entries = {}
        # key -> tombstone generation (monotonic, terminal)
        self._tombstones = {}
        self._tombstone_seq = 0

    def forget(self, key: str) -> int:
        self._tombstone_seq += 1
        self._tombstones[key] = self._tombstone_seq
        self._entries.pop(key, None)
        return self._tombstone_seq

    def commit_write(self, key: str, value, observed_gen: int, now: float) -> None:
        """Commit a write only if the tombstone generation matches what the writer saw."""
        current_gen = self._tombstones.get(key, 0)

        # The core invariant: forgetting is terminal and monotonic.
        if current_gen > observed_gen:
            raise MemoryResurrectionError(
                f"write to {key!r} rejected: tombstone_gen advanced "
                f"{observed_gen} -> {current_gen}; identity is forgotten"
            )

        entry = self._entries.get(key)
        if entry is not None and entry["expires_at"] is not None and entry["expires_at"] <= now:
            # Expiry is recoverable, but only if no tombstone appeared.
            if current_gen != observed_gen:
                raise MemoryResurrectionError(
                    f"write to {key!r} straddled expiry and forget; refusing"
                )
            raise MemoryExpiredError(
                f"entry {key!r} expired during write; safe to retry against absent state"
            )

        self._entries[key] = {
            "value": value,
            "expires_at": None,
            "tombstone_gen": current_gen,
        }
