# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-tool-output-as-tainted-when-provenance-labels-are-lost-at-a-serialization-boundary.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-treat-a-tool-output-as-tainted-when-provenance-labels-are-lost-at-a-serialization-boundary.warrant.json
# Markdown SHA-256:   26c9a8591b1bd72527683b536853db029073622c05a00aa616b3795db6729465
# Code AST SHA-256:   f6a238b0260dace9b198d3884e0249458d16b1518d69eac3021eed2517383324
# Verified Classes:   TaintLaunderingError, Provenance, Value
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from __future__ import annotations

from dataclasses import dataclass, field


class TaintLaunderingError(RuntimeError):
    """Raised when a tool output drops taint inherited from its inputs."""


@dataclass(frozen=True)
class Provenance:
    labels: frozenset[str] = field(default_factory=frozenset)

    def join(self, other: "Provenance") -> "Provenance":
        return Provenance(self.labels | other.labels)

    @property
    def is_clean(self) -> bool:
        return not self.labels


@dataclass(frozen=True)
class Value:
    payload: str
    provenance: Provenance


def propagate_tool_output(
    tool_name: str,
    inputs: list[Value],
    output: Value,
    *,
    lineage_preserving: frozenset[str] = frozenset(),
) -> Value:
    """Conservatively join input provenance onto a tool output.

    A tool that is not on the lineage-preserving allowlist must not emit a
    value cleaner than its inputs. Absence-of-label is treated as a taint
    signal, not a clean bill of health.
    """
    inherited = Provenance()
    for v in inputs:
        inherited = inherited.join(v.provenance)

    if inherited.is_clean:
        return output

    if tool_name in lineage_preserving:
        return output

    if output.provenance.is_clean:
        raise TaintLaunderingError(
            f"tool {tool_name!r} emitted clean output from tainted input "
            f"{sorted(inherited.labels)!r}; label was dropped at a "
            f"serialization boundary"
        )

    return Value(
        payload=output.payload,
        provenance=output.provenance.join(inherited),
    )
