# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-rehydration-divergence-in-auto-compacted-long-horizon-agent-sessions.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-rehydration-divergence-in-auto-compacted-long-horizon-agent-sessions.warrant.json
# Markdown SHA-256:   03baed6010239cb032e6dd913e638c5a4b27aa00eb98ad62e47660168548db69
# Code AST SHA-256:   85e581d443b53cbbc48e05bf9bcb3291621c9bf12efa192ff31d195d2efb9c00
# Verified Classes:   RehydrationDivergenceError, CompactionSignals
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Iterable


class RehydrationDivergenceError(RuntimeError):
    """Raised when summary-layer and retrieval-layer representations have diverged."""


@dataclass(frozen=True)
class CompactionSignals:
    summary_cited_ids: frozenset[str]
    store_recent_ids: frozenset[str]
    self_cited_hit_rate: float
    external_cited_hit_rate: float
    duplicate_representation_density: float
    references_per_summary: tuple[int, ...]  # oldest -> newest


def check_compaction_boundary(signals: CompactionSignals) -> str:
    """Return 'proceed', 'freeze', or 'freeze_and_rollback' per the decision boundary."""
    # Signal 1: summarizer cites IDs the retriever cannot surface.
    citation_overhang = len(signals.summary_cited_ids - signals.store_recent_ids) > 0

    # Signal 2: self-citations rehydrate worse than external queries.
    self_citation_asymmetry = (
        signals.self_cited_hit_rate < signals.external_cited_hit_rate - 0.05
    )

    # Signal 4: references-per-summary should be non-increasing across epochs.
    refs = signals.references_per_summary
    monotonicity_violated = any(
        refs[i] > refs[i - 1] for i in range(1, len(refs))
    )

    # Signal 3: redundant framing above threshold.
    duplicate_density_high = signals.duplicate_representation_density > 0.08

    if monotonicity_violated:
        return "freeze_and_rollback"

    if citation_overhang and self_citation_asymmetry:
        return "freeze"

    if citation_overhang and duplicate_density_high:
        return "freeze"

    return "proceed"


def auto_compact(signals: CompactionSignals, summarizer) -> object:
    decision = check_compaction_boundary(signals)
    if decision == "freeze_and_rollback":
        raise RehydrationDivergenceError(
            "references-per-summary increased across compaction epochs; "
            "summarizer is re-deriving citations from rehydrated content. "
            "Discard most recent epoch and reconcile before resuming."
        )
    if decision == "freeze":
        raise RehydrationDivergenceError(
            "summary layer cites artifacts the retrieval store cannot rehydrate "
            f"({len(signals.summary_cited_ids - signals.store_recent_ids)} overhang, "
            f"self-cite hit rate {signals.self_cited_hit_rate:.2f} vs external "
            f"{signals.external_cited_hit_rate:.2f}). Reconcile pass required."
        )
    return summarizer(signals)
