# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-refuse-to-prune-its-own-context-the-self-edit-reentrancy-hazard.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-refuse-to-prune-its-own-context-the-self-edit-reentrancy-hazard.warrant.json
# Markdown SHA-256:   db1c1ec4b2e98869ee83efd267bac8cafd8eb284ce0758353edfd59c032cfed6
# Code AST SHA-256:   67e5c9df97f92038b105b301710a5ba270701826104d5adbffe19a0489f412dd
# Verified Classes:   SelfEditInvariantError, Span
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass


class SelfEditInvariantError(RuntimeError):
    """Raised when a model-driven context edit would destroy its own provenance."""


@dataclass(frozen=True)
class Span:
    span_id: str
    text: str
    tag: str  # "constraint" | "instruction" | "tool_output" | "rationale" | ...

    @property
    def content_hash(self) -> str:
        return hashlib.sha256(self.text.encode("utf-8")).hexdigest()[:16]


def apply_self_edit(
    window: list[Span],
    plan: list[str],
    rationale_span_ids: set[str],
    tombstones: dict[str, str],
    editable: bool = True,
) -> tuple[list[Span], dict[str, str]]:
    """Apply a model-authored prune/rewrite plan, enforcing the self-edit invariant.

    Refuses (and escalates) rather than silently corrupting provenance.
    """
    if not editable:
        raise SelfEditInvariantError(
            "editor is not authorized to mutate its own window; escalate to supervisor"
        )

    plan_set = set(plan)
    for span in window:
        if span.span_id not in plan_set:
            continue

        # 1. The plan must not delete the reasoning that produced it.
        if span.span_id in rationale_span_ids:
            raise SelfEditInvariantError(
                f"plan would prune its own rationale span {span.span_id!r}; "
                "refuse and escalate"
            )

        # 2. Constraints and instructions are immutable under re-ranking/rewrite.
        if span.tag in ("constraint", "instruction"):
            raise SelfEditInvariantError(
                f"span {span.span_id!r} is tagged {span.tag!r}; "
                "may be moved but not pruned or paraphrased"
            )

        # 3. Re-admission of tombstoned content requires an explicit override.
        prior = tombstones.get(span.content_hash)
        if prior is not None:
            raise SelfEditInvariantError(
                f"content hash {span.content_hash} already tombstoned at {prior}; "
                "re-admission requires an override turn"
            )

    new_window: list[Span] = []
    new_tombstones = dict(tombstones)
    for span in window:
        if span.span_id in plan_set:
            new_tombstones[span.content_hash] = span.span_id
        else:
            new_window.append(span)

    return new_window, new_tombstones
