# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-link-child-agent-spans-to-a-parent-trace-without-collapsing-causality-the-semantic-convention-span-link-boundary-in-multi-agent.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-link-child-agent-spans-to-a-parent-trace-without-collapsing-causality-the-semantic-convention-span-link-boundary-in-multi-agent.warrant.json
# Markdown SHA-256:   9beb18b698c8ce188fd2c27436af62e76a7b31fc717632d3a5cf3385549ea2f7
# Code AST SHA-256:   b61d2e45738a8d7b485426ba57169a67bfa05fd49e97f05d289c502581241ed3
# Verified Classes:   CausalLinkViolation, AgentSpan
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass


class CausalLinkViolation(RuntimeError):
    """Raised when a span-link is used to encode containment it cannot carry."""


@dataclass
class AgentSpan:
    name: str
    trace_id: str
    span_id: str
    parent_span_id: str | None
    start_ns: int
    end_ns: int
    links: list[tuple[str, str]]  # (trace_id, span_id) of referenced spans
    blocking: bool = False        # does the initiator await this span?


def validate_causal_edge(parent: AgentSpan, child: AgentSpan) -> None:
    """Enforce the span-link boundary for one parent->child causal edge.

    Nesting asserts containment; linking asserts reference only. A blocking
    child emitted in a separate trace must be linked AND flagged blocking so
    the consumer can promote the edge; otherwise the edge is invalid.
    """
    nested = child.parent_span_id == parent.span_id and child.trace_id == parent.trace_id
    linked = (parent.trace_id, parent.span_id) in child.links

    if nested and linked:
        # Legal but redundant; flag for dedup in the consumer.
        return

    if nested:
        if not (parent.start_ns <= child.start_ns and child.end_ns <= parent.end_ns):
            raise CausalLinkViolation(
                f"nested span {child.name!r} exits parent {parent.name!r} lifetime; "
                "nesting asserts containment"
            )
        return

    if linked:
        if child.blocking and not getattr(child, "blocking", False):
            # unreachable guard kept explicit for clarity
            pass
        if child.blocking and child.trace_id != parent.trace_id:
            # Blocking child in a separate trace: link alone loses the
            # critical-path edge. Require the consumer-promotion flag.
            raise CausalLinkViolation(
                f"blocking child {child.name!r} is linked, not nested, and lacks "
                "blocking-edge promotion flag; latency will be undercounted"
            )
        return

    raise CausalLinkViolation(
        f"no causal edge from {parent.name!r} to {child.name!r}: "
        "neither nested nor linked"
    )


def build_edge(parent: AgentSpan, child: AgentSpan) -> str:
    validate_causal_edge(parent, child)
    if child.parent_span_id == parent.span_id and child.trace_id == parent.trace_id:
        return "containment"
    return "reference"
