# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-when-hierarchical-summarization-has-silently-invalidated-a-checkpoint-s-causal-frontier.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-when-hierarchical-summarization-has-silently-invalidated-a-checkpoint-s-causal-frontier.warrant.json
# Markdown SHA-256:   d5138a511a020b5be585501d90cbdc20e13d3960090d928ab6920c3cadb1f4b8
# Code AST SHA-256:   38fb37f443d725e28b0df2c56c4ad2226ff2152ffc95c1ee22672f60027b0681
# Verified Classes:   FrontierInvalidationError, Edge, Checkpoint, EventLog
# Defensive Guards:   5 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Iterable


class FrontierInvalidationError(RuntimeError):
    """Raised when a recovered checkpoint's causal frontier cannot be verified
    against the monotonic event log. Signals that the checkpoint must not be
    used to authorize irreversible actions."""


@dataclass(frozen=True)
class Edge:
    action_id: str
    precondition_id: str


@dataclass
class Checkpoint:
    frontier_lo: int
    frontier_hi: int
    asserted_edges: frozenset[Edge]
    summary_text: str = ""
    # Content metrics that are necessary-but-insufficient; kept for logging.
    entity_coverage: float = 1.0
    contradiction_score: float = 0.0


@dataclass
class EventLog:
    events: list[tuple[int, str]] = field(default_factory=list)  # (seq, kind)
    edges: set[Edge] = field(default_factory=set)

    def max_seq(self) -> int:
        return max((s for s, _ in self.events), default=0)

    def edges_in_range(self, lo: int, hi: int) -> set[Edge]:
        # In a real system edges carry seq numbers; simplified here.
        return {e for e in self.edges}

    def has_authorization_in_gap(self, lo: int, hi: int) -> bool:
        return any(lo < s <= hi and kind == "authorization" for s, kind in self.events)


def verify_frontier(ckpt: Checkpoint, log: EventLog) -> None:
    """Raise FrontierInvalidationError if the checkpoint's frontier is unsafe.

    Content metrics are deliberately ignored: a checkpoint can have perfect
    entity coverage and zero contradiction while still being frontier-invalid.
    """
    log_edges = log.edges_in_range(ckpt.frontier_lo, ckpt.frontier_hi)

    added = ckpt.asserted_edges - log_edges
    if added:
        # Summarizer hallucinated causality: over-authorization risk. Hard fail.
        raise FrontierInvalidationError(
            f"checkpoint asserts {len(added)} edge(s) absent from event log "
            f"(hallucinated causality); over-authorization risk: {sorted(added)}"
        )

    dropped = log_edges - ckpt.asserted_edges
    if dropped:
        # Edges were lost during hierarchical merge: under-authorization risk.
        raise FrontierInvalidationError(
            f"checkpoint dropped {len(dropped)} causal edge(s) during summarization; "
            f"under-authorization risk: {sorted(dropped)}"
        )

    if (
        ckpt.frontier_hi < log.max_seq()
        and log.has_authorization_in_gap(ckpt.frontier_hi, log.max_seq())
    ):
        raise FrontierInvalidationError(
            f"checkpoint stale on authorization: frontier_hi={ckpt.frontier_hi} "
            f"< max_seq={log.max_seq()} with authorization events in gap"
        )


def authorize_action(ckpt: Checkpoint, log: EventLog, action_id: str) -> None:
    """Gate an irreversible action behind frontier verification."""
    try:
        verify_frontier(ckpt, log)
    except FrontierInvalidationError:
        # Do not proceed; caller must re-expand from a verified checkpoint.
        raise
    if not any(e.action_id == action_id for e in ckpt.asserted_edges):
        raise FrontierInvalidationError(
            f"action {action_id!r} not authorized by any verified edge"
        )
