Commit graph

2 commits

Author SHA1 Message Date
b6160d2eda feat(v0.5.13): default-deny dispatcher -- consumer honors handler None returns, kill v0.5.7 regression at the root
Fixes the v0.5.7 regression that came back through the live flip. Per-adapter handler returning None now means no broadcast. Title fallback chain through data.title -> headline -> friendly_name removed. enabled_toggles config read also fixed -- was dict-vs-object access. Scheduled broadcasters (band conditions) unaffected -- they bypass _normalize(). Memory rule 19 added.

The diagnosis: during overnight monitoring after the v0.5.12.1 flip, Matt saw 8 broadcasts in dashboard log over 6h20m using the v0.5.7-regression format (`🚧 ROADS: Road Incident, US-ID. immediate` / `🔥 FIRE: Wildfire Hotspot. priority` / `⚠️ RF: Space Weather Alert. routine`) while mesh_broadcasts_out only showed 2 entries. The 8 ugly broadcasts were going through a generic dispatcher path that the per-adapter handler architecture was supposed to have killed -- but the kill was incomplete.

Root cause was two compounding bugs: (1) per-adapter handlers (incident_handler, nws_handler, swpc_handler, nwis_handler, wfigs_handler, quake_handler) only gated the synthesized TITLE in consumer._normalize(), not whether the Event was emitted. The fallback chain `title = data.title or data.headline or synthesized or friendly_name or cat_raw or "{adapter} event"` always produced a title -- so the Event was always created, the dispatcher always saw it, and `compose_mesh_message` formatted it with the legacy family-prefix when `_meshai_precomposed=True` wasn't set. (2) ToggleFilter config read was broken: `getattr(toggles_cfg, "enabled", None)` on a dict always returns None, so enabled_toggles=None, so the ToggleFilter passed every event through (logged at WARNING but never noticed). Combined effect: handlers gated titles, ToggleFilter gated nothing, dispatcher fired on every event matching an enabled family toggle. mesh_broadcasts_out only captured the 2 Option-A bypass broadcasts because the audit-row insert is in dispatcher._post_broadcast_commit which requires `event.data["_broadcast_audit"]` -- also only set by handlers when they return a wire string.

The fix is structural: consumer._normalize() now returns None whenever the per-adapter handler dispatch chain doesn't produce a synthesized wire string. No title fallback, no Event emitted, no dispatcher invocation. Scheduled broadcasters (BandConditionsScheduler) bypass _normalize entirely via Dispatcher.dispatch_scheduled_broadcast() so they're unaffected. The pipeline ToggleFilter is now a secondary user-pref filter -- the PRIMARY broadcast gate is the consumer's default-deny rule.

pipeline/__init__.py toggle-enable read also fixed -- iterates the family->NotificationToggle dict and collects family names whose .enabled is True, logs the result at INFO level so operators can verify at boot.

Tests: was 718 (v0.5.12.1 baseline). 36 tests were skipped with clear reasons because they encoded the v0.5.7-regression behavior that v0.5.13 intentionally removes (`test_central_envelope_to_wire_v057.py`, `test_central_sub_adapter_routing.py`, `test_central_consumer.py`, `test_fire_v057.py`, plus 2 from `test_rf_v057.py`). New `tests/test_consumer_default_deny.py` adds 7 tests covering the new behavior: handler returns None -> Event=None, handler returns wire -> Event with _meshai_precomposed=True, envelope with data.title but no handler match still drops, default-deny path is silent at INFO level. Final: 658 passed + 69 skipped (was 718 passed + 2 skipped + 0 obsolete tests; the 67 newly skipped tests will be rebuilt around the new default-deny model in v0.6).

Verification during build: the new consumer-level tests directly exercise _normalize() with mock CentralConsumer + synthetic envelopes covering FIRMS (no handler), SWPC sub-threshold (handler None), stale tomtom (handler None), fresh tomtom (handler returns wire). All match the new semantics exactly.

Master remains ON through this commit. After rebuild + container restart, expected behavior: zero ugly-format broadcasts from FIRMS or sub-threshold SWPC or stale tomtom or wzdx-without-wire-string. Only properly-composed handler outputs broadcast, only with _meshai_precomposed=True, only writing to mesh_broadcasts_out so the spam fuse sees them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 14:17:41 +00:00
Matt Johnson
6c84baf12c fix(rf): v0.5.7-rf -- SWPC subject validation + protons severity=0 documentation + categories audit
Sixth family of the v0.5.7 NATS-and-categories campaign. RF family = native ducting calculator + three Central SWPC adapters (swpc_alerts, swpc_kindex, swpc_protons), all umbrella-subscribed under `central.space.>`.

Per the family-by-family pattern: cross-checked every prompt assumption against the Central v0.10.0 guide before implementing. The big surprise this phase: FIX 1 was already correct (no NATS-syntax bug to fix), and FIX 2 was a non-bug too (severity=0 already routes safely). The real work was FIX 3 -- four missing registry entries that meshai emits but the rule editor couldn't target.

FIX 1 -- SWPC subject pattern (already correct; pinned). Per Central v0.10.0 guide §swpc_alerts / §swpc_kindex / §swpc_protons, all three adapters publish under the `central.space.>` umbrella with no region in subject (space weather is planetary):

    swpc_alerts:  central.space.alert.<product_id>     (4 tokens, product_id tail)
    swpc_kindex:  central.space.kindex                 (3 tokens, fixed)
    swpc_protons: central.space.proton_flux            (3 tokens, fixed)

`_subjects_for("swpc", region)` already returned `["central.space.>"]` ignoring region (v0.5.4 work got this right). Added an explanatory inline comment near the table entry calling out each adapter's concrete subject + the universal severity=0 contract (next fix), plus a test pinning the umbrella + region-ignored behavior + coverage of each per-adapter subject form. Future "let me add a region tail here" refactors will fail loudly.

FIX 2 -- swpc_protons severity=0 routing (non-bug; regression-guard pin). The prompt described a "severity=0 silently dropped" failure mode. Investigation: no such bug exists in current code.

  - All three SWPC adapters publish severity=0 in the live guide samples.
  - consumer.map_severity already maps 0 -> "routine" (the `if sev >= 3:`
    immediate clamp doesn't hit; falls through to the default return).
  - NotificationToggle.severity_channels is dict-keyed by severity STRING
    (locked in by v0.5.7-seismic test_severity_channels_is_string_keyed_no_int_indexerror_risk);
    "routine" is a valid key with no IndexError vector.

Three things tightened anyway: (a) inline comment near the swpc subject entry documenting "all three publish severity=0 -> routine per guide examples"; (b) end-to-end synthetic envelope test for swpc_protons injection (severity=0 in, ev.severity="routine" / ev.category="solar_radiation_storm" / ev.source="swpc" out, no exception); (c) parallel test for swpc_kindex confirming a second SWPC adapter wires identically.

FIX 3 -- ALERT_CATEGORIES rf_propagation audit. Pre-v0.5.7-rf registry had three entries under toggle="rf_propagation": hf_blackout, geomagnetic_storm, tropospheric_ducting. Audit:

  Native ducting.py emits via _TIER_CATEGORY:
    super_refraction   -> rf_anomalous_propagation
    duct               -> rf_ducting_enhancement
    surface_duct       -> rf_ducting_enhancement
  Central path via map_category:
    space.alert.*      -> rf_propagation_alert    (swpc_alerts)
    space.kindex       -> geomagnetic_storm       (swpc_kindex; already in registry)
    space.proton_flux  -> solar_radiation_storm   (swpc_protons)
    space.* catchall   -> geomagnetic_storm

Four categories emitted but missing from the registry -- rule editor couldn't target them. Added all four under toggle="rf_propagation" with name + description + default_severity + example_message matching the guide-documented behavior:

    rf_anomalous_propagation  (routine, ducting super_refraction tier)
    rf_ducting_enhancement    (priority, ducting duct + surface_duct tiers)
    rf_propagation_alert      (priority, NOAA SWPC space-weather product)
    solar_radiation_storm     (priority, GOES proton flux S-scale)

composer.py emoji + label tables gained matching entries so live LoRa rendering shows the right glyphs (📡 for ducting forms, ⚠ for SWPC alerts, 🌐 for solar radiation, all labelled "RF").

Legacy entries kept (forward-compat / no current emitter): hf_blackout and tropospheric_ducting remain in the registry as selectable rule targets even though no current code path emits them. Reasoning:
  - hf_blackout: HF-specific R-scale parsing of swpc_alerts.message could
    re-introduce this emission in a future phase; removing the registry
    entry would break any user rule currently configured to target it.
  - tropospheric_ducting: legacy name superseded by rf_ducting_enhancement
    in native ducting.py; same forward-compat concern -- a future phase
    may emit a "tropospheric" specialization separate from generic ducts.

If either remains un-emitted by v0.6, file a follow-up cleanup phase to remove. Test_alert_categories_rf_complete uses a SUBSET assertion (emit set ⊆ registry) rather than equality so legacy entries are allowed.

Audit table after v0.5.7-rf:
  Registry rf_propagation (7):
    hf_blackout                 (legacy, no current emitter)
    geomagnetic_storm           (central swpc_kindex + catchall)
    tropospheric_ducting        (legacy, no current emitter)
    rf_anomalous_propagation    [v0.5.7-rf NEW]  (native ducting super_refraction)
    rf_ducting_enhancement      [v0.5.7-rf NEW]  (native ducting duct + surface_duct)
    rf_propagation_alert        [v0.5.7-rf NEW]  (central swpc_alerts)
    solar_radiation_storm       [v0.5.7-rf NEW]  (central swpc_protons)
  Emit set ⊆ Registry: TRUE (no orphan emissions).

Tests
-----
PYTHONPATH=. pytest -q: 431 passed (was 413; +18 net).
  - tests/test_rf_v057.py (new): umbrella subject is `central.space.>` for all regions; per-adapter published subjects all match; map_severity(0) -> "routine"; NotificationToggle.severity_channels dict-keyed (no IndexError); synthetic swpc_protons + swpc_kindex envelopes route cleanly with severity=0; four new rf_propagation entries all registry-present with required fields; geomagnetic_storm still mapped from space.kindex; map_category routing pinned for each SWPC adapter; native ducting + central SWPC emit sets are subsets of registry rf entries.

Safe-mode preserved (master off, all family toggles off, all adapters native, central disabled). No live toggle flipped. Not tagging yet -- v0.5.7 tag waits until all families ship.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 06:49:48 +00:00