# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-forgetting-as-a-two-phase-commit-rather-than-a-single-delete.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-forgetting-as-a-two-phase-commit-rather-than-a-single-delete.warrant.json
# Markdown SHA-256:   67d456a90a1e2f4e4ff8d8bd57eba95d0f25851dbccfc3378133c83fcc4039b1
# Code AST SHA-256:   d055676757265f6ca5717a4b6072f5408f1b13a48e2b13a339cfdbedfab66a49
# Verified Classes:   ResidualContextLeakError, ForgettingCoordinator
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class ResidualContextLeakError(RuntimeError):
    """Raised when a deletion is requested to seal while residual context
    remains reachable in a derived structure (ghost edges, consolidation
    summaries, context-keyed caches, or restorable snapshots)."""
    pass


SIMILARITY_LEAK_THRESHOLD = 0.85


class ForgettingCoordinator:
    def __init__(self, store, ann_index, cache, snapshots):
        self.store = store
        self.ann_index = ann_index
        self.cache = cache
        self.snapshots = snapshots
        self.pending = {}

    def mark(self, record_id):
        # Phase 1: logical hide. Record is invisible to retrieval but not sealed.
        self.store.soft_delete(record_id)
        self.pending[record_id] = {"state": "PENDING_SEAL", "embedding": self.store.embedding_of(record_id)}

    def verify_absence(self, record_id):
        emb = self.pending[record_id]["embedding"]
        # Negative-space probe: query the neighborhood, not the key.
        neighbors = self.ann_index.query(emb, k=32)
        leaks = [n for n in neighbors if n.similarity >= SIMILARITY_LEAK_THRESHOLD and n.record_id != record_id]
        ghost_edges = self.ann_index.ghost_edges_for(record_id)
        cache_hits = self.cache.probe_prefix(emb)
        snapshot_hits = self.snapshots.contains(record_id)
        return {"neighbor_leaks": leaks, "ghost_edges": ghost_edges,
                "cache_hits": cache_hits, "snapshot_hits": snapshot_hits}

    def seal(self, record_id):
        # Phase 2: commit. Only seal if the audit proves absence everywhere.
        report = self.verify_absence(record_id)
        if report["neighbor_leaks"] or report["ghost_edges"]:
            raise ResidualContextLeakError(
                f"cannot seal {record_id}: residual context reachable via "
                f"{len(report['neighbor_leaks'])} neighbor(s), "
                f"{len(report['ghost_edges'])} ghost edge(s)"
            )
        if report["cache_hits"]:
            raise ResidualContextLeakError(
                f"cannot seal {record_id}: {len(report['cache_hits'])} context-cache entr(ies) still serve the span"
            )
        if report["snapshot_hits"]:
            raise ResidualContextLeakError(
                f"cannot seal {record_id}: record restorable from snapshot without deletion replay"
            )
        self.ann_index.prune(record_id)
        self.cache.invalidate_prefix(self.pending[record_id]["embedding"])
        self.store.hard_delete(record_id)
        self.pending[record_id]["state"] = "SEALED"
        return {"record_id": record_id, "state": "SEALED", "audit": report}
