# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-quarantine-a-tool-output-whose-provenance-label-was-downgraded-by-an-upstream-summarizer.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-quarantine-a-tool-output-whose-provenance-label-was-downgraded-by-an-upstream-summarizer.warrant.json
# Markdown SHA-256:   27b73bcfa662d98c277e711b091375cf3b10d207456e8931e64cc940747d591b
# Code AST SHA-256:   19507ea412ef7c9c3f0c6a2a323d74f84d263641c33250e3afb969d6614f2402
# Verified Classes:   ProvenanceJoinError, LabeledChunk
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Iterable

TRUST_ORDER = {"untrusted": 0, "external": 1, "internal": 2, "trusted": 3}


class ProvenanceJoinError(RuntimeError):
    """Raised when a generative transform cannot prove a sound taint join."""


@dataclass
class LabeledChunk:
    text: str
    trust: str
    injection_flags: frozenset[str] = field(default_factory=frozenset)


def join_labels(inputs: Iterable[LabeledChunk]) -> tuple[str, frozenset[str]]:
    inputs = list(inputs)
    if not inputs:
        raise ProvenanceJoinError("cannot join over zero inputs")
    min_trust = min((c.trust for c in inputs), key=lambda t: TRUST_ORDER[t])
    flags = frozenset().union(*(c.injection_flags for c in inputs))
    return min_trust, flags


def admit_summary(
    inputs: Iterable[LabeledChunk],
    summary_text: str,
    consumer_threshold: str,
    generative: bool,
    span_aligned: bool = False,
) -> LabeledChunk:
    inputs = list(inputs)
    min_trust, flags = join_labels(inputs)

    if not generative or span_aligned:
        # Extractive/span-aligned: caller must carry per-span labels; we only
        # attest the join for the batch and refuse to relabel upward.
        return LabeledChunk(summary_text, min_trust, flags)

    # Laundering signature: summary reproduces an untrusted span nearly verbatim.
    def overlaps(a: str, b: str) -> float:
        ta, tb = set(a.lower().split()), set(b.lower().split())
        return len(ta & tb) / max(1, len(ta | tb))

    laundered = any(
        overlaps(summary_text, c.text) > 0.8
        for c in inputs
        if TRUST_ORDER[c.trust] < TRUST_ORDER[consumer_threshold]
    )

    if TRUST_ORDER[min_trust] < TRUST_ORDER[consumer_threshold] or flags or laundered:
        raise ProvenanceJoinError(
            f"quarantine: min_trust={min_trust} flags={sorted(flags)} "
            f"laundered={laundered} threshold={consumer_threshold}"
        )

    return LabeledChunk(summary_text, min_trust, flags)
