# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-reject-a-delegated-token-whose-audience-matches-but-whose-scope-was-widened-by-an-intermediary.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-reject-a-delegated-token-whose-audience-matches-but-whose-scope-was-widened-by-an-intermediary.warrant.json
# Markdown SHA-256:   3ee3731bb0de36d1b00e9564c62330da7a0ac95fd443bf6ecc3a92de68950682
# Code AST SHA-256:   eadb77a0585c821d347ee6b11ad0537dd401140eca1411e6350c73c0e63ebab9
# Verified Classes:   DelegationScopeViolation, Hop
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Sequence


class DelegationScopeViolation(RuntimeError):
    """Raised when a delegated token's scope set is not monotone non-increasing
    across the exchange chain, or when hop vocabularies disagree."""


@dataclass(frozen=True)
class Hop:
    hop_id: str
    granted_scope: frozenset[str]
    vocabulary_id: str


def validate_delegation_chain(chain: Sequence[Hop]) -> None:
    """Enforce scope-set monotonicity under a single pinned vocabulary.

    chain[0] is the original issuer; chain[-1] is the terminal token.
    """
    if not chain:
        raise DelegationScopeViolation("empty delegation chain: nothing to validate")

    vocab = chain[0].vocabulary_id
    for hop in chain:
        if hop.vocabulary_id != vocab:
            raise DelegationScopeViolation(
                f"scope vocabulary mismatch at hop {hop.hop_id!r}: "
                f"expected {vocab!r}, got {hop.vocabulary_id!r}; "
                f"subset checks would be semantically unsound"
            )

    for parent, child in zip(chain, chain[1:]):
        if not child.granted_scope <= parent.granted_scope:
            widened = child.granted_scope - parent.granted_scope
            raise DelegationScopeViolation(
                f"scope widened at hop {child.hop_id!r} relative to "
                f"{parent.hop_id!r}: added {sorted(widened)}; "
                f"delegation must be non-increasing"
            )

    terminal = chain[-1].granted_scope
    original = chain[0].granted_scope
    if not terminal <= original:
        raise DelegationScopeViolation(
            f"terminal scope {sorted(terminal)} exceeds original issuer grant "
            f"{sorted(original)}; chain is not auditable as a narrowing delegation"
        )
