mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
Move wfigs wire rendering into notifications/formatters/fire.py and the
full fire state machine into notifications/gating/fire.py, behind the
registry, NO cutover. handle_wfigs builds a canonical dict, calls
decide(), keeps the inline fires INSERT/UPDATE of current_* and the
tombstoned_at stamp unconditional, then branches on is_cutover(...) —
legacy _attach_commit_handles/all-clear path stays byte-identical while
the new path bakes in shadow.
Reproduces every legacy stamp through GateResult.data_patch:
- forward-only acres/containment growth + 8h cooldown gating
- tombstone wildfire_closed all-clear (row exists AND last_broadcast_at
IS NOT NULL) with _severity_override="priority", _dedup_suffix="closed"
- growth _dedup_suffix=f"{acres}|{contained_pct}", _cooldown_suffix=irwin_id
- idempotent commit UPSERT of fires(last_broadcast_*) + event_log flip
- full _location_anchor fallback chain (geocoder_city -> resolve_anchor
-> landclass -> county -> state) preserved in the formatter
Registered under the three explicit categories (wildfire_declared,
wildfire_incident, wildfire_closed) rather than the `fire` toggle, so the
family-fallback does NOT capture the still-deferred FIRMS categories
(wildfire_hotspot/new_ignition/wildfire_growth); a registration test
asserts those resolve elsewhere.
Native env/fires.py deferred (missing IRWIN/cause/landclass, no tombstone
concept); non-cutover so store._emit_event won't run it.
tier-a: 19 new golden+gate-sequence tests; wfigs handler 23/23 preserved;
suite at 34-failure baseline (1571 passed).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.7 KiB
Python
102 lines
4.7 KiB
Python
"""Gating/budgeting decision registry for the Phase-1+ dispatch path.
|
|
|
|
Phase-1: earthquake_event registered (quake.py).
|
|
All other categories retain their existing handler-module gating.
|
|
|
|
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
|
|
|
|
# Populated by per-category gating modules imported below.
|
|
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
|
|
|
|
|
|
# ── Phase-1 registrations ────────────────────────────────────────────────────
|
|
from meshai.notifications.gating import quake as _quake_gate_mod # noqa: E402,F401
|
|
register("earthquake_event", _quake_gate_mod.decide)
|
|
|
|
from meshai.notifications.gating import avalanche as _avy_gate_mod # noqa: E402,F401
|
|
register("avalanche_warning", _avy_gate_mod.decide)
|
|
register("avalanche_watch", _avy_gate_mod.decide)
|
|
|
|
# SWPC: geomagnetic_storm (swpc_kindex / native G-scale) and rf_propagation_alert
|
|
# (swpc_alerts flare / native R-scale). solar_radiation_storm (proton) stays on
|
|
# the legacy path — NOT registered here.
|
|
from meshai.notifications.gating import swpc as _swpc_gate_mod # noqa: E402,F401
|
|
register("geomagnetic_storm", _swpc_gate_mod.decide)
|
|
register("rf_propagation_alert", _swpc_gate_mod.decide)
|
|
|
|
# Phase-2: NWS weather alerts (weather_warning + weather_statement).
|
|
# weather_watch and weather_advisory are not yet migrated (Phase-2 scope).
|
|
from meshai.notifications.gating import nws as _nws_gate_mod # noqa: E402,F401
|
|
register("weather_warning", _nws_gate_mod.decide)
|
|
register("weather_statement", _nws_gate_mod.decide)
|
|
|
|
# Phase-2: incident / roads categories.
|
|
# One decider handles all four; it reads external_id to decide dedup path.
|
|
from meshai.notifications.gating import incident as _incident_gate_mod # noqa: E402,F401
|
|
register("work_zone", _incident_gate_mod.decide)
|
|
register("road_incident", _incident_gate_mod.decide)
|
|
register("road_closure", _incident_gate_mod.decide)
|
|
register("traffic_congestion", _incident_gate_mod.decide)
|
|
|
|
# Phase-3: USGS NWIS stream-gauge hydro. Registered under `stream_flow` — the
|
|
# flat category the Central nwis path produces for every `central.hydro.*`
|
|
# envelope (map_category "hydro." -> "stream_flow"). Native usgs categories
|
|
# (stream_flood_warning / stream_high_water, emitted only by env/usgs.py) are a
|
|
# deferred follow-up: env/usgs.py is NOT migrated this phase and, since hydro is
|
|
# NOT cut over, store._emit_event's native decider hook won't run it.
|
|
from meshai.notifications.gating import hydro as _hydro_gate_mod # noqa: E402,F401
|
|
register("stream_flow", _hydro_gate_mod.decide)
|
|
|
|
# Phase-3b: WFIGS wildfire. Three explicit categories the wfigs_handler emits:
|
|
# `wildfire_declared` (New), `wildfire_incident` (growth Update), and
|
|
# `wildfire_closed` (tombstone all-clear). One decider handles all three; it
|
|
# keys off canonical `_kind` (wfigs_incident / wfigs_tombstone / wfigs_perimeter)
|
|
# to route New/Update/suppress vs the all-clear eligibility. Registered under
|
|
# explicit strings (not the "fire" toggle) so FIRMS categories are untouched —
|
|
# FIRMS + native env/fires.py are a deferred follow-up (env/fires.py lacks the
|
|
# IRWIN/FireCause/landclass fields and has no tombstone concept, and since fire
|
|
# is NOT cut over, store._emit_event's native decider hook won't run it).
|
|
from meshai.notifications.gating import fire as _fire_gate_mod # noqa: E402,F401
|
|
register("wildfire_declared", _fire_gate_mod.decide)
|
|
register("wildfire_incident", _fire_gate_mod.decide)
|
|
register("wildfire_closed", _fire_gate_mod.decide)
|