# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-refuse-model-driven-pruning-of-intermediate-reasoning-that-is-load-bearing-for-later-tool-calls.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-refuse-model-driven-pruning-of-intermediate-reasoning-that-is-load-bearing-for-later-tool-calls.warrant.json
# Markdown SHA-256:   5ef7a8a5e84b5e2d2d5c7e2cfa9efeebb6fe19489c80b48575b2f60060ae4c5d
# Code AST SHA-256:   ee0e0e55053c69aa3f618bd9476bb0478c8a69d2255dd04fbcd32dbab13b0fd7
# Verified Classes:   RegenerabilityClass, UnsafePruneError, ContextMessage
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from enum import Enum


class RegenerabilityClass(Enum):
    DETERMINISTIC = "deterministic"      # re-run same tool, same bytes
    DURABLE_POINTER = "durable_pointer"  # externalized, pointer survives
    IRREDUCIBLE = "irreducible"          # must never be pruned


class UnsafePruneError(RuntimeError):
    """Raised when a prune would evict context that cannot be regenerated."""


@dataclass
class ContextMessage:
    id: str
    content: str
    regenerability: RegenerabilityClass
    is_negative_result: bool = False
    is_one_time_secret: bool = False
    referenced_by_pending_step: bool = False


def classify(msg: ContextMessage) -> RegenerabilityClass:
    # Negative results and one-time secrets are irreducible regardless of tags.
    if msg.is_negative_result or msg.is_one_time_secret:
        return RegenerabilityClass.IRREDUCIBLE
    return msg.regenerability


def safe_prune(messages: list[ContextMessage], candidate_ids: set[str]) -> list[ContextMessage]:
    """Evict only messages that are provably reconstructible.

    Refuses the whole prune if any candidate is irreducible or referenced
    by a pending step -- partial pruning of a dependency set is unsafe.
    """
    unsafe = []
    for msg in messages:
        if msg.id not in candidate_ids:
            continue
        cls = classify(msg)
        if cls is RegenerabilityClass.IRREDUCIBLE:
            unsafe.append((msg.id, "irreducible"))
        elif msg.referenced_by_pending_step:
            unsafe.append((msg.id, "referenced_by_pending_step"))
        elif cls is RegenerabilityClass.DURABLE_POINTER and not msg.content:
            unsafe.append((msg.id, "pointer_lost"))

    if unsafe:
        raise UnsafePruneError(
            f"refusing prune: {len(unsafe)} candidate(s) not safely evictable: {unsafe}"
        )

    return [m for m in messages if m.id not in candidate_ids]


def prune_round(messages: list[ContextMessage], model_ranked_ids: list[str]) -> list[ContextMessage]:
    # The model proposes; the oracle disposes.
    proposed = set(model_ranked_ids)
    return safe_prune(messages, proposed)


if __name__ == "__main__":
    msgs = [
        ContextMessage("m1", "ls output", RegenerabilityClass.DETERMINISTIC),
        ContextMessage("m2", "OOM not from shell: ulimit unlimited",
                       RegenerabilityClass.IRREDUCIBLE, is_negative_result=True),
        ContextMessage("m3", "nonce=8f3a...", RegenerabilityClass.IRREDUCIBLE,
                       is_one_time_secret=True),
    ]
    try:
        prune_round(msgs, ["m1", "m2", "m3"])
    except UnsafePruneError as exc:
        print(f"blocked: {exc}")
    # Only m1 is safe to evict.
    kept = prune_round(msgs, ["m1"])
    assert [m.id for m in kept] == ["m2", "m3"]
