# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-reconcile-ttl-expiry-against-scoped-retention-when-a-forget-request-races-a-read.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-reconcile-ttl-expiry-against-scoped-retention-when-a-forget-request-races-a-read.warrant.json
# Markdown SHA-256:   2469cbdcf942122e873ab8e0bf1fe7f76738f6ab9302fd4130585d2cdf8828d7
# Code AST SHA-256:   1a2177445080c353cb1aac4cedb156a5711578d43e89c2643f33f13943ffefcd
# Verified Classes:   RetentionViolation, MemoryScope
# Defensive Guards:   2 fail-closed invariants (zero external deps)

import time


class RetentionViolation(RuntimeError):
    """Raised when a forget/read interleaving breaks scoped retention."""
    pass


class MemoryScope:
    def __init__(self, scope_id: str, tombstone_gc_horizon_s: float,
                 max_inflight_read_s: float):
        if tombstone_gc_horizon_s <= max_inflight_read_s:
            raise RetentionViolation(
                f"scope {scope_id}: tombstone_gc_horizon "
                f"({tombstone_gc_horizon_s}s) must exceed max_inflight_read "
                f"({max_inflight_read_s}s) or forget is not verifiable"
            )
        self.scope_id = scope_id
        self.records = {}       # key -> (value, expires_at)
        self.tombstones = {}    # key -> forgotten_at
        self.gc_horizon = tombstone_gc_horizon_s

    def read(self, key: str, now: float | None = None):
        now = time.monotonic() if now is None else now
        entry = self.records.get(key)
        if entry is None:
            return None
        value, expires_at = entry
        # TTL admission
        if now >= expires_at:
            return None
        # Revalidate against forget tombstone before returning.
        tomb = self.tombstones.get(key)
        if tomb is not None and tomb <= now:
            return None
        return value

    def forget(self, predicate, now: float | None = None) -> int:
        now = time.monotonic() if now is None else now
        count = 0
        for key in list(self.records):
            if predicate(key):
                self.tombstones[key] = now
                del self.records[key]
                count += 1
        return count

    def verify_forgotten(self, keys, now: float | None = None) -> None:
        now = time.monotonic() if now is None else now
        for key in keys:
            if self.read(key, now=now) is not None:
                raise RetentionViolation(
                    f"scope {self.scope_id}: key {key!r} readable after forget"
                )
