# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-equivocation-collapse-in-multi-agent-task-commitment-under-partial-synchrony.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-equivocation-collapse-in-multi-agent-task-commitment-under-partial-synchrony.warrant.json
# Markdown SHA-256:   e73404691236d6fcf1bdc533887d67bfe73b52e11b443899e9df7b8557cb9531
# Code AST SHA-256:   3d5cb08604672f7f991e611f6b3f52f286bddf12ffd91ee2f028e9b451ae0b7e
# Verified Classes:   EquivocationCollapseError, SignedPrepare
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Dict, Tuple

class EquivocationCollapseError(RuntimeError):
    """Raised when a commitment certificate lacks per-agent epoch-scoped uniqueness."""
    pass

@dataclass(frozen=True)
class SignedPrepare:
    agent_id: str
    task_id: str
    epoch: int
    value: str  # "COMMIT" | "ABORT"

def validate_certificate(sigs: list[SignedPrepare], f: int) -> str:
    """Certify a task commitment ONLY with per-agent tuple uniqueness.

    Quorum size alone is insufficient: a Byzantine agent can sign both
    COMMIT and ABORT for the same (task, epoch) and satisfy two disjoint
    quorums. Reject on any duplicate (agent, task, epoch) tuple.
    """
    if len(sigs) < 2 * f + 1:
        raise EquivocationCollapseError(
            f"undersized certificate: {len(sigs)} < {2*f+1}"
        )
    seen: Dict[Tuple[str, str, int], str] = {}
    for s in sigs:
        key = (s.agent_id, s.task_id, s.epoch)
        if key in seen:
            if seen[key] != s.value:
                # Fraud proof: same agent, same epoch, conflicting values.
                raise EquivocationCollapseError(
                    f"equivocation on {key}: {seen[key]!r} vs {s.value!r}"
                )
            # Duplicate identical signature is a replay, not a new vote.
            raise EquivocationCollapseError(f"replay of {key}")
        seen[key] = s.value
    values = {s.value for s in sigs}
    if len(values) != 1:
        raise EquivocationCollapseError(f"mixed values in certificate: {values}")
    return values.pop()
