# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-model-driven-context-pruning-as-a-lossy-non-idempotent-mutation-rather-than-a-recoverable-compaction.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-model-driven-context-pruning-as-a-lossy-non-idempotent-mutation-rather-than-a-recoverable-compaction.warrant.json
# Markdown SHA-256:   3584f5b8f71dcca4223e9bd8d565639e68c47c6303e96760adee04e318078c98
# Code AST SHA-256:   cdc9c27e45a2e620d45db66fee789929319a80446b4930583767634d46839cab
# Verified Classes:   UnrecoverablePruneError, Span, Window
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Optional

PRECONDITION_TAGS = frozenset({"invariant", "constraint", "negative_result", "scope_qualifier"})


class UnrecoverablePruneError(RuntimeError):
    """Raised when a model-driven prune would destroy a precondition-bearing span
    without an externalized, addressable copy. This is a mutation, not a compaction."""


@dataclass
class Span:
    span_id: str
    text: str
    tags: frozenset = frozenset()
    externalized_key: Optional[str] = None  # non-None => recoverable from side store


@dataclass
class Window:
    spans: list = field(default_factory=list)
    derivation_depth: int = 0


def prune(window: Window, evict_ids: set, source_store: dict) -> Window:
    """Model-proposed eviction. Refuses to paraphrase-evict precondition spans
    unless they are externalized; refuses to summarize an un-externalized summary."""
    if window.derivation_depth >= 1 and any(
        s.externalized_key is None for s in window.spans
    ):
        raise UnrecoverablePruneError(
            "refusing to prune a window that is already a derived summary with "
            "unresolvable provenance (derivation_depth >= 1); re-ground against source store first"
        )

    kept = []
    for span in window.spans:
        if span.span_id not in evict_ids:
            kept.append(span)
            continue
        if span.tags & PRECONDITION_TAGS:
            if span.externalized_key is None or span.externalized_key not in source_store:
                raise UnrecoverablePruneError(
                    f"span {span.span_id!r} is precondition-bearing ({sorted(span.tags & PRECONDITION_TAGS)}) "
                    f"and has no externalized copy; eviction would be an irreversible mutation"
                )
            # retain a resolvable pointer, not a paraphrase
            kept.append(Span(span.span_id, f"<ref:{span.externalized_key}>", tags=span.tags,
                             externalized_key=span.externalized_key))
        # else: safely evictable, drop

    return Window(spans=kept, derivation_depth=window.derivation_depth + 1)
