# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-when-model-driven-context-pruning-has-silently-invalidated-a-tool-call-dependency-chain.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-when-model-driven-context-pruning-has-silently-invalidated-a-tool-call-dependency-chain.warrant.json
# Markdown SHA-256:   857ffd856b8f3aacd226ac02559db127216c82bd029fe70bd555c58c695faff0
# Code AST SHA-256:   faa3e02f837f46d9c5fb363cfc745fd43b6e5fdd2fc4beef0f382ecdec616dd7
# Verified Classes:   ContextClosureError, Span, EvictionResult
# Defensive Guards:   2 fail-closed invariants (zero external deps)

import re
from dataclasses import dataclass, field

HANDLE_RE = re.compile(r"\b(?:run|job|sess|doc)_[0-9a-f]{4,}\b")
DEF_RE = re.compile(r'"id"\s*:\s*"((?:run|job|sess|doc)_[0-9a-f]{4,})"')


class ContextClosureError(RuntimeError):
    """Raised when an eviction cycle leaves a retained span referencing an evicted binding."""


@dataclass
class Span:
    text: str
    score: float
    pinned: bool = False


@dataclass
class EvictionResult:
    retained: list = field(default_factory=list)
    evicted: list = field(default_factory=list)
    margin: float = 0.0


def evict_by_relevance(spans: list, budget_tokens: int, margin_floor: float = 0.03) -> EvictionResult:
    """Score-based eviction with a structural closure gate. Raises on closure violation."""
    ranked = sorted(spans, key=lambda s: (s.pinned, s.score), reverse=True)
    retained, evicted, used = [], [], 0
    for s in ranked:
        cost = len(s.text.split())
        if s.pinned or used + cost <= budget_tokens:
            retained.append(s)
            used += cost
        else:
            evicted.append(s)

    if not evicted:
        return EvictionResult(retained, evicted, margin=1.0)

    margin = min(s.score for s in retained) - max(s.score for s in evicted)

    defined = set()
    for s in retained:
        defined.update(DEF_RE.findall(s.text))
    referenced = set()
    for s in retained:
        referenced.update(HANDLE_RE.findall(s.text))

    dangling = referenced - defined
    if dangling:
        raise ContextClosureError(
            f"eviction would orphan references {sorted(dangling)}; "
            f"margin={margin:.3f} (floor {margin_floor}). "
            f"Externalize these bindings as tombstones or evict their dependency components atomically."
        )

    if margin < margin_floor:
        # Leading indicator: ranking is near-arbitrary; throttle rather than trust it.
        raise ContextClosureError(
            f"eviction margin {margin:.3f} below floor {margin_floor}: scorer cannot "
            f"distinguish load-bearing spans; defer pruning or raise threshold."
        )

    return EvictionResult(retained, evicted, margin=margin)
