# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-distinguish-self-editing-context-from-context-mutation-the-re-entrant-pruning-invariant.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-distinguish-self-editing-context-from-context-mutation-the-re-entrant-pruning-invariant.warrant.json
# Markdown SHA-256:   1202c674b0cf5b28318ada65a904b48e79bb892eb5bab8514952eb528dcfb2a6
# Code AST SHA-256:   0ae46596090ef5b176afafd24fd823ce4772914b42f33f2289db58bce7df0b44
# Verified Classes:   PolicyMutationError, Span, SelfEditor
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass, field


class PolicyMutationError(RuntimeError):
    """Raised when a self-edit would touch or alter the pinned policy region."""
    pass


@dataclass
class Span:
    span_id: str
    text: str
    is_policy: bool = False

    def digest(self) -> str:
        return hashlib.sha256(self.text.encode("utf-8")).hexdigest()


@dataclass
class SelfEditor:
    spans: dict[str, Span]
    pinned_policy_ids: frozenset[str]
    max_passes: int = 4
    passes_used: int = 0
    provenance: list = field(default_factory=list)

    def _assert_policy_untouched(self, target_ids: set[str]) -> None:
        overlap = target_ids & self.pinned_policy_ids
        if overlap:
            raise PolicyMutationError(
                f"edit targets pinned policy spans {sorted(overlap)}; "
                f"refusing self-edit and escalating to host"
            )

    def prune(self, drop_ids: set[str]) -> None:
        if self.passes_used >= self.max_passes:
            raise PolicyMutationError(
                f"compaction pass cap {self.max_passes} reached; "
                f"report residual pressure instead of editing"
            )
        self._assert_policy_untouched(drop_ids)
        pre = {sid: self.spans[sid].digest() for sid in drop_ids}
        for sid in drop_ids:
            del self.spans[sid]
        self.passes_used += 1
        self.provenance.append(("prune", pre, None, self.passes_used))

    def rewrite(self, span_id: str, new_text: str) -> None:
        if self.passes_used >= self.max_passes:
            raise PolicyMutationError(
                f"compaction pass cap {self.max_passes} reached; "
                f"report residual pressure instead of editing"
            )
        self._assert_policy_untouched({span_id})
        span = self.spans[span_id]
        old_digest = span.digest()
        span.text = new_text
        self.passes_used += 1
        self.provenance.append(
            ("rewrite", {span_id: old_digest}, {span_id: span.digest()}, self.passes_used)
        )

    def rerank(self, new_order: list[str]) -> None:
        # Position-sensitive models: a re-rank is a mutation, not a selection.
        if set(new_order) != set(self.spans.keys()):
            raise PolicyMutationError("re-rank changes span membership; use prune/rewrite")
        self._assert_policy_untouched(set(new_order))
        self.passes_used += 1
        self.provenance.append(("rerank", None, tuple(new_order), self.passes_used))
        self.spans = {sid: self.spans[sid] for sid in new_order}
