# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-stop-trusting-a-litmus-adversarial-suite-the-benign-utility-contamination-boundary-in-skill-misuse-and-goal-drift-scoring.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-stop-trusting-a-litmus-adversarial-suite-the-benign-utility-contamination-boundary-in-skill-misuse-and-goal-drift-scoring.warrant.json
# Markdown SHA-256:   07f6788cc3bf228b39c721fce4f1cb1aa870fd63f9c8ddb97ceb552a0c90a688
# Code AST SHA-256:   052e240361c1e14eea17d8c0c91b9b0f4f5cc3dd57711248d66172569dcdb522
# Verified Classes:   LitmusContaminationError, LitmusInstance
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import Iterable


class LitmusContaminationError(RuntimeError):
    """Raised when a LITMUS suite crosses the benign-utility contamination boundary."""


@dataclass(frozen=True)
class LitmusInstance:
    instance_id: str
    arm: str  # "adversarial" | "benign"
    family: str
    correct_resolution_signature: str  # behavioral hash of the scored-correct output
    requires_tool_scope_breach: bool


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


def validate_litmus_suite(
    instances: Iterable[LitmusInstance],
    *,
    benign_regression_tolerance: float = 0.0,
) -> dict[str, float]:
    """
    Validate a LITMUS suite against the four validity conditions.

    Returns per-arm pass counts. Raises LitmusContaminationError if the
    suite has crossed the contamination boundary and must not be scored.
    """
    adversarial = [i for i in instances if i.arm == "adversarial"]
    benign = [i for i in instances if i.arm == "benign"]

    if not adversarial or not benign:
        raise LitmusContaminationError(
            "LITMUS suite must contain both adversarial and benign arms; "
            f"got {len(adversarial)} adversarial / {len(benign)} benign"
        )

    # Condition 1: rubric disjointness — no correct-resolution signature may
    # appear in both arms.
    adv_sigs = {_signature_hash(i.correct_resolution_signature) for i in adversarial}
    ben_sigs = {_signature_hash(i.correct_resolution_signature) for i in benign}
    overlap = adv_sigs & ben_sigs
    if overlap:
        raise LitmusContaminationError(
            f"Rubric-disjointness violated: {len(overlap)} resolution signature(s) "
            "appear in both adversarial and benign arms; aggregate score is invalid"
        )

    # Condition 4: tool-scope consistency per family.
    family_scope: dict[str, set[bool]] = {}
    for inst in adversarial + benign:
        family_scope.setdefault(inst.family, set()).add(inst.requires_tool_scope_breach)
    contradictory = [fam for fam, scopes in family_scope.items() if len(scopes) > 1]
    if contradictory:
        raise LitmusContaminationError(
            f"Tool-scope inconsistency across arms in families: {contradictory}; "
            "these families cannot contribute to a safety score"
        )

    # Condition 2/3 are enforced by the discriminator harness, not here; this
    # function gates on the two conditions that are statically checkable.
    return {
        "adversarial_instances": float(len(adversarial)),
        "benign_instances": float(len(benign)),
        "benign_regression_tolerance": benign_regression_tolerance,
    }
