# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-retain-embeddings-after-a-right-to-erasure-request-when-the-episodic-store-reports-the-record-deleted.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-retain-embeddings-after-a-right-to-erasure-request-when-the-episodic-store-reports-the-record-deleted.warrant.json
# Markdown SHA-256:   acf80a2076ac769d86616db2f2f6cc2dad9b2a753b75adcbd68e0783f72c0e40
# Code AST SHA-256:   256870996d1f21c933fe1cff28757de1c40f2ca45c372377d44abd58742402bd
# Verified Classes:   ErasureLineageIncompleteError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class ErasureLineageIncompleteError(RuntimeError):
    """Raised when an erasure request cannot reach all derived artifacts."""
    pass


def enforce_erasure(subject_id, episodic, vector, derived_indexes,
                    consolidation_quiesced, similarity_threshold=0.95):
    # Phase 0: refuse to proceed if any derivation job can run against
    # deletion-pending state. This is the invariant, not an optimization.
    if not consolidation_quiesced:
        raise ErasureLineageIncompleteError(
            f"consolidation not quiesced for subject {subject_id}; "
            f"derivation-from-deletion-pending-state would resurrect content"
        )

    # Phase 1: compute lineage closure BEFORE tombstoning anything.
    closure = episodic.derived_closure(subject_id)
    if closure is None or closure.unbounded:
        raise ErasureLineageIncompleteError(
            f"lineage closure for {subject_id} is unbounded; "
            f"cannot guarantee derived artifacts are reachable"
        )

    # Phase 2: tombstone + compact, blocking until compaction completes.
    episodic.delete(subject_id)
    vector.delete_and_compact(filter={"subject_id": subject_id})
    for idx in derived_indexes:
        idx.delete_and_compact(filter={"lineage_root": subject_id})

    # Phase 3: semantic probe across every index, including derived.
    probe = vector.embed(episodic.original_text(subject_id))
    for idx in [vector] + list(derived_indexes):
        hits = idx.query(probe, k=5)
        for hit in hits:
            if hit.score >= similarity_threshold and hit.lineage_root == subject_id:
                raise ErasureLineageIncompleteError(
                    f"erased content still reachable in {idx.name}: "
                    f"hit {hit.id} score {hit.score:.3f}"
                )
    return True
