# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-silent-truncation-collapse-during-hierarchical-checkpoint-compaction.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-silent-truncation-collapse-during-hierarchical-checkpoint-compaction.warrant.json
# Markdown SHA-256:   db8f6bb5451b52bd77434062ad29b154d5f3455bb163cb5d3a03e317a66f1f9e
# Code AST SHA-256:   efdd3da7e6a731ead4a969eef86873bf1031d1c978126e9a62918091c215a0d3
# Verified Classes:   RecallFloorViolation, Fact, CheckpointLevel
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Iterable


class RecallFloorViolation(RuntimeError):
    """Raised when a checkpoint level drops or degrades a fact below its declared recall floor."""
    pass


@dataclass(frozen=True)
class Fact:
    fact_id: str
    text: str
    floor_tokens: int          # minimum survivable representation, in tokens
    actionable_anchor: str     # substring that must survive to remain actionable


@dataclass
class CheckpointLevel:
    name: str
    token_budget: int
    input_tokens: int
    emitted_tokens: int
    retained: dict[str, str] = field(default_factory=dict)  # fact_id -> surviving text


def compact_level(level: CheckpointLevel, facts: Iterable[Fact], summarizer) -> CheckpointLevel:
    # Condition 1: budget-closure. The summarizer must consume the full input.
    if level.input_tokens != level.token_budget:
        raise RecallFloorViolation(
            f"{level.name}: budget-open compaction (input={level.input_tokens}, "
            f"budget={level.token_budget}); tail may be silently dropped"
        )

    level.retained = summarizer(facts, level.token_budget)
    level.emitted_tokens = sum(len(t.split()) for t in level.retained.values())

    # Condition 2: declared recall floor, checked at every level.
    for fact in facts:
        surviving = level.retained.get(fact.fact_id)
        if surviving is None:
            raise RecallFloorViolation(
                f"{level.name}: fact {fact.fact_id!r} dropped entirely; floor={fact.floor_tokens}"
            )
        if len(surviving.split()) < fact.floor_tokens:
            raise RecallFloorViolation(
                f"{level.name}: fact {fact.fact_id!r} below recall floor "
                f"({len(surviving.split())} < {fact.floor_tokens} tokens)"
            )
        # Condition 3: salience monotonicity — anchor must remain actionable.
        if fact.actionable_anchor not in surviving:
            raise RecallFloorViolation(
                f"{level.name}: fact {fact.fact_id!r} abstracted past actionability; "
                f"anchor {fact.actionable_anchor!r} lost"
            )

    # Budget check is secondary; a chain can pass budget and fail floor.
    if level.emitted_tokens > level.token_budget:
        raise RecallFloorViolation(
            f"{level.name}: emitted {level.emitted_tokens} > budget {level.token_budget}"
        )
    return level
