# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-equivocation-when-the-quorum-certificate-is-valid-but-the-committed-result-contradicts-an-earlier-attestation.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-equivocation-when-the-quorum-certificate-is-valid-but-the-committed-result-contradicts-an-earlier-attestation.warrant.json
# Markdown SHA-256:   fcbfb766d82c052e12e8026836dce8487aff27379669e351b916d4946cff09e1
# Code AST SHA-256:   21e1fb3761c39a406536ecb07e494e52c357342b64a99ad3098c52202ee86dfe
# Verified Classes:   EquivocationDetected, JustificationGap, Attestation
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import FrozenSet


class EquivocationDetected(RuntimeError):
    """Two quorum-valid attestations for one task disagree on result_digest."""


class JustificationGap(RuntimeError):
    """Incoming attestation lacks a superseding justification for a prior QC."""


@dataclass(frozen=True)
class Attestation:
    task_id: str
    result_digest: str
    view: int
    signers: FrozenSet[str]


def classify(prior: Attestation, incoming: Attestation, quorum: int) -> str:
    if prior.task_id != incoming.task_id:
        raise ValueError("attestations are for different tasks")
    if prior.result_digest == incoming.result_digest:
        return "re_attestation" if incoming.view > prior.view else "stale"
    if len(prior.signers) < quorum or len(incoming.signers) < quorum:
        raise JustificationGap(
            f"sub-quorum attestation for {prior.task_id}: "
            f"{len(prior.signers)}/{len(incoming.signers)} < {quorum}"
        )
    overlap = prior.signers & incoming.signers
    if not overlap:
        raise JustificationGap(
            f"disjoint signer sets for {prior.task_id}; proposer omitted prior QC"
        )
    raise EquivocationDetected(
        f"task {prior.task_id}: {prior.result_digest}@{prior.view} vs "
        f"{incoming.result_digest}@{incoming.view}; "
        f"overlapping signers={sorted(overlap)}"
    )


def admit(prior: Attestation | None, incoming: Attestation, quorum: int) -> str:
    if prior is None:
        return "accept_first"
    return classify(prior, incoming, quorum)
