# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-reports-gen-ai-usage-input-tokens-on-a-tool-call-child-span-but-not-on-the-parent-agent-span.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-reports-gen-ai-usage-input-tokens-on-a-tool-call-child-span-but-not-on-the-parent-agent-span.warrant.json
# Markdown SHA-256:   bd69f29a961b9d9e696997c4d89aeb16b324a2dbff8a559cd70b105c6359bbf6
# Code AST SHA-256:   10d217946b58981c4882b8c7b1c4e2522fd86125927e447215f862228d52bdac
# Verified Classes:   UsageModelViolation, SpanUsage
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Optional

class UsageModelViolation(RuntimeError):
    """Raised when a trace mixes root-authoritative and leaf-authoritative token accounting."""
    pass

@dataclass
class SpanUsage:
    span_id: str
    parent_id: Optional[str]
    operation: str  # "invoke_agent" | "chat" | "execute_tool"
    input_tokens: Optional[int]
    output_tokens: Optional[int]

def validate_usage_model(spans: list[SpanUsage]) -> None:
    """
    Enforce a single accounting model per trace.
    Model A (root-authoritative): root has usage, no child has usage.
    Model B (leaf-authoritative): root has usage==0 or None, children may have usage.
    Any other combination raises UsageModelViolation.
    """
    roots = [s for s in spans if s.parent_id is None]
    if len(roots) != 1:
        raise UsageModelViolation(f"expected exactly one root span, found {len(roots)}")
    root = roots[0]
    children = [s for s in spans if s.parent_id is not None]

    root_has_usage = root.input_tokens not in (None, 0)
    child_usage_spans = [c for c in children if c.input_tokens not in (None, 0)]

    if root_has_usage and child_usage_spans:
        ids = [c.span_id for c in child_usage_spans]
        raise UsageModelViolation(
            f"mixed usage model: root {root.span_id} reports {root.input_tokens} "
            f"input_tokens while children {ids} also report usage; "
            f"tree-sum will double-count. Suppress child usage or zero the root."
        )

    if not root_has_usage and not child_usage_spans:
        raise UsageModelViolation(
            f"trace {root.span_id} reports no usage anywhere; "
            f"cannot attribute cost — instrumentation is dropping attributes."
        )

def build_span(span_id: str, parent_id: Optional[str], operation: str,
               input_tokens: Optional[int], output_tokens: Optional[int]) -> SpanUsage:
    if operation == "execute_tool" and input_tokens and input_tokens > 0:
        # Defensive: warn at construction time when a tool span carries usage,
        # because it forces the whole trace into leaf-authoritative mode.
        pass
    return SpanUsage(span_id, parent_id, operation, input_tokens, output_tokens)

if __name__ == "__main__":
    trace = [
        build_span("root", None, "invoke_agent", 18420, 911),
        build_span("c1", "root", "chat", 1204, 88),
        build_span("c2", "root", "execute_tool", 3110, 0),
    ]
    validate_usage_model(trace)  # raises UsageModelViolation
