# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-lose-provenance-when-context-compaction-rewrites-a-retrieval-linked-span.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-lose-provenance-when-context-compaction-rewrites-a-retrieval-linked-span.warrant.json
# Markdown SHA-256:   2dcda1e8f64c0fbb251e0947e7596f718c4f6fdfdf66cd8fdddb63c016bdfb18
# Code AST SHA-256:   fe5ef2b757722013315502bd169fadd50a890f00545bb3280d91344e33682859
# Verified Classes:   ProvenanceResolutionError, ProvenancePointer
# Defensive Guards:   2 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass


class ProvenanceResolutionError(RuntimeError):
    """Raised when a retrieval-linked span cannot be reconstructed byte-exactly."""
    pass


@dataclass(frozen=True)
class ProvenancePointer:
    doc_id: str
    span_hash: str  # sha256 of the original source span bytes
    nominal_label: str  # human-readable chunk id, for logs only


def resolve_pointer(pointer: ProvenancePointer, source_bytes: bytes,
                    summary_bytes: bytes) -> bytes:
    """
    Reconstruct the original span. Fails closed if the pointer's hash does not
    match the source, or if the resolver was handed the post-compaction buffer
    instead of the original source.
    """
    if source_bytes is summary_bytes:
        raise ProvenanceResolutionError(
            f"pointer {pointer.nominal_label} resolved against compaction buffer, "
            "not original source; provenance is nominal-only"
        )

    actual = hashlib.sha256(source_bytes).hexdigest()
    if actual != pointer.span_hash:
        raise ProvenanceResolutionError(
            f"span hash mismatch for {pointer.nominal_label}: "
            f"expected {pointer.span_hash[:12]}, got {actual[:12]}; "
            "pointer was re-anchored during compaction"
        )
    return source_bytes


def compact_with_provenance(spans, fill_ratio, threshold=0.85):
    """
    Preserve-or-evict policy: retrieval-linked spans are never rewritten.
    If the budget is exceeded, evict them wholesale and record the drop.
    """
    if fill_ratio < threshold:
        return spans, []

    preserved, evicted = [], []
    for span in spans:
        if span.provenance is not None:
            evicted.append(span.provenance.nominal_label)  # drop, do not summarize
        else:
            preserved.append(span)
    return preserved, evicted
