# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-refuse-to-propagate-a-federation-route-after-capability-negotiation-narrows-the-effective-policy-set.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-refuse-to-propagate-a-federation-route-after-capability-negotiation-narrows-the-effective-policy-set.warrant.json
# Markdown SHA-256:   0e1a3d5e541506b62bac58e10147051843cce34aad2fdb13156aa085ceea158d
# Code AST SHA-256:   3d8e98cbb7afedb13373908720ec3fbae16b553ef88604ee3f33f6f4bb6c53d7
# Verified Classes:   CapabilityBelowFloor, NegotiationResult
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import FrozenSet


class CapabilityBelowFloor(RuntimeError):
    """Raised when a negotiated federation route drops below the caller's MVCC."""

    def __init__(self, missing: FrozenSet[str], dropping_hop: str, negotiated: FrozenSet[str]):
        self.missing = missing
        self.dropping_hop = dropping_hop
        self.negotiated = negotiated
        super().__init__(
            f"route refused at hop {dropping_hop!r}: negotiated {sorted(negotiated)} "
            f"missing mandatory {sorted(missing)}"
        )


@dataclass(frozen=True)
class NegotiationResult:
    negotiated: FrozenSet[str]
    policy_epochs: dict = field(default_factory=dict)
    dropping_hop: str = "unknown"


def admit_route(mvcc: FrozenSet[str], result: NegotiationResult) -> NegotiationResult:
    """Admit a negotiated route only if it satisfies the caller's mandatory set."""
    if not result.negotiated:
        raise CapabilityBelowFloor(mvcc, result.dropping_hop, result.negotiated)
    missing = mvcc - result.negotiated
    if missing:
        # Non-empty intersection is NOT sufficient; the floor is the MVCC.
        raise CapabilityBelowFloor(missing, result.dropping_hop, result.negotiated)
    if not result.policy_epochs:
        raise CapabilityBelowFloor(mvcc, result.dropping_hop, result.negotiated)
    return result


def cache_key(result: NegotiationResult) -> tuple:
    """Cache under the negotiated set and policy epochs, never the requested set."""
    return (frozenset(result.negotiated), tuple(sorted(result.policy_epochs.items())))
