# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-context-compaction-from-context-corruption-in-a-self-editing-scratchpad-agent.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-context-compaction-from-context-corruption-in-a-self-editing-scratchpad-agent.warrant.json
# Markdown SHA-256:   25ea584eaecb7bc1221aae5ef8261a1960104bfa6496a2ddaf38743c612c1196
# Code AST SHA-256:   b7bde71be55efcacd5c39f33044fcf5a93ee79f41ee31ee0c17530052b7e32fd
# Verified Classes:   UnrecoverableCompactionError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class UnrecoverableCompactionError(RuntimeError):
    """Raised when a self-edit would destroy a constraint or its loss record."""
    pass


SEALED_KINDS = frozenset({"constraint", "negative_fact", "loss_record"})


def compact(scratchpad, budget_tokens, tokenize):
    """
    scratchpad: list of dicts with keys 'kind', 'text', 'sealed'.
    Returns a new scratchpad within budget, or raises if the rewrite
    would sever reconstructability or evict a loss record.
    """
    rewritable = [s for s in scratchpad if not s["sealed"]]
    sealed = [s for s in scratchpad if s["sealed"]]

    sealed_cost = sum(len(tokenize(s["text"])) for s in sealed)
    if sealed_cost > budget_tokens:
        # Sealed region alone exceeds budget: do not silently drop it.
        raise UnrecoverableCompactionError(
            f"sealed region costs {sealed_cost} tokens, budget is "
            f"{budget_tokens}; refusing to compact sealed constraints"
        )

    remaining = budget_tokens - sealed_cost
    kept, dropped = [], []
    for item in rewritable:
        cost = len(tokenize(item["text"]))
        if cost <= remaining:
            kept.append(item)
            remaining -= cost
        else:
            dropped.append(item)

    # A loss record must survive the pass that creates it.
    needs_record = any(d["kind"] in SEALED_KINDS for d in dropped)
    if needs_record and not any(s["kind"] == "loss_record" for s in sealed):
        raise UnrecoverableCompactionError(
            "compaction dropped a constraint or negative fact but no "
            "sealed loss_record exists to make the loss declarable; "
            "externalize before compacting"
        )

    return sealed + kept
