mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
Foundation for making all hazard formatting+gating source-agnostic. ZERO behavior change — the formatter/decider registries are empty (get_formatter/ get_decider return None → existing precomposed/Mode-B path preserved), and the shadow comparator is off unless MESHAI_SHADOW_CATEGORIES is set. - notifications/formatters/ (registry+dispatch with family fallback), gating/ (GateResult + deferred-commit contract), both empty registries. - notifications/clock.py determinism seam; route wfigs/quake/nws gating time reads through it (identical values) so goldens can freeze time. - formatters/_budget.py = copy of central/budget.py; central/budget.py is now a re-export shim (import-smoke test guards it). - compose_mesh_message consults the registry first (verbatim, no Mode-B re-cap), falls back to legacy; _resolve_budget injects per-category budget. - notifications/shadow.py + two DRY-RUN hooks (consumer._normalize, dispatcher render): compute the new result and diff-log SHADOW_MISMATCH JSONL, but NEVER commit/emit/write tables and always broadcast the OLD result. Inert by default. - tests/harness (pinned_time/pinned_tz, byte-golden, gate-sequence) + scripts/capture_fixtures.py (ephemeral read-only NATS capture); tzdata pinned. Tests: +60 (18 scaffold + 42 harness/shadow); 0 new failures (34 baseline). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
1.6 KiB
Python
50 lines
1.6 KiB
Python
"""Gating/budgeting decision registry for the Phase-1+ dispatch path.
|
|
|
|
DECIDERS is intentionally empty at this phase (Phase 0 scaffold).
|
|
No category is migrated yet — all gating logic remains in the existing
|
|
handler modules (wfigs_handler, nws_handler, etc.). Zero behavior change.
|
|
|
|
Usage (future phases):
|
|
from meshai.notifications.gating import register
|
|
from meshai.notifications.gating.base import GateResult
|
|
|
|
@register("earthquake_event")
|
|
def _gate_quake(event, *, now: float) -> GateResult:
|
|
...
|
|
"""
|
|
|
|
from typing import Callable, Optional
|
|
|
|
# Empty registry — populated by per-category gating modules (Phase 1+).
|
|
DECIDERS: dict = {}
|
|
|
|
|
|
def register(category: str, fn: Callable) -> Callable:
|
|
"""Register a gating callable for a category (or family toggle name)."""
|
|
DECIDERS[category] = fn
|
|
return fn
|
|
|
|
|
|
def get_decider(category: str) -> Optional[Callable]:
|
|
"""Return the gating decider for *category*, or None if none is registered.
|
|
|
|
Resolution order:
|
|
1. Direct category match in DECIDERS.
|
|
2. Family/toggle fallback: look up the category's toggle name via
|
|
get_toggle(), then check DECIDERS for that toggle key.
|
|
3. None — caller falls through to legacy per-handler gating logic.
|
|
"""
|
|
fn = DECIDERS.get(category)
|
|
if fn is not None:
|
|
return fn
|
|
# Family fallback — mirrors the toggle lookup pattern in composer.py.
|
|
try:
|
|
from meshai.notifications.categories import get_toggle
|
|
tog = get_toggle(category)
|
|
if tog:
|
|
fn = DECIDERS.get(tog)
|
|
if fn is not None:
|
|
return fn
|
|
except Exception:
|
|
pass
|
|
return None
|