# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-does-an-idempotency-key-scoped-narrower-than-the-retry-client-permit-duplicate-execution.md
# Remnant Warrant:    https://corpusfetch.com/c/when-does-an-idempotency-key-scoped-narrower-than-the-retry-client-permit-duplicate-execution.warrant.json
# Markdown SHA-256:   eab220a5d9064b1ac37a32643e7e8f8e8a7b0003ccd2585930428e26cd889bed
# Code AST SHA-256:   29a61e70bb07f588ffa163d1fc01319252a2bee94f177f2ca410f58923950320
# Verified Classes:   IdempotencyScopeError, IdempotencyGate
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from dataclasses import dataclass


class IdempotencyScopeError(RuntimeError):
    """Raised when the deduplication namespace cannot cover every endpoint a retry may reach."""


@dataclass(frozen=True)
class IdempotencyGate:
    key_scope_endpoints: frozenset
    retry_scope_endpoints: frozenset
    downstream_idempotent: bool = False

    def verify(self) -> bool:
        if self.downstream_idempotent:
            return True
        uncovered = self.retry_scope_endpoints - self.key_scope_endpoints
        if uncovered:
            raise IdempotencyScopeError(
                f"retry may reach {sorted(uncovered)} outside dedup scope"
            )
        return True


def dispatch_dedup_mode(gate: IdempotencyGate) -> str:
    try:
        gate.verify()
    except IdempotencyScopeError:
        return "downstream-idempotency-required"
    return "key-scoped"
