# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-treat-opentelemetry-genai-tool-call-spans-as-semantically-incomplete.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-treat-opentelemetry-genai-tool-call-spans-as-semantically-incomplete.warrant.json
# Markdown SHA-256:   a1afbbf5fa2935cd8f5cfc328e873a05681c97da6858a1e5caa104306db0f293
# Code AST SHA-256:   849e01816718bcc41b3d4594675a0bfd91731a24d1937d0abd2e77c5e022c104
# Verified Classes:   IncompleteToolCallSpanError, ToolCallSpan
# Defensive Guards:   7 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Optional


class IncompleteToolCallSpanError(RuntimeError):
    """Raised when a GenAI tool-call span cannot be treated as a complete,
    correlated request/response record under OTel GenAI semantic conventions."""


@dataclass
class ToolCallSpan:
    trace_id: str
    span_id: str
    parent_span_id: Optional[str]
    tool_name: Optional[str]
    call_id: object          # may be str or int depending on emitter revision
    has_result: bool
    linked_span_ids: tuple = ()


def assert_tool_call_complete(
    request: ToolCallSpan,
    response: Optional[ToolCallSpan],
    siblings: tuple,
) -> None:
    if request.call_id is None:
        raise IncompleteToolCallSpanError(
            f"span {request.span_id} has no gen_ai.tool.call.id; result is unattributable"
        )
    if not isinstance(request.call_id, str):
        raise IncompleteToolCallSpanError(
            f"span {request.span_id} call_id is {type(request.call_id).__name__}, "
            "expected str per GenAI semconv >=1.27"
        )
    if response is None:
        raise IncompleteToolCallSpanError(
            f"span {request.span_id} call_id={request.call_id!r} has no correlated response span"
        )
    if response.call_id != request.call_id:
        raise IncompleteToolCallSpanError(
            f"call_id mismatch: request={request.call_id!r} response={response.call_id!r}"
        )
    if not response.has_result:
        raise IncompleteToolCallSpanError(
            f"response span {response.span_id} lacks gen_ai.tool.call.result"
        )
    structurally_linked = (
        response.parent_span_id == request.span_id
        or response.span_id in request.linked_span_ids
    )
    if not structurally_linked:
        raise IncompleteToolCallSpanError(
            f"response span {response.span_id} is neither child nor link of {request.span_id}"
        )
    collisions = [
        s for s in siblings
        if s.span_id != request.span_id
        and s.call_id == request.call_id
        and s.tool_name != request.tool_name
    ]
    if collisions:
        raise IncompleteToolCallSpanError(
            f"call_id {request.call_id!r} collides across tool names in trace {request.trace_id}"
        )
