# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-when-model-authored-context-rewrites-cross-the-lossy-diff-boundary.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-when-model-authored-context-rewrites-cross-the-lossy-diff-boundary.warrant.json
# Markdown SHA-256:   e6b5ff4b81176d3c4d674cfb62dd958f5ebb91a71312bee4db565ce8b2e03b65
# Code AST SHA-256:   829d5b6dd967c2658a4b396ccf385d667dc4c550c274115f235518edbe175a39
# Verified Classes:   ReferentGraphViolation, RewriteCandidate
# Defensive Guards:   5 fail-closed invariants (zero external deps)

import re
from dataclasses import dataclass

DEICTIC = re.compile(r"\b(the above|that|as noted|the prior|aforementioned)\b", re.I)
CONSTRAINT = re.compile(r"\b(must not|do not|only if|never|MAX_\w+=\d+)\b", re.I)
IDENT = re.compile(r"\b[A-Z][A-Z0-9_]{2,}\b")


class ReferentGraphViolation(RuntimeError):
    """Raised when a model-authored context rewrite would orphan references."""


@dataclass
class RewriteCandidate:
    before: str
    after: str
    declared_span: tuple[int, int]
    touched_span: tuple[int, int]
    intent: str  # "prune" | "summarize" | "rewrite"


def _antecedent_present(token: str, context: str) -> bool:
    return token.lower() in context.lower()


def validate_rewrite(cand: RewriteCandidate) -> None:
    # Signal 1: fan-out beyond declared span.
    if cand.touched_span[0] < cand.declared_span[0] or cand.touched_span[1] > cand.declared_span[1]:
        raise ReferentGraphViolation(
            f"patch fan-out: touched {cand.touched_span} exceeds declared {cand.declared_span}"
        )

    # Signal 2: dangling deictic / identifier referents in the candidate.
    for m in DEICTIC.finditer(cand.after):
        window = cand.after[max(0, m.start() - 200):m.start()]
        if not window.strip():
            raise ReferentGraphViolation(
                f"orphaned deictic {m.group(0)!r} with no surviving antecedent"
            )

    # Signal 3: uses-without-definitions for identifiers and constraints.
    deleted = set(IDENT.findall(cand.before)) - set(IDENT.findall(cand.after))
    surviving = set(IDENT.findall(cand.after))
    if deleted & surviving:
        raise ReferentGraphViolation(
            f"identifier used but definition pruned: {sorted(deleted & surviving)}"
        )

    # Signal 4: constraint deletions when uses survive.
    deleted_constraints = set(CONSTRAINT.findall(cand.before)) - set(CONSTRAINT.findall(cand.after))
    if deleted_constraints and any(_antecedent_present(c, cand.after) for c in deleted_constraints):
        raise ReferentGraphViolation(
            f"constraint pruned but still referenced: {sorted(deleted_constraints)}"
        )

    # Signal 5: prune intent must reduce tokens.
    if cand.intent == "prune" and len(cand.after) >= len(cand.before):
        raise ReferentGraphViolation("prune intent did not reduce context length")
