# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-reject-a-delegated-capability-token-whose-chain-is-cryptographically-valid-the-aaif-depth-scope-collapse-at-att.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-reject-a-delegated-capability-token-whose-chain-is-cryptographically-valid-the-aaif-depth-scope-collapse-at-att.warrant.json
# Markdown SHA-256:   bb7e0ba18a47066bd6cb9faa089851e5824c615b6f6292c41043cd2d05932c35
# Code AST SHA-256:   23f3833bbedd9d54e2dc786b833bb61c0b4d80069c0c8d4fadc06b239c06aacc
# Verified Classes:   DepthScopeCollapseError, Hop
# Defensive Guards:   6 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Iterable

class DepthScopeCollapseError(RuntimeError):
    """Raised when an AAIF chain is signature-valid but over-privileged."""
    pass

@dataclass
class Hop:
    issuer: str
    subject: str
    scope: frozenset[str]
    max_delegation_depth: int
    signature_valid: bool = True
    narrowed: bool = False          # did this hop reduce scope vs parent?
    retain_depth_claim: bool = False

def verify_chain(chain: Iterable[Hop]) -> frozenset[str]:
    hops = list(chain)
    if not hops:
        raise DepthScopeCollapseError("empty chain")

    for i, hop in enumerate(hops):
        if not hop.signature_valid:
            raise DepthScopeCollapseError(f"hop {i}: invalid signature")

    effective = hops[0].scope
    prev_depth = hops[0].max_delegation_depth

    for i in range(1, len(hops)):
        parent, child = hops[i - 1], hops[i]

        # Per-hop subset check (AAIF §6.2) -- necessary, not sufficient.
        if not child.scope <= parent.scope:
            raise DepthScopeCollapseError(
                f"hop {i}: child scope {child.scope} not subset of parent {parent.scope}"
            )

        # Depth monotonicity (AAIF §9.1).
        if child.max_delegation_depth > parent.max_delegation_depth:
            raise DepthScopeCollapseError(
                f"hop {i}: depth {child.max_delegation_depth} exceeds "
                f"parent depth {parent.max_delegation_depth}"
            )

        # Narrowing hop that preserves depth without explicit retention claim.
        if parent.narrowed and child.max_delegation_depth == parent.max_delegation_depth \
                and not child.retain_depth_claim:
            raise DepthScopeCollapseError(
                f"hop {i}: narrowing hop preserved depth without aaif:retain-depth"
            )

        # Path-intersection: effective scope can only shrink.
        effective = effective & child.scope
        prev_depth = child.max_delegation_depth

    if not hops[-1].scope <= effective:
        raise DepthScopeCollapseError(
            f"leaf claims {hops[-1].scope - effective} not jointly granted by ancestors"
        )
    return effective
