# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-refuse-to-auto-compact-a-lossy-summary-when-the-transcript-contains-unresolved-tool-call-obligations.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-refuse-to-auto-compact-a-lossy-summary-when-the-transcript-contains-unresolved-tool-call-obligations.warrant.json
# Markdown SHA-256:   923d378cba8ee27e3f1f052aa4181cbaecb4e934fbf76357373616157b99ec76
# Code AST SHA-256:   3cc1260f36473b2ea9e954d3b84d5ae974a638f4cc6dfca4d64b283e5948e7c6
# Verified Classes:   OpenObligationError, ToolCall, Span
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Optional


class OpenObligationError(RuntimeError):
    """Raised when compaction would sever a tool call from its resolution."""
    pass


@dataclass
class ToolCall:
    call_id: str
    name: str
    resolved: bool = False


@dataclass
class Span:
    calls: list[ToolCall] = field(default_factory=list)
    live_identifiers: set[str] = field(default_factory=set)

    def is_obligation_closed(self) -> bool:
        return all(c.resolved for c in self.calls)

    def open_obligations(self) -> list[str]:
        return [c.call_id for c in self.calls if not c.resolved]


def compact(span: Span, budget_critical: bool, older_closed_span: Optional[Span]) -> str:
    if span.is_obligation_closed():
        # Safe: lossy summary over a quiescent span.
        return _summarize(span)

    if not budget_critical and older_closed_span is not None:
        # Defer: absorb budget pressure from an older, closed span.
        return _summarize(older_closed_span)

    # Refuse: compacting here would drop a live obligation into prose.
    raise OpenObligationError(
        f"refusing lossy compaction: unresolved tool calls {span.open_obligations()}"
    )


def _summarize(span: Span) -> str:
    # Placeholder for the model-backed summarizer.
    return f"<summary of {len(span.calls)} calls; live ids={sorted(span.live_identifiers)}>"
