# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-hierarchical-summarization-compaction-as-a-lossy-recall-budget-violation-rather-than-a-routine-maintenance-ev.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-hierarchical-summarization-compaction-as-a-lossy-recall-budget-violation-rather-than-a-routine-maintenance-ev.warrant.json
# Markdown SHA-256:   48b5b5ccf80eea52acf2ad325b43ce54c6a7da8a1fab66ff8135bf4c6c3ee20a
# Code AST SHA-256:   1f7a692ae97198ed30574dfede596d0b6f0c42274b6623aee2f5ee5a1ee006c6
# Verified Classes:   RecallBudgetViolation
# Defensive Guards:   1 fail-closed invariants (zero external deps)

class RecallBudgetViolation(RuntimeError):
    """Raised when a compaction fold would drop a discriminating predicate
    below the recall floor required by an open task."""
    pass


DISCRIMINATING_MARKERS = ("not ", "do not", "because", "v", "ms", "id:", "->")


def classify_salience(span_text: str) -> str:
    lowered = span_text.lower()
    if any(marker in lowered for marker in DISCRIMINATING_MARKERS):
        return "discriminating"
    if any(ch.isdigit() for ch in span_text):
        return "discriminating"
    return "narrative"


def fold_span(span_text: str, target_tier: str, recall_floor: int, retained_refs: int) -> str:
    salience = classify_salience(span_text)
    if salience == "discriminating" and retained_refs < recall_floor:
        raise RecallBudgetViolation(
            f"Refusing to fold discriminating span into tier '{target_tier}': "
            f"retained_refs={retained_refs} < recall_floor={recall_floor}. "
            f"Retain a pointer to the raw span or raise the budget before compacting."
        )
    # Safe path: narrative span, or discriminating span with sufficient retained refs.
    return f"[{target_tier}] {span_text[:80]}..."


def compact(spans: list[str], target_tier: str, recall_floor: int, retained_refs: int) -> list[str]:
    return [fold_span(s, target_tier, recall_floor, retained_refs) for s in spans]
