# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-refuse-a-model-driven-prunable-eviction-plan-that-is-internally-consistent-but-not-yet-committed.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-refuse-a-model-driven-prunable-eviction-plan-that-is-internally-consistent-but-not-yet-committed.warrant.json
# Markdown SHA-256:   8310904acdd5a353272f33b13c03ffaa2f11507803e820a3a4fa0ebc27ebf962
# Code AST SHA-256:   9f77f8591429627c735e6f41fc07bd734c2fcc7531eaef66037e4bad697fd44c
# Verified Classes:   EvictionPlanRefused, Span, PlanEntry, EvictionPlan
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field


class EvictionPlanRefused(RuntimeError):
    """Raised when a prunable-eviction plan is internally consistent but uncommittable."""


@dataclass
class Span:
    span_id: str
    generation: int


@dataclass
class PlanEntry:
    span_id: str
    relevance: float
    cited_dependencies: set[str] = field(default_factory=set)


@dataclass
class EvictionPlan:
    window_generation: int
    entries: list[PlanEntry]


def validate_and_commit(plan: EvictionPlan, live_generation: int,
                        can_rescore: bool) -> list[str]:
    # Condition 2: generation must be unchanged.
    if plan.window_generation != live_generation:
        raise EvictionPlanRefused(
            f"plan generation {plan.window_generation} != live {live_generation}; defer and re-run"
        )

    # Condition 4: re-score closure must be available to verify condition 1.
    if not can_rescore:
        raise EvictionPlanRefused(
            "no re-score closure; cannot prove dependency independence; defer"
        )

    evict = {e.span_id for e in plan.entries if e.relevance < 0.5}
    retained = [e for e in plan.entries if e.span_id not in evict]

    # Condition 1: no retained span may cite an evicted span.
    orphans: list[tuple[str, set[str]]] = []
    for r in retained:
        broken = r.cited_dependencies & evict
        if broken:
            orphans.append((r.span_id, broken))

    if orphans:
        detail = "; ".join(f"{sid} depends on {sorted(deps)}" for sid, deps in orphans)
        raise EvictionPlanRefused(
            f"plan has {len(orphans)} orphaned retained span(s): {detail}. "
            f"Retain cited dependencies or re-score under post-commit state."
        )

    # Condition 3: atomic apply, not plan-order apply.
    return sorted(evict)


def try_commit(plan: EvictionPlan, live_generation: int, can_rescore: bool) -> list[str]:
    try:
        return validate_and_commit(plan, live_generation, can_rescore)
    except EvictionPlanRefused as exc:
        # Refusal is loud and non-destructive: window retained, orphan list escalated.
        raise EvictionPlanRefused(f"commit aborted, window unchanged: {exc}") from exc
