# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-reject-a-quorum-certificate-before-applying-it-to-a-shared-state-task-graph.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-reject-a-quorum-certificate-before-applying-it-to-a-shared-state-task-graph.warrant.json
# Markdown SHA-256:   7698fa6cb248a0c2066775670c721999fd3fefffafeac138899c617a68558499
# Code AST SHA-256:   28bd67fe48427bb7e8638cf3ddbc86a15e4500bedcd30ad14170471cba162424
# Verified Classes:   ApplicabilityError, Disposition, QC, LocalGraph
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from enum import Enum


class ApplicabilityError(RuntimeError):
    """Raised when a QC cannot be applied to the local task graph."""


class Disposition(Enum):
    APPLY = "apply"
    BUFFER = "buffer"
    HALT = "halt"
    RESYNC = "resync"


@dataclass(frozen=True)
class QC:
    node_id: str
    parent_hash: str
    payload_hash: str
    view: int
    validator_epoch: int


@dataclass
class LocalGraph:
    nodes: dict          # hash -> node
    applied: dict        # node_id -> (view, payload_hash)
    highest_view: int
    validator_epoch: int


def classify(qc: QC, g: LocalGraph) -> Disposition:
    if qc.validator_epoch != g.validator_epoch:
        return Disposition.RESYNC
    prior = g.applied.get(qc.node_id)
    if prior is not None:
        prior_view, prior_payload = prior
        if prior_view >= qc.view and prior_payload != qc.payload_hash:
            return Disposition.HALT
    if qc.parent_hash not in g.nodes:
        if qc.view > g.highest_view:
            return Disposition.RESYNC
        return Disposition.BUFFER
    return Disposition.APPLY


def apply_qc(qc: QC, g: LocalGraph) -> None:
    disp = classify(qc, g)
    if disp is Disposition.APPLY:
        g.nodes[qc.payload_hash] = qc
        g.applied[qc.node_id] = (qc.view, qc.payload_hash)
        g.highest_view = max(g.highest_view, qc.view)
        return
    if disp is Disposition.HALT:
        raise ApplicabilityError(
            f"equivocation on {qc.node_id}: applied "
            f"{g.applied[qc.node_id]} vs certified "
            f"{(qc.view, qc.payload_hash)}"
        )
    if disp is Disposition.RESYNC:
        raise ApplicabilityError(
            f"epoch/view mismatch on {qc.node_id}: "
            f"local_epoch={g.validator_epoch} qc_epoch={qc.validator_epoch} "
            f"local_view={g.highest_view} qc_view={qc.view}"
        )
    # BUFFER: caller is responsible for targeted fetch of qc.parent_hash
