# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-treat-opentelemetry-genai-semantic-conventions-as-a-lossy-projection-rather-than-a-faithful-record.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-treat-opentelemetry-genai-semantic-conventions-as-a-lossy-projection-rather-than-a-faithful-record.warrant.json
# Markdown SHA-256:   dec19c9852777da4f840b299f9cc0e17042b938198ac8eb66553018113a34bc8
# Code AST SHA-256:   f8d18c2c5a77537f48e080133e86916200d5432fc8448bfaf96ebcb2538241ef
# Verified Classes:   GenAISpanContractError, GenAISpan
# Defensive Guards:   6 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Optional


class GenAISpanContractError(RuntimeError):
    """Raised when a span cannot be trusted as a faithful GenAI record."""


REQUIRED_USAGE_FIELDS = ("input_tokens", "output_tokens")
CACHE_FIELDS = ("cache_read_input_tokens",)


@dataclass
class GenAISpan:
    name: str
    attributes: dict
    instrumentor_version: Optional[str] = None
    has_attempted_tool_span: bool = False
    tool_was_executed: bool = False
    parent_span_id: Optional[str] = None
    trace_id: Optional[str] = None
    children: list = field(default_factory=list)


def assert_faithful_for_cost(span: GenAISpan) -> None:
    """Gate a span for use in cost attribution. Raises if lossy."""
    if span.instrumentor_version is None:
        raise GenAISpanContractError(
            f"span {span.name!r}: instrumentor_version unset; "
            "cache/usage presence is ambiguous"
        )
    for f in REQUIRED_USAGE_FIELDS:
        if f not in span.attributes:
            raise GenAISpanContractError(
                f"span {span.name!r}: missing required usage field {f!r}; "
                "cannot attribute tokens"
            )
    if not any(f in span.attributes for f in CACHE_FIELDS):
        raise GenAISpanContractError(
            f"span {span.name!r}: no cache_read field; summed input_tokens "
            "would double-count re-sent prefixes"
        )


def assert_faithful_for_tool_audit(span: GenAISpan) -> None:
    """Gate a span for tool-invocation auditing. Rejected calls must be visible."""
    if span.tool_was_executed and not span.has_attempted_tool_span:
        raise GenAISpanContractError(
            f"span {span.name!r}: tool executed with no attempted-span marker; "
            "rejected/aborted calls would be invisible"
        )


def assert_faithful_handoff(parent: GenAISpan, child: GenAISpan) -> None:
    """Gate topology-derived handoffs. Detached tasks break parent linkage."""
    if child.trace_id != parent.trace_id:
        raise GenAISpanContractError(
            f"handoff {parent.name!r}->{child.name!r}: trace_id mismatch; "
            "not a valid parent-child edge"
        )
    if child.parent_span_id != parent.attributes.get("span_id"):
        raise GenAISpanContractError(
            f"handoff {parent.name!r}->{child.name!r}: no parent_span_id edge; "
            "detached-task handoff is invisible to topology"
        )
