# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-silent-loss-of-provenance-when-a-model-driven-context-pruner-rewrites-the-working-set.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-silent-loss-of-provenance-when-a-model-driven-context-pruner-rewrites-the-working-set.warrant.json
# Markdown SHA-256:   d417d9189648858ee9ef226c7a278435fbac21e3c06eab08fb76b88f5577344d
# Code AST SHA-256:   9240bf592abf660c3c0547db56ab900d77d07458b006137b3c0f5dc40da33c32
# Verified Classes:   ProvenanceInvariantError
# Defensive Guards:   5 fail-closed invariants (zero external deps)

class ProvenanceInvariantError(RuntimeError):
    """Raised when a context prune/rewrite violates a provenance invariant."""
    pass


def commit_context_edit(pre_window, post_window, durable_handle_store,
                        citation_retention_floor=0.95):
    """Gate a model-driven prune/rewrite before it replaces the working set.

    Enforces I1 (provenance monotonicity), I2 (lossless-operator purity),
    I3 (handle durability), and I5 (citation decay budget).
    """
    # I1: every surviving grounded span must resolve to a source handle.
    for span in post_window.grounded_spans():
        if not span.source_handles:
            raise ProvenanceInvariantError(
                f"ungrounded span {span.id!r} survived edit; provenance severed"
            )

    # I2: lossless operators may not mutate grounded fact values.
    pre_facts = {(s.key, s.value) for s in pre_window.grounded_spans()}
    post_facts = {(s.key, s.value) for s in post_window.grounded_spans()}
    introduced = post_facts - pre_facts
    dropped = pre_facts - post_facts
    if introduced and not post_window.operator_is_lossy:
        raise ProvenanceInvariantError(
            f"structural operator introduced new facts {introduced!r}; "
            "reclassify as 'derived' or reject"
        )
    if dropped and not post_window.operator_is_lossy:
        # allowed only with explicit supersession edges
        for key, value in dropped:
            if not post_window.has_supersession_edge(key, value):
                raise ProvenanceInvariantError(
                    f"structural operator dropped grounded fact {key}={value!r} "
                    "without supersession edge"
                )

    # I3: derived summaries must point at handles that outlive the window.
    for summary in post_window.summaries():
        for handle in summary.derived_from:
            if not durable_handle_store.resolves(handle):
                raise ProvenanceInvariantError(
                    f"summary {summary.id!r} references unresolvable handle {handle!r}"
                )

    # I5: citation-marker retention must not decay below floor.
    pre_ratio = pre_window.citation_marker_count() / max(pre_window.fact_count(), 1)
    post_ratio = post_window.citation_marker_count() / max(post_window.fact_count(), 1)
    if pre_ratio > 0 and (post_ratio / pre_ratio) < citation_retention_floor:
        raise ProvenanceInvariantError(
            f"citation retention {post_ratio / pre_ratio:.2f} below floor "
            f"{citation_retention_floor:.2f}"
        )

    return post_window
