# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-context-provenance-as-a-first-class-invariant-rather-than-a-retrieval-time-convenience-staleness-detection-un.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-context-provenance-as-a-first-class-invariant-rather-than-a-retrieval-time-convenience-staleness-detection-un.warrant.json
# Markdown SHA-256:   797a0f0e1adb08bd41acb5ade9e93a6bcdbbabbf990c25d418ae78aa60fecf65
# Code AST SHA-256:   64cada78038fd00edd592701d9ed07fd11c0a9d15509ce2db4be6b21f2ae80bc
# Verified Classes:   StaleProvenanceError, MemoryEntry, ActionLog
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import FrozenSet, List, Tuple


class StaleProvenanceError(RuntimeError):
    """Raised when a persisted memory entry is used despite causal invalidation."""
    pass


@dataclass(frozen=True)
class MemoryEntry:
    key: str
    value: object
    fetched_at: int
    dependencies: FrozenSet[str]  # world-objects this entry's truth depends on


@dataclass
class ActionLog:
    # (turn, set of world-objects written by the agent's action)
    writes: List[Tuple[int, FrozenSet[str]]] = field(default_factory=list)

    def write_set_between(self, lo: int, hi: int) -> FrozenSet[str]:
        acc = set()
        for turn, objs in self.writes:
            if lo < turn <= hi:
                acc |= objs
        return frozenset(acc)


def use_entry(entry: MemoryEntry, now: int, log: ActionLog) -> object:
    """
    Enforce the provenance invariant: an entry is usable only if its
    dependency-set is disjoint from the agent's own writes since fetch.
    Age is deliberately NOT consulted.
    """
    if entry.dependencies is None:
        # Unknown provenance is dirty by default, not fresh by default.
        raise StaleProvenanceError(
            f"entry {entry.key!r} has no provenance; must be re-validated"
        )

    dirty = entry.dependencies & log.write_set_between(entry.fetched_at, now)
    if dirty:
        raise StaleProvenanceError(
            f"entry {entry.key!r} fetched at turn {entry.fetched_at} is "
            f"causally invalidated by agent writes to {sorted(dirty)} "
            f"before turn {now}; re-fetch or treat as absent"
        )
    return entry.value


def revalidate(entry: MemoryEntry, now: int, refetch) -> MemoryEntry:
    """Explicit re-fetch path: the only sound way to clear a dirty entry."""
    fresh_value = refetch(entry.key)
    return MemoryEntry(
        key=entry.key,
        value=fresh_value,
        fetched_at=now,
        dependencies=entry.dependencies,
    )
