# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-reconcile-aaif-identity-lineage-with-otel-genai-span-re-parenting-when-memory-forget-events-orphan-provenance-edges.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-reconcile-aaif-identity-lineage-with-otel-genai-span-re-parenting-when-memory-forget-events-orphan-provenance-edges.warrant.json
# Markdown SHA-256:   f8097a5b6d361ec7bc33d7b52783b2fe42321fb545e298e612bbdc3e54b788a4
# Code AST SHA-256:   a667f8be9d3df24229422237263c513ce861592641a610a820262ca4528e2e3a
# Verified Classes:   ProvenanceInvariantError, WriteEdge, ForgetEvent
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass
from typing import Optional


class ProvenanceInvariantError(RuntimeError):
    """Raised when a forget event cannot be reconciled to exactly one live write edge."""


@dataclass(frozen=True)
class WriteEdge:
    content_id: str          # stable identity, NOT trace/span id
    agent_instance: str
    principal_chain: tuple   # AAIF delegation path, ordered
    trace_id: str            # evidence only
    span_id: str             # evidence only
    live: bool = True


@dataclass(frozen=True)
class ForgetEvent:
    content_id: str
    reason: str              # "ttl" | "compaction" | "erasure"
    trace_id: Optional[str]  # may be None (reaper, erasure job)


def content_identity(agent_instance: str, memory_key: str, write_epoch: int) -> str:
    return hashlib.sha256(
        f"{agent_instance}|{memory_key}|{write_epoch}".encode()
    ).hexdigest()


def reconcile_forget(event: ForgetEvent, edges: list[WriteEdge]) -> WriteEdge:
    matches = [e for e in edges if e.content_id == event.content_id and e.live]
    if not matches:
        raise ProvenanceInvariantError(
            f"forget for {event.content_id!r} (reason={event.reason}) "
            f"matched no live write edge; graph/store have diverged"
        )
    if len(matches) > 1:
        raise ProvenanceInvariantError(
            f"forget for {event.content_id!r} matched {len(matches)} live edges; "
            f"compaction likely duplicated an edge without carrying content identity"
        )
    edge = matches[0]
    if not edge.principal_chain:
        raise ProvenanceInvariantError(
            f"write edge {edge.content_id!r} has empty AAIF delegation chain; "
            f"cannot attribute forget to a principal"
        )
    return edge


def audit(edges: list[WriteEdge], forgets: list[ForgetEvent]) -> dict:
    for ev in forgets:
        reconcile_forget(ev, edges)
    orphaned = [e for e in edges if e.live and not e.principal_chain]
    if orphaned:
        raise ProvenanceInvariantError(
            f"{len(orphaned)} live write edges lack a valid AAIF delegation path"
        )
    return {"live": sum(e.live for e in edges), "forgets": len(forgets), "ok": True}
