# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-commits-a-speculative-tool-call-side-effect-before-its-parent-branch-is-reconciled.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-commits-a-speculative-tool-call-side-effect-before-its-parent-branch-is-reconciled.warrant.json
# Markdown SHA-256:   207c9809cb90abbe4d1ae25ad7b865487bca149c2578268c4774df8ddf7a1d72
# Code AST SHA-256:   3d9d26674403495142f4fe15aee04aa895d8f85f6142ff5c611f6bec03e86210
# Verified Classes:   EffectReversibility, SpeculativeEffectViolation, EffectRecord, ReconciliationLedger
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


class EffectReversibility(Enum):
    EFFECT_FREE = "effect_free"
    REVERSIBLE = "reversible"
    IRREVERSIBLE = "irreversible"


class SpeculativeEffectViolation(RuntimeError):
    """Raised when a branch issues or attempts to reconcile an external
    effect in a way that the reconciliation model cannot soundly undo."""
    pass


@dataclass
class EffectRecord:
    branch_id: str
    tool: str
    key: str                      # identity of the effect surface (slot, user, charge id)
    reversibility: EffectReversibility
    outcome: str                  # not_issued | issued_unknown | issued_ok | issued_err
    compensation_id: Optional[str] = None


@dataclass
class ReconciliationLedger:
    effects: list = field(default_factory=list)
    winner_branch: Optional[str] = None

    def record_issue(self, rec: EffectRecord) -> None:
        if rec.outcome == "issued_unknown" and rec.reversibility is EffectReversibility.IRREVERSIBLE:
            # Unknown-outcome irreversible effects must be surfaced immediately,
            # not deferred to reconciliation, because they constrain the winner.
            raise SpeculativeEffectViolation(
                f"branch {rec.branch_id} issued irreversible effect {rec.tool} "
                f"on key {rec.key} with unknown outcome; winner plan is now constrained"
            )
        self.effects.append(rec)

    def reconcile(self, winner: str) -> list:
        self.winner_branch = winner
        winner_keys = {e.key for e in self.effects if e.branch_id == winner}
        compensations = []
        for e in self.effects:
            if e.branch_id == winner:
                continue
            if e.outcome == "not_issued":
                continue
            if e.reversibility is EffectReversibility.IRREVERSIBLE:
                raise SpeculativeEffectViolation(
                    f"cannot reconcile: loser {e.branch_id} committed irreversible "
                    f"effect {e.tool} on key {e.key}; winner must emit corrective effect"
                )
            if e.outcome == "issued_unknown":
                raise SpeculativeEffectViolation(
                    f"cannot reconcile: loser {e.branch_id} has issued_unknown effect "
                    f"{e.tool} on key {e.key}; quiesce before committing winner"
                )
            if e.key in winner_keys and e.compensation_id is None:
                raise SpeculativeEffectViolation(
                    f"cannot compensate loser {e.branch_id} on key {e.key}: winner "
                    f"mutated same key and no preconditioned compensation was recorded"
                )
            compensations.append((e.branch_id, e.tool, e.compensation_id))
        return compensations


def commit_winner(ledger: ReconciliationLedger, winner: str) -> None:
    comps = ledger.reconcile(winner)
    for branch_id, tool, comp_id in comps:
        if comp_id is None:
            raise SpeculativeEffectViolation(
                f"branch {branch_id} effect {tool} has no recorded compensation; "
                f"refusing to commit winner over unquiesced speculative effect"
            )
        # apply_compensation(comp_id) would go here; it must fail closed if
        # the compensation precondition (reservation id, version) no longer holds.
