# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-trace-parent-loss-across-a2a-hops-and-mcp-transports-and-where-the-boundary-actually-fails.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-trace-parent-loss-across-a2a-hops-and-mcp-transports-and-where-the-boundary-actually-fails.warrant.json
# Markdown SHA-256:   fcdb654fee5bf439b39fc4e9a15f18afeee0ab69703ecd4f002b1496bbe0f06d
# Code AST SHA-256:   73050e4982563c2d74719fd696d4099de2356da13e35ff3874ce7551fcdb3473
# Verified Classes:   CorrelationInvariantError, ReceivedContext
# Defensive Guards:   4 fail-closed invariants (zero external deps)

"""Correlation guard for A2A/MCP trace propagation.

Enforces call-scoped context injection (C1), fresh context read (C2),
and header preservation (C3) before a span is admitted to the trace store.
"""

from dataclasses import dataclass
from typing import Optional


class CorrelationInvariantError(RuntimeError):
    """Raised when a span violates a call-scoped correlation invariant."""
    pass


@dataclass(frozen=True)
class ReceivedContext:
    trace_id: str
    parent_span_id: str          # span portion of the received traceparent
    tracestate: Optional[str]
    injected_scope: str          # "call" | "session" | "task" | "connection"
    call_identity: Optional[str] # MCP request id / A2A message id, if recorded


def admit_span(
    ctx: ReceivedContext,
    *,
    expected_call_identity: str,
    prior_tracestate: Optional[str] = None,
) -> None:
    """Validate a span's inherited context against C1-C3.

    Raises CorrelationInvariantError on any violation; the caller is expected
    to flag-and-preserve rather than drop or silently re-parent.
    """
    # C1: context must be injected per logical call, not per session/task.
    if ctx.injected_scope != "call":
        raise CorrelationInvariantError(
            f"session/task-scoped traceparent bleed: scope={ctx.injected_scope!r} "
            f"trace_id={ctx.trace_id} parent={ctx.parent_span_id}"
        )

    # Call identity is mandatory; without it the failure class is undetectable.
    if not ctx.call_identity:
        raise CorrelationInvariantError(
            f"missing call identity on trace_id={ctx.trace_id}; "
            "cannot distinguish correct from session-scoped parenting"
        )

    if ctx.call_identity != expected_call_identity:
        raise CorrelationInvariantError(
            f"call identity mismatch: span carries {ctx.call_identity!r}, "
            f"expected {expected_call_identity!r} (possible re-parenting)"
        )

    # C3: tracestate must survive transport restarts unchanged.
    if prior_tracestate is not None and ctx.tracestate != prior_tracestate:
        raise CorrelationInvariantError(
            f"tracestate mutated across transport boundary on trace_id={ctx.trace_id}: "
            f"{prior_tracestate!r} -> {ctx.tracestate!r}"
        )
