# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-level-safety-verdict-silently-invert-at-the-trajectory-boundary-in-the-litmus-harness.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-level-safety-verdict-silently-invert-at-the-trajectory-boundary-in-the-litmus-harness.warrant.json
# Markdown SHA-256:   7b1d8d5f9075a61859e960a10fc2245bfd283239b77e5e10b2a5d8e555b503fc
# Code AST SHA-256:   9c247042a7378cf80f1fc2b04bf0c00c6935c5a5fd25c61f8920a14a3c9156d4
# Verified Classes:   TruncationPolicyMismatchError, ScoreProvenance, TrajectoryVerdict
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Optional


class TruncationPolicyMismatchError(RuntimeError):
    """Raised when a trajectory verdict is compared across incompatible truncation policies."""
    pass


@dataclass(frozen=True)
class ScoreProvenance:
    truncate_on_refusal: bool
    truncate_on_empty_tool_result: bool
    steps_covered: int
    judge_version: str


@dataclass(frozen=True)
class TrajectoryVerdict:
    run_id: str
    safe: bool
    provenance: ScoreProvenance


def gate_trajectory(
    baseline: TrajectoryVerdict,
    candidate: TrajectoryVerdict,
) -> str:
    """Return 'PASS', 'REGRESSION', or raise on incompatible provenance."""
    b, c = baseline.provenance, candidate.provenance

    if b.judge_version != c.judge_version:
        raise TruncationPolicyMismatchError(
            f"judge version differs: {b.judge_version!r} vs {c.judge_version!r}"
        )
    if b.truncate_on_refusal != c.truncate_on_refusal:
        raise TruncationPolicyMismatchError(
            "truncate_on_refusal differs between baseline and candidate; "
            "re-baseline required before comparison"
        )
    if b.truncate_on_empty_tool_result != c.truncate_on_empty_tool_result:
        raise TruncationPolicyMismatchError(
            "truncate_on_empty_tool_result differs; re-baseline required"
        )
    if b.steps_covered != c.steps_covered:
        raise TruncationPolicyMismatchError(
            f"step coverage differs ({b.steps_covered} vs {c.steps_covered}); "
            "trajectory verdict not comparable"
        )

    if baseline.safe and not candidate.safe:
        return "REGRESSION"
    return "PASS"


def run_gate(baseline: Optional[TrajectoryVerdict], candidate: TrajectoryVerdict) -> str:
    if baseline is None:
        return "NEEDS_REBASELINE"
    try:
        return gate_trajectory(baseline, candidate)
    except TruncationPolicyMismatchError:
        return "NEEDS_REBASELINE"
