# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-refuse-to-rewrite-its-own-context-the-invariant-witness-divergence-boundary-in-self-editing-context-policies.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-refuse-to-rewrite-its-own-context-the-invariant-witness-divergence-boundary-in-self-editing-context-policies.warrant.json
# Markdown SHA-256:   f9ae5c7c3f13d7c38884aa40ee4af4745dadb25bd103601184d61675bd1db802
# Code AST SHA-256:   51a2714998529881efbac3509d7dd10962123cc44b150e9023b283a031978256
# Verified Classes:   WitnessDegradationError, Invariant, Context
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Dict, List, Set


class WitnessDegradationError(RuntimeError):
    """Raised when a self-rewrite would leave an invariant uncheckable."""
    pass


@dataclass
class Invariant:
    name: str
    load_bearing: bool          # pending decision depends on it
    negative_result: bool       # witnessed only by a failed attempt / error
    witnesses: Set[str] = field(default_factory=set)  # primitive observation ids


@dataclass
class Context:
    invariants: Dict[str, Invariant]
    primitives: Set[str]        # ids of raw observations still retained


def check_rewrite(before: Context, after: Context) -> List[str]:
    """Return list of invariant names that would become uncheckable."""
    degraded = []
    for name, inv in before.invariants.items():
        after_inv = after.invariants.get(name)
        if after_inv is None:
            degraded.append(name)
            continue
        surviving = inv.witnesses & after.primitives
        if not surviving and inv.witnesses:
            degraded.append(name)
    return degraded


def apply_rewrite(before: Context, after: Context) -> Context:
    degraded = check_rewrite(before, after)
    for name in degraded:
        inv = before.invariants[name]
        # Negative results and load-bearing invariants cannot be demoted:
        # their witness is the only thing distinguishing "tried and failed"
        # from "never tried", and that distinction is not recoverable.
        if inv.load_bearing or inv.negative_result:
            raise WitnessDegradationError(
                f"rewrite would drop the only witness for invariant {name!r} "
                f"(load_bearing={inv.load_bearing}, "
                f"negative_result={inv.negative_result}); refusing"
            )
        # Non-load-bearing, non-negative: allow, but demote the assertion.
        after.invariants[name] = Invariant(
            name=name,
            load_bearing=False,
            negative_result=False,
            witnesses=set(),
        )
    return after
