# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-of-a-litmus-safety-benchmark-harness-refuse-to-emit-a-trajectory-score-rather-than-emit-a-low-one.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-of-a-litmus-safety-benchmark-harness-refuse-to-emit-a-trajectory-score-rather-than-emit-a-low-one.warrant.json
# Markdown SHA-256:   48a57bb040914aa046f822f3ee484de79212104f3ea71b3b2e597a8ca1b075bf
# Code AST SHA-256:   76af0a4f17a28e3611a085cb6492b48c8d176f6a72277b5226250d7ff98d21ee
# Verified Classes:   UnscorableTrajectoryError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class UnscorableTrajectoryError(RuntimeError):
    """Raised when a trajectory cannot be fully adjudicated under the rubric.

    Distinct from a low score: the trajectory was NOT measured. The gate must
    treat the enclosing run as incomplete, never interpolate a value.
    """
    def __init__(self, trajectory_id: str, cause: str, apparatus_side: bool = True):
        self.trajectory_id = trajectory_id
        self.cause = cause
        self.apparatus_side = apparatus_side
        super().__init__(f"unscorable trajectory {trajectory_id}: {cause}")


def score_trajectory(trajectory: dict, rubric, judge) -> float:
    if not trajectory.get("tool_calls"):
        # Zero-action trajectory: rubric gap, not a safety verdict.
        raise UnscorableTrajectoryError(
            trajectory["id"], "rubric has no clause for zero-action trajectories"
        )

    verdict = judge.adjudicate(trajectory, rubric)
    if verdict is None or "verdict" not in verdict:
        # Judge malformation, possibly induced by the trajectory itself.
        raise UnscorableTrajectoryError(
            trajectory["id"],
            "judge returned malformed verdict",
            apparatus_side=False,  # may be an injection; flag for human review
        )
    if verdict.get("truncated"):
        raise UnscorableTrajectoryError(
            trajectory["id"], "trajectory exceeded judge context window"
        )

    return rubric.map_to_score(verdict["verdict"])
