# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-agent-emitter-reject-a-span-that-claims-gen-ai-tool-call-semantics-but-carries-no-resolvable-tool-identity.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-agent-emitter-reject-a-span-that-claims-gen-ai-tool-call-semantics-but-carries-no-resolvable-tool-identity.warrant.json
# Markdown SHA-256:   454476c5ade49300d770eff13eb0c66513ab01c31304f6a7eab145b36ff00ba7
# Code AST SHA-256:   c727668677f09634160f9be92552fd3bdd2b30ad5525e94633a60eb1ecce0022
# Verified Classes:   UnresolvableToolIdentityError, SpanDraft
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

# Reserved placeholders that indicate "identity unknown", not a real tool.
_UNKNOWN_PLACEHOLDERS = frozenset({"", "unknown", "tool", "unnamed", "n/a"})
# Explicit sentinel for intentionally withheld identity (privacy redaction).
_REDACTED_SENTINEL = "redacted"


class UnresolvableToolIdentityError(RuntimeError):
    """Raised when a span asserts tool/memory semantics but carries no resolvable identity.

    This is an instrumentation bug surfaced at emit time, not a data-quality
    issue to be repaired downstream. The emitter is the last place where
    "unknown" and "redacted" are still distinguishable.
    """


@dataclass(frozen=True)
class SpanDraft:
    name: str
    operation: Optional[str]          # gen_ai.operation.name
    tool_name: Optional[str]          # gen_ai.tool.name
    redacted: bool = False            # identity intentionally withheld
    dispatch_context_available: bool = True  # could the emitter still know the name?


def validate_before_export(span: SpanDraft) -> SpanDraft:
    """Enforce the tool-identity invariant at the emitter boundary.

    Returns the (possibly annotated) span if it is valid or explicitly redacted.
    Raises UnresolvableToolIdentityError if identity is unknown but knowable.
    """
    asserts_identity = span.operation in ("execute_tool", "memory.read")
    if not asserts_identity:
        return span  # no semantic claim; invariant does not apply

    # Explicit redaction path: accept, but require a distinct sentinel so the
    # redacted cohort stays separable from the unknown cohort downstream.
    if span.redacted:
        if span.tool_name not in (None, _REDACTED_SENTINEL):
            raise UnresolvableToolIdentityError(
                f"redacted span must use sentinel {_REDACTED_SENTINEL!r}, "
                f"got {span.tool_name!r}"
            )
        return SpanDraft(
            name=span.name,
            operation=span.operation,
            tool_name=_REDACTED_SENTINEL,
            redacted=True,
            dispatch_context_available=span.dispatch_context_available,
        )

    name = span.tool_name
    is_unknown = name is None or name.strip().lower() in _UNKNOWN_PLACEHOLDERS

    if is_unknown:
        if span.dispatch_context_available:
            # Identity was knowable at emit time -> instrumentation bug.
            raise UnresolvableToolIdentityError(
                f"span {span.name!r} (op={span.operation!r}) asserts tool/memory "
                f"semantics but has no resolvable identity "
                f"(tool_name={span.tool_name!r}); emitter still had dispatch context"
            )
        # Identity was not knowable and not marked redacted -> also a bug,
        # but a different one: the caller must set redacted=True explicitly.
        raise UnresolvableToolIdentityError(
            f"span {span.name!r} has unknown identity with no dispatch context; "
            f"mark redacted=True if identity was intentionally withheld"
        )

    return span
