# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-fence-speculative-tool-call-prefetching-against-non-idempotent-side-effects.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-fence-speculative-tool-call-prefetching-against-non-idempotent-side-effects.warrant.json
# Markdown SHA-256:   84a5b53cb2fe34a965831e1a0ff05d9495486c786fd64b59cf4f03f3bf3ae778
# Code AST SHA-256:   bcb19b1cdf4c4a5c3342eb197816d199978d6dd44f0860a60fc231881031d630
# Verified Classes:   EffectClass, UnsafeSpeculationError, ToolCall
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from enum import Enum


class EffectClass(Enum):
    PURE = "pure"
    IDEMPOTENT = "idempotent"
    NON_IDEMPOTENT = "non_idempotent"
    UNKNOWN = "unknown"


class UnsafeSpeculationError(RuntimeError):
    """Raised when a speculative branch is fenced over an effect it cannot undo."""


@dataclass(frozen=True)
class ToolCall:
    tool: str
    effect_class: EffectClass
    idempotency_key: str | None
    branch_id: str


def fence_discard(branch_id: str, dispatched: list[ToolCall]) -> None:
    """Discard a speculative branch, asserting every effect is safely reversible.

    A fence may only claim rollback for effects that live inside fenced runtime
    state (PURE) or that collide on a stable key (IDEMPOTENT). Anything else
    has already escaped the fence and cannot be rolled back here.
    """
    for call in dispatched:
        if call.branch_id != branch_id:
            continue
        if call.effect_class is EffectClass.PURE:
            continue
        if call.effect_class is EffectClass.IDEMPOTENT:
            if not call.idempotency_key or call.branch_id in call.idempotency_key:
                raise UnsafeSpeculationError(
                    f"idempotency key for {call.tool!r} is branch-scoped "
                    f"({call.idempotency_key!r}); replay will duplicate effect"
                )
            continue
        raise UnsafeSpeculationError(
            f"cannot fence {call.tool!r} with effect_class={call.effect_class.value}; "
            f"effect already escaped the branch and is not reversible"
        )
