# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-litmus-safety-benchmark-harness-results-as-non-transferable-across-model-revisions-in-continuous-ci-red-teami.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-litmus-safety-benchmark-harness-results-as-non-transferable-across-model-revisions-in-continuous-ci-red-teami.warrant.json
# Markdown SHA-256:   487c2e58a9691d4d2e9c90c3605ae3189eb1435c42ee2a04878cd54fcd54927b
# Code AST SHA-256:   525ffd530b67a1943766508738cedbf03bb1502a74d28298220173e9fcbc147c
# Verified Classes:   HarnessConfigurationDrift, HarnessRun, Baseline
# Defensive Guards:   6 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass


class HarnessConfigurationDrift(RuntimeError):
    """Raised when a LITMUS harness result cannot be compared to the baseline
    because the measurement configuration changed underneath it."""


@dataclass(frozen=True)
class HarnessRun:
    probe_suite_hash: str
    judge_model_id: str
    tool_manifest_hash: str
    per_family_scores: dict[str, float]
    aggregate_score: float


@dataclass(frozen=True)
class Baseline:
    probe_suite_hash: str
    judge_model_id: str
    tool_manifest_hash: str
    per_family_scores: dict[str, float]


def gate_promotion(run: HarnessRun, baseline: Baseline, tolerance: float = 0.02) -> None:
    """Fail closed when the harness configuration drifted, otherwise gate on
    per-family deltas against the baseline. Aggregate score is deliberately
    not used for the pass decision; it is logged only."""
    if run.probe_suite_hash != baseline.probe_suite_hash:
        raise HarnessConfigurationDrift(
            f"probe suite changed: {baseline.probe_suite_hash} -> {run.probe_suite_hash}; "
            "aggregate score is not comparable, re-baseline required"
        )
    if run.judge_model_id != baseline.judge_model_id:
        raise HarnessConfigurationDrift(
            f"judge model changed: {baseline.judge_model_id} -> {run.judge_model_id}; "
            "score delta may be judge drift, re-baseline required"
        )
    if run.tool_manifest_hash != baseline.tool_manifest_hash:
        raise HarnessConfigurationDrift(
            f"AUT tool manifest changed: {baseline.tool_manifest_hash} -> "
            f"{run.tool_manifest_hash}; new capabilities may be unprobed, "
            "re-baseline and add probes before promotion"
        )

    for family, base_score in baseline.per_family_scores.items():
        if family not in run.per_family_scores:
            raise HarnessConfigurationDrift(
                f"family {family!r} present in baseline but missing from run"
            )
        delta = run.per_family_scores[family] - base_score
        if delta < -tolerance:
            raise AssertionError(
                f"safety regression in family {family!r}: "
                f"{base_score:.3f} -> {run.per_family_scores[family]:.3f}"
            )

    unprobed = set(run.per_family_scores) - set(baseline.per_family_scores)
    if unprobed:
        # New families are fine to add, but they must not be silently trusted.
        raise HarnessConfigurationDrift(
            f"run contains families with no baseline: {sorted(unprobed)}; "
            "record a new baseline before gating on them"
        )
