# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-of-a-delegated-credential-chain-fail-scope-narrowing-validation-when-the-parent-token-is-refreshed-mid-chain.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-of-a-delegated-credential-chain-fail-scope-narrowing-validation-when-the-parent-token-is-refreshed-mid-chain.warrant.json
# Markdown SHA-256:   146ab84ce3fd3e03e3101584aa0ffd681d243a55ea330d7e3a55a695fb52b317
# Code AST SHA-256:   681c883282b4850ec0291cd9b74f0a27004480fad7ced17cedde7a5a8c114cc9
# Verified Classes:   ScopeNarrowingViolation
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class ScopeNarrowingViolation(RuntimeError):
    """Raised when a child token's scope is not a subset of its pinned parent's scope."""
    pass


def validate_delegated_chain(child_scope: frozenset[str],
                             parent_scope: frozenset[str],
                             parent_version: str,
                             expected_parent_version: str,
                             normalization_idempotent: bool) -> None:
    if not normalization_idempotent:
        raise ScopeNarrowingViolation(
            "scope normalization is not idempotent; subset relation is undefined across hops"
        )
    if parent_version != expected_parent_version:
        raise ScopeNarrowingViolation(
            f"parent token version drift: child pinned {expected_parent_version!r}, "
            f"validator saw {parent_version!r}; re-issuance required"
        )
    if not child_scope.issubset(parent_scope):
        raise ScopeNarrowingViolation(
            f"child scope {sorted(child_scope)} is not a subset of "
            f"parent scope {sorted(parent_scope)} at version {parent_version!r}"
        )


def refresh_parent_and_validate(child_scope: frozenset[str],
                                old_parent_scope: frozenset[str],
                                new_parent_scope: frozenset[str],
                                new_parent_version: str,
                                pinned_version: str) -> None:
    if not old_parent_scope.issubset(new_parent_scope):
        # Parent legitimately narrowed; any live child must be re-issued.
        raise ScopeNarrowingViolation(
            f"parent scope narrowed on refresh to {new_parent_version!r}; "
            "live children must be re-issued before validation"
        )
    validate_delegated_chain(
        child_scope=child_scope,
        parent_scope=new_parent_scope,
        parent_version=new_parent_version,
        expected_parent_version=pinned_version,
        normalization_idempotent=True,
    )
