# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-when-a-lightweight-critic-model-is-insufficient-to-gate-an-irreversible-tool-invocation.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-when-a-lightweight-critic-model-is-insufficient-to-gate-an-irreversible-tool-invocation.warrant.json
# Markdown SHA-256:   eddfae0714f311a1f06acea91feb4c4c055fccf076a8700237ac35550599ce34
# Code AST SHA-256:   0bc3cc73b3aef2f086ddec1c588f370314d63b168f45d4e6bd74da65e3bf1846
# Verified Classes:   IrreversibleGateError, PlanStep, Plan
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field


class IrreversibleGateError(RuntimeError):
    """Raised when a plan must not pass the critic gate without human adjudication."""


@dataclass
class PlanStep:
    tool: str
    args: dict
    postcondition: str | None = None
    precondition: str | None = None
    reversible: bool = True


@dataclass
class Plan:
    steps: list[PlanStep] = field(default_factory=list)
    approved_entities: set[str] = field(default_factory=set)


def _entity_novelty(plan: Plan) -> float:
    entities = [str(v) for s in plan.steps for v in s.args.values()]
    if not entities:
        return 0.0
    novel = sum(1 for e in entities if e not in plan.approved_entities)
    return novel / len(entities)


def _has_cross_step_precondition(plan: Plan) -> bool:
    postconds = {s.postcondition for s in plan.steps if s.postcondition}
    for s in plan.steps:
        if s.precondition and s.precondition in postconds and s.precondition != s.postcondition:
            return True
    return False


def _rollback_asymmetry(plan: Plan) -> bool:
    return any(not s.reversible for s in plan.steps)


def gate(plan: Plan, critic_score: float, threshold: float = 0.85) -> bool:
    signals = {
        "entity_novelty": _entity_novelty(plan),
        "cross_step_precondition": _has_cross_step_precondition(plan),
        "rollback_asymmetry": _rollback_asymmetry(plan),
    }
    if signals["entity_novelty"] > 0.0:
        raise IrreversibleGateError(
            f"novel entity binding under familiar verb: ratio={signals['entity_novelty']:.2f}; "
            f"critic_score={critic_score} carries no evidential weight"
        )
    if signals["cross_step_precondition"]:
        raise IrreversibleGateError(
            "plan correctness depends on sibling-step postcondition; critic cannot represent dependency"
        )
    if signals["rollback_asymmetry"]:
        raise IrreversibleGateError(
            "forward effect cardinality exceeds rollback effect cardinality"
        )
    return critic_score >= threshold
