# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-double-counting-when-attributing-observability-cost-per-agent-skill-invocation-and-memory-operation.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-double-counting-when-attributing-observability-cost-per-agent-skill-invocation-and-memory-operation.warrant.json
# Markdown SHA-256:   86a3164b01c6aae2298554ad4571a5cf254ca69e7cc5660cf02db2110685c618
# Code AST SHA-256:   237b521c1c5af211557768681b8a2e8523e2f203e4d40eddba978a41d07628db
# Verified Classes:   CostAttributionError, Span
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Optional


class CostAttributionError(RuntimeError):
    """Raised when a billable unit would be attributed to zero or multiple spans."""
    pass


@dataclass
class Span:
    span_id: str
    parent_id: Optional[str]
    billable: bool
    cost: Optional[float]
    cost_owner: Optional[str] = None
    children: list = field(default_factory=list)


def attribute_cost(span: Span, registry: dict) -> float:
    """Return the cost attributable to `span`, enforcing the single-owner invariant.

    Invariant: a billable unit is owned by exactly one leaf span. Aggregate
    spans must be marked billable=False. A billable span with children, or a
    span whose cost_owner points at a different span that also claims the
    unit, is a violation.
    """
    if span.span_id in registry and registry[span.span_id] is not span:
        raise CostAttributionError(
            f"duplicate span id {span.span_id!r}: two emitters produced a span "
            "for the same unit; canonicalize to one span and link the other"
        )
    registry[span.span_id] = span

    if span.billable and span.children:
        raise CostAttributionError(
            f"span {span.span_id!r} is billable but has {len(span.children)} "
            "children; aggregate spans must set billable=False to avoid "
            "double-counting child cost"
        )

    if span.billable and span.cost is None:
        raise CostAttributionError(
            f"billable span {span.span_id!r} has no cost; assign cost at "
            "exactly one layer or mark the span non-billable"
        )

    if span.cost_owner is not None and span.cost_owner != span.span_id:
        owner = registry.get(span.cost_owner)
        if owner is not None and owner.billable and owner.cost is not None:
            raise CostAttributionError(
                f"span {span.span_id!r} references cost_owner "
                f"{span.cost_owner!r} which is itself billable; cost would be "
                "counted twice"
            )

    if not span.billable:
        return 0.0

    return span.cost or 0.0


def rollup(root: Span) -> float:
    """Sum only billable leaves; aggregate spans contribute zero by construction."""
    registry: dict = {}
    total = 0.0
    stack = [root]
    while stack:
        node = stack.pop()
        total += attribute_cost(node, registry)
        stack.extend(node.children)
    return total
