# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-of-a-delegated-aaif-credential-survive-revocation-when-the-scope-narrowing-was-applied-at-the-wrong-layer.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-of-a-delegated-aaif-credential-survive-revocation-when-the-scope-narrowing-was-applied-at-the-wrong-layer.warrant.json
# Markdown SHA-256:   fde75a3556f9f408983bccc42aa7f21218b907b0579c855ec83c95790f944e19
# Code AST SHA-256:   3ec51ece70da2c90252cf711c3ad8e2a53891a6a3a018d5d0b337de8624803fa
# Verified Classes:   OrphanedDelegationError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class OrphanedDelegationError(RuntimeError):
    """Raised when a narrowed delegated credential has lost its revocation lineage."""
    pass


LINEAGE_CLAIMS = ("parent_jti", "root_jti", "delegation_depth")


def narrow_scope(parent_credential: dict, requested_scope: list[str]) -> dict:
    """Derive a child credential from a parent, narrowing scope.

    Raises OrphanedDelegationError if lineage cannot be preserved, because a
    child without lineage cannot have its revocation status resolved at the
    relying party (see incident 2026-11-12).
    """
    parent_scope = set(parent_credential.get("scope", []))
    requested = set(requested_scope)

    if not requested.issubset(parent_scope):
        raise OrphanedDelegationError(
            f"requested scope {sorted(requested)} exceeds parent scope "
            f"{sorted(parent_scope)}; narrowing must be a subset"
        )

    missing = [c for c in LINEAGE_CLAIMS if c not in parent_credential]
    if missing:
        # Refuse to mint: a scope-reconstructed child would be an orphan whose
        # revocation status the relying party cannot resolve.
        raise OrphanedDelegationError(
            f"cannot narrow scope: parent credential missing lineage claims "
            f"{missing}; child would be detached from revocation tree"
        )

    child = dict(parent_credential)
    child["scope"] = sorted(requested)
    child["parent_jti"] = parent_credential["jti"]
    child["delegation_depth"] = parent_credential["delegation_depth"] + 1
    # root_jti is inherited unchanged so the verifier can walk to the root.
    return child


def verify_revocation(credential: dict, status_source) -> bool:
    """Walk to root_jti and fail closed on any unresolved node."""
    node = credential
    while node is not None:
        status = status_source.get(node["jti"])
        if status is None:
            raise OrphanedDelegationError(
                f"revocation status unresolved for jti={node['jti']}; "
                f"failing closed rather than assuming valid"
            )
        if status.get("revoked"):
            return False
        parent_jti = node.get("parent_jti")
        node = status_source.get(parent_jti) if parent_jti else None
    return True
