# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-of-byzantine-fault-tolerant-task-commitment-fail-to-distinguish-equivocation-from-abandonment-under-partial-synchrony.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-of-byzantine-fault-tolerant-task-commitment-fail-to-distinguish-equivocation-from-abandonment-under-partial-synchrony.warrant.json
# Markdown SHA-256:   d60b91abc24731e2bd277880108c0b6a59e422122606311431d0aea353ec47a4
# Code AST SHA-256:   092ce7caa49ec9053a2f67b5f03c51589018ccb5c2057cb99b69ee1ef23a5ca5
# Verified Classes:   CommitmentDuplicationError, TaskCommitmentLedger
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class CommitmentDuplicationError(RuntimeError):
    """Raised when a task reassignment would race a valid held certificate."""
    pass


class TaskCommitmentLedger:
    def __init__(self, f: int):
        self.f = f
        self.certificates: dict[str, list[dict]] = {}
        self.negative_attestations: dict[str, set[str]] = {}

    def record_certificate(self, task_id: str, cert: dict) -> None:
        self.certificates.setdefault(task_id, []).append(cert)

    def record_negative_attestation(self, task_id: str, agent_id: str) -> None:
        self.negative_attestations.setdefault(task_id, set()).add(agent_id)

    def reassign(self, task_id: str, new_agent: str, quorum_size: int) -> None:
        held = self.certificates.get(task_id, [])
        if held:
            raise CommitmentDuplicationError(
                f"task {task_id} already has {len(held)} valid certificate(s); "
                f"reassignment to {new_agent} would duplicate commitment"
            )
        neg = self.negative_attestations.get(task_id, set())
        if len(neg) < quorum_size:
            raise CommitmentDuplicationError(
                f"task {task_id} reassignment requires {quorum_size} signed negative "
                f"attestations, only {len(neg)} present; timeouts are not evidence"
            )
        self.certificates[task_id] = [{"agent": new_agent, "kind": "reassignment"}]
