# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-boundary-be-drawn-between-adversarial-prompt-mutability-and-scoring-reproducibility-in-the-litmus-safety-benchmark-harness.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-boundary-be-drawn-between-adversarial-prompt-mutability-and-scoring-reproducibility-in-the-litmus-safety-benchmark-harness.warrant.json
# Markdown SHA-256:   ab8168b40792e31511360422f7152dfab2fe55d1301e2390ad6965ef1734bfd9
# Code AST SHA-256:   4405973972a32ecb5aac05a21f4d2f9f8bb11acab3c1a403aba1d9fb3e70486e
# Verified Classes:   ReproducibilityBoundaryError
# Defensive Guards:   5 fail-closed invariants (zero external deps)

class ReproducibilityBoundaryError(RuntimeError):
    """Raised when a LITMUS instance's scoring inputs are not fully
    re-derivable from the manifest. Fail closed: never emit a score."""
    pass


def score_instance(instance, manifest, scratch_root):
    # Invariant 1: mutation must be pre-materialized and pure.
    if instance.mutation_key not in manifest.materialized_mutations:
        raise ReproducibilityBoundaryError(
            f"instance {instance.id}: mutation not materialized in manifest "
            f"(key={instance.mutation_key!r}); cannot re-derive prompt"
        )

    # Invariant 2: instance isolation enforced at filesystem level.
    if scratch_root.exists() and any(scratch_root.iterdir()):
        raise ReproducibilityBoundaryError(
            f"instance {instance.id}: scratch root {scratch_root} non-empty at "
            f"start; cross-instance contamination possible"
        )

    # Invariant 3: judge input must be hashed verbatim in the manifest.
    judge_input = instance.build_judge_input()
    expected_hash = manifest.judge_input_hash(instance.id)
    if expected_hash is None:
        raise ReproducibilityBoundaryError(
            f"instance {instance.id}: judge input hash absent from manifest; "
            f"truncation policy may vary across replays"
        )
    if hash_text(judge_input) != expected_hash:
        raise ReproducibilityBoundaryError(
            f"instance {instance.id}: judge input hash mismatch "
            f"(expected {expected_hash}, got {hash_text(judge_input)})"
        )

    # Invariant 4: retries must be idempotent w.r.t. the manifest.
    if instance.retry_count > 0 and not instance.idempotency_token_matches(manifest):
        raise ReproducibilityBoundaryError(
            f"instance {instance.id}: non-idempotent retry "
            f"(count={instance.retry_count}); mark CONTAMINATED, do not score"
        )

    return instance.judge(judge_input)
