# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-eviction-from-rewrite-in-self-editing-context-policies-under-token-budgets.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-eviction-from-rewrite-in-self-editing-context-policies-under-token-budgets.warrant.json
# Markdown SHA-256:   727f9eeee30d037e17f6b5409215decd96b420e5079e35a9108332e61f834df6
# Code AST SHA-256:   63b685734684e03adc33dbc0d4452fbe4dfae9584a8563521eab63b73a3a64c7
# Verified Classes:   UntaggedRewriteError, BudgetLedgerInconsistency, Span, Ledger
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Literal

class UntaggedRewriteError(RuntimeError):
    pass

class BudgetLedgerInconsistency(RuntimeError):
    pass

@dataclass
class Span:
    span_id: str
    text: str
    tokens: int
    has_external_copy: bool
    provenance: str | None = None      # span_id of source if this is a surrogate
    lossy: bool = False

@dataclass
class Ledger:
    evicted: set[str] = field(default_factory=set)
    rewritten: dict[str, str] = field(default_factory=dict)  # surrogate_id -> source_id
    truncated: set[str] = field(default_factory=set)

def apply_rewrite(span: Span, surrogate_text: str, ledger: Ledger,
                  surrogate_id: str, tag_emitted: bool) -> Span:
    if not tag_emitted:
        # Untaggable surrogate: must be reclassified as eviction, not compression.
        raise UntaggedRewriteError(
            f"rewrite of {span.span_id} produced untaggable surrogate {surrogate_id}; "
            f"drop-and-evict instead of compress"
        )
    new_tokens = len(surrogate_text.split())
    if new_tokens >= span.tokens and not span.has_external_copy:
        # A "compression" that grows a span without a recoverable copy is a bug.
        raise BudgetLedgerInconsistency(
            f"rewrite of {span.span_id} grew {span.tokens}->{new_tokens} with no external copy"
        )
    ledger.rewritten[surrogate_id] = span.span_id
    return Span(span_id=surrogate_id, text=surrogate_text, tokens=new_tokens,
                has_external_copy=span.has_external_copy,
                provenance=span.span_id, lossy=True)

def audit_truncation(ledger: Ledger, removed_by_truncation: set[str]) -> None:
    unexplained = removed_by_truncation - ledger.evicted - set(ledger.rewritten)
    if unexplained:
        raise BudgetLedgerInconsistency(
            f"truncation removed spans with no eviction/rewrite attribution: {unexplained}"
        )
