# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-replay-divergence-as-evidence-rather-than-noise-in-deterministic-agent-trajectory-forensics.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-replay-divergence-as-evidence-rather-than-noise-in-deterministic-agent-trajectory-forensics.warrant.json
# Markdown SHA-256:   0c99c2ccb8859ad859a6f437b483c2aab1b80e877ff669f2970f211e8b8bb212
# Code AST SHA-256:   05443896dee607c21f5665a25347ca7e40905ef94922afd1a4ad2388d35afd71
# Verified Classes:   ReplayClosureError, StepRecord, ClosureReport
# Defensive Guards:   2 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass, field
from typing import Literal


class ReplayClosureError(RuntimeError):
    """Raised when a trajectory is not replay-closed and divergence cannot be attributed."""


@dataclass(frozen=True)
class StepRecord:
    step: int
    model_context_bytes: bytes
    tool_results: dict
    sampling_seed: int | None
    prng_reads: dict = field(default_factory=dict)
    clock_reads: dict = field(default_factory=dict)


@dataclass(frozen=True)
class ClosureReport:
    step: int
    status: Literal["closed", "open"]
    missing: tuple[str, ...]


def audit_closure(record: StepRecord) -> ClosureReport:
    missing: list[str] = []
    if not record.model_context_bytes:
        missing.append("model_context_bytes")
    if record.sampling_seed is None:
        missing.append("sampling_seed")
    # Tool results must be recorded, never re-executed, for non-pure tools.
    for name, result in record.tool_results.items():
        if result is None:
            missing.append(f"tool_result:{name}")
    for name in record.prng_reads:
        if record.prng_reads[name] is None:
            missing.append(f"prng_read:{name}")
    for name in record.clock_reads:
        if record.clock_reads[name] is None:
            missing.append(f"clock_read:{name}")
    status = "open" if missing else "closed"
    return ClosureReport(step=record.step, status=status, missing=tuple(missing))


def replay_and_diff(
    record: StepRecord,
    replayed_context_bytes: bytes,
    replayed_decision: str,
    recorded_decision: str,
) -> Literal["match", "divergence"]:
    report = audit_closure(record)
    if report.status == "open":
        # Divergence here is meaningless: the record is insufficient.
        raise ReplayClosureError(
            f"step {record.step} is not replay-closed; missing {report.missing}. "
            f"Divergence cannot be attributed to nondeterminism or tampering."
        )
    if hashlib.sha256(replayed_context_bytes).digest() != hashlib.sha256(
        record.model_context_bytes
    ).digest():
        # Closed but context differs: a real input-closure violation at a lower layer.
        raise ReplayClosureError(
            f"step {record.step} context hash mismatch despite closed audit; "
            f"serialization layer is non-canonical."
        )
    return "match" if replayed_decision == recorded_decision else "divergence"
