# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-refuse-to-prune-its-own-context-window-the-retention-rewrite-collision.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-refuse-to-prune-its-own-context-window-the-retention-rewrite-collision.warrant.json
# Markdown SHA-256:   f175a4d3f2404818f55a09ee36eb7148e4b5ecc451bc14ba0c1f06691a65879c
# Code AST SHA-256:   decc2a93f7515adfb2d8399194332331aa76ad709fda2eabe7323ac5e219c3f1
# Verified Classes:   RetentionInvariantError
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class RetentionInvariantError(RuntimeError):
    """Raised when a self-edit would destroy the provenance of a load-bearing span."""
    pass


CONSTRAINT_MARKERS = ("do not", "must", "never", "without", "at most", "at least", "only if")


def classify(span: str) -> str:
    low = span.lower()
    if any(m in low for m in CONSTRAINT_MARKERS):
        return "constraint"
    return "narrative"


def safe_prune(span: str, relevance: float, tau: float,
               external_copy_durable: bool, scored_this_turn: bool,
               produced_by_prior_rewrite: bool) -> str:
    kind = classify(span)

    if kind == "constraint" and not external_copy_durable:
        raise RetentionInvariantError(
            f"refusing to prune constraint span without durable verbatim copy: {span!r}"
        )
    if scored_this_turn:
        raise RetentionInvariantError(
            "relevance score is self-conditioned on the rewriting turn; "
            "cannot use it to justify a destructive edit"
        )
    if produced_by_prior_rewrite:
        raise RetentionInvariantError(
            "rewrite-of-rewrite: compression error compounds and provenance is degraded"
        )

    if relevance < tau:
        return "DROP"
    return "KEEP"


def rewrite(span: str, external_copy_durable: bool) -> str:
    if classify(span) == "constraint" and not external_copy_durable:
        raise RetentionInvariantError(
            "cannot paraphrase a constraint without a verbatim external copy; "
            "paraphrase changes truth-conditional force"
        )
    return span  # placeholder for model-authored compression
