# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-bound-self-editing-context-the-retention-weight-inversion-at-the-prune-frontier.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-bound-self-editing-context-the-retention-weight-inversion-at-the-prune-frontier.warrant.json
# Markdown SHA-256:   717a2231d41aa30e4fa2c5a16e657d93a810aa82e561e3b6c1b0aefd055320d3
# Code AST SHA-256:   8f8e057a76780b90b320a23a2042708e41ec594ae04e25aae3399221550dd392
# Verified Classes:   UnstablePruneError, Segment, SelfEditingWindow
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Callable


class UnstablePruneError(RuntimeError):
    """Raised when a prune decision is not stable under its own execution."""


@dataclass
class Segment:
    id: str
    text: str
    is_constraint: bool = False


@dataclass
class SelfEditingWindow:
    segments: list[Segment] = field(default_factory=list)
    pruned: dict[str, str] = field(default_factory=dict)  # id -> reason

    def prune(
        self,
        score: Callable[[Segment, list[Segment]], float],
        threshold: float,
    ) -> list[str]:
        # Score against the POST-prune denominator, then verify idempotence.
        candidates = [s for s in self.segments if not s.is_constraint]
        survivors_pre = [s for s in candidates if score(s, candidates) >= threshold]
        survivors_post = [s for s in candidates if score(s, survivors_pre) >= threshold]

        if {s.id for s in survivors_pre} != {s.id for s in survivors_post}:
            raise UnstablePruneError(
                "retention-weight inversion: survivors differ when scored against "
                "the post-prune set; decision is not idempotent and must not be applied"
            )

        keep = {s.id for s in survivors_post}
        removed = []
        for s in self.segments:
            if s.is_constraint or s.id in keep:
                continue
            if s.id in self.pruned:
                continue
            self.pruned[s.id] = f"score<{threshold} under stable denominator"
            removed.append(s.id)
        self.segments = [s for s in self.segments if s.id not in removed]
        return removed

    def assert_premise_present(self, premise_id: str) -> None:
        if premise_id in self.pruned:
            raise UnstablePruneError(
                f"premise {premise_id!r} was pruned ({self.pruned[premise_id]}); "
                "current turn depends on a removed premise — do not re-derive, halt"
            )
