""" T19 Event Types โ€” typed events for the UV clock host. Per Fable's T19 spec (ANNEX B-CORRECTION): "iceoryx2 = the wires; ASEx = the state cells; UV clock = the conductor." Events are published via iceoryx2/zinc topics (transport). ASEx guards the state cells that handlers mutate. Staleness Law (ANNEX A): 1. BarFire is EDGE-TRIGGERED on scan_number monotonic advance โ€” NEVER timer-synthesized. 2. Every event carries provenance: scan_number, scan_ts, ingest_ts, AGE at dispatch. 3. Freshness watchdog TimerEvent emits StaleInput when stale. """ from __future__ import annotations import time from dataclasses import dataclass, field from enum import Enum from typing import Any, Optional class EventType(str, Enum): SCAN = "SCAN" TICK = "TICK" TIMER = "TIMER" BAR_FIRE = "BAR_FIRE" STALE_INPUT = "STALE_INPUT" @dataclass(frozen=True, slots=True) class EventProvenance: """ First-class freshness payload. Handlers CANNOT NOT know this. Every event carries provenance per T19 Staleness Law ยง2. """ scan_number: int scan_ts: int # ns, original scan timestamp ingest_ts: int # ns, when we received it source: str # "live" | "replay" @property def age_ns(self) -> int: """Age at construction time (not dispatch โ€” dispatch adds overhead).""" return time.time_ns() - self.ingest_ts @property def is_fresh(self) -> bool: """Fresh if age < 1.5x cadence (default 5.85s * 1.5 = 8.775s).""" return self.age_ns < 8_775_000_000 # 8.775s in ns def stale_threshold_ns(self, cadence_ns: int = 5_850_000_000) -> int: return int(cadence_ns * 1.5) @dataclass(frozen=True, slots=True) class ScanEvent: """ NG7 scan from HZ (live) or recorded source (replay). ~5.85s cadence live. Edge-triggered on scan_number advance. """ event_type: EventType = EventType.SCAN scan_number: int = 0 symbol: str = "" data: Any = None # scan payload (dict, bytes, etc.) provenance: EventProvenance = None def __post_init__(self): if self.provenance is None: object.__setattr__(self, 'provenance', EventProvenance( scan_number=self.scan_number, scan_ts=time.time_ns(), ingest_ts=time.time_ns(), source="live", )) @dataclass(frozen=True, slots=True) class TickEvent: """ OBF price tick. ~0.18s cadence live, per-symbol. TP/SL handlers subscribe to this. """ event_type: EventType = EventType.TICK symbol: str = "" price: float = 0.0 bid: float = 0.0 ask: float = 0.0 volume: float = 0.0 provenance: EventProvenance = None def __post_init__(self): if self.provenance is None: object.__setattr__(self, 'provenance', EventProvenance( scan_number=0, scan_ts=time.time_ns(), ingest_ts=time.time_ns(), source="live", )) @dataclass(frozen=True, slots=True) class TimerEvent: """ Scheduled wakeup: TTL expiries, watchdogs, cadence ticks. """ event_type: EventType = EventType.TIMER timer_id: str = "" payload: Any = None provenance: EventProvenance = None def __post_init__(self): if self.provenance is None: object.__setattr__(self, 'provenance', EventProvenance( scan_number=0, scan_ts=time.time_ns(), ingest_ts=time.time_ns(), source="live", )) @dataclass(frozen=True, slots=True) class BarFire: """ Brain dispatch event. EDGE-TRIGGERED on scan_number monotonic advance. Never timer-synthesized. No new scan = no BarFire. """ event_type: EventType = EventType.BAR_FIRE scan_number: int = 0 symbol: str = "" data: Any = None provenance: EventProvenance = None def __post_init__(self): if self.provenance is None: object.__setattr__(self, 'provenance', EventProvenance( scan_number=self.scan_number, scan_ts=time.time_ns(), ingest_ts=time.time_ns(), source="live", )) @dataclass(frozen=True, slots=True) class StaleInput: """ Freshness watchdog alarm. Emitted when: now - last_scan_ts > 1.5x cadence This is the June-22 scan-freeze / silent-scanner-death detector, native. Fixes BLUE's oldest observability hole as a side effect. """ event_type: EventType = EventType.STALE_INPUT source: str = "" last_scan_ts: int = 0 current_ts: int = 0 stale_duration_ns: int = 0 provenance: EventProvenance = None def __post_init__(self): if self.provenance is None: object.__setattr__(self, 'provenance', EventProvenance( scan_number=0, scan_ts=self.last_scan_ts, ingest_ts=time.time_ns(), source="live", ))