# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-equivocates-across-shard-boundaries-in-heterogeneous-llm-agent-bft-consensus.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-equivocates-across-shard-boundaries-in-heterogeneous-llm-agent-bft-consensus.warrant.json
# Markdown SHA-256:   e6a359d1d684ad0d5415d9692dcce7da304b8abe431d5fa15ab389233cf4db21
# Code AST SHA-256:   543ae0634eb7520bd55c2ff7ccba9dc0bf72df09f6dea032d899c49cd287d63d
# Verified Classes:   ShardCoverageError, Commitment
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from collections import defaultdict


class ShardCoverageError(RuntimeError):
    """Raised when the union of (agent, shard) commitments does not cover
    every shard with at least `quorum` distinct signers."""
    pass


@dataclass(frozen=True)
class Commitment:
    agent_id: str
    shard_id: int
    result_hash: str


def verify_shard_union(
    commitments: list[Commitment],
    shard_count: int,
    quorum: int,
) -> None:
    """
    Enforce the shard-union invariant: every shard must be signed by at
    least `quorum` distinct agents, and no agent may sign two different
    result hashes for the same shard (per-shard equivocation).

    Raises ShardCoverageError on coverage failure or per-shard equivocation.
    """
    signers_per_shard: dict[int, set[str]] = defaultdict(set)
    hashes_per_agent_shard: dict[tuple[str, int], set[str]] = defaultdict(set)

    for c in commitments:
        signers_per_shard[c.shard_id].add(c.agent_id)
        hashes_per_agent_shard[(c.agent_id, c.shard_id)].add(c.result_hash)

    # Per-shard equivocation check (the weak check most detectors implement).
    for (agent_id, shard_id), hashes in hashes_per_agent_shard.items():
        if len(hashes) > 1:
            raise ShardCoverageError(
                f"agent {agent_id} equivocated on shard {shard_id}: "
                f"{sorted(hashes)}"
            )

    # Shard-union coverage check (the invariant that is usually missed).
    for shard_id in range(shard_count):
        signers = signers_per_shard.get(shard_id, set())
        if len(signers) < quorum:
            raise ShardCoverageError(
                f"shard {shard_id} covered by only {len(signers)} distinct "
                f"agents ({sorted(signers)}); quorum requires {quorum}. "
                f"Union of commitments does not satisfy the shard-coverage "
                f"invariant."
            )
