meshai/work/tests/test_fire_refactor.py

448 lines
22 KiB
Python
Raw Normal View History

refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
"""Phase-3b WFIGS wildfire refactor tests.
Verifies the source-agnostic formatter+decider migration for the wildfire
hazard, mirroring test_hydro_refactor.py / test_quake_refactor.py:
1. Golden byte-identical: formatters.fire.format() reproduces the legacy
wfigs_handler._render() wire exactly for a New incident, an Update-with-
growth ((+delta) size line), a movement-line case, an anchor-line case, and
the wildfire_closed all-clear.
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
2. Gate-sequence: an explicit `now`-timeline of WFIGS events driven through
`gating.fire.decide()` (the LIVE decider -- see
tests/test_fire_native_growth.py for its full end-to-end native-adapter
coverage) exercises the New/cooldown/Update/closed lifecycle and the
data-dict stamps (category / _severity_override / _dedup_suffix /
_cooldown_suffix) `decide()` hands the dispatcher.
3. Registration: the three explicit categories the WFIGS decider/formatter
pair emits (wildfire_declared / wildfire_incident / wildfire_closed)
resolve to the fire formatter + decider, and FIRMS categories do NOT.
chore/ripout-2dii: `handle_wfigs` (the dead Central NATS-envelope entrypoint
this file used purely as a byte-identity driver) has been REMOVED from
`meshai.env.fire_render` -- zero live production callers. `fire_format` IS
live (reached for wildfire_declared/wildfire_incident via the native WFIGS
adapter `env/fires.py` -> `env/store.py::_emit_event`, forced onto
`gating.fire.decide` + this formatter via `cutover.NATIVE_ALWAYS_DECIDE`
independent of any env var -- see `notifications/renderers/composer.py:343-347`
and `tests/test_fire_native_growth.py`), so this file's fire_format coverage
stays green; only the handle_wfigs-as-oracle plumbing was replaced with
direct `_wfigs_render` calls (also live -- see env/fire_fusion.py's FIRMS
growth path) and direct `fire_decide()` driving.
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
"""
from __future__ import annotations
import pytest
chore(central-ripout 2a): remove the budget shim + dead work_zone renderer (#161) * chore: remove central.budget re-export shim The shim's implementation lived at notifications.formatters._budget from the start; central.budget was only a 9-line re-export kept around for import-path compatibility. Point every importer directly at the real module and delete the shim: - notifications/renderers/composer.py: lazy import inside a function - central/wfigs_handler.py, central/satpass_handler.py: import line only - tests/test_fire_refactor.py, test_nws_refactor.py, test_firms_refactor.py: import line only, no behavior change test_budget_shim.py existed solely to assert identity-equality between the shim and the real module; with the shim gone there is nothing left for it to test, so it is deleted too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: delete dead renderers/work_zone.py format_work_zone_mesh() had exactly one production caller, the central/consumer.py NATS bridge deleted in the prior Central-excision pass. Its live replacement, formatters.incident._render_work_zone() (registered for category "work_zone" in formatters/__init__.py), is an already-shipped byte-identical replica per that module's own docstring. All remaining references to renderers.work_zone were prose/comments describing the replica relationship, not imports. Test fallout: - tests/test_work_zone_renderer.py tested only the dead renderer in isolation (17 cases). Deleted — the live path has its own coverage (test_adapter_wzdx.py's formatter-integration tests, plus TestCrossSourceIdentity::test_work_zone_category_uses_wz_renderer and TestWorkZoneGolden in test_incident_refactor.py). - tests/test_itd_511_work_zone.py::test_itd_511_work_zone_renderer_produces_wire only smoke-tested the dead renderer's wire output for itd_511 data; redundant with TestWorkZoneGolden's byte-identical fixture coverage for the same adapter. Deleted. - tests/test_incident_refactor.py::TestWorkZoneGolden compared the live formatters.incident.format() output against a golden computed by calling the dead renderer live on two real fixtures. Mirroring the precedent already in test_nws_refactor.py for this exact situation (golden generator deleted out from under a parity test), the two golden strings were captured by running format_work_zone_mesh() against these fixtures immediately before deletion and are now pinned as literals — same coverage, no live dependency on the dead module. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:12:55 -06:00
from meshai.notifications.formatters._budget import budget_for
chore(central-ripout 2d-i): relocate the fire engine + renderer out of central/ (#164) Moves the last two live files out of the retired-Central folder. central/ is now EMPTY and deleted entirely (incl. __init__.py). - fire-fusion engine (ingest_hotspot_pixel + the growth/cluster/spotting engine) → env/fire_fusion.py, next to its sole consumer env/firms.py. - wildfire text renderer (_render + its live helpers) → env/fire_render.py. THE COUPLING RESOLVED: firms_handler._handle_pass_boundary had a LAZY import inside a function body — `from meshai.central.wfigs_handler import _render` — sitting on the live FIRMS fire-growth path. It is now a normal top-of-file import (`from meshai.env.fire_render import _render`), visible and greppable. PURE MOVE — no behavior change: - The parity oracle (test_fire_refactor.py) PASSES UNCHANGED (only its import paths updated) — proving the fire wire output is byte-for-byte identical before and after. Fire alerts say exactly what they said. - handle_firms / handle_wfigs (dead entrypoints, only caller was the deleted consumer.py) were KEPT and moved rather than dropped — the wording-cleanup PR removes them deliberately. "When unsure, keep." - fire_render.py's geo-helper imports still point at central_normalizer — that file's split is a SEPARATE PR; carried the imports along, did not touch it. Consumers + test import paths rewired. Full suite: 2059 passed, 0 failed. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:31:48 -06:00
from meshai.env.fire_render import (
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
_build_canonical,
_render as _wfigs_render,
)
from meshai.notifications.formatters.fire import format as fire_format
from meshai.notifications.gating.fire import decide as fire_decide
from meshai.persistence import close_thread_connection, init_db
from meshai.persistence import db as persistence_db
from tests.harness.goldens import assert_byte_identical
from tests.test_wfigs_handler import (
_IRWIN_A,
_make_active_envelope,
_make_tombstone,
chore(central-ripout 2e): split central_normalizer.py; eliminate the "central" name (#165) Final structural step. central_normalizer.py (973 LOC, misnamed — it lived OUTSIDE central/ but its docstring described "the Central firehose") is DELETED. No file or symbol named "central" survives (only immutable historical comments in an already-applied migration .sql remain — those are not edited). Split per the owner's rule ("next to what uses it"): - Live geo/place-name utilities (nearest_town, _haversine_miles, _bearing_compass, _compute_distance_bearing, _h3_cell, _photon_reverse_places, geocoder config) → the EXISTING meshai/geo.py ("Geographic utilities…"), same domain. NOT a junk drawer. geo.py's nearest_city (hardcoded table) and central_normalizer's nearest_town (live Photon+H3 reverse-geocode) are different implementations — both kept, no false consolidation. - WZDx work-zone parsers (_parse_wzdx_federal, _norm_wzdx_sub_type, _norm_direction, _parse_mile_posts, _clean_description, _is_uninformative_road) → env/wzdx_parse.py, next to their sole consumer env/wzdx.py. - ~400 dead lines deleted: the Central-envelope machinery (normalize, _parse_state_511_atis, _parse_wfigs_incidents, _parse_itd_511_work_zone, should_skip_state_511_atis_id, is_incident_envelope_stale, normalize_road_name, _norm_sub_type, _parse_ends_at) — only caller was the deleted Central consumer. Consumers rewired: env/wzdx.py, env/fire_render.py, formatters/fire.py, formatters/_anchor.py, main.py, persistence/curation.py, composer.py. Tests: test_central_normalizer.py RENAMED → test_geo_wzdx_parse.py (live geo/wzdx tests kept + rewired; dead-parser tests dropped). test_itd_511_work_zone.py deleted (tested only the dead itd_511 parser — verified gone from source). Other test files: import-path rewires. Live coverage for nearest_town / _parse_wzdx_federal / bearing preserved across test_geo_wzdx_parse, test_adapter_wzdx, test_wfigs_handler, test_incident_refactor. Suite: 2010 passed, 0 failed. The 49-test drop is the dead Central-envelope parser tests, correctly removed. Stale "Central firehose" docstrings on the moved code updated to describe present behavior. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:15:50 -06:00
_normalize_wfigs,
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
)
_AT = 1_800_000_000.0 # pinned epoch (unused by fire render; kept for parity)
@pytest.fixture
def mem_db(monkeypatch, tmp_path):
db_path = str(tmp_path / "fire-refactor-test.sqlite")
monkeypatch.setenv("MESHAI_DB_PATH", db_path)
persistence_db._initialised.clear()
close_thread_connection()
conn = init_db()
try:
from meshai.adapter_config import adapter_config as _ac
_ac.invalidate()
except Exception:
pass
try:
chore(central-ripout 2d-i): relocate the fire engine + renderer out of central/ (#164) Moves the last two live files out of the retired-Central folder. central/ is now EMPTY and deleted entirely (incl. __init__.py). - fire-fusion engine (ingest_hotspot_pixel + the growth/cluster/spotting engine) → env/fire_fusion.py, next to its sole consumer env/firms.py. - wildfire text renderer (_render + its live helpers) → env/fire_render.py. THE COUPLING RESOLVED: firms_handler._handle_pass_boundary had a LAZY import inside a function body — `from meshai.central.wfigs_handler import _render` — sitting on the live FIRMS fire-growth path. It is now a normal top-of-file import (`from meshai.env.fire_render import _render`), visible and greppable. PURE MOVE — no behavior change: - The parity oracle (test_fire_refactor.py) PASSES UNCHANGED (only its import paths updated) — proving the fire wire output is byte-for-byte identical before and after. Fire alerts say exactly what they said. - handle_firms / handle_wfigs (dead entrypoints, only caller was the deleted consumer.py) were KEPT and moved rather than dropped — the wording-cleanup PR removes them deliberately. "When unsure, keep." - fire_render.py's geo-helper imports still point at central_normalizer — that file's split is a SEPARATE PR; carried the imports along, did not touch it. Consumers + test import paths rewired. Full suite: 2059 passed, 0 failed. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:31:48 -06:00
from meshai.env import fire_render as _wh
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
_wh._last_cleanup = 0
except Exception:
pass
yield conn
close_thread_connection()
persistence_db._initialised.discard(db_path)
class _FakeEvent:
def __init__(self, data, category=None):
self.data = data
self.category = category
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
def _write_fire_state(conn, *, irwin_id, name, acres, contained_pct,
lat, lon, county, state, declared_at_epoch=None,
now):
"""Unconditional current_* state write, mirroring the LIVE native path's
own upsert (env/store.py::_ingest_fires INSERT/UPDATE current_acres /
current_contained_pct) -- the same shape handle_wfigs used to do inline
before it was deleted (chore/ripout-2dii)."""
row = conn.execute(
"SELECT irwin_id FROM fires WHERE irwin_id=?", (irwin_id,)).fetchone()
if row is None:
conn.execute(
"INSERT INTO fires(irwin_id, incident_name, current_acres, "
"current_contained_pct, lat, lon, county, state, declared_at, "
"last_event_at, last_broadcast_at, last_broadcast_acres, "
"last_broadcast_contained) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
(irwin_id, name, acres, contained_pct, lat, lon, county, state,
declared_at_epoch, now, None, None, None),
)
else:
conn.execute(
"UPDATE fires SET current_acres=?, current_contained_pct=?, "
"lat=COALESCE(?, lat), lon=COALESCE(?, lon), last_event_at=? "
"WHERE irwin_id=?",
(acres, contained_pct, lat, lon, now, irwin_id),
)
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# ─────────────────────────────────────────────────────────────────────────────
# 1. Golden byte-identical — formatter reproduces _render()/all-clear exactly
# ─────────────────────────────────────────────────────────────────────────────
class TestFormatterGolden:
"""formatters.fire.format() == wfigs_handler._render() for the same inputs."""
def _budget(self) -> int:
return budget_for("wfigs")
def _incident_data(self, **over) -> dict:
d = {
"incident_name": "Cache Peak Fire",
"acres": 1847.0,
"contained_pct": 23,
"fire_cause": "Lightning",
"declared_at_epoch": 1_781_204_400,
"unique_fire_id": "2026-IDSCF-000987",
"geocoder_city": "Burley", # short-circuits anchor (no DB/Photon)
"lat": 42.197,
"lon": -113.710,
"county": "Cassia",
"state": "ID",
"landclass": None,
}
d.update(over)
return d
def test_new_incident(self, mem_db):
n = self._incident_data()
old = _wfigs_render(n, prefix="New")
new = fire_format(_FakeEvent({**n, "is_update": False}),
now=_AT, budget=self._budget())
assert_byte_identical(new, old)
assert new.startswith("🔥 Cache Peak Fire — New")
assert "1,847 ac" in new
assert "containment 23%" in new
def test_update_with_growth_delta(self, mem_db):
n = self._incident_data(acres=3000.0, contained_pct=35)
old = _wfigs_render(n, prefix="Update",
last_bcast_acres=1847.0, last_bcast_contained=23)
new = fire_format(
_FakeEvent({**n, "is_update": True,
"last_bcast_acres": 1847.0, "last_bcast_contained": 23}),
now=_AT, budget=self._budget())
assert_byte_identical(new, old)
assert new.startswith("🔥 Cache Peak Fire — Update")
assert "3,000 ac (+1,153)" in new # delta line
assert "containment 35%" in new
def test_movement_line(self, mem_db):
# movement is FIRMS-injected; the formatter must read it from event.data
# exactly as _render(movement=...) does today.
mv = {"direction": "NE", "speed_mph": 1.2}
n = self._incident_data()
old = _wfigs_render(n, prefix="New", movement=mv)
new = fire_format(_FakeEvent({**n, "is_update": False, "movement": mv}),
now=_AT, budget=self._budget())
assert_byte_identical(new, old)
assert "Moving NE 1.2 mi/h" in new
def test_anchor_line(self, mem_db):
# No geocoder_city → line 3 is resolved via the shared resolve_anchor +
# the legacy fallback tiers. Both paths hit the same seeded town_anchors
# table, so the wire must stay byte-identical.
n = self._incident_data(geocoder_city=None)
old = _wfigs_render(n, prefix="New")
new = fire_format(_FakeEvent({**n, "is_update": False}),
now=_AT, budget=self._budget())
assert_byte_identical(new, old)
# line 3 is NOT a movement line and NOT a raw city name
assert "Moving" not in new
def test_wildfire_closed_all_clear(self, mem_db):
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
# Drive the LIVE tombstone decision (gating.fire.decide) directly --
# handle_wfigs (the dead Central entrypoint that used to wrap this) is
# gone (chore/ripout-2dii). State is written the same unconditional
# shape the live native path uses (_write_fire_state).
irwin_id = "IRWIN-CLOSED-1"
_write_fire_state(
mem_db, irwin_id=irwin_id, name="Cache Peak Fire",
acres=1847.0, contained_pct=23, lat=42.197, lon=-113.710,
county="Cassia", state="ID", now=1_000_000)
gate0 = fire_decide(
{"_kind": "wfigs_incident", "irwin_id": irwin_id,
"incident_name": "Cache Peak Fire", "acres": 1847.0,
"contained_pct": 23, "declared_at_epoch": None,
"lat": 42.197, "lon": -113.710, "county": "Cassia", "state": "ID"},
source="wfigs", now=1_000_000.0)
assert gate0.broadcast is True
gate0.commit(1_000_000.0) # arm last_broadcast_* (fire "reached mesh")
gate_t = fire_decide(
{"_kind": "wfigs_tombstone", "irwin_id": irwin_id},
source="wfigs", now=2_000_000.0)
assert gate_t.broadcast is True
assert gate_t.data_patch["category"] == "wildfire_closed"
assert gate_t.data_patch["incident_name"] == "Cache Peak Fire"
new_wire = fire_format(
_FakeEvent(gate_t.data_patch, category="wildfire_closed"),
now=_AT, budget=budget_for("wfigs"))
assert new_wire.startswith("✅ Cache Peak Fire — contained & closed")
# Golden literal (captured from the live fire_format all-clear branch;
# this is the SAME format string handle_wfigs used to build inline
# before its removal -- see notifications/formatters/fire.py::_render_allclear).
assert new_wire == "✅ Cache Peak Fire — contained & closed\n1,847 ac | 23% contained | 24 mi S of Burley"
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# ─────────────────────────────────────────────────────────────────────────────
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
# 2. Gate-sequence — decide() drives New/cooldown/Update/closed
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# ─────────────────────────────────────────────────────────────────────────────
class TestGateSequenceParity:
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
"""decide() correctly negotiates a full fire lifecycle end to end.
New/Update/cooldown/suppress behavior through the REAL native entrypoint
(env/fires.py -> env/store.py -> compose_mesh_message) is covered more
faithfully by tests/test_fire_native_growth.py; this class focuses on the
parts that file doesn't reach -- the tombstone/closed lifecycle step (no
live producer currently emits `_kind=wfigs_tombstone`, but the decider +
formatter branch is live/shipped code and deserves regression coverage)
-- plus a defense-in-depth pass over the whole sequence via decide()
directly (no handle_wfigs; that entrypoint is gone).
"""
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
def _decide(self, env, now):
chore(central-ripout 2e): split central_normalizer.py; eliminate the "central" name (#165) Final structural step. central_normalizer.py (973 LOC, misnamed — it lived OUTSIDE central/ but its docstring described "the Central firehose") is DELETED. No file or symbol named "central" survives (only immutable historical comments in an already-applied migration .sql remain — those are not edited). Split per the owner's rule ("next to what uses it"): - Live geo/place-name utilities (nearest_town, _haversine_miles, _bearing_compass, _compute_distance_bearing, _h3_cell, _photon_reverse_places, geocoder config) → the EXISTING meshai/geo.py ("Geographic utilities…"), same domain. NOT a junk drawer. geo.py's nearest_city (hardcoded table) and central_normalizer's nearest_town (live Photon+H3 reverse-geocode) are different implementations — both kept, no false consolidation. - WZDx work-zone parsers (_parse_wzdx_federal, _norm_wzdx_sub_type, _norm_direction, _parse_mile_posts, _clean_description, _is_uninformative_road) → env/wzdx_parse.py, next to their sole consumer env/wzdx.py. - ~400 dead lines deleted: the Central-envelope machinery (normalize, _parse_state_511_atis, _parse_wfigs_incidents, _parse_itd_511_work_zone, should_skip_state_511_atis_id, is_incident_envelope_stale, normalize_road_name, _norm_sub_type, _parse_ends_at) — only caller was the deleted Central consumer. Consumers rewired: env/wzdx.py, env/fire_render.py, formatters/fire.py, formatters/_anchor.py, main.py, persistence/curation.py, composer.py. Tests: test_central_normalizer.py RENAMED → test_geo_wzdx_parse.py (live geo/wzdx tests kept + rewired; dead-parser tests dropped). test_itd_511_work_zone.py deleted (tested only the dead itd_511 parser — verified gone from source). Other test files: import-path rewires. Live coverage for nearest_town / _parse_wzdx_federal / bearing preserved across test_geo_wzdx_parse, test_adapter_wzdx, test_wfigs_handler, test_incident_refactor. Suite: 2010 passed, 0 failed. The 49-test drop is the dead Central-envelope parser tests, correctly removed. Stale "Central firehose" docstrings on the moved code updated to describe present behavior. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:15:50 -06:00
n = _normalize_wfigs(env)
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
canonical = _build_canonical(n, n["_kind"])
return fire_decide(canonical, source="wfigs", now=float(now))
def test_full_lifecycle(self, mem_db):
irwin = _IRWIN_A
base = 1_800_000_000
disc_ms = (base - 3600) * 1000 # discovered 1h before first sight (fresh)
def _active(acres, pct, subject_n="a"):
return _make_active_envelope(
irwin_id=irwin, geocoder_city="Burley",
daily_acres=acres, pct_contained=pct,
fire_discovery_dt_ms=disc_ms)
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
def _step(env, now, *, expect_broadcast, expect_lifecycle):
n = _normalize_wfigs(env)
gate = self._decide(env, now)
assert gate.broadcast is expect_broadcast, (
f"decide broadcast {gate.broadcast} != {expect_broadcast} "
f"@ {now} ({expect_lifecycle})")
assert gate.lifecycle == expect_lifecycle, (
f"decide lifecycle {gate.lifecycle} != {expect_lifecycle} @ {now}")
# Unconditional state write, mirroring the live native path.
_write_fire_state(
mem_db, irwin_id=irwin, name=n.get("incident_name"),
acres=n.get("acres"), contained_pct=n.get("contained_pct"),
lat=n.get("lat"), lon=n.get("lon"), county=n.get("county"),
state=n.get("state"), declared_at_epoch=n.get("declared_at_epoch"),
now=now)
if expect_broadcast:
if expect_lifecycle == "new":
assert gate.data_patch.get("category") == "wildfire_declared"
elif expect_lifecycle == "update":
assert "category" not in gate.data_patch
assert gate.data_patch.get("_severity_override") == "priority"
gate.commit(float(now))
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# [0] first sight → New broadcast
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
_step(_active(250.0, 0), base,
expect_broadcast=True, expect_lifecycle="new")
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# [1] small growth 1h later (inside 8h cooldown) → suppress
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
_step(_active(300.0, 0), base + 3600,
expect_broadcast=False, expect_lifecycle="cooldown")
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# [2] growth after cooldown (8h) → Update
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
_step(_active(500.0, 0), base + 28800,
expect_broadcast=True, expect_lifecycle="update")
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# [3] containment change after another cooldown → Update
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
_step(_active(500.0, 40), base + 28800 * 2,
expect_broadcast=True, expect_lifecycle="update")
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# [4] tombstone → all-clear (fire was broadcast earlier)
chore(central-ripout 2d-ii): remove dead fire scraps (NOT the 'rewrite' — there wasn't one) (#166) * chore(fire-ripout 2dii-a): remove dead wildfire_growth cutover path + handle_firms wildfire_growth events are always fully precomposed (env/firms.py sets _meshai_precomposed=True + title=<wire from _render()>) and the category is not in cutover.NATIVE_ALWAYS_DECIDE, so the registered fire formatter could never be reached live via compose_mesh_message for this category -- the precomposed-title bypass always won first. Removes the dead is_cutover("wildfire_growth") NEW-PATH branch in fire_fusion._handle_pass_boundary (kept the live legacy leg that calls _render unconditionally) and the now-unreachable wildfire_growth formatter registration in notifications/formatters/__init__.py. Also removes handle_firms, the dead Central NATS-envelope entrypoint (zero live production callers -- Central's consumer that drove it is gone), plus its envelope-specific filtering helpers (_confidence_passes, _in_bbox, _coerce_severity, _log_event, FIRMS_CONFIDENCE_FLOOR/FRP_FLOOR/BBOX_OPTIONAL) that had no live callers left. The shared, LIVE fusion core (_ingest_pixel_core, attribution, clustering, growth/spotting/halt) and _parse_acq_epoch are unaffected -- still the engine behind the native ingest_hotspot_pixel entrypoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(fire-ripout 2dii-b): remove dead handle_wfigs entrypoint handle_wfigs (the dead Central NATS-envelope entrypoint) had zero live production callers -- Central's consumer that drove it is gone. The LIVE WFIGS path is env/fires.py (native adapter) -> env/store.py::_emit_event, forced onto gating.fire.decide + the shared fire formatter via cutover.NATIVE_ALWAYS_DECIDE, independent of this dead handler. Removes handle_wfigs and its private-only helpers (_coerce_severity, _log_event, _log_event_returning_id) that had no callers left. _render remains -- it is called directly by env.fire_fusion._handle_pass_boundary on the FIRMS wildfire_growth path, and is used as a byte-identity oracle by tests against the shared, live fire formatter. _build_canonical, _attach_commit_handles, _fire_too_old_to_announce, _now, _cleanup_stale_fires and WFIGS_BROADCAST_COOLDOWN_S are kept -- each has its own direct test coverage independent of handle_wfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-ripout 2dii-c): rewire fire tests off the deleted dead entrypoints handle_firms and handle_wfigs (both removed in the prior two commits) were used throughout this test suite purely as convenient DRIVERS for live logic (pixel attribution/clustering/growth/spotting/halt fusion; the WFIGS New/Update/cooldown/closed decider). Per-file disposition: Rewired to drive the LIVE native entrypoint directly (no behavior change -- same underlying _ingest_pixel_core / gating.fire.decide engines): - test_fire_tracker_phase1/2/3.py: handle_firms -> ingest_hotspot_pixel - test_firms_refactor.py: handle_firms driver helpers -> ingest_hotspot_pixel; dropped the dead wildfire_growth cutover test + the fully-redundant TestNotCutoverLegacyVerbatim class (already covered by test_firms_native_fusion.py's TestIngestGrowth/Spotting/Halt against the live entrypoint); wildfire_growth formatter registration test now asserts None (matches source change). - test_wfigs_handler.py: handle_wfigs -> _render (the live WFIGS renderer) called directly, for the anchor-priority + missing-acres cases that exercise shared/live code (_location_anchor). - test_fire_refactor.py: handle_wfigs -> gating.fire.decide() driven directly, with state written the same unconditional shape the live native path uses; TestGateSequenceParity trimmed to focus on the tombstone/closed lifecycle step (not covered by test_fire_native_growth.py's native-adapter New/Update/cooldown coverage). - test_tombstone_broadcast.py, test_fire_age_gate.py: handle_wfigs -> gating.fire.decide() driven directly; same assertions, off the dead path. Deleted as pure dead-entrypoint contract testing with no live equivalent: - test_firms_native_fusion.py::TestCentralPathUnchanged (2 tests) -- existed only to guard handle_firms's own envelope parsing. - test_firms_handler.py: confidence/FRP/bbox filtering, missing-coords, non-firms-adapter guard, event_log accounting (all handle_firms-specific; the native adapter filters upstream in a different code path). Kept + rewired: the shared _ingest_pixel_core dedup behavior and _parse_acq_epoch's int/short acq_time parsing (both genuinely live/shared). - test_wfigs_handler.py: envelope field-extraction (acres-fallback chain, IA-placeholder-as-name), tombstone/perimeter subject -> event_log, New/Update/cooldown decision + audit-row wiring (all redundant with tests/test_fire_native_growth.py's coverage of gating.fire.decide() through the real native adapter). - test_tombstone_broadcast.py::test_commit_callback_flips_handled -- asserted handle_wfigs's own event_log-row flip on commit, a Central-only concept the native path never used. - test_tail_followups.py::test_wfigs_tombstone_stamps_column -- asserted handle_wfigs's own inline tombstoned_at UPDATE; no live producer emits _kind=wfigs_tombstone today, so nothing in the live system stamps it. Both live renderers stay covered: fire_format (wildfire_declared/incident, via test_fire_refactor.py + test_fire_native_growth.py, untouched) and _render (wildfire_growth via FIRMS, via test_fire_tracker_phase2.py + test_firms_native_fusion.py's TestIngestGrowth, both driving the live ingest_hotspot_pixel entrypoint). Collection: 2010 -> 1974 tests (net -36: dead-only tests deleted, live behavior rewired 1:1 or consolidated onto already-existing native-path coverage). Full suite: 1974 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:11:00 -06:00
gate_t = fire_decide({"_kind": "wfigs_tombstone", "irwin_id": irwin},
source="wfigs", now=float(base + 100000))
assert gate_t.broadcast is True
assert gate_t.lifecycle == "closed"
assert gate_t.data_patch["category"] == "wildfire_closed"
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
def test_dedup_suffix_tracks_state(self, mem_db):
"""_dedup_suffix carries the acres|contained that justified the
broadcast (so unchanged re-deliveries dedup but genuine updates pass)."""
base = 1_800_000_000
disc_ms = (base - 3600) * 1000
env_new = _make_active_envelope(
irwin_id=_IRWIN_A, geocoder_city="Burley",
daily_acres=250.0, pct_contained=0, fire_discovery_dt_ms=disc_ms)
gate = self._decide(env_new, base)
chore(central-ripout 2e): split central_normalizer.py; eliminate the "central" name (#165) Final structural step. central_normalizer.py (973 LOC, misnamed — it lived OUTSIDE central/ but its docstring described "the Central firehose") is DELETED. No file or symbol named "central" survives (only immutable historical comments in an already-applied migration .sql remain — those are not edited). Split per the owner's rule ("next to what uses it"): - Live geo/place-name utilities (nearest_town, _haversine_miles, _bearing_compass, _compute_distance_bearing, _h3_cell, _photon_reverse_places, geocoder config) → the EXISTING meshai/geo.py ("Geographic utilities…"), same domain. NOT a junk drawer. geo.py's nearest_city (hardcoded table) and central_normalizer's nearest_town (live Photon+H3 reverse-geocode) are different implementations — both kept, no false consolidation. - WZDx work-zone parsers (_parse_wzdx_federal, _norm_wzdx_sub_type, _norm_direction, _parse_mile_posts, _clean_description, _is_uninformative_road) → env/wzdx_parse.py, next to their sole consumer env/wzdx.py. - ~400 dead lines deleted: the Central-envelope machinery (normalize, _parse_state_511_atis, _parse_wfigs_incidents, _parse_itd_511_work_zone, should_skip_state_511_atis_id, is_incident_envelope_stale, normalize_road_name, _norm_sub_type, _parse_ends_at) — only caller was the deleted Central consumer. Consumers rewired: env/wzdx.py, env/fire_render.py, formatters/fire.py, formatters/_anchor.py, main.py, persistence/curation.py, composer.py. Tests: test_central_normalizer.py RENAMED → test_geo_wzdx_parse.py (live geo/wzdx tests kept + rewired; dead-parser tests dropped). test_itd_511_work_zone.py deleted (tested only the dead itd_511 parser — verified gone from source). Other test files: import-path rewires. Live coverage for nearest_town / _parse_wzdx_federal / bearing preserved across test_geo_wzdx_parse, test_adapter_wzdx, test_wfigs_handler, test_incident_refactor. Suite: 2010 passed, 0 failed. The 49-test drop is the dead Central-envelope parser tests, correctly removed. Stale "Central firehose" docstrings on the moved code updated to describe present behavior. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:15:50 -06:00
n = _normalize_wfigs(env_new)
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
assert gate.data_patch["_dedup_suffix"] == f"{n['acres']}|{n['contained_pct']}"
assert gate.data_patch["_cooldown_suffix"] == _IRWIN_A
def test_never_broadcast_tombstone_suppressed(self, mem_db):
"""A tombstone for a fire that never reached the mesh is silent."""
tomb = _make_tombstone(irwin_id=_IRWIN_A)
gate = self._decide(tomb, 1_000_000)
assert gate.broadcast is False
assert gate.lifecycle == "suppress"
def test_perimeter_never_broadcasts(self, mem_db):
from tests.test_wfigs_handler import _make_perimeter
per = _make_perimeter(irwin_id=_IRWIN_A)
gate = self._decide(per, 1_000_000)
assert gate.broadcast is False
assert gate.lifecycle == "suppress"
# ─────────────────────────────────────────────────────────────────────────────
# 2b. Tombstone mute — decide() must honor fires.tombstoned_at
# ─────────────────────────────────────────────────────────────────────────────
class TestTombstoneMute:
"""A non-null fires.tombstoned_at is a durable operator kill-switch: an
incident row stamped tombstoned must never broadcast again through
decide() -- new, update, or otherwise -- regardless of forward
acreage/containment growth or the 8h cooldown. This is the SAME column
the dashboard's /api/env/active view already filters on
(WHERE tombstoned_at IS NULL); decide() previously never read it."""
_IRWIN = "IRWIN-TOMBSTONE-1"
def _seed(self, conn, *, acres, contained, last_bcast_at, tombstoned_at=None):
_write_fire_state(
conn, irwin_id=self._IRWIN, name="Lake Channel Fire",
acres=acres, contained_pct=contained, lat=42.0, lon=-114.0,
county="Twin Falls", state="ID", now=int(last_bcast_at or 1_000_000))
conn.execute(
"UPDATE fires SET last_broadcast_at=?, last_broadcast_acres=?, "
"last_broadcast_contained=?, tombstoned_at=? WHERE irwin_id=?",
(last_bcast_at, acres, contained, tombstoned_at, self._IRWIN),
)
def _incident(self, *, acres, contained):
return {
"_kind": "wfigs_incident", "irwin_id": self._IRWIN,
"incident_name": "Lake Channel Fire", "acres": acres,
"contained_pct": contained, "declared_at_epoch": None,
"lat": 42.0, "lon": -114.0, "county": "Twin Falls", "state": "ID",
}
# (a) non-tombstoned row, forward growth past cooldown -> still broadcasts.
def test_non_tombstoned_growth_still_broadcasts(self, mem_db):
self._seed(mem_db, acres=100, contained=10,
last_bcast_at=1_000_000 - 9 * 3600, tombstoned_at=None)
gate = fire_decide(self._incident(acres=200, contained=10),
source="wfigs", now=1_000_000.0)
assert gate.broadcast is True
assert gate.lifecycle == "update"
# (b) same row, tombstoned_at set -> suppressed regardless of growth/cooldown.
def test_tombstoned_row_suppressed_despite_growth(self, mem_db):
self._seed(mem_db, acres=100, contained=10,
last_bcast_at=1_000_000 - 9 * 3600, tombstoned_at=999_000)
gate = fire_decide(self._incident(acres=9999, contained=99),
source="wfigs", now=1_000_000.0)
assert gate.broadcast is False
assert gate.lifecycle == "suppress"
assert "tombston" in gate.reason.lower()
# (c) brand-new incident, not tombstoned (no row at all) -> broadcasts new.
def test_brand_new_incident_not_tombstoned_broadcasts(self, mem_db):
gate = fire_decide(self._incident(acres=50, contained=0),
source="wfigs", now=1_000_000.0)
assert gate.broadcast is True
assert gate.lifecycle == "new"
# (d) defensive: a row lacking the tombstoned_at column must not raise.
def test_missing_tombstoned_at_column_does_not_raise(self, mem_db, monkeypatch):
self._seed(mem_db, acres=100, contained=10,
last_bcast_at=1_000_000 - 9 * 3600, tombstoned_at=None)
import sqlite3
import meshai.notifications.gating.fire as fire_mod
class _NoTombstoneColConn:
"""Proxies the real connection but simulates a pre-v12 schema:
any SELECT naming tombstoned_at raises OperationalError, exactly
as a real un-migrated `fires` table would."""
def __init__(self, real):
self._real = real
def execute(self, sql, *args, **kwargs):
if "tombstoned_at" in sql and "SELECT" in sql.upper():
raise sqlite3.OperationalError(
"no such column: tombstoned_at")
return self._real.execute(sql, *args, **kwargs)
def __getattr__(self, name):
return getattr(self._real, name)
monkeypatch.setattr(
fire_mod, "get_db", lambda: _NoTombstoneColConn(mem_db))
gate = fire_decide(self._incident(acres=200, contained=10),
source="wfigs", now=1_000_000.0)
# Falls back to "not tombstoned" and proceeds with normal gating --
# must not raise, and growth past cooldown still broadcasts.
assert gate.broadcast is True
assert gate.lifecycle == "update"
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
# ─────────────────────────────────────────────────────────────────────────────
# 3. Registration — the three explicit categories resolve; FIRMS does not
# ─────────────────────────────────────────────────────────────────────────────
class TestRegistration:
@pytest.mark.parametrize(
"cat", ["wildfire_declared", "wildfire_incident", "wildfire_closed"])
def test_formatter_registered(self, cat):
from meshai.notifications.formatters import get_formatter
assert get_formatter(cat) is fire_format
@pytest.mark.parametrize(
"cat", ["wildfire_declared", "wildfire_incident", "wildfire_closed"])
def test_decider_registered(self, cat):
from meshai.notifications.gating import get_decider
assert get_decider(cat) is fire_decide
@pytest.mark.parametrize(
"cat", ["wildfire_hotspot", "new_ignition",
refactor(phase3c): migrate FIRMS fusion broadcasts to formatter+decider (#34) Migrate the three FIRMS fire-tracker BROADCAST paths (wildfire_growth, wildfire_spotting, wildfire_halted) behind the registry, NO cutover. Attribution/pass/centroid/perimeter plumbing stays inline; legacy live path is byte-identical. - gating/firms.py decide() discriminates on handler-stamped _kind (firms_growth/firms_spotting/firms_halt) - wildfire_growth reuses formatters/fire.py (verified byte-identical: growth SELECT uses current_* columns _render doesn't read, so the wire is the movement+anchor line with "size/containment unknown" — a latent legacy quirk, reproduced exactly, NOT fixed) - formatters/firms.py renders spotting + halt wires - tier-b (flagged): the eager latch writes (fires.last_spotting_broadcast_at, fires.halt_broadcast_at) move into deferred commit closures — a dropped broadcast no longer burns the latch. Validated by gate-sequence, not golden bytes. Not-cutover live path keeps eager latches verbatim. - FIRMS broadcasts never touch event_log (eager handled=1 at pixel storage) → no event_log flip to wrap - _maybe_emit_cluster stays dead (unconditional return None) + test Deferred follow-ups (unchanged): env/firms.py native hotspot broadcast neutralization; native canonical emission. 27 new tests; fire-tracker + firms handler suites preserved; full suite at 34-failure baseline (1597 passed). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:18:51 -06:00
"unattributed_hotspot_cluster"])
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
def test_firms_categories_not_captured(self, cat):
refactor(phase3c): migrate FIRMS fusion broadcasts to formatter+decider (#34) Migrate the three FIRMS fire-tracker BROADCAST paths (wildfire_growth, wildfire_spotting, wildfire_halted) behind the registry, NO cutover. Attribution/pass/centroid/perimeter plumbing stays inline; legacy live path is byte-identical. - gating/firms.py decide() discriminates on handler-stamped _kind (firms_growth/firms_spotting/firms_halt) - wildfire_growth reuses formatters/fire.py (verified byte-identical: growth SELECT uses current_* columns _render doesn't read, so the wire is the movement+anchor line with "size/containment unknown" — a latent legacy quirk, reproduced exactly, NOT fixed) - formatters/firms.py renders spotting + halt wires - tier-b (flagged): the eager latch writes (fires.last_spotting_broadcast_at, fires.halt_broadcast_at) move into deferred commit closures — a dropped broadcast no longer burns the latch. Validated by gate-sequence, not golden bytes. Not-cutover live path keeps eager latches verbatim. - FIRMS broadcasts never touch event_log (eager handled=1 at pixel storage) → no event_log flip to wrap - _maybe_emit_cluster stays dead (unconditional return None) + test Deferred follow-ups (unchanged): env/firms.py native hotspot broadcast neutralization; native canonical emission. 27 new tests; fire-tracker + firms handler suites preserved; full suite at 34-failure baseline (1597 passed). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:18:51 -06:00
# These native FIRMS categories remain deferred; they must NOT resolve
# to the fire formatter/decider via the "fire" toggle family fallback.
refactor(phase3b): migrate WFIGS fire to formatter+decider (tier-a) (#33) 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>
2026-07-04 22:51:26 -06:00
from meshai.notifications.formatters import get_formatter
from meshai.notifications.gating import get_decider
assert get_formatter(cat) is not fire_format
assert get_decider(cat) is not fire_decide