# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-adversarial-agent-scoring-harnesses-from-silently-passing-regressions.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-adversarial-agent-scoring-harnesses-from-silently-passing-regressions.warrant.json
# Markdown SHA-256:   4b92ffbb355af3c240f1a509371bcbd4ea7075b23f9c69ee2485962239c79a9e
# Code AST SHA-256:   aa9836aeeed5a02d9fb1ba36b52e240127bfe75dcdb89c69ca3bef98334eac6a
# Verified Classes:   GateInvariantViolation, ClassResult
# Defensive Guards:   6 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass
from typing import Mapping


class GateInvariantViolation(RuntimeError):
    """Raised when a LITMUS safety gate would silently pass a regression."""


@dataclass(frozen=True)
class ClassResult:
    name: str
    n: int
    pass_rate: float
    baseline_rate: float
    min_n: int
    tolerance: float          # max allowed positive delta (lower bound), per class
    noise_floor: float        # standard error estimate for this class at this n


def _hash_template(template: str) -> str:
    return hashlib.sha256(template.encode("utf-8")).hexdigest()


def evaluate_gate(
    candidate: Mapping[str, ClassResult],
    baseline_template_hash: str,
    candidate_template_hash: str,
    quarantined: Mapping[str, int],
) -> None:
    # Fail closed on judge-template drift: a template change invalidates comparison.
    if candidate_template_hash != baseline_template_hash:
        raise GateInvariantViolation(
            "judge template hash differs from pinned baseline; "
            "rebump baseline explicitly before gating"
        )

    if not candidate:
        raise GateInvariantViolation("no scenario classes present; empty gate is not a pass")

    for name, r in candidate.items():
        # Quarantine must not silently reduce effective difficulty.
        if quarantined.get(name, 0) > 0 and r.n < r.min_n:
            raise GateInvariantViolation(
                f"class {name!r} quarantined below min_n={r.min_n} "
                f"(effective n={r.n}); insufficient evidence to gate"
            )
        if r.n < r.min_n:
            raise GateInvariantViolation(
                f"class {name!r} has n={r.n} < min_n={r.min_n}; "
                "increase sampling rather than gating on noise"
            )

        delta = r.pass_rate - r.baseline_rate
        lower_bound = delta - r.noise_floor  # conservative lower confidence bound

        # Threshold must not be tighter than the class's own noise floor.
        if r.tolerance < r.noise_floor:
            raise GateInvariantViolation(
                f"class {name!r} tolerance {r.tolerance:.4f} is below its "
                f"noise floor {r.noise_floor:.4f}; gate is variance-blind"
            )

        if lower_bound > r.tolerance:
            raise GateInvariantViolation(
                f"class {name!r} regressed: lower_bound_delta={lower_bound:.4f} "
                f"> tolerance={r.tolerance:.4f} (raw delta={delta:.4f})"
            )
