""" sensorium/audio/arena.py — audio Delta-CRDT arena + merge kernel (ADR-0181 PR-5). Wires `AudioCompilationUnit`s into the Delta-CRDT substrate (ADR-0180 §2.1/§2.2): each compiled chunk is one delta; the audio adapter accumulates units in a thread-local `AudioArena` (share-nothing, never writes global state); the merge kernel folds arena snapshots into one content-addressed, deduplicated, totally ordered set keyed by the unit's `merge_key` (`canonical_sha256, ir_sha256, projection_sha256`). This is the **Python-layer mirror** of the Rust substrate (`core-rs/src/vault.rs` — `LocalArena` / `SemilatticeDelta` / `merge_kernel`, ADR-0180 §4.1): same content-addressed sort + byte-identical dedup, so the two stay in parity when the Rust↔Python binding lands (ADR-0180 §1.5.5 — deferred to a follow-up; the substrate works on the pure-CPU path first). Audio is the first concrete exerciser of ADR-0180 (ADR-0181 §3.1). The merge result is **permutation- and duplicate-invariant** — the property ADR-0180 §4.3 / ADR-0181 §4.2 (A-2, A-3) require: hash(Sequential_Ingest) == hash(Concurrent_CRDT_Ingest) Idempotence is structural, not asserted (ADR-0181 §2.2): identical canonical bytes under an identical pack produce an identical `merge_key`, so the join deduplicates them by construction. """ from __future__ import annotations import hashlib import json import threading from dataclasses import dataclass from sensorium.audio.trace import audio_evidence_trace from sensorium.audio.types import AudioCompilationUnit MergeKey = tuple[str, str, str] def _canonicalize( units: tuple[AudioCompilationUnit, ...] | list[AudioCompilationUnit], ) -> tuple[AudioCompilationUnit, ...]: """Order by content-addressed `merge_key`, drop byte-identical duplicates. `merge_key` encodes the canonical bytes, the IR, and the projection (the versor) — so equal keys mean content-identical units; the dedup is exact and arrival-independent, mirroring `core-rs` `Delta::from_entries`. """ ordered = sorted(units, key=lambda u: u.merge_key) deduped: list[AudioCompilationUnit] = [] last_key: MergeKey | None = None for unit in ordered: if unit.merge_key != last_key: deduped.append(unit) last_key = unit.merge_key return tuple(deduped) @dataclass(frozen=True, slots=True) class AudioDelta: """A canonical, order-invariant set of compiled audio chunks (ADR-0181 §2.2). Always held in content-addressed `merge_key` order with byte-identical duplicates removed, so it is a canonical join-semilattice element regardless of insertion order. Mirrors `core-rs` `Delta`. """ units: tuple[AudioCompilationUnit, ...] @classmethod def from_units( cls, units: tuple[AudioCompilationUnit, ...] | list[AudioCompilationUnit], ) -> AudioDelta: return cls(_canonicalize(units)) def join(self, other: AudioDelta) -> AudioDelta: """Join semilattice op: commutative, associative, idempotent under content-addressed equality (ADR-0180 §2.2).""" return AudioDelta.from_units((*self.units, *other.units)) @property def merge_keys(self) -> tuple[MergeKey, ...]: return tuple(u.merge_key for u in self.units) def __len__(self) -> int: return len(self.units) class AudioArena: """Thread-local, share-nothing accumulation arena for the audio adapter (ADR-0180 §2.1). Push compiled units lock-free; nothing is ever written to global state from an arena. `snapshot` emits a canonical, order-invariant `AudioDelta` and is **non-destructive** — flush/GC is the Merge Kernel's concern, so a delayed merge (the §3.2 eventual-consistency window) cannot lose a unit. Mirrors `core-rs` `LocalArena`. """ __slots__ = ("_units",) def __init__(self) -> None: self._units: list[AudioCompilationUnit] = [] def push(self, unit: AudioCompilationUnit) -> None: self._units.append(unit) def is_empty(self) -> bool: return not self._units def snapshot(self) -> AudioDelta: return AudioDelta.from_units(self._units) def __len__(self) -> int: return len(self._units) _THREAD_LOCAL = threading.local() def thread_local_audio_arena() -> AudioArena: """The calling thread's audio arena (ADR-0180 §2.1: each active adapter gets a thread-local arena). Created on first access per thread; never shared across threads, so it needs no lock.""" arena = getattr(_THREAD_LOCAL, "audio_arena", None) if arena is None: arena = AudioArena() _THREAD_LOCAL.audio_arena = arena return arena def reset_thread_local_audio_arena() -> None: """Drop the calling thread's arena (flush/test helper). Pools reuse threads, so callers that want a fresh arena per task must reset first.""" if hasattr(_THREAD_LOCAL, "audio_arena"): del _THREAD_LOCAL.audio_arena def merge_audio_deltas(deltas: list[AudioDelta] | tuple[AudioDelta, ...]) -> AudioDelta: """The Merge Kernel (ADR-0180 §2.2): fold arena deltas into one content- addressed, deduplicated, totally ordered `AudioDelta`. Implemented as a single canonicalisation of the union rather than a `fold(join)` chain; `tests/test_audio_crdt_merge.py` pins that the two are equal, so the cheap path can never silently diverge from the semilattice fold. Permutation- and duplicate-invariant — the property §4.3's `hash(Sequential) == hash(Concurrent)` rides on. """ units: list[AudioCompilationUnit] = [] for delta in deltas: units.extend(delta.units) return AudioDelta.from_units(units) def audio_merge_trace_hash(merged: AudioDelta) -> str: """Deterministic, PCM-free trace hash over a merged `AudioDelta` — the `sequential == concurrent` anchor (ADR-0181 §3.1, §4.2 A-3/A-6). The payload is the per-unit evidence trace (no waveform — ADR-0180 §1.5.5) in canonical `merge_key` order. Identical content in any arrival order yields the same hash; distinct content yields a different hash. Determinism here is order-invariance on a fixed platform; the compiler's A-1 gate owns cross-platform float stability of the underlying versors. """ payload = [audio_evidence_trace(unit) for unit in merged.units] blob = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") return hashlib.sha256(blob).hexdigest()