# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-causal-trace-corruption-at-agent-handoff-boundaries.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-causal-trace-corruption-at-agent-handoff-boundaries.warrant.json
# Markdown SHA-256:   ae2d2e1b1480e094334e97871c0e1c6231d207c801e1b3377fa1eed6b126657e
# Code AST SHA-256:   c0257a80db2bf5d37900b3a177cce91ea7a6cd9086276feff46a7ea2d693332a
# Verified Classes:   CausalLinkError, Span
# Defensive Guards:   6 fail-closed invariants (zero external deps)

import time
from dataclasses import dataclass

MAX_CLOCK_SKEW_MS = 250.0


class CausalLinkError(RuntimeError):
    """Raised when a handoff span cannot be causally linked to its parent."""


@dataclass
class Span:
    trace_id: str
    span_id: str
    parent_span_id: str | None
    agent_id: str
    start_ms: float
    end_ms: float | None = None
    handoff_from: str | None = None


def link_handoff(child: Span, parent: Span | None) -> Span:
    if child.parent_span_id is None:
        raise CausalLinkError(
            f"span {child.span_id} has no parent_span_id at handoff boundary"
        )
    if parent is None:
        # Parent not in-process: annotate, never reparent to root.
        child.handoff_from = child.handoff_from or "unknown"
        return child
    if parent.span_id != child.parent_span_id:
        raise CausalLinkError(
            f"parent mismatch: child expects {child.parent_span_id}, "
            f"got {parent.span_id}"
        )
    if parent.end_ms is None:
        raise CausalLinkError(
            f"parent {parent.span_id} still open at child {child.span_id} emission"
        )
    if child.start_ms < parent.start_ms - MAX_CLOCK_SKEW_MS:
        raise CausalLinkError(
            f"clock inversion: child start {child.start_ms} precedes "
            f"parent start {parent.start_ms} beyond skew tolerance"
        )
    if parent.end_ms > child.start_ms + MAX_CLOCK_SKEW_MS:
        raise CausalLinkError(
            f"temporal impossibility: parent {parent.span_id} ends "
            f"{parent.end_ms} after child {child.span_id} starts {child.start_ms}"
        )
    if child.handoff_from != parent.agent_id:
        raise CausalLinkError(
            f"handoff provenance mismatch: child claims from "
            f"{child.handoff_from}, parent is {parent.agent_id}"
        )
    return child
