# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-classify-a-multi-agent-stall-as-livelock-rather-than-deadlock.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-classify-a-multi-agent-stall-as-livelock-rather-than-deadlock.warrant.json
# Markdown SHA-256:   169e894cde084aecc7449928c96df6d8a26672e6c868d4bedc6e9d1eb3d4afba
# Code AST SHA-256:   eb57347da57d0e0caaf45eb986108bd92ba9d728f7476d7af0e9de77facc6b70
# Verified Classes:   StallClassificationError, StallKind, Tick
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from enum import Enum
from typing import Sequence


class StallClassificationError(RuntimeError):
    """Raised when a stall cannot be classified under the validity conditions."""


class StallKind(Enum):
    DEADLOCK = "deadlock"
    LIVELOCK = "livelock"
    SLOW_PROGRESS = "slow_progress"
    GOAL_DRIFT = "goal_drift"
    INDETERMINATE = "indeterminate"


@dataclass(frozen=True)
class Tick:
    enabled_transitions: int
    fired: int
    state_hash: int
    goal_distance: float
    has_wait_cycle: bool


def classify_stall(window: Sequence[Tick], epsilon: float = 0.01) -> StallKind:
    if len(window) < 3:
        raise StallClassificationError(
            f"window of {len(window)} ticks too short to observe an orbit period"
        )

    fired_every_tick = all(t.fired >= 1 for t in window)
    enabled_every_tick = all(t.enabled_transitions > 0 for t in window)
    cycle_present = any(t.has_wait_cycle for t in window)

    # Deadlock: wait-for cycle AND nothing can fire.
    if cycle_present and not enabled_every_tick and not fired_every_tick:
        return StallKind.DEADLOCK

    # Recurrence: state hash seen at least twice with a gap of >= 2.
    hashes = [t.state_hash for t in window]
    recurring = any(
        hashes[i] == hashes[j] for i in range(len(hashes)) for j in range(i + 2, len(hashes))
    )

    d0, d1 = window[0].goal_distance, window[-1].goal_distance
    improving = (d0 - d1) > epsilon
    converging_wrong = (d0 - d1) > epsilon and d1 > 0.10  # acceptance threshold

    # Livelock: alive (firing) but orbiting without goal progress.
    if fired_every_tick and enabled_every_tick and recurring and not improving:
        return StallKind.LIVELOCK

    # Goal drift: converging, but not toward acceptance.
    if converging_wrong and recurring:
        return StallKind.GOAL_DRIFT

    if improving:
        return StallKind.SLOW_PROGRESS

    raise StallClassificationError(
        "stall did not satisfy any classification invariant; "
        "abstain and surface raw window rather than firing an intervention"
    )


def choose_intervention(kind: StallKind) -> str:
    if kind is StallKind.LIVELOCK:
        return "semantic_perturbation"   # machinery is alive; it will consume it
    if kind is StallKind.DEADLOCK:
        return "break_cycle"             # perturbation cannot be consumed
    if kind is StallKind.GOAL_DRIFT:
        return "halt_and_reanchor"       # more compute deepens the wrong fixed point
    if kind is StallKind.INDETERMINATE:
        raise StallClassificationError("cannot act on an indeterminate classification")
    return "no_intervention"             # SLOW_PROGRESS: let it run
