"""Debug journaling surfaces for DITAv2.""" from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any, Callable, Dict, List, Optional, Protocol from .contracts import KernelTransition, TradeSlot, TradeStage, VenueEvent from .control import KernelControlSnapshot from .utils import json_safe, json_text JournalSink = Callable[[str, Dict[str, Any]], None] class KernelJournal(Protocol): """Append-only debug journal interface.""" def record(self, row: Dict[str, Any]) -> None: ... def record_transition( self, *, transition: KernelTransition, slot: TradeSlot, event: Optional[VenueEvent] = None, control: Optional[KernelControlSnapshot] = None, ) -> None: ... @dataclass class MemoryKernelJournal: """In-memory journal used in tests.""" rows: List[Dict[str, Any]] = field(default_factory=list) capture_limit: int = 10_000 def record(self, row: Dict[str, Any]) -> None: if len(self.rows) < self.capture_limit: self.rows.append(dict(row)) def record_transition( self, *, transition: KernelTransition, slot: TradeSlot, event: Optional[VenueEvent] = None, control: Optional[KernelControlSnapshot] = None, ) -> None: row = _transition_row(transition=transition, slot=slot, event=event, control=control) self.record(row) class ClickHouseKernelJournal: """Fire-and-forget ClickHouse journal. The sink is a small callable of the form ``sink(table_name, row_dict)``. """ def __init__(self, sink: Optional[JournalSink] = None): self.sink = sink def record(self, row: Dict[str, Any]) -> None: if self.sink is not None: self.sink("dita_kernel_debug", row) def record_transition( self, *, transition: KernelTransition, slot: TradeSlot, event: Optional[VenueEvent] = None, control: Optional[KernelControlSnapshot] = None, ) -> None: self.record(_transition_row(transition=transition, slot=slot, event=event, control=control)) def _transition_row( *, transition: KernelTransition, slot: TradeSlot, event: Optional[VenueEvent], control: Optional[KernelControlSnapshot], ) -> Dict[str, Any]: return { "ts": transition.timestamp.isoformat() if hasattr(transition.timestamp, "isoformat") else str(transition.timestamp), "trade_id": transition.trade_id, "slot_id": transition.slot_id, "prev_state": transition.prev_state.value, "next_state": transition.next_state.value, "trigger": transition.trigger, "intent_id": transition.intent_id, "event_id": transition.event_id, "control_mode": transition.control_mode, "control_verbosity": transition.control_verbosity, "slot_state": slot.to_dict(), "event_payload": json_safe(event) if event is not None else {}, "control_snapshot": control.as_dict() if control is not None else {}, "slot_state_json": json_text(slot.to_dict()), }