# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-the-lossy-self-rewrite-boundary-in-budget-constrained-context-agents.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-the-lossy-self-rewrite-boundary-in-budget-constrained-context-agents.warrant.json
# Markdown SHA-256:   4c1e9db0d81eeec20353225d77ee909334cb091f1a971de57ec20155471ae563
# Code AST SHA-256:   e1524d8478597787862e02817ac586248474cf390b0778c7336e2f90f3df91ae
# Verified Classes:   LossySelfRewriteError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class LossySelfRewriteError(RuntimeError):
    """Raised when a budget-driven memory rewrite would erase load-bearing evidence
    of its own destructiveness (invariant or tombstone)."""
    pass


INVARIANT = "INVARIANT"
VOLATILE = "VOLATILE"


def rewrite_memory(memory, budget_tokens, token_cost):
    """
    memory: list of dicts with keys 'id', 'class', 'tokens', 'content'
    Returns the rewritten memory, or raises if the rewrite would be lossy.
    """
    total = sum(token_cost(m) for m in memory)
    if total <= budget_tokens:
        return memory  # nothing to do

    invariants = [m for m in memory if m["class"] == INVARIANT]
    volatile = [m for m in memory if m["class"] == VOLATILE]

    inv_cost = sum(token_cost(m) for m in invariants)
    if inv_cost > budget_tokens:
        # Cannot fit invariants without touching them. Refuse; do not shrink.
        raise LossySelfRewriteError(
            f"invariants alone cost {inv_cost} > budget {budget_tokens}; "
            "rewrite would erase load-bearing memory. Halt and reconstruct "
            "from external log."
        )

    # Rewrite only volatile items; emit tombstones for what is dropped.
    remaining = budget_tokens - inv_cost
    kept, dropped = [], []
    for m in sorted(volatile, key=lambda x: x.get("salience", 0), reverse=True):
        c = token_cost(m)
        if c <= remaining:
            kept.append(m)
            remaining -= c
        else:
            dropped.append(m)

    # Tombstone ledger must exist and be non-rewritable; otherwise refuse.
    ledger = [{"dropped_id": m["id"], "reason": "budget"} for m in dropped]
    if dropped and not ledger:
        raise LossySelfRewriteError(
            "rewrite dropped items without a tombstone ledger; "
            "destructiveness is undetectable from the rewritten state."
        )

    return invariants + kept, ledger


def assert_bounded_rewrite(before, after):
    """Invariant count must be monotonic non-decreasing across a rewrite."""
    before_inv = sum(1 for m in before if m["class"] == INVARIANT)
    after_inv = sum(1 for m in after if m["class"] == INVARIANT)
    if after_inv < before_inv:
        raise LossySelfRewriteError(
            f"invariant count dropped {before_inv} -> {after_inv}; "
            "memory shrank by erasing load-bearing facts."
        )
