# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-scope-erasure-races-between-concurrent-forget-and-read-in-retention-scoped-memory-apis.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-scope-erasure-races-between-concurrent-forget-and-read-in-retention-scoped-memory-apis.warrant.json
# Markdown SHA-256:   8fb535dc63b1c2c26c3096066e7df150ad52592b7432a0955fd568910ce4b89d
# Code AST SHA-256:   f982972ac35428cd010328cc5ca63d47749ae611c9243dc90ce0d220077aadf7
# Verified Classes:   ScopeErasedDuringRead, ReadToken
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class ScopeErasedDuringRead(RuntimeError):
    """Raised when a read's authorized segment generation no longer matches
    the live segment at materialization time, i.e. a concurrent forget on an
    overlapping scope triggered lineage compaction mid-read."""
    pass


class ReadToken:
    __slots__ = ("scope_id", "segment_generation")

    def __init__(self, scope_id: str, segment_generation: int):
        self.scope_id = scope_id
        self.segment_generation = segment_generation


def materialize(token: ReadToken, segment) -> bytes:
    # segment exposes .scope_id and .generation; reclamation bumps generation
    # in place, so a stale token must fail closed rather than read torn bytes.
    if segment.scope_id != token.scope_id:
        raise ScopeErasedDuringRead(
            f"scope mismatch: token={token.scope_id!r} segment={segment.scope_id!r}"
        )
    if segment.generation != token.segment_generation:
        raise ScopeErasedDuringRead(
            f"generation drift on {token.scope_id!r}: "
            f"authorized={token.segment_generation} live={segment.generation}"
        )
    return segment.payload()


def read_scoped(store, scope_id: str, key: str) -> bytes:
    token = store.authorize(scope_id, key)  # mints (scope_id, generation)
    if token is None:
        raise KeyError(key)
    segment = store.lookup(key)
    if segment is None:
        raise KeyError(key)
    return materialize(token, segment)
