# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-prunes-its-own-context-and-re-summarizes-the-summary.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-prunes-its-own-context-and-re-summarizes-the-summary.warrant.json
# Markdown SHA-256:   ef477608db7611299ca235226b1d88c9293becc02027f0f4044cf32620959e04
# Code AST SHA-256:   c6837860f7978c2e0a15f41a9338fcf31cdf202353e9ab202a2d1c815465f013
# Verified Classes:   ContextProvenanceError
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class ContextProvenanceError(RuntimeError):
    """Raised when a re-summarized context loses the provenance of a fact
    it originated, or evicts its own disconfirming evidence."""
    pass


PROTECTED_ORIGINS = {"user-said", "tool-output", "hedged", "inferred"}
MAX_SELF_SUMMARY_CYCLES = 2


def validate_compaction(summary_claims, surviving_spans, cycle_depth, pending_action_irreversible):
    """Gate a model-authored prune+re-summarize cycle.

    summary_claims: list of dicts like
        {"text": "...", "origin": "user-said", "primary_span_id": "s17"}
    surviving_spans: set of span ids still in the window.
    """
    for claim in summary_claims:
        origin = claim.get("origin")
        span_id = claim.get("primary_span_id")

        # A claim with no origin tag is laundered assertion -> fact.
        if origin not in PROTECTED_ORIGINS:
            raise ContextProvenanceError(
                f"claim lacks valid provenance tag: {claim.get('text')!r} origin={origin!r}"
            )

        # Every retained fact needs a surviving primary witness.
        if span_id is None or span_id not in surviving_spans:
            raise ContextProvenanceError(
                f"claim has no surviving primary span (span_id={span_id!r}): "
                f"{claim.get('text')!r}; summary cannot be sole witness to its own fact"
            )

    # A summary may not have evicted every span that could contradict it.
    disconfirmers = [s for s in surviving_spans if s.startswith("contra:")]
    if summary_claims and not disconfirmers and cycle_depth > 0:
        raise ContextProvenanceError(
            "compaction removed all disconfirming spans; summary is now unfalsifiable in-window"
        )

    if cycle_depth > MAX_SELF_SUMMARY_CYCLES and pending_action_irreversible:
        raise ContextProvenanceError(
            f"cycle_depth={cycle_depth} exceeds {MAX_SELF_SUMMARY_CYCLES} and action is "
            "irreversible; halt and re-ground against primary spans before proceeding"
        )

    return True
