# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-deletion-from-forgetting-in-a-persistent-agent-store-under-erasure-obligations.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-deletion-from-forgetting-in-a-persistent-agent-store-under-erasure-obligations.warrant.json
# Markdown SHA-256:   2b34b9ecfc8c765ac5d0d5477895ea146b684f8a69ca8bf51494d98b6b332534
# Code AST SHA-256:   7f09ff7c2459df5d20b871fa3c3e691febeb4f45a068d4a47083897230dee5e1
# Verified Classes:   ErasureClass, ErasureIncompleteError, Record, ErasurePlanner
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from enum import Enum
from typing import Iterable


class ErasureClass(Enum):
    ROW_ONLY = "A"
    CASCADING = "B"
    PARAMETRIC = "C"


class ErasureIncompleteError(RuntimeError):
    """Raised when a store cannot prove the residual for an erasure request."""

    def __init__(self, class_: ErasureClass, residual: str):
        self.class_ = class_
        self.residual = residual
        super().__init__(f"erasure class {class_.value} incomplete: {residual}")


@dataclass
class Record:
    record_id: str
    user_id: str
    text: str
    provenance: set[str] = field(default_factory=set)  # ids of source records
    is_derived: bool = False


class ErasurePlanner:
    def __init__(self, records: dict[str, Record], finetune_corpus_ids: set[str]):
        self.records = records
        self.finetune_corpus_ids = finetune_corpus_ids

    def _dependents(self, target: str) -> list[Record]:
        return [r for r in self.records.values() if target in r.provenance]

    def classify(self, target: str) -> ErasureClass:
        if target in self.finetune_corpus_ids:
            return ErasureClass.PARAMETRIC
        if self._dependents(target):
            return ErasureClass.CASCADING
        return ErasureClass.ROW_ONLY

    def plan(self, target: str) -> dict:
        cls = self.classify(target)
        if cls is ErasureClass.ROW_ONLY:
            return {"class": cls, "delete": [target], "regenerate": [], "residual": "vector-gist"}
        if cls is ErasureClass.CASCADING:
            deps = self._dependents(target)
            regen = [d.record_id for d in deps]
            return {"class": cls, "delete": [target] + regen, "regenerate": regen,
                    "residual": "vector-gist + re-embed after regen"}
        raise ErasureIncompleteError(
            cls,
            "datum present in fine-tune corpus; row and cascade do not bound parametric residual",
        )

    def execute(self, target: str, unlearn_bound: float | None = None) -> dict:
        cls = self.classify(target)
        if cls is ErasureClass.PARAMETRIC and unlearn_bound is None:
            raise ErasureIncompleteError(
                cls,
                "no unlearning bound supplied; refusing to report compliance",
            )
        return self.plan(target)


def verify_no_reconstruction(store: ErasurePlanner, target: str, probe_embeddings: Iterable[str]) -> None:
    """Refuse to certify erasure if any probe recovers the erased gist."""
    cls = store.classify(target)
    for probe in probe_embeddings:
        if probe == target:
            raise ErasureIncompleteError(
                cls,
                f"reconstruction probe recovered erased record {target!r}",
            )
