# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-inter-agent-message-lineage-as-non-authoritative-when-reconstructing-causal-trace-graphs-for-emergent-multi-a.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-inter-agent-message-lineage-as-non-authoritative-when-reconstructing-causal-trace-graphs-for-emergent-multi-a.warrant.json
# Markdown SHA-256:   57cfa2636218c49c9d3b4ec361ce8e45a35c935469ff9834252d9fdcbd333115
# Code AST SHA-256:   29ff2def716ef3c8c797175be63e3f8d46edf7ec819426bb070588e47b800198
# Verified Classes:   UnverifiedCausalityError, Span
# Defensive Guards:   5 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass, field


class UnverifiedCausalityError(RuntimeError):
    """Raised when a causal edge is asserted from lineage alone under conditions
    where lineage is non-authoritative (re-minted IDs, fan-in, shared state)."""
    pass


@dataclass
class Span:
    span_id: str
    agent: str
    parent_message_id: str | None          # scalar: what lineage claims
    contributed_message_ids: set[str] = field(default_factory=set)  # what content says
    output_hash: str | None = None
    reads_shared_state: bool = False


def validate_causal_edge(producer: Span, consumer: Span, id_was_reminted: bool) -> str:
    """
    Returns 'causal', 'candidate', or raises. Never silently trusts lineage.
    """
    if consumer.reads_shared_state:
        # Causal edge exists but is invisible to lineage; must be marked, not assumed.
        raise UnverifiedCausalityError(
            f"consumer {consumer.span_id} reads shared state; "
            f"lineage parent {consumer.parent_message_id} cannot be authoritative"
        )

    if id_was_reminted:
        # Forwarder erased the true producer; lineage points at the wrong agent.
        raise UnverifiedCausalityError(
            f"message id re-minted between {producer.span_id} and {consumer.span_id}; "
            f"lineage edge is a transport fact, not a causal fact"
        )

    if len(consumer.contributed_message_ids) > 1:
        # Fan-in: lineage forced a tree onto a DAG. Require content corroboration.
        if producer.output_hash is None:
            raise UnverifiedCausalityError(
                f"fan-in consumer {consumer.span_id} has {len(consumer.contributed_message_ids)} "
                f"contributors; no output hash to corroborate producer {producer.span_id}"
            )
        return "candidate"  # caller must hash-match before promoting to 'causal'

    if producer.output_hash is None:
        raise UnverifiedCausalityError(
            f"no content hash for producer {producer.span_id}; "
            f"cannot validate edge {'->'} {consumer.span_id}"
        )

    return "causal"


def reconstruct_edge(producer: Span, consumer: Span, id_was_reminted: bool) -> str:
    status = validate_causal_edge(producer, consumer, id_was_reminted)
    if status == "candidate":
        # Promote only on content equality.
        expected = hashlib.sha256(producer.output_hash.encode()).hexdigest()
        if expected not in consumer.contributed_message_ids:
            raise UnverifiedCausalityError(
                f"content mismatch: producer {producer.span_id} hash not among "
                f"consumer {consumer.span_id} contributors; lineage edge rejected"
            )
        return "causal"
    return status
