# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-gen-ai-tool-call-id-as-a-span-local-correlation-key-rather-than-a-globally-unique-identifier-in-opentelem.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-gen-ai-tool-call-id-as-a-span-local-correlation-key-rather-than-a-globally-unique-identifier-in-opentelem.warrant.json
# Markdown SHA-256:   4ab2ca68b122605ccd51254ab8e0b4886ca734329670d174f7d86f62264a6136
# Code AST SHA-256:   333ab277835bbe25489b01c5771c7a11e497eb63d963dd012a217e07e33c760d
# Verified Classes:   ToolCallIdInvariantError, ToolCallSpan
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Optional


class ToolCallIdInvariantError(RuntimeError):
    """Raised when a tool-call span violates the span-local correlation invariant."""
    pass


@dataclass(frozen=True)
class ToolCallSpan:
    trace_id: str
    parent_span_id: str
    gen_ai_tool_call_id: str
    gen_ai_tool_name: str
    call_ordinal: int
    id_generated_at_creation: bool


def assert_join_safe(
    span: ToolCallSpan,
    *,
    join_on: str,
    provider_uniqueness_known: bool,
) -> None:
    """
    Enforce that gen_ai.tool.call.id is only used as a join key when the
    creation-time-stability and scoping conditions both hold.
    """
    if join_on == "gen_ai.tool.call.id" and not span.id_generated_at_creation:
        raise ToolCallIdInvariantError(
            f"tool call {span.gen_ai_tool_call_id!r} was generated at emission time; "
            f"it is span-local only and must not be joined on globally"
        )
    if join_on == "gen_ai.tool.call.id" and not provider_uniqueness_known:
        raise ToolCallIdInvariantError(
            f"tool call {span.gen_ai_tool_call_id!r}: provider uniqueness is unknown; "
            f"join on (trace_id, gen_ai.tool.call.id) instead"
        )


def composite_key(span: ToolCallSpan) -> tuple:
    """The only join key that is safe under all conditions."""
    return (
        span.trace_id,
        span.parent_span_id,
        span.gen_ai_tool_name,
        span.call_ordinal,
    )
