# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-quorum-intersection-as-a-liveness-hazard-rather-than-a-safety-guarantee-in-bft-multi-agent-decision-quorums.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-quorum-intersection-as-a-liveness-hazard-rather-than-a-safety-guarantee-in-bft-multi-agent-decision-quorums.warrant.json
# Markdown SHA-256:   8825fc0202e42bc492aba9a82ace4ed9f8c0f816d5552c90a6c6670bba2cf679
# Code AST SHA-256:   cf9950b0cc1a3e75895d990c56315050e455167d75d75e776b0fbc28ad5a1166
# Verified Classes:   QuorumIntersectionLivenessError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class QuorumIntersectionLivenessError(RuntimeError):
    """Raised when quorum intersection is honest-in-theory but adversarial-in-practice:
    the guaranteed honest overlap is partitioned away from the view leader, so the
    protocol is safe but cannot make progress within the view timeout."""
    pass


def classify_view_change_stall(
    missing_agents_by_view: list[set[str]],
    honest_set: set[str],
    view_timeout_ms: int,
) -> str:
    """
    Decide whether a stalled view-change sequence is a liveness event (continue
    with backoff) or a safety-adjacent partition event (escalate to reconfiguration).

    missing_agents_by_view: for each consecutive stalled view, the set of honest
    agents that did not respond to the leader's proposal within the timeout.
    """
    if len(missing_agents_by_view) < 2:
        return "insufficient_history"

    # Stability check: if the same honest agents are missing across consecutive
    # views, the intersection is partitioned away -> escalate.
    stable = all(s == missing_agents_by_view[0] for s in missing_agents_by_view[1:])
    rotating = not stable

    # Rotating missing sets indicate equivocation-driven vote-splitting:
    # the adversary is legal (<= f faults) and backoff is the correct response.
    if rotating:
        return "liveness_equivocation_backoff"

    # Stable missing set: the honest intersection is isolated. Backoff will not
    # help because the adversary can sustain the partition. Escalate.
    isolated = missing_agents_by_view[0]
    if not isolated.issubset(honest_set):
        raise QuorumIntersectionLivenessError(
            f"Missing-agent set {isolated} contains non-honest members; "
            f"fault model violated or membership state corrupted."
        )
    if len(isolated) >= len(honest_set):
        raise QuorumIntersectionLivenessError(
            f"All honest agents isolated from leader across "
            f"{len(missing_agents_by_view)} views; quorum intersection is "
            f"adversarial-in-practice. Escalate to membership reconfiguration."
        )
    return "safety_adjacent_partition_escalate"
