# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-lossy-summarization-integrity-failure-in-long-horizon-agent-context-auto-compaction.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-lossy-summarization-integrity-failure-in-long-horizon-agent-context-auto-compaction.warrant.json
# Markdown SHA-256:   92d543ecc5237ebacd3b3fa3031b05e9631c9b493c0c47d4d5d3e49acf474434
# Code AST SHA-256:   bccff69f3bb1cf78ca4dadd4dc9a1b4bed1707c7b2bad51a884871f1dc6353a0
# Verified Classes:   InvariantLossError, InvariantManifest
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class InvariantLossError(RuntimeError):
    pass


class InvariantManifest:
    def __init__(self):
        self._active = {}   # id -> text
        self._retired = {}  # id -> reason

    def add(self, inv_id: str, text: str) -> None:
        self._active[inv_id] = text

    def retire(self, inv_id: str, reason: str) -> None:
        if inv_id not in self._active:
            raise InvariantLossError(
                f"cannot retire unknown invariant {inv_id!r}"
            )
        self._retired[inv_id] = reason
        del self._active[inv_id]

    def verify_after_compaction(self, post_summary: str, depth: int) -> None:
        # Structural signal: constraint density must not collapse.
        prohibitions = sum(
            post_summary.count(w)
            for w in ("never", "must not", "do not", "forbidden")
        )
        if self._active and prohibitions == 0:
            raise InvariantLossError(
                f"{len(self._active)} active invariants but summary "
                f"contains zero prohibitions; lossy promotion detected"
            )

        # Recursive compaction depth cap.
        if depth > 2:
            raise InvariantLossError(
                f"compaction depth {depth} exceeds cap; "
                f"rehydrate from manifest instead of re-summarizing"
            )

        # Every active invariant must be re-injected verbatim, not summarized.
        for inv_id, text in self._active.items():
            if text not in post_summary:
                raise InvariantLossError(
                    f"invariant {inv_id!r} not present verbatim "
                    f"post-compaction; summarizer dropped or paraphrased it"
                )
