# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-a-quorum-intersection-failure-in-multi-agent-task-commitment-before-byzantine-agreement-stalls.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-a-quorum-intersection-failure-in-multi-agent-task-commitment-before-byzantine-agreement-stalls.warrant.json
# Markdown SHA-256:   1ef379c02d0be09090e5eec66570c73136b710daf72e625ca8c2e15073fadb60
# Code AST SHA-256:   f93d8e3a9f5d20f9cccffcf9c91fba8e082a0377e15d3fb3bcd0d03bfd245bbd
# Verified Classes:   QuorumIntersectionError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class QuorumIntersectionError(RuntimeError):
    """Raised when quorum intersection is inconsistent, not merely small."""
    pass


def check_commitment_safety(vote_graph, epoch):
    # vote_graph: dict signer_id -> set of proposal digests signed this epoch
    # Returns True if safe to finalize; raises on intersection inconsistency.
    divergent = {signer: digests
                 for signer, digests in vote_graph.items()
                 if len(digests) > 1}

    if divergent:
        # Per-signer digest-set cardinality > 1 => possible equivocation.
        # This is the structural signal; the tally is irrelevant here.
        raise QuorumIntersectionError(
            f"epoch {epoch}: signer digest divergence detected: "
            f"{ {s: sorted(d) for s, d in divergent.items()} }. "
            f"Quorum intersection consistency is unproven; "
            f"refuse to finalize and trigger view-change excluding equivocators."
        )

    # Tally check happens only after consistency is established.
    total_acks = sum(len(d) for d in vote_graph.values())
    if total_acks < 5:  # 2f+1 for f=2
        # Low tally with consistent digests = liveness loss, not safety breach.
        return False
    return True


def finalize_task(vote_graph, epoch, task_id):
    if not check_commitment_safety(vote_graph, epoch):
        # Ordinary liveness path: retry / extend timeout, NOT view-change.
        raise QuorumIntersectionError(
            f"epoch {epoch} task {task_id}: insufficient acks with consistent "
            f"digests; retry path, do not escalate to safety handling."
        )
    return {"task_id": task_id, "epoch": epoch, "status": "committed"}
