# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-reconcile-speculative-tool-side-effects-when-optimistic-parallel-branches-are-rolled-back.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-reconcile-speculative-tool-side-effects-when-optimistic-parallel-branches-are-rolled-back.warrant.json
# Markdown SHA-256:   044d0fdaf2eb3d7204df01f345618b069d45f45ac397197ef6accba4dcf67ec3
# Code AST SHA-256:   69221c0e99081e32fa27457bf50a7a8318e511390310f00c9a2e85c5327a100f
# Verified Classes:   EffectClass, SpeculativeEffectError, Invocation
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from enum import Enum


class EffectClass(Enum):
    REVERSIBLE = "reversible"
    IDEMPOTENT = "idempotent"
    IRREVERSIBLE = "irreversible"


class SpeculativeEffectError(RuntimeError):
    """Raised when a speculative branch's effect cannot be reconciled on rollback."""


@dataclass(frozen=True)
class Invocation:
    tool: str
    logical_target: str
    argument_hash: str
    branch_id: str
    effect_class: EffectClass


def reconcile_rollback(
    losing: Invocation,
    committed: list[Invocation],
    compensator_available: bool,
) -> str:
    # Dedup key is the logical target, not the branch.
    competing = [
        c for c in committed
        if c.tool == losing.tool and c.logical_target == losing.logical_target
    ]

    if losing.effect_class is EffectClass.IRREVERSIBLE and not compensator_available:
        # Irreversible effect already left the process boundary; it cannot be
        # silently dropped, and it cannot be compensated. This is the boundary.
        raise SpeculativeEffectError(
            f"irreversible speculative effect on {losing.tool}:{losing.logical_target} "
            f"from branch {losing.branch_id} has no compensation and "
            f"{len(competing)} committed competitor(s); manual reconciliation required"
        )

    if competing and any(c.argument_hash != losing.argument_hash for c in competing):
        # Same logical target, divergent arguments: the losing branch's effect is
        # an orphan unless explicitly attributed or compensated.
        raise SpeculativeEffectError(
            f"divergent speculative invocation on {losing.tool}:{losing.logical_target}: "
            f"losing args {losing.argument_hash} vs committed "
            f"{[c.argument_hash for c in competing]}"
        )

    if losing.effect_class is EffectClass.REVERSIBLE:
        return "compensate"
    return "suppress-duplicate"
