# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-a-tombstone-has-become-a-ghost-distinguishing-lawful-forgetting-completion-from-retention-policy-shadow-state-in-read.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-a-tombstone-has-become-a-ghost-distinguishing-lawful-forgetting-completion-from-retention-policy-shadow-state-in-read.warrant.json
# Markdown SHA-256:   5e3201e12bc0b8a59a4ecab086dd07346fb9941f47b4c082fd70180e963915cc
# Code AST SHA-256:   ae3d822e0af6723e892977afea42e665f322b33847a278c2302ee63e27469e98
# Verified Classes:   ForgetVerificationError
# Defensive Guards:   5 fail-closed invariants (zero external deps)

class ForgetVerificationError(RuntimeError):
    """Raised when a forget operation cannot be verified as physical erasure."""
    pass


def assert_lawful_erasure(forget_response: dict, audit_trail: list[dict]) -> None:
    """
    Verify that a forget operation reached completed erasure rather than
    a tombstone/ghost shadow state. Raises on any unverified condition.
    """
    record_id = forget_response.get("record_id")
    version = forget_response.get("record_version")

    # Signal 1: tombstone without destruction attestation
    if forget_response.get("tombstone_id") and not forget_response.get("destruction_receipt"):
        raise ForgetVerificationError(
            f"record {record_id} v{version}: tombstone present but no destruction "
            f"attestation; forget is logical-only (ghost risk)"
        )

    # Signal 2: redaction without co-transactional key destruction
    redactions = [e for e in audit_trail if e.get("event") == "redaction"]
    destructions = [e for e in audit_trail if e.get("event") == "key_destruction"]
    for red in redactions:
        txn = red.get("transaction_id")
        if not any(d.get("transaction_id") == txn for d in destructions):
            raise ForgetVerificationError(
                f"record {record_id} v{version}: redaction in txn {txn} has no "
                f"co-transactional key destruction; data is pseudonymized, not erased"
            )

    # Signal 3: TTL shadow (expired but not reclaimed)
    retention = forget_response.get("retention", {})
    if retention.get("expired_at") and not retention.get("reclaimed_at"):
        raise ForgetVerificationError(
            f"record {record_id} v{version}: TTL expired but not physically "
            f"reclaimed; record persists in shadow state"
        )

    # Signal 4: replica convergence unconfirmed
    if forget_response.get("replica_convergence") != "confirmed":
        raise ForgetVerificationError(
            f"record {record_id} v{version}: replica convergence not confirmed; "
            f"read-after-forget may observe pre-forget value"
        )

    # Signal 5: status vocabulary
    if forget_response.get("status") not in ("erased", "purged"):
        raise ForgetVerificationError(
            f"record {record_id} v{version}: status "
            f"{forget_response.get('status')!r} does not assert physical erasure"
        )
