meshai/tests
Matt Johnson (via Claude) f5c566c6c0 feat(v0.7-fire-tracker-2): movement analysis -- growth + halt detection
Phase 2 of FIRMS+WFIGS fusion. v14.sql adds fire_passes table for
per-satellite-pass centroid tracking + drift computation. FIRMS handler
now detects pass boundaries (satellite + time bucket), computes pass
centroid (median of pass pixels), Haversine drift from previous pass,
bearing to 8-way direction, mi/h speed. Drift >= 0.5 mi (configurable)
emits wildfire_growth broadcast with wire including movement vector
and nearest-town context. Halt detection: fire with no new pixels for
>=12h (configurable) emits wildfire_halted broadcast (routine). Two
new ALERT_CATEGORIES: wildfire_growth (priority), wildfire_halted
(routine). All thresholds GUI-editable via adapter_config.fires.*.
Phase 3 (spotting) and Phase 4 (LLM summaries) deferred to subsequent
commits.

Schema (v14.sql):
- fire_passes table (irwin_id FK CASCADE, pass_id, pass_centroid_lat/lon,
  pixel_count, total_frp, pass_started_at, pass_ended_at,
  drift_mi_from_prev, drift_direction, drift_mi_per_hour). PRIMARY KEY
  (irwin_id, pass_id) so the UPSERT path is cheap; secondary index on
  (irwin_id, pass_ended_at) for the prev-pass lookup + halt counter.
- fires gains last_pass_id, last_pass_at, halt_broadcast_at columns.
  halt_broadcast_at is latched per halt event; the detector filter
  (halt_broadcast_at IS NULL OR halt_broadcast_at < last_pass_at)
  reopens eligibility automatically when an idle fire receives a new
  attributed pixel that advances last_pass_at.

adapter_config (defaults.py REGISTRY):
- fires.growth_drift_threshold_mi = 0.5 (float). Per-pass centroid drift
  at or above this fires wildfire_growth. 0.5 mi matches the design
  doc Phase 2 spec and is roughly 2x the VIIRS 375m pixel size (i.e.,
  detectable as more than centroid jitter).
- fires.halt_passes_threshold = 2 (int). Documented intent; the
  operational rule uses halt_minimum_seconds below as the time gate
  because per-satellite pass-count enforcement would require modeling
  the global VIIRS schedule per satellite. The 12h gate subsumes it
  (4 passes/day in Idaho).
- fires.halt_minimum_seconds = 43200 (int, 12h).

ALERT_CATEGORIES (notifications/categories.py):
- wildfire_growth: priority/fire. FIRMS handler tags data["category"]
  + data["severity"] on the pass-boundary path when drift >= threshold.
- wildfire_halted: routine/fire. Halt detector tags data["category"]
  + data["severity"] when a fire transitions to idle for >=12h.

FIRMS handler (central/firms_handler.py):
- The Phase 1 attribution branch now passes through
  _handle_pass_boundary(): UPSERT fire_passes row for the current
  (irwin_id, pass_id) with median centroid + pixel count + total FRP
  + min/max acq_time; lookup the prior pass; compute drift mi +
  8-way direction + mi/h speed and write them into the current pass
  row (only the FIRST boundary fills these; subsequent in-pass pixels
  COALESCE keep them stable). Update fires cursor (last_pass_id,
  last_pass_at) and current_centroid_lat/lon to the latest pass
  centroid -- this overrides Phase 1's 24h all-pixels median for
  fires that have pass data.
- Growth wire emitted ONLY at the boundary (last_pass_id != current,
  prev pass exists, drift >= threshold). Subsequent in-pass pixels
  stay silent because pass_id == last_pass_id.
- _maybe_emit_halt runs as a final fallback when neither growth nor
  cluster has fired. SELECT one fire matching the halt criteria,
  stamp halt_broadcast_at, return the wire. The fallback ordering is
  growth > cluster > halt so a busy fire's growth broadcast doesn't
  starve a quiet fire's halt.
- New helpers: _bearing() (great-circle initial bearing, deg CW from N),
  _direction_8() (compass 8-way mapping with +/-22.5 deg sectors).

Wire strings:
- wildfire_growth: `🔥 <incident_name> moving <dir> <speed:.1f> mi/h
  ~<dist_to_nearest_town:.1f> mi from <nearest_town>`. nearest_town
  via meshai.central_normalizer.nearest_town (same Photon-backed
  cache that wfigs_handler uses); failure falls back to bare
  "moving <dir> <speed> mi/h".
- wildfire_halted: `🔥 <incident_name> no growth in <hours>h`.

Tests (tests/test_fire_tracker_phase2.py, 10 cases all green):
- 2-pass attribution with pass2 1.0 mi N of pass1 -> drift=1.0,
  direction='N', mi/h computed, growth wire returned, data tagged.
- Drift below threshold (0.3 mi) -> NO growth broadcast; pass row
  still records the (sub-threshold) drift for ops visibility.
- Halt detector: last_pass_at 14h ago -> fires once, halt_broadcast_at
  stamped.
- Re-run halt detector with halt latched -> NO second broadcast.
- Halt re-eligibility: halt_broadcast_at < last_pass_at -> eligible
  again (a resurrected then re-idled fire).
- Bearing + direction round-trip across all 8 cardinals.
- Direction sector boundary (22.5/67.5 deg) correctness.
- adapter_config seed for 3 new fires.* keys.
- Two new ALERT_CATEGORIES registered.
- 5-pixel single-pass aggregate (pixel_count, total_frp sum, median
  centroid, started/ended_at min/max).

Phase 1 test fix:
- tests/test_fire_tracker_phase1.py::test_centroid_recomputes_as_median_across_passes
  retimed to 12:00/12:10/12:20 so all 3 pixels land in one
  N20 bucket. Phase 2 makes current_centroid_* the per-pass median
  (latest pass overrides Phase 1's 24h median); the same-pass shape
  preserves the original median-computation intent. 39 total tests
  green across phase1/phase2/or-arch/include-roundtrip.

Live verification on CT108 after rebuild:
- v14 migration applied (schema_meta version=14, no Traceback in 3 min).
- adapter_config.fires.growth_drift_threshold_mi = 0.5
- adapter_config.fires.halt_passes_threshold = 2
- adapter_config.fires.halt_minimum_seconds = 43200
- Container healthy.

Synthetic 100-pixel probe inside prod container (PROBE-V07P2-*,
cleaned up after):
- Pass A (50 pixels @ 12:00-12:25, N20 bucket 329768): centroid
  (44.30000, -115.50000), pixel_count=50, total_frp=975.0, drift=NULL
  (first pass).
- Pass B (50 pixels @ 18:00-18:25, N20 bucket 329772, centered 1.2 mi
  NE of A): centroid (44.31230, -115.48282), pixel_count=50,
  total_frp=975.0, drift_mi_from_prev=1.1703 (~design target 1.2 mi
  with -0.03 mi rounding), drift_direction="NE",
  drift_mi_per_hour=0.209 (1.17 mi over 5.5h between pass ends).
- Growth wire: "🔥 Probe Movement Fire moving NE 0.2 mi/h, ~13.0 mi
  from Long Creek Summit Home" (Photon nearest-town anchor populated
  successfully).
- Exactly ONE growth broadcast (first pixel of pass B); 99 other
  pixels stayed silent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 06:12:36 +00:00
..
fixtures/central_envelopes feat(content): v0.5.8-state_511_atis -- central_normalizer with Photon nearest_town + composer bypass + SB->S route normalization 2026-06-04 21:38:40 +00:00
conftest.py feat(v0.6-3a): adapter_config foundation -- migration + defaults registry + typed accessor 2026-06-05 17:06:51 +00:00
test_adapter_avalanche.py
test_adapter_config_api.py feat(v0.6-tail): close 5 v0.6-phase1-complete.md follow-ups 2026-06-05 21:37:05 +00:00
test_adapter_config_foundation.py feat(v0.6-tail): close 5 v0.6-phase1-complete.md follow-ups 2026-06-05 21:37:05 +00:00
test_adapter_ducting.py
test_adapter_fires.py
test_adapter_firms.py fix(fire): v0.5.7-fire -- FIRMS NATS pattern + WFIGS tombstone dedup + remove fire_proximity + categories audit 2026-06-04 06:25:42 +00:00
test_adapter_nws.py
test_adapter_roads511.py
test_adapter_swpc.py
test_adapter_traffic.py
test_adapter_usgs.py
test_adapter_usgs_quake.py
test_avalanche_v057.py fix(avalanche): v0.5.7-avalanche -- Central avalanche check + categories audit 2026-06-04 06:55:27 +00:00
test_band_conditions.py feat(v0.5.11): band conditions scheduled broadcaster (3x/day HF propagation) 2026-06-05 07:38:51 +00:00
test_central_consumer.py feat(v0.5.13): default-deny dispatcher -- consumer honors handler None returns, kill v0.5.7 regression at the root 2026-06-05 14:17:41 +00:00
test_central_envelope_to_wire_v057.py feat(v0.5.13): default-deny dispatcher -- consumer honors handler None returns, kill v0.5.7 regression at the root 2026-06-05 14:17:41 +00:00
test_central_normalizer.py feat(v0.5.9): unified incident pipeline + state_511_atis Idaho cutover + two-sided freshness gate 2026-06-05 06:41:21 +00:00
test_central_region_routing.py fix(water): v0.5.7-water -- USGS NWIS hydro NATS pattern + categories audit 2026-06-04 06:42:06 +00:00
test_central_sub_adapter_routing.py feat(v0.5.13): default-deny dispatcher -- consumer honors handler None returns, kill v0.5.7 regression at the root 2026-06-05 14:17:41 +00:00
test_channel_rendering.py
test_cold_start_grace.py feat(v0.5.8b): persistence foundation + WFIGS handler + universal cold-start grace 2026-06-05 03:54:04 +00:00
test_config_loader.py
test_config_source_field.py
test_consumer_default_deny.py feat(v0.6-1): FIRMS handler -- storage-only, closes silent-drop on central.fire.hotspot.> 2026-06-05 15:50:33 +00:00
test_curation.py feat(v0.6-4): gauge_sites + town_anchors curation tables + GUI CRUD 2026-06-05 20:19:13 +00:00
test_dashboard_config_save.py
test_dispatcher_persistence.py feat(v0.6-2): dispatcher state persistence -- cold-start, cooldowns, dedup LRU to SQLite 2026-06-05 16:35:40 +00:00
test_env_reporter.py feat(v0.6-5): env_reporter + router wiring + include_in_llm_context per-adapter toggle -- LLM gains read access to every adapter table via the existing mesh_reporter pre-rendered prompt-injection pattern 2026-06-05 20:11:40 +00:00
test_fire_tracker_phase1.py feat(v0.7-fire-tracker-2): movement analysis -- growth + halt detection 2026-06-06 06:12:36 +00:00
test_fire_tracker_phase2.py feat(v0.7-fire-tracker-2): movement analysis -- growth + halt detection 2026-06-06 06:12:36 +00:00
test_fire_v057.py feat(v0.5.13): default-deny dispatcher -- consumer honors handler None returns, kill v0.5.7 regression at the root 2026-06-05 14:17:41 +00:00
test_firms_handler.py feat(v0.6-1): FIRMS handler -- storage-only, closes silent-drop on central.fire.hotspot.> 2026-06-05 15:50:33 +00:00
test_incident_handler.py feat(v0.6-tail): close 5 v0.6-phase1-complete.md follow-ups 2026-06-05 21:37:05 +00:00
test_include_roundtrip.py fix(v0.6-tail-4): register !include YAML tag constructor in config loader -- closes prod PUT 500 2026-06-06 04:37:24 +00:00
test_itd_511_work_zone.py feat(v0.5.9): unified incident pipeline + state_511_atis Idaho cutover + two-sided freshness gate 2026-06-05 06:41:21 +00:00
test_notification_toggles.py feat(v0.6-phase2): rip out quiet hours entirely -- dashboard toggle, config schema, pipeline checks. Per Matt's repeated feedback (saved as feedback-quiet-hours-trash.md): silent is better than ugly, mesh users who need a fire alert at 3 AM need it at 3 AM. No replacement. 2026-06-05 20:39:36 +00:00
test_nwis_handler.py feat(v0.6-4): gauge_sites + town_anchors curation tables + GUI CRUD 2026-06-05 20:19:13 +00:00
test_nws_dedup_relaxation.py feat(v0.6-phase3): reminder system + schema split + NWS dedup relaxation 2026-06-05 21:11:32 +00:00
test_nws_handler.py feat(v0.5.10): nws + usgs_quake + swpc handlers 2026-06-05 07:27:01 +00:00
test_or_arch_continuous.py feat(v0.6-tail-3): enforce OR-not-AND continuously -- close USGS direct-lookup leak + flag environmental config changes as restart-required 2026-06-06 03:51:10 +00:00
test_persistence.py feat(v0.5.8b): persistence foundation + WFIGS handler + universal cold-start grace 2026-06-05 03:54:04 +00:00
test_pipeline_digest.py fix(fire): v0.5.7-fire -- FIRMS NATS pattern + WFIGS tombstone dedup + remove fire_proximity + categories audit 2026-06-04 06:25:42 +00:00
test_pipeline_grouper.py
test_pipeline_inhibitor_grouper.py
test_pipeline_persistence.py feat(v0.6-6): inhibit_state + grouper_held persistence + ToggleFilter live-reload + Inhibitor/Grouper config knobs 2026-06-05 20:23:34 +00:00
test_pipeline_scheduler.py
test_pipeline_skeleton.py chore(meshai): v0.5.5 -- cleanup bundle (gitignore env anchor, ducting health event_count, mesh_sources secret stripping, delete unused SeverityRouter) 2026-06-04 02:50:45 +00:00
test_pipeline_toggle_filter.py fix(fire): v0.5.7-fire -- FIRMS NATS pattern + WFIGS tombstone dedup + remove fire_proximity + categories audit 2026-06-04 06:25:42 +00:00
test_quake_handler.py feat(v0.5.10): nws + usgs_quake + swpc handlers 2026-06-05 07:27:01 +00:00
test_reminders.py feat(v0.6-phase3): reminder system + schema split + NWS dedup relaxation 2026-06-05 21:11:32 +00:00
test_renderers.py fix(notifications): v0.5.7-regression -- consumer title fallback uses registry name, mesh renderer drops [Family] prefix 2026-06-04 16:06:47 +00:00
test_rf_v057.py feat(v0.5.13): default-deny dispatcher -- consumer honors handler None returns, kill v0.5.7 regression at the root 2026-06-05 14:17:41 +00:00
test_router_env_scope.py feat(v0.6-5): env_reporter + router wiring + include_in_llm_context per-adapter toggle -- LLM gains read access to every adapter table via the existing mesh_reporter pre-rendered prompt-injection pattern 2026-06-05 20:11:40 +00:00
test_save_section_secret_preserve.py chore(meshai): v0.5.5 -- cleanup bundle (gitignore env anchor, ducting health event_count, mesh_sources secret stripping, delete unused SeverityRouter) 2026-06-04 02:50:45 +00:00
test_seismic_v057.py fix(seismic): v0.5.7-seismic -- USGS quake NATS pattern + severity=5 great-quake clamp + categories audit 2026-06-04 06:33:31 +00:00
test_swpc_handler.py feat(v0.5.10): nws + usgs_quake + swpc handlers 2026-06-05 07:27:01 +00:00
test_tail_followups.py feat(v0.6-tail): close 5 v0.6-phase1-complete.md follow-ups 2026-06-05 21:37:05 +00:00
test_tracking_v057.py fix(tracking): v0.5.7-tracking -- Central tracking check + categories audit 2026-06-04 07:00:22 +00:00
test_traffic_v057.py fix(traffic): v0.5.7-traffic -- NATS pattern fix + itd_511 sub-adapter routing + categories audit 2026-06-04 06:10:12 +00:00
test_v052_dispatcher.py feat(v0.5.8b): persistence foundation + WFIGS handler + universal cold-start grace 2026-06-05 03:54:04 +00:00
test_water_v057.py fix(water): v0.5.7-water -- USGS NWIS hydro NATS pattern + categories audit 2026-06-04 06:42:06 +00:00
test_weather_v057.py fix(weather): v0.5.7-weather -- NWS HTML strip + ALERT_CATEGORIES audit (NATS pattern already valid) 2026-06-04 06:00:10 +00:00
test_wfigs_handler.py feat(v0.5.8b): persistence foundation + WFIGS handler + universal cold-start grace 2026-06-05 03:54:04 +00:00
test_work_zone_renderer.py feat(content): v0.5.8-state_511_atis -- central_normalizer with Photon nearest_town + composer bypass + SB->S route normalization 2026-06-04 21:38:40 +00:00