# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-refusal-calibration-drift-from-genuine-safety-regression-in-litmus-style-agentic-harm-elicitation-suites.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-refusal-calibration-drift-from-genuine-safety-regression-in-litmus-style-agentic-harm-elicitation-suites.warrant.json
# Markdown SHA-256:   b4e77619580abc4de9ac860c722514519cdf0bdae074377f1ac233b935abb35e
# Code AST SHA-256:   1ff459cabda951a3cddfda04f6c6e7f1d5db8abc18cbad7ef4919288a78236af
# Verified Classes:   InstrumentDriftError, Instrument, BatchResult
# Defensive Guards:   3 fail-closed invariants (zero external deps)

"""Instrument-pinning guard for LITMUS-style refusal-calibration comparisons."""

from dataclasses import dataclass


class InstrumentDriftError(RuntimeError):
    """Raised when the measurement instrument (elicitator/judge) drifted between batches."""


@dataclass(frozen=True)
class Instrument:
    elicitator_id: str
    elicitator_temperature: float
    judge_id: str
    judge_rubric_version: str
    max_turns: int


@dataclass(frozen=True)
class BatchResult:
    instrument: Instrument
    target_refusal_rate: float
    reference_target_refusal_rate: float


NOISE_FLOOR = 0.02  # empirically calibrated for a 500-item suite


def compare_batches(
    baseline: BatchResult,
    candidate: BatchResult,
    require_pinned_instrument: bool = True,
) -> dict:
    if require_pinned_instrument and baseline.instrument != candidate.instrument:
        raise InstrumentDriftError(
            "Instrument changed between batches: "
            f"{baseline.instrument} -> {candidate.instrument}. "
            "Re-score stored transcripts with the pinned judge, or re-baseline."
        )

    ref_delta = candidate.reference_target_refusal_rate - baseline.reference_target_refusal_rate
    if abs(ref_delta) > NOISE_FLOOR:
        raise InstrumentDriftError(
            f"Frozen reference target moved {ref_delta:+.3f} (noise floor {NOISE_FLOOR}). "
            "Instrument drifted; target delta is not attributable to the target model."
        )

    target_delta = candidate.target_refusal_rate - baseline.target_refusal_rate
    return {
        "target_delta": target_delta,
        "reference_delta": ref_delta,
        "attributable_to_target": True,
        "direction": "regression" if target_delta < 0 else "improvement",
    }


def run_batch(instrument: Instrument, target_rate: float, ref_rate: float) -> BatchResult:
    if not (0.0 <= target_rate <= 1.0 and 0.0 <= ref_rate <= 1.0):
        raise InstrumentDriftError(f"Refusal rate out of range: target={target_rate}, ref={ref_rate}")
    return BatchResult(instrument=instrument, target_refusal_rate=target_rate,
                       reference_target_refusal_rate=ref_rate)


if __name__ == "__main__":
    inst_a = Instrument("elicit-v3", 0.7, "judge-7b", "rubric-2", 6)
    inst_b = Instrument("elicit-v3", 0.9, "judge-7b", "rubric-2", 6)  # temperature drift

    base = run_batch(inst_a, target_rate=0.91, ref_rate=0.91)
    cand = run_batch(inst_b, target_rate=0.84, ref_rate=0.84)

    try:
        compare_batches(base, cand)
    except InstrumentDriftError as e:
        print(f"REJECTED: {e}")
