# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-forget-before-it-can-prove-it-forgot-deletion-attestation-vs-retention-scope-closure-in-memory-tool-apis.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-forget-before-it-can-prove-it-forgot-deletion-attestation-vs-retention-scope-closure-in-memory-tool-apis.warrant.json
# Markdown SHA-256:   b2919e13034677d07f7ba5658cec837055b02c680307e1306d176521b7d37ebb
# Code AST SHA-256:   2b6455923ef039b72021ec320436996230f3567bfc280a9c58386a3027c0d04e
# Verified Classes:   AttestationScopeError, RetentionFloorViolation, Binding, Record
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass, field


class AttestationScopeError(RuntimeError):
    """Raised when an attestation would claim more closure than was achieved."""
    pass


class RetentionFloorViolation(RuntimeError):
    """Raised when closure would require deleting a record under a retention floor."""
    pass


@dataclass
class Binding:
    scope: str
    retention_floor_days: int
    deleted: bool = False


@dataclass
class Record:
    record_id: str
    bindings: list[Binding] = field(default_factory=list)
    derivations: list[str] = field(default_factory=list)  # soft links to derived record ids


def _closure_depth(record: Record, store: dict[str, Record], seen: set[str]) -> tuple[int, list[str]]:
    """Return (achieved_depth, blocking_scopes). Depth 0 = no closure, 1 = caller scope only, 2 = all bindings + derivations."""
    if record.record_id in seen:
        return 2, []
    seen.add(record.record_id)

    blocking: list[str] = []
    depth = 2
    for b in record.bindings:
        if not b.deleted:
            if b.retention_floor_days > 0:
                blocking.append(b.scope)
                depth = min(depth, 1)
            else:
                depth = min(depth, 1)
    for d_id in record.derivations:
        if d_id in store:
            sub_depth, sub_block = _closure_depth(store[d_id], store, seen)
            depth = min(depth, sub_depth)
            blocking.extend(sub_block)
    return depth, blocking


def forget(
    record_id: str,
    caller_scope: str,
    store: dict[str, Record],
    claimed_depth: int = 2,
) -> dict:
    """Delete caller-visible binding and issue an attestation only if closure meets claimed_depth.

    claimed_depth: 1 = assert closure within caller scope only; 2 = assert global closure
    (all bindings + derivations). Defaults to the strict claim.
    """
    record = store.get(record_id)
    if record is None:
        raise AttestationScopeError(f"record {record_id} not addressable from {caller_scope}")

    # Delete the caller-visible binding first (the part we are allowed to do unconditionally).
    for b in record.bindings:
        if b.scope == caller_scope:
            b.deleted = True

    achieved_depth, blocking_scopes = _closure_depth(record, store, set())

    if claimed_depth == 2 and achieved_depth < 2:
        if blocking_scopes:
            raise RetentionFloorViolation(
                f"cannot attest global closure for {record_id}: "
                f"retention floors block deletion in scopes {sorted(set(blocking_scopes))}"
            )
        raise AttestationScopeError(
            f"cannot attest global closure for {record_id}: "
            f"achieved depth {achieved_depth} < claimed {claimed_depth}; "
            f"undeleted bindings or derivations remain"
        )

    if claimed_depth > achieved_depth:
        raise AttestationScopeError(
            f"attestation claim depth {claimed_depth} exceeds achieved {achieved_depth} for {record_id}"
        )

    payload = f"{record_id}|{caller_scope}|depth={achieved_depth}"
    return {
        "record_id": record_id,
        "scope": caller_scope,
        "claimed_depth": claimed_depth,
        "achieved_depth": achieved_depth,
        "blocking_scopes": sorted(set(blocking_scopes)),
        "digest": hashlib.sha256(payload.encode()).hexdigest(),
    }
