# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-of-the-genai-trace-correlation-id-be-minted-to-survive-memory-mutation-and-where-does-late-minting-silently-break-the-invariant.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-of-the-genai-trace-correlation-id-be-minted-to-survive-memory-mutation-and-where-does-late-minting-silently-break-the-invariant.warrant.json
# Markdown SHA-256:   c7a7a4ffe9a41eb1798637d6023459bfe6261f2ba3899ef529daca66be8fd378
# Code AST SHA-256:   d94987f7e6558dae580c794b263eb2bebef6bafad8bed23ac6252d2430c63259
# Verified Classes:   UnattributedMutationError, MutationRecord
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class UnattributedMutationError(RuntimeError):
    """Raised when a memory mutation reaches the store without a causal trace_id.

    This is a hard invariant violation, not a warning: the mutation would become
    an orphan that cannot be attributed during incident reconstruction.
    """
    pass


class MutationRecord:
    __slots__ = ("trace_id", "mutation_id", "payload", "idempotency_key")

    def __init__(self, trace_id, mutation_id, payload, idempotency_key):
        self.trace_id = trace_id
        self.mutation_id = mutation_id
        self.payload = payload
        self.idempotency_key = idempotency_key


def apply_mutation(raw: dict, *, trace_registry) -> MutationRecord:
    """Apply a memory mutation, enforcing causality-scoped correlation.

    `raw` is what arrives at the applier/compactor/tool server. If it lacks a
    trace_id -- because the ID lived only in the transport envelope and was lost
    at the async boundary -- we refuse the write rather than create an orphan.
    """
    trace_id = raw.get("trace_id")
    if not trace_id:
        raise UnattributedMutationError(
            f"mutation {raw.get('mutation_id')!r} has no trace_id in payload; "
            "envelope-only propagation is not sufficient across async boundaries"
        )
    if trace_id not in trace_registry:
        raise UnattributedMutationError(
            f"trace_id {trace_id!r} is not registered; refusing to mint an orphan mutation"
        )
    # Idempotency key must be derived from trace_id so retries collapse onto the
    # same causal event rather than producing a second, unattributable record.
    idem = raw.get("idempotency_key") or f"{trace_id}:{raw['mutation_id']}"
    return MutationRecord(trace_id, raw["mutation_id"], raw.get("payload", {}), idem)


def compact_memory(entries, *, originating_trace_id, trace_registry):
    """Background compaction MUST carry the originating turn's trace_id.

    A compactor that mints its own trace_id severs the causal chain between the
    rewritten entry and the turn that produced it.
    """
    if not originating_trace_id:
        raise UnattributedMutationError(
            "compaction run has no originating trace_id; rewritten entries would be orphans"
        )
    out = []
    for e in entries:
        out.append(apply_mutation(
            {"trace_id": originating_trace_id,
             "mutation_id": f"compact:{e['id']}",
             "payload": e},
            trace_registry=trace_registry,
        ))
    return out
