# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-refuse-to-commit-the-quorum-intersection-invariant-under-partial-asynchrony-in-multi-agent-task-commitment.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-refuse-to-commit-the-quorum-intersection-invariant-under-partial-asynchrony-in-multi-agent-task-commitment.warrant.json
# Markdown SHA-256:   ab45df886307db0176f8fd44a4c23dbfb37a701f24a0bcfa6f08eee014172f9a
# Code AST SHA-256:   51d4c242904653228175b5bd418fa95020c47d78eaad0d870e57500b2b1f84d9
# Verified Classes:   UncommittableObligation, CommitmentCertificate
# Defensive Guards:   6 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass
from typing import FrozenSet


class UncommittableObligation(RuntimeError):
    """Raised when a commitment certificate is safe but not actionable.

    A certificate is actionable only if it carries an explicit, signed
    obligated set and the local view has not removed any obligated agent
    without a certified reassignment.
    """


@dataclass(frozen=True)
class CommitmentCertificate:
    task_id: str
    view: int
    signers: FrozenSet[str]
    # The obligated set MUST be signed into the proposal. If this is None,
    # the certificate is a bare quorum proof and cannot bind any agent.
    obligated_set: FrozenSet[str] | None

    def quorum_size(self, n: int) -> int:
        return 2 * (n // 3) + 1


def verify_actionable(
    cert: CommitmentCertificate,
    local_membership: FrozenSet[str],
    n: int,
    f: int,
) -> FrozenSet[str]:
    """Return the obligated set if and only if the certificate is actionable.

    Raises UncommittableObligation otherwise. This is the decision boundary:
    refuse rather than infer.
    """
    if cert.obligated_set is None:
        raise UncommittableObligation(
            f"certificate for task {cert.task_id} carries no signed obligated "
            f"set; obligated membership cannot be inferred from signers"
        )

    if len(cert.signers) < cert.quorum_size(n):
        raise UncommittableObligation(
            f"certificate for task {cert.task_id} has {len(cert.signers)} "
            f"signers, below quorum {cert.quorum_size(n)}"
        )

    # Every obligated agent must have signed the commitment. An agent that
    # is obligated but did not sign was added by inference, not by signature.
    unsigned = cert.obligated_set - cert.signers
    if unsigned:
        raise UncommittableObligation(
            f"obligated agents {sorted(unsigned)} did not sign commitment "
            f"for task {cert.task_id}; obligation is not bound to signature"
        )

    # Any obligated agent missing from local membership must have been
    # removed by a certified reassignment, not by local policy.
    removed = cert.obligated_set - local_membership
    if removed:
        raise UncommittableObligation(
            f"obligated agents {sorted(removed)} absent from local membership "
            f"view for task {cert.task_id} without certified reassignment"
        )

    return cert.obligated_set


def verify_result_intersection(
    commitment: CommitmentCertificate,
    result_signers: FrozenSet[str],
    n: int,
    f: int,
) -> None:
    """Reject results certified by a quorum disjoint from the obligation."""
    if commitment.obligated_set is None:
        raise UncommittableObligation(
            f"cannot verify result for task {commitment.task_id}: "
            f"commitment has no signed obligated set"
        )

    intersection = commitment.obligated_set & result_signers
    if len(intersection) < f + 1:
        raise UncommittableObligation(
            f"result for task {commitment.task_id} certified by quorum "
            f"intersecting obligation in {len(intersection)} agents, "
            f"below required {f + 1}; result is about a task no live "
            f"agent was bound to perform"
        )
