# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-invariant-collapse-in-auto-compaction-with-semantic-deduplication-and-lossy-summarization-under-token-budget-constraints.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-invariant-collapse-in-auto-compaction-with-semantic-deduplication-and-lossy-summarization-under-token-budget-constraints.warrant.json
# Markdown SHA-256:   2f9ae200c268c6c834a88b297800732e073546bb69a133dbdb07d7557716f6a2
# Code AST SHA-256:   37e480dd7c448838e06ceb7269d347c8bdd024262e049a6074269ebafa10eb33
# Verified Classes:   CompactionInvariantError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class CompactionInvariantError(RuntimeError):
    """Raised when auto-compaction converges on budget while coverage decays."""
    pass


def check_compaction_cycle(
    cycle: int,
    tokens_before: int,
    tokens_after: int,
    coverage_before: float,
    coverage_after: float,
    dedup_rate: float,
    prior_token_deltas: list[float],
    k: float = 3.0,
) -> None:
    token_saved_pct = (tokens_before - tokens_after) / max(tokens_before, 1)
    coverage_lost_pct = coverage_before - coverage_after

    # Primary boundary: marginal coverage loss vs marginal token savings.
    if token_saved_pct > 0 and coverage_lost_pct / token_saved_pct > k:
        raise CompactionInvariantError(
            f"cycle {cycle}: coverage loss {coverage_lost_pct:.3f} per "
            f"token saved {token_saved_pct:.3f} exceeds k={k}; "
            f"budget convergence is masking coverage collapse"
        )

    # Secondary boundary: summary-convergence attractor.
    if dedup_rate > 0.5 and len(prior_token_deltas) >= 2:
        recent = prior_token_deltas[-2:] + [abs(token_saved_pct)]
        if max(recent) < 0.05:
            raise CompactionInvariantError(
                f"cycle {cycle}: dedup_rate={dedup_rate:.2f} with token "
                f"delta <5% for 3 cycles — store is in the summary-"
                f"convergence fixed point, not a satisfied invariant"
            )
