# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-propagates-taint-via-span-links-instead-of-parent-child-edges-in-genai-semantic-convention-traces.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-propagates-taint-via-span-links-instead-of-parent-child-edges-in-genai-semantic-convention-traces.warrant.json
# Markdown SHA-256:   f1ef6f08970e77139dd0080706a6294985b1bed6e0ee83fd91b87ae2742a8be1
# Code AST SHA-256:   cc623b6b88a8e3e2b0325b2db6d53d124ad02259a30079fc8a6d060135d01c56
# Verified Classes:   TaintClosureError, Span
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Iterable


class TaintClosureError(RuntimeError):
    """Raised when link-based taint provenance cannot be soundly closed."""


@dataclass
class Span:
    span_id: str
    parent_id: str | None
    links: list[str] = field(default_factory=list)          # target span_ids
    taint: set[str] = field(default_factory=set)            # taint labels on this span
    exported: bool = True


def compute_taint_closure(
    spans: Iterable[Span],
    *,
    consumer_traverses_links: bool,
) -> dict[str, set[str]]:
    """
    Forward taint closure over BOTH parent edges and link edges.
    A span's effective taint = own taint UNION parent's closure UNION
    closure of every link target. Children inherit the parent's closure.
    """
    if not consumer_traverses_links:
        raise TaintClosureError(
            "policy consumer traverses parent_span_id only; link-based taint "
            "provenance would be silently invisible (fail-closed required)"
        )

    by_id = {s.span_id: s for s in spans}
    closure: dict[str, set[str]] = {}
    visiting: set[str] = set()

    def resolve(span_id: str) -> set[str]:
        if span_id in closure:
            return closure[span_id]
        if span_id in visiting:
            raise TaintClosureError(f"cycle in taint graph at {span_id}")
        span = by_id.get(span_id)
        if span is None or not span.exported:
            # dangling link target: tainted-unknown, never untainted
            raise TaintClosureError(
                f"taint link target {span_id} not exported; "
                f"treat as tainted-unknown, do not default to clean"
            )
        visiting.add(span_id)
        acc: set[str] = set(span.taint)
        if span.parent_id is not None:
            acc |= resolve(span.parent_id)
        for target in span.links:
            acc |= resolve(target)
        visiting.discard(span_id)
        closure[span_id] = acc
        return acc

    for s in by_id:
        resolve(s)
    return closure


def gate_tool_call(
    tool_span: Span,
    closure: dict[str, set[str]],
    *,
    denied_taints: set[str] = frozenset({"untrusted_document"}),
) -> None:
    effective = closure.get(tool_span.span_id, set())
    hit = effective & denied_taints
    if hit:
        raise TaintClosureError(
            f"tool span {tool_span.span_id} ({tool_span.span_id}) blocked: "
            f"inherited taint {sorted(hit)} via link closure"
        )
