# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-forget-acknowledgement-as-evidence-of-deletion-rather-than-evidence-of-intent.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-forget-acknowledgement-as-evidence-of-deletion-rather-than-evidence-of-intent.warrant.json
# Markdown SHA-256:   b0baa6ce5349f17ac046dbd5db6d062385ba06dbaf86aaf6ba30f73a891628f3
# Code AST SHA-256:   e0b389b63f0415ec3f299e8f6b93a9713aee43e06ebadd7f7ba3f6924e4e2ff5
# Verified Classes:   UnverifiableDeletionError, AckKind, ForgetAck, RetentionScope
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from __future__ import annotations

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


class UnverifiableDeletionError(RuntimeError):
    """Raised when a forget acknowledgement cannot be promoted to deletion evidence."""


class AckKind(Enum):
    EVIDENCE = "evidence"
    INTENT = "intent"


@dataclass(frozen=True)
class ForgetAck:
    # Raw service response, before any interpretation.
    verification_handle: str | None
    cleared_scopes: frozenset[str]
    origin_routed_read_confirmed: bool
    derived_structures_covered: bool
    max_propagation_seconds: float | None
    elapsed_since_ack_seconds: float


@dataclass(frozen=True)
class RetentionScope:
    name: str
    # True if this scope's retention outlives the others and therefore governs
    # the effective retention of the key. Must be cleared explicitly.
    governs_key: bool = True


def classify_ack(ack: ForgetAck, required_scopes: Iterable[RetentionScope]) -> AckKind:
    """Promote an acknowledgement to EVIDENCE only if every validity condition holds."""
    required = {s.name for s in required_scopes if s.governs_key}

    if ack.verification_handle is None:
        # Bare boolean response: intent only, regardless of anything else.
        return AckKind.INTENT

    missing = required - ack.cleared_scopes
    if missing:
        # Retention scopes compose as a union; an unenumerated scope means the
        # forget is incomplete by construction.
        raise UnverifiableDeletionError(
            f"forget ack {ack.verification_handle!r} did not clear retention "
            f"scopes {sorted(missing)}; union of scopes is not covered"
        )

    if not ack.origin_routed_read_confirmed:
        # A read that could be served by a cache or lagging replica is not evidence.
        raise UnverifiableDeletionError(
            f"forget ack {ack.verification_handle!r} verified against a read path "
            "that is not origin-routed; empty result may reflect cache invalidation, "
            "not deletion"
        )

    if not ack.derived_structures_covered:
        # Indexes/embeddings are outside the forget contract unless declared.
        raise UnverifiableDeletionError(
            f"forget ack {ack.verification_handle!r} does not cover derived "
            "structures (indexes, embeddings); semantic reads may still surface content"
        )

    if ack.max_propagation_seconds is None:
        raise UnverifiableDeletionError(
            f"forget ack {ack.verification_handle!r} has no bounded propagation "
            "window; 'eventually' is not a condition of validity"
        )

    if ack.elapsed_since_ack_seconds < ack.max_propagation_seconds:
        # Still inside the propagation window: intent with a handle, not evidence.
        return AckKind.INTENT

    return AckKind.EVIDENCE


def assert_deleted(ack: ForgetAck, required_scopes: Iterable[RetentionScope]) -> None:
    """Gate: callers that need verifiable deletion must fail closed on INTENT."""
    kind = classify_ack(ack, required_scopes)
    if kind is not AckKind.EVIDENCE:
        raise UnverifiableDeletionError(
            f"deletion of handle {ack.verification_handle!r} is intent, not evidence; "
            "refusing to assert absence (fail-closed)"
        )
