# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-scratchpad-rewriting-as-a-commit-boundary-rather-than-a-mutation.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-scratchpad-rewriting-as-a-commit-boundary-rather-than-a-mutation.warrant.json
# Markdown SHA-256:   f9bc148cfc2a4c2cabae501b48ea5674dd23d4eb66c59325a8e5698e3f80c286
# Code AST SHA-256:   d62140dd882dc94e2bd1ca5e4eefb167c685efebf878f36428a240beb8bbc7c9
# Verified Classes:   UnsafeEvictionError, Span, Scratchpad
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Iterable


class UnsafeEvictionError(RuntimeError):
    """Raised when a scratchpad rewrite would evict an unfalsifiable decision."""


@dataclass
class Span:
    turn: int
    text: str
    is_falsifier: bool = False  # contains "X rejected because Y" provenance


@dataclass
class Scratchpad:
    spans: list[Span] = field(default_factory=list)

    def live_falsifiers(self) -> list[Span]:
        return [s for s in self.spans if s.is_falsifier]


def commit_rewrite(
    pad: Scratchpad,
    kept_turns: Iterable[int],
    new_summary: str,
) -> Scratchpad:
    """
    Apply a destructive rewrite under commit semantics.

    A rewrite is a commit, not a mutation: evicted spans cannot be re-read.
    Refuse the rewrite if it would drop any falsifier that no retained span
    or the new summary preserves.
    """
    kept = set(kept_turns)
    evicted = [s for s in pad.spans if s.turn not in kept]

    dropped_falsifiers = [
        s for s in evicted
        if s.is_falsifier and s.text not in new_summary
    ]
    if dropped_falsifiers:
        turns = sorted(s.turn for s in dropped_falsifiers)
        raise UnsafeEvictionError(
            f"rewrite would evict {len(dropped_falsifiers)} falsifier(s) "
            f"from turns {turns} with no verbatim preservation in summary; "
            f"refusing commit — promote to rejected_alternatives or retain span"
        )

    retained = [s for s in pad.spans if s.turn in kept]
    retained.append(Span(turn=max(kept, default=-1) + 1, text=new_summary))
    return Scratchpad(spans=retained)


def safe_compact(pad: Scratchpad, keep_last_n: int, summarize) -> Scratchpad:
    """Convenience wrapper: summarize all but the last N turns, commit-checked."""
    if len(pad.spans) <= keep_last_n:
        return pad
    kept = [s.turn for s in pad.spans[-keep_last_n:]]
    summary = summarize(pad.spans[: -keep_last_n])
    return commit_rewrite(pad, kept_turns=kept, new_summary=summary)
