# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-treat-right-to-erasure-as-a-cross-store-invariant-rather-than-a-per-store-delete.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-treat-right-to-erasure-as-a-cross-store-invariant-rather-than-a-per-store-delete.warrant.json
# Markdown SHA-256:   2dbeab71c451c72f70a25e4b7c12952c2bc373cc50b7b845c2a599bc06081fdd
# Code AST SHA-256:   ce0547f92cdc4996db0b584490be9632721ec9c761ea49144c626efc1efed935
# Verified Classes:   IncompleteErasureError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class IncompleteErasureError(RuntimeError):
    """Raised when an erasure request leaves the subject reconstructable."""
    pass


DERIVED_STORES = ("summary_store", "topic_digest", "extracted_facts")
RECONSTRUCTION_THRESHOLD = 0.15  # max recoverable fraction of private content


def enforce_erasure(subject_id, stores, provenance, reconstruction_probe):
    # Phase 1: per-store delete by identifier. Zero-row deletes on derived
    # stores are a hard failure, not a no-op.
    for name in DERIVED_STORES:
        result = stores[name].delete_by_subject(subject_id)
        if result.deleted == 0 and provenance.has_contribution(subject_id, name):
            raise IncompleteErasureError(
                f"store {name!r} reported deleted=0 for subject {subject_id!r} "
                f"but provenance shows contribution; derived artifacts are not "
                f"keyed by subject id and must be erased by traversal"
            )

    # Phase 2: crypto-shred append-only tiers so tombstones are unrecoverable.
    for name in ("episodic_log", "backups"):
        stores[name].destroy_subject_key(subject_id)

    # Phase 3: reconstruction test over all remaining stores.
    recovered = reconstruction_probe(subject_id)
    if recovered > RECONSTRUCTION_THRESHOLD:
        raise IncompleteErasureError(
            f"subject {subject_id!r} still reconstructable at {recovered:.2%} "
            f"(threshold {RECONSTRUCTION_THRESHOLD:.0%}); derived summaries "
            f"likely retain paraphrased content without provenance edges"
        )
    return True
