# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-refuse-to-learn-an-edit-retain-policy-the-budget-coupled-credit-assignment-trap-in-self-editing-context-managers.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-refuse-to-learn-an-edit-retain-policy-the-budget-coupled-credit-assignment-trap-in-self-editing-context-managers.warrant.json
# Markdown SHA-256:   4c9d987150350eb5bb3aff7e57a35ba47631804bb7b520379acd7497345c0158
# Code AST SHA-256:   b4a3a6c3c410f42d53f5398ee58ab08d395a2e1ddc3b0538458e7ef4ac24234c
# Verified Classes:   BudgetCreditAssignmentError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class BudgetCreditAssignmentError(RuntimeError):
    """Raised when an edit/retain policy update is attributed to terminal reward
    under a hard budget, making the signal invalid for policy learning."""
    pass


def validate_policy_update(
    in_loop_reward_gain: float,
    relaxed_budget_delta: float,
    edit_prob_budget_correlation: float,
    hard_budget: bool,
    on_policy_only: bool,
    max_negative_relaxed_delta: float = 0.0,
    max_abs_anti_correlation: float = 0.5,
) -> None:
    """Gate a proposed edit/retain policy promotion.

    Refuses promotion when the observed gain is a budget-truncation artifact.
    """
    if not hard_budget or not on_policy_only:
        # Signal is valid; no truncation-mediated credit assignment possible.
        return

    if relaxed_budget_delta < -max_negative_relaxed_delta:
        raise BudgetCreditAssignmentError(
            f"Rejecting promotion: in-loop gain {in_loop_reward_gain:+.2f} but "
            f"relaxed-budget delta {relaxed_budget_delta:+.2f} is negative. "
            f"The gain is truncation, not selection."
        )

    if abs(edit_prob_budget_correlation) > max_abs_anti_correlation and \
            edit_prob_budget_correlation < 0:
        raise BudgetCreditAssignmentError(
            f"Rejecting promotion: edit probability anti-correlates with "
            f"remaining budget (rho={edit_prob_budget_correlation:+.2f}). "
            f"Policy is pre-truncating the high-variance tail."
        )

    if in_loop_reward_gain > 0 and relaxed_budget_delta <= 0:
        raise BudgetCreditAssignmentError(
            "Rejecting promotion: gain does not survive budget relaxation; "
            "hold downstream feasible set fixed before attributing reward."
        )
