# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-scope-escalation-in-aaif-verifiable-credential-delegation-chains-under-partial-revocation.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-scope-escalation-in-aaif-verifiable-credential-delegation-chains-under-partial-revocation.warrant.json
# Markdown SHA-256:   1bf68c1625fedd343d9724c545e49fdb4e854d3eeb764023c59bc4533410ea14
# Code AST SHA-256:   a96918403fa29029cd3b96e8275973c0d6b2b37fbb8c63b59fa3f6ce0392d7cd
# Verified Classes:   DelegationCascadeError
# Defensive Guards:   5 fail-closed invariants (zero external deps)

class DelegationCascadeError(RuntimeError):
    """Raised when a presented credential chain is not contained by a live ancestor grant."""
    pass


def verify_chain(chain, revocation_status, scope_of):
    """
    chain: list of credentials ordered root -> leaf.
    revocation_status: callable(credential_id) -> bool (True if revoked).
    scope_of: callable(credential) -> frozenset of scope strings.
    """
    if not chain:
        raise DelegationCascadeError("empty chain presented")

    # Root must be live; a revoked root invalidates everything beneath it.
    root = chain[0]
    if revocation_status(root.credential_id):
        raise DelegationCascadeError(
            f"root credential {root.credential_id} is revoked; entire chain invalid"
        )

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

        # Step 2: check parent liveness BEFORE any containment reasoning.
        # Step 3: a revoked ancestor rejects the whole subtree, not just itself.
        if revocation_status(parent.credential_id):
            raise DelegationCascadeError(
                f"ancestor {parent.credential_id} revoked; "
                f"descendant {child.credential_id} must be rejected despite clean status"
            )

        if child.parent_credential_id != parent.credential_id:
            raise DelegationCascadeError(
                f"dangling parent binding on {child.credential_id}: "
                f"expected {parent.credential_id}, got {child.parent_credential_id}"
            )

        # Step 4: containment only against a live parent scope.
        if not scope_of(child).issubset(scope_of(parent)):
            raise DelegationCascadeError(
                f"scope escalation: {scope_of(child)} not contained in {scope_of(parent)} "
                f"for credential {child.credential_id}"
            )

    return True
