# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-a-scoped-memory-deletion-has-silently-failed-to-revoke-derived-indexes.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-a-scoped-memory-deletion-has-silently-failed-to-revoke-derived-indexes.warrant.json
# Markdown SHA-256:   014ed8a15198ca21a1a7e3f11971f5caeb224316db2660f0d8a1e84c0697ff49
# Code AST SHA-256:   ebc4d20105ec0ecce6393a5fc31ae396a09b1379594c38a9cbd694048a556b7e
# Verified Classes:   RevocationIncompleteError, DerivedArtifact, ForgetReceipt
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass, field


class RevocationIncompleteError(RuntimeError):
    """Raised when a forget receipt cannot be shown to revoke all derived artifacts."""


@dataclass
class DerivedArtifact:
    artifact_id: str
    contributing_scopes: frozenset[str]
    build_seq: int
    kind: str  # "vector_index", "summary_cache", "term_index", ...


@dataclass
class ForgetReceipt:
    scope: str
    forget_seq: int
    revoked_artifacts: set[str] = field(default_factory=set)
    pending_artifacts: set[str] = field(default_factory=set)


def verify_forget(receipt: ForgetReceipt, artifacts: list[DerivedArtifact]) -> None:
    """Assert that every artifact touching the forgotten scope is revoked or rebuilt.

    Sound only if each artifact carries a non-empty provenance vector. An artifact
    with empty contributing_scopes cannot be proven safe and must fail closed.
    """
    for art in artifacts:
        touches_scope = receipt.scope in art.contributing_scopes
        if not art.contributing_scopes:
            # No provenance: cannot prove this artifact is unrelated to the scope.
            # Fail closed rather than assume innocence.
            raise RevocationIncompleteError(
                f"artifact {art.artifact_id} ({art.kind}) has no provenance vector; "
                f"cannot prove it is unaffected by forget of scope {receipt.scope!r}. "
                f"Rebuild required."
            )
        if touches_scope and art.artifact_id not in receipt.revoked_artifacts:
            if art.build_seq > receipt.forget_seq and art.artifact_id not in receipt.pending_artifacts:
                # Rebuilt after the forget: safe.
                continue
            raise RevocationIncompleteError(
                f"artifact {art.artifact_id} ({art.kind}) contributes to scope "
                f"{receipt.scope!r} but was not revoked by forget_seq {receipt.forget_seq} "
                f"(build_seq={art.build_seq}). Receipt is not a revocation guarantee."
            )
    if receipt.pending_artifacts:
        raise RevocationIncompleteError(
            f"forget_seq {receipt.forget_seq} still has pending revocations: "
            f"{sorted(receipt.pending_artifacts)}"
        )
