# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-the-tombstone-shadow-invariant-violation-when-a-scoped-retention-memory-api-permits-a-forget-to-succeed-while-a-derived-ag.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-the-tombstone-shadow-invariant-violation-when-a-scoped-retention-memory-api-permits-a-forget-to-succeed-while-a-derived-ag.warrant.json
# Markdown SHA-256:   f5977d4fb34bd21ee477c8018cca0bc6245dd88c04fa93706edb9a9f42fab79b
# Code AST SHA-256:   628da752da87ec37ab2d4d7400b8d2aa2e027f6347ea6f6c94923a9a57a65acf
# Verified Classes:   TombstoneShadowError, TombstoneExpiryError, ForgetReceipt, ProvenanceGraph
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Set, Dict, Optional

class TombstoneShadowError(RuntimeError):
    """Raised when a forget receipt does not cover the provenance closure."""
    pass

class TombstoneExpiryError(TombstoneShadowError):
    """Raised when tombstone lifetime is shorter than derived-artifact TTL + propagation lag."""
    pass

@dataclass
class ForgetReceipt:
    root_id: str
    scope: str
    invalidated_artifacts: Set[str]
    tombstone_expires_at: float
    reported_at: float

@dataclass
class ProvenanceGraph:
    # edges: artifact_id -> set of source record ids it was derived from
    edges: Dict[str, Set[str]] = field(default_factory=dict)
    # ttl: artifact_id -> absolute expiry time
    ttl: Dict[str, float] = field(default_factory=dict)

    def closure(self, root: str) -> Set[str]:
        # reverse reachability: artifacts that depend on root
        result: Set[str] = set()
        frontier = {root}
        while frontier:
            cur = frontier.pop()
            for art, sources in self.edges.items():
                if cur in sources and art not in result:
                    result.add(art)
                    frontier.add(art)
        return result

def verify_forget(
    receipt: ForgetReceipt,
    graph: ProvenanceGraph,
    propagation_lag: float,
    now: float,
) -> None:
    expected = graph.closure(receipt.root_id)
    missing = expected - receipt.invalidated_artifacts
    if missing:
        raise TombstoneShadowError(
            f"forget of {receipt.root_id} left {len(missing)} derived artifacts live: "
            f"{sorted(missing)[:5]}"
        )
    # tombstone must outlive every derived artifact plus propagation lag
    worst_ttl = max((graph.ttl.get(a, now) for a in expected), default=now)
    required = worst_ttl + propagation_lag
    if receipt.tombstone_expires_at < required:
        raise TombstoneExpiryError(
            f"tombstone for {receipt.root_id} expires at {receipt.tombstone_expires_at} "
            f"but derived artifacts require {required} (ttl {worst_ttl} + lag {propagation_lag})"
        )
    # receipt is authoritative: instance may compact and serve reconstructions
