# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-of-a-self-editing-context-policy-fail-to-reclaim-tokens-when-pruning-stale-reasoning-traces-under-a-hard-budget.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-of-a-self-editing-context-policy-fail-to-reclaim-tokens-when-pruning-stale-reasoning-traces-under-a-hard-budget.warrant.json
# Markdown SHA-256:   cf705823a39b3ca833c7c57679d180e1c40b0ca6934003596cf8f3066568f178
# Code AST SHA-256:   a259912ce2034aa1ac366243fe57291b4ea7a12595151935f03dc16e8645ace2
# Verified Classes:   PruneBudgetError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class PruneBudgetError(RuntimeError):
    """Raised when a model-driven prune pass cannot reclaim budget net of its own overhead."""
    pass


def evaluate_prune_pass(
    utilization: int,
    budget: int,
    stale_candidate_sizes: list[int],
    index_size: int,
    expected_rationale_size: int,
    overhead_margin_factor: float = 1.5,
) -> bool:
    """
    Return True iff a model-driven prune pass is net-positive and safe to run.
    Raises PruneBudgetError when the policy is in the decision-in-band failure state:
    headroom exists but is smaller than the cost of deciding what to prune.
    """
    headroom = budget - utilization
    overhead = index_size + expected_rationale_size
    reclaimable = sum(stale_candidate_sizes) - overhead
    margin = overhead_margin_factor * expected_rationale_size

    if headroom < 0:
        raise PruneBudgetError(
            f"utilization {utilization} exceeds hard budget {budget}; "
            "model-driven pruning cannot run inside an over-budget window"
        )

    if headroom < overhead:
        # Decision-in-band: deciding costs more than the available slack.
        # A model-driven pass here is net-negative by construction.
        raise PruneBudgetError(
            f"decision-in-band: headroom {headroom} < overhead {overhead}; "
            "defer to non-model-driven eviction (recency/TTL/truncate)"
        )

    if reclaimable <= margin:
        # Even with slack, the pass does not clear the margin. Refuse rather than
        # emit a rationale that becomes a new prunable trace.
        raise PruneBudgetError(
            f"insufficient net reclamation: reclaimable {reclaimable} <= margin {margin}; "
            "prune pass would feed its own rationale back into the candidate set"
        )

    return True


def run_prune_pass(ctx, policy) -> None:
    try:
        ok = evaluate_prune_pass(
            utilization=ctx.utilization,
            budget=policy.budget,
            stale_candidate_sizes=policy.stale_sizes(ctx),
            index_size=policy.index_size(ctx),
            expected_rationale_size=policy.expected_rationale_size,
        )
    except PruneBudgetError as e:
        policy.log_boundary_event(e)
        policy.fallback_evict(ctx)  # recency/TTL/truncate, no model call
        return
    if ok:
        policy.model_driven_evict(ctx)
