# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-silent-context-amnesia-when-hierarchical-summarization-evicts-the-load-bearing-invariant.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-silent-context-amnesia-when-hierarchical-summarization-evicts-the-load-bearing-invariant.warrant.json
# Markdown SHA-256:   d1a87ee11cb93d2cb4afeea7ec402dba7a00e07903292fdac27d35380ef39f60
# Code AST SHA-256:   7055d1a3eea2e812e69f7836498e9f231b03d7be450400037751277c4169fede
# Verified Classes:   ContextAmnesiaError, PinnedInvariant, CompactionResult
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass, field


class ContextAmnesiaError(RuntimeError):
    """Raised when a pinned invariant is not verifiably present post-compaction."""


@dataclass
class PinnedInvariant:
    key: str
    text: str
    must_survive_verbatim: bool = True


@dataclass
class CompactionResult:
    summary: str
    evicted_keys: list[str] = field(default_factory=list)


def verify_invariants_survived(
    pinned: list[PinnedInvariant],
    result: CompactionResult,
    *,
    normalized: bool = True,
) -> None:
    """Assert every pinned invariant is still present after hierarchical compaction.

    Call this immediately after any leaf/parent summary step and before the
    compacted context is used for a tool action. Failure means the lossy
    channel has dropped or paraphrased a binding obligation.
    """
    haystack = result.summary
    if normalized:
        haystack = " ".join(haystack.split()).lower()

    missing: list[str] = []
    for inv in pinned:
        needle = inv.text if not normalized else " ".join(inv.text.split()).lower()
        if inv.must_survive_verbatim and needle not in haystack:
            missing.append(inv.key)
        elif inv.key in result.evicted_keys:
            missing.append(inv.key)

    if missing:
        raise ContextAmnesiaError(
            "Pinned invariants lost during compaction: "
            f"{sorted(missing)}. Refusing to proceed; re-inject from "
            "out-of-band source before any dependent action."
        )


def compact_with_guard(
    pinned: list[PinnedInvariant],
    summarize,
    evict,
    context: str,
) -> str:
    result = summarize(context)
    evict(result)
    verify_invariants_survived(pinned, result)
    # Re-inject pinned block so downstream turns cannot lose it again.
    block = "\n".join(f"[PINNED {p.key}] {p.text}" for p in pinned)
    return block + "\n\n" + result.summary
