feat(firms): curated new-fire cluster broadcasts (no per-pixel, no cold-start dump) (#73)

* feat(firms): curated new-fire cluster broadcasts (no per-pixel, no cold-start dump)

Enable the built _maybe_emit_cluster path (was dead-coded) so FIRMS broadcasts
curated hotspot clusters as possible new fires — clustered, deduped via
cluster_broadcast_at, attributed against known WFIGS fires first (so MORA's
hotspots don't false-cluster). Give FIRMS a default Idaho bbox so it fetches
when coverage is off (coverage bbox still overrides). First-fetch silent-seed
prevents a cold-start dump of the day's existing hotspots. Raw pixels stay
store-only. Coverage geometry gate filters cluster broadcasts to the region.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(firms): first-fetch silent-seed suppresses fusion wires too (no cold-start)

Extend the FIRMS cold-start seed to suppress growth/spotting/halt fusion
broadcasts on the first fetch, not just clusters — enabling FIRMS must emit
zero broadcasts on the initial hotspot sweep. Persistence, attribution, and
dedup baselines still run during seed; only later new activity broadcasts.

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>
This commit is contained in:
malice 2026-07-06 16:09:25 -06:00 committed by GitHub
commit d479ca537a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 600 additions and 49 deletions

View file

@ -220,10 +220,18 @@ def test_three_unattributed_pixels_fire_cluster_once():
assert data.get("category") == "unattributed_hotspot_cluster"
assert data.get("severity") == "priority"
# Cluster detection is currently stubbed (_maybe_emit_cluster returns None).
# All three calls return None.
# F3: cluster detection is ENABLED. The 3rd pixel reaches cluster_min_pixels
# (3) within 1 mi / 60 min, so exactly ONE cluster wire fires (on pixel 3);
# pixels 1 and 2 return None (below threshold at the time they arrive).
fired = [w for w in wires if w is not None]
assert len(fired) == 0, f"expected 0 cluster wires (stub), got {len(fired)}: {wires}"
assert len(fired) == 1, f"expected 1 cluster wire, got {len(fired)}: {wires}"
assert fired[0].startswith("🔥 Possible new fire:")
# All three members must be stamped so a later pixel can't re-fire them.
conn = get_db()
stamped = conn.execute(
"SELECT COUNT(*) FROM firms_pixels WHERE cluster_broadcast_at IS NOT NULL"
).fetchone()[0]
assert stamped == 3
def test_fourth_pixel_in_same_cluster_does_not_refire():
@ -256,7 +264,13 @@ def test_fourth_pixel_in_same_cluster_does_not_refire():
assert wire is None
assert "category" not in data4
# Cluster detection is stubbed; no cluster_broadcast_at stamped on any pixel.
# F3: the first 3 members were stamped by the cluster that fired; the 4th
# pixel is unattributed + unstamped (it alone can't re-form the cluster).
conn = get_db()
stamped = conn.execute(
"SELECT COUNT(*) FROM firms_pixels WHERE cluster_broadcast_at IS NOT NULL"
).fetchone()[0]
assert stamped == 3
def test_fifth_pixel_after_time_window_can_form_new_cluster():
@ -293,9 +307,12 @@ def test_fifth_pixel_after_time_window_can_form_new_cluster():
env, subject="central.fire.hotspot.N20.high.unknown",
data={}, now=1780728000 + 7200 + i,
))
# Cluster detection is stubbed (_maybe_emit_cluster returns None).
# F3: the first cluster's 3 members are stamped (excluded from the query),
# so the second batch forms an independent NEW cluster -- one wire on its
# 3rd pixel.
fired = [w for w in wires2 if w is not None]
assert len(fired) == 0, f"expected 0 cluster wires (stub), got: {wires2}"
assert len(fired) == 1, f"expected 1 new cluster wire, got: {wires2}"
assert fired[0].startswith("🔥 Possible new fire:")
# ---------------------------------------------------------------------------