# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-mutation-surface-drift-when-litmus-safety-harness-adapters-silently-re-bind-to-a-mutated-target.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-mutation-surface-drift-when-litmus-safety-harness-adapters-silently-re-bind-to-a-mutated-target.warrant.json
# Markdown SHA-256:   cba0d35cb145f032ca4b7d386d20bee46163d707beb79039b6e5f7a0df47c95d
# Code AST SHA-256:   37c02b8e9dbfa453ba84dbfdf8dc5407534217c641c97cada6a21362fdfd162f
# Verified Classes:   MutationSurfaceDriftError, BindingTrace, LitmusAdapter
# Defensive Guards:   5 fail-closed invariants (zero external deps)

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


class MutationSurfaceDriftError(RuntimeError):
    """Raised when the adapter's bound target diverges from the mutation manifest."""


@dataclass
class BindingTrace:
    target_id: str
    artifact_digest: str
    fallback_count: int = 0
    events: list = field(default_factory=list)


class LitmusAdapter:
    def __init__(self, mutation_manifest: dict, measurement_mode: str = "STRICT_IDENTITY"):
        if measurement_mode not in ("STRICT_IDENTITY", "AVAILABILITY"):
            raise ValueError(f"unknown measurement_mode: {measurement_mode}")
        self.manifest = mutation_manifest
        self.mode = measurement_mode
        self.trace: Optional[BindingTrace] = None

    def bind(self, target_id: str, artifact_bytes: bytes) -> BindingTrace:
        digest = hashlib.sha256(artifact_bytes).hexdigest()
        expected = self.manifest.get("expected_targets", {}).get(target_id)
        if self.mode == "STRICT_IDENTITY" and expected is not None and digest != expected:
            raise MutationSurfaceDriftError(
                f"bound digest {digest[:12]} for {target_id} "
                f"does not match manifest {expected[:12]}; "
                f"refusing to measure a non-mutant"
            )
        self.trace = BindingTrace(target_id=target_id, artifact_digest=digest)
        return self.trace

    def on_fallback(self, new_artifact_bytes: bytes) -> None:
        if self.trace is None:
            raise MutationSurfaceDriftError("fallback before initial bind")
        self.trace.fallback_count += 1
        new_digest = hashlib.sha256(new_artifact_bytes).hexdigest()
        if self.mode == "STRICT_IDENTITY":
            raise MutationSurfaceDriftError(
                f"transparent fallback during STRICT_IDENTITY run: "
                f"{self.trace.artifact_digest[:12]} -> {new_digest[:12]}; "
                f"session invalidated, result must be INCONCLUSIVE"
            )
        self.trace.events.append(("fallback", new_digest))

    def verdict(self, probe_passed: bool) -> str:
        if self.trace is None:
            raise MutationSurfaceDriftError("verdict requested with no binding trace")
        if self.trace.fallback_count > 0:
            return "INCONCLUSIVE"
        return "PASS" if probe_passed else "FAIL"
