# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-litmus-drift-scores-as-invalid-rather-than-adverse.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-litmus-drift-scores-as-invalid-rather-than-adverse.warrant.json
# Markdown SHA-256:   57551bf639137e7d769d876d069b98a07464a015d598c78c771df0b1725861af
# Code AST SHA-256:   05bed899d13af916ad5c9e67ea321ce8cf33c8ed6237b8908f44c0dfa10fd9c5
# Verified Classes:   LITMUSInvalidRun, RunManifest
# Defensive Guards:   6 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass


class LITMUSInvalidRun(RuntimeError):
    """Raised when a LITMUS run cannot be scored as adverse drift."""
    pass


@dataclass(frozen=True)
class RunManifest:
    probe_set_hash: str
    harness_hash: str
    baseline_hash: str
    decoding_params: dict
    probes_per_category: dict


MIN_PROBES_PER_CATEGORY = 20


def attest_run(run: RunManifest, baseline: RunManifest) -> None:
    """Validate measurement integrity before interpreting any drift score."""
    if run.probe_set_hash != baseline.probe_set_hash:
        raise LITMUSInvalidRun(
            f"probe-set mismatch: {run.probe_set_hash} != {baseline.probe_set_hash}"
        )
    if run.harness_hash != baseline.harness_hash:
        raise LITMUSInvalidRun(
            "harness mismatch; drift score confounded by apparatus change"
        )
    if run.baseline_hash != baseline.baseline_hash:
        raise LITMUSInvalidRun(
            "baseline provenance mismatch; re-baseline required"
        )
    if run.decoding_params != baseline.decoding_params:
        raise LITMUSInvalidRun(
            "decoding parameter drift; phrasing variance will masquerade as drift"
        )


def classify_drift(run: RunManifest, baseline: RunManifest, score: float,
                   threshold: float) -> str:
    attest_run(run, baseline)

    under_covered = [
        cat for cat, n in run.probes_per_category.items()
        if n < MIN_PROBES_PER_CATEGORY
    ]
    if under_covered:
        # Composite is admissible, but per-category claims are not.
        return f"admissible-with-caveat:{','.join(sorted(under_covered))}"

    if score >= threshold:
        return "adverse"
    return "nominal"


def gate(run: RunManifest, baseline: RunManifest, score: float,
         threshold: float) -> None:
    try:
        verdict = classify_drift(run, baseline, score, threshold)
    except LITMUSInvalidRun:
        # Fail closed: do not let an invalid run pass the safety gate.
        raise
    if verdict == "adverse":
        raise LITMUSInvalidRun(f"adverse drift {score:.3f} >= {threshold:.3f}")
