# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-lose-reversible-context-across-a-lossy-compaction-checkpoint-when-the-summarizer-s-token-budget-is-exceeded.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-lose-reversible-context-across-a-lossy-compaction-checkpoint-when-the-summarizer-s-token-budget-is-exceeded.warrant.json
# Markdown SHA-256:   0d352a0e3526b0b0e1f008d5bb455ddada0b54ca050d4881f7c609ddf1abdc50
# Code AST SHA-256:   989a09a6fba0835882263b43d6e43f197b0e0e1e904a344da20ab6f5eb95a090
# Verified Classes:   CompactionReversibilityError, Turn, Checkpoint
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import re
from dataclasses import dataclass, field
from typing import Iterable

# Conservative floor for prose; lower for structured tool output.
COMPRESSION_RATIO_LOW = 5


class CompactionReversibilityError(RuntimeError):
    """Raised when a lossy checkpoint claims coverage it cannot substantiate."""


@dataclass
class Turn:
    id: int
    anchor_token: str  # a token guaranteed to survive faithful summarization
    tokens: int


@dataclass
class Checkpoint:
    summary: str
    summary_budget: int
    covered_ids: list[int] = field(default_factory=list)
    uncovered_ids: list[int] = field(default_factory=list)


def build_checkpoint(
    turns: Iterable[Turn],
    summary: str,
    summary_budget: int,
    summarizer_reported_ids: Iterable[int] | None = None,
) -> Checkpoint:
    turns = list(turns)
    prefix_tokens = sum(t.tokens for t in turns)
    ceiling = summary_budget * COMPRESSION_RATIO_LOW

    # Path A: summarizer told us what it consumed. Trust only that.
    if summarizer_reported_ids is not None:
        reported = set(summarizer_reported_ids)
        covered = [t.id for t in turns if t.id in reported]
        uncovered = [t.id for t in turns if t.id not in reported]
        return Checkpoint(summary, summary_budget, covered, uncovered)

    # Path B: no coverage report. We may only claim coverage below the ceiling,
    # and only for turns whose anchor token actually appears in the summary.
    if prefix_tokens > ceiling:
        # Fail closed: never advance the pointer set past the ceiling.
        covered, uncovered = [], [t.id for t in turns]
        missing = [t.id for t in turns if t.anchor_token not in summary]
        if missing:
            raise CompactionReversibilityError(
                f"prefix {prefix_tokens}t > ceiling {ceiling}t and summary omits "
                f"anchors for turns {missing[:5]}; refusing to claim reversible coverage"
            )
        raise CompactionReversibilityError(
            f"prefix {prefix_tokens}t exceeds reversible ceiling {ceiling}t; "
            f"leave tail un-compacted or supply summarizer coverage report"
        )

    covered = [t.id for t in turns if t.anchor_token in summary]
    uncovered = [t.id for t in turns if t.anchor_token not in summary]
    if uncovered:
        raise CompactionReversibilityError(
            f"summary within budget but omits anchors for turns {uncovered[:5]}; "
            f"pointer set must exclude them"
        )
    return Checkpoint(summary, summary_budget, covered, uncovered)


def reconstruct(checkpoint: Checkpoint, raw_log: dict[int, str], needed_id: int) -> str:
    if needed_id in checkpoint.covered_ids:
        # Summary is authoritative for covered spans.
        return f"[from summary] fact for turn {needed_id}"
    if needed_id in checkpoint.uncovered_ids:
        # Route to raw log rather than returning a false negative.
        return raw_log[needed_id]
    raise CompactionReversibilityError(
        f"turn {needed_id} is in neither covered nor uncovered set; "
        f"pointer set is inconsistent with checkpoint"
    )
