# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-that-lossy-context-compaction-has-silently-destroyed-a-recovery-invariant.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-that-lossy-context-compaction-has-silently-destroyed-a-recovery-invariant.warrant.json
# Markdown SHA-256:   063d881f9cd84ec793af13feee45c1d8d1fff4c4fd50613a9020c1dccfc54cd2
# Code AST SHA-256:   36f40bb51ca764dc784e405770ba29de644e78e93ec9a0063811d5121a2410ac
# Verified Classes:   CompactionInvariantError, Binding, Obligation, ContextRegion
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import FrozenSet


class CompactionInvariantError(RuntimeError):
    """Raised when a lossy compaction would destroy a recovery invariant."""


@dataclass(frozen=True)
class Binding:
    token: str
    regenerable: bool  # True if re-fetchable without side effects


@dataclass(frozen=True)
class Obligation:
    name: str
    required_bindings: FrozenSet[str]


@dataclass
class ContextRegion:
    text: str
    present_tokens: FrozenSet[str] = field(default_factory=frozenset)


def assert_compaction_safe(
    region: ContextRegion,
    obligations: tuple[Obligation, ...],
    summary: str,
    bindings: dict[str, Binding],
) -> None:
    """Gate a lossy compaction against the recovery-reachability invariant.

    Safe iff every non-regenerable binding required by an open obligation
    survives verbatim in the summary. Regenerable bindings may be dropped.
    """
    surviving = {tok for tok in region.present_tokens if tok in summary}

    for ob in obligations:
        for tok in ob.required_bindings:
            b = bindings.get(tok)
            if b is None:
                raise CompactionInvariantError(
                    f"obligation {ob.name!r} references unknown binding {tok!r}"
                )
            if b.regenerable:
                continue
            if tok not in surviving:
                raise CompactionInvariantError(
                    f"destructive compaction: obligation {ob.name!r} requires "
                    f"non-regenerable binding {tok!r} which the summary dropped; "
                    f"refuse compaction or spill region to addressable store"
                )


def compact_or_spill(
    region: ContextRegion,
    obligations: tuple[Obligation, ...],
    summarizer,
    bindings: dict[str, Binding],
    spill_store,
) -> str:
    """Attempt lossy compaction; on invariant violation, spill instead."""
    summary = summarizer(region.text)
    try:
        assert_compaction_safe(region, obligations, summary, bindings)
    except CompactionInvariantError:
        handle = spill_store.put(region.text)
        return f"[full context retained at handle {handle}; retrieve before mutating]"
    return summary
