# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-a-forget-call-on-a-scoped-memory-tool-as-a-tombstone-rather-than-a-physical-deletion.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-a-forget-call-on-a-scoped-memory-tool-as-a-tombstone-rather-than-a-physical-deletion.warrant.json
# Markdown SHA-256:   8547551188da3f122e5629382067c5802a0c5402ac02ba53eb4c7cf121ab10bf
# Code AST SHA-256:   2119acb42963edd962e75afef90ac5facf7d6c2effe2bda2be198de3904bf4e0
# Verified Classes:   ErasureNotProvableError, Scope, MemoryTool
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional


class ErasureNotProvableError(RuntimeError):
    """Raised when a forget() has tombstoned a scope but physical erasure
    cannot be confirmed (derived children, pending sweep, or no purge primitive)."""
    pass


@dataclass
class Scope:
    scope_id: str
    parent_id: Optional[str] = None
    deleted_at: Optional[datetime] = None
    erasure_pending: bool = True
    derived_children: list = field(default_factory=list)


class MemoryTool:
    def __init__(self):
        self.scopes: dict[str, Scope] = {}
        self._purge_primitive = False  # substrate exposes hard delete + compaction

    def forget(self, scope_id: str) -> dict:
        s = self.scopes[scope_id]
        s.deleted_at = datetime.utcnow()
        s.erasure_pending = True
        return {"status": "ok", "tombstoned": scope_id}

    def confirm_erasure(self, scope_id: str) -> None:
        s = self.scopes[scope_id]
        if s.deleted_at is None:
            raise ErasureNotProvableError(f"{scope_id} was never forgotten")
        if s.derived_children:
            raise ErasureNotProvableError(
                f"{scope_id} has derived children {s.derived_children}; "
                f"tombstone does not cover them"
            )
        if s.erasure_pending and not self._purge_primitive:
            raise ErasureNotProvableError(
                f"{scope_id} tombstoned but substrate exposes no purge; "
                f"physical erasure cannot be proven"
            )
        s.erasure_pending = False


def handle_deletion_request(tool: MemoryTool, scope_id: str) -> str:
    """Agent entry point: forget, then attempt to prove erasure. Refuses to
    emit a compliance claim it cannot back with a physical guarantee."""
    result = tool.forget(scope_id)
    if result.get("status") != "ok":
        raise ErasureNotProvableError(f"forget failed for {scope_id}: {result}")
    try:
        tool.confirm_erasure(scope_id)
    except ErasureNotProvableError as exc:
        # Do NOT report "deleted". Escalate with the precise gap.
        raise ErasureNotProvableError(
            f"GDPR erasure for {scope_id} not provable: {exc}. "
            f"Escalate to physical-erasure workflow."
        ) from exc
    return "ERASED"
