feat(3-L.a): finish enrichment_locations across adapters

First half of the split PR L (events-tab + map deferred to L-b). Only FIRMS
declared enrichment_locations, so apply_enrichment silently bypassed every
other adapter. This declares it for all 12.

Pre-read finding (resolved per PM): apply_enrichment is a FLAT lookup
(event.data.get(lat_path)); FIRMS/usgs_quake already carry top-level
latitude/longitude in event.data, but the other point adapters kept coords
only in Geo.centroid where the flat path can't reach them. Per PM (option b),
the 5 centroid-only adapters now also write top-level latitude/longitude into
event.data, mirroring their existing Geo.centroid (lon, lat) — 2-3 lines each,
no framework refactor. Geo retained for existing rendering uses.

Declarations (verbatim):
  firms              [("latitude","longitude")]   (unchanged)
  usgs_quake         [("latitude","longitude")]   (already top-level in data)
  nwis               [("latitude","longitude")]   + centroid mirror
  eonet              [("latitude","longitude")]   + centroid mirror
  gdacs              [("latitude","longitude")]   + centroid mirror
  wfigs_incidents    [("latitude","longitude")]   + centroid mirror (inline data)
  inciweb            [("latitude","longitude")]   + centroid mirror (inline data)
  wfigs_perimeters   []   # polygons, no point
  nws                []   # forecast zones/counties, no point
  swpc_alerts        []   # space weather, no coordinate
  swpc_kindex        []   # space weather, no coordinate
  swpc_protons       []   # space weather, no coordinate

Centroid mirror is `latitude = centroid[1]; longitude = centroid[0]` (centroid
is GeoJSON (lon, lat)); guarded on centroid presence so coordinate-less events
get no lat/lon keys (apply_enrichment then skips them).

map_render_kind concept dropped — the existing /events map is already
geometry-kind-agnostic (renders any row's data-geometry via L.geoJSON), so it
was unnecessary. Events-tab enhancements are PR L-b.

Tests (test_enrichment_locations_coverage.py, 6, all registry-derived):
- every adapter explicitly declares enrichment_locations in its own class body
- declarations are valid list[(str,str)]
- point adapters all use the canonical ("latitude","longitude") paths
- >=5 point adapters are non-empty (regression guard)
- synthetic-event builders prove the keys resolve: usgs_quake._feature_to_event
  and nwis._build_event (the two adapters with isolated builders; the four
  inline-build adapters are covered by the post-merge live smoke).

Verification: full pytest 552 passed, 1 skipped (was 546; +6). grep
subject_for_event/_ADAPTER_REGISTRY and grep 100.64.0./192.168.1. in src empty.

Follow-ups (NOT here): consumer-doc per-adapter _enriched.geocoder notes for
the newly-enriched adapters belong in L-b's doc pass; live end-to-end smoke
runs post-merge (USGS quake + one other) per the acceptance bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-05-21 01:48:23 +00:00
commit c918e8d259
12 changed files with 167 additions and 0 deletions

View file

@ -148,6 +148,9 @@ class EONETAdapter(SourceAdapter):
wizard_order = None
default_cadence_s = 1800
# Event lat/lon mirrored from Geo.centroid into event.data (see poll()).
enrichment_locations = [("latitude", "longitude")]
def __init__(
self,
config: AdapterConfig,
@ -370,6 +373,12 @@ class EONETAdapter(SourceAdapter):
"latest_geometry_date": latest_date_iso or None,
}
# Mirror centroid (lon, lat) into top-level data keys for the flat
# enrichment path (see enrichment_locations).
if centroid is not None:
data["latitude"] = centroid[1]
data["longitude"] = centroid[0]
dedup_key = _dedup_key(event_id, latest_date_iso)
if self.is_published(dedup_key):

View file

@ -150,6 +150,9 @@ class GDACSAdapter(SourceAdapter):
wizard_order = None
default_cadence_s = 600
# Event lat/lon mirrored from Geo.centroid into event.data (see poll()).
enrichment_locations = [("latitude", "longitude")]
def __init__(
self,
config: AdapterConfig,
@ -391,6 +394,12 @@ class GDACSAdapter(SourceAdapter):
"iscurrent": iscurrent,
}
# Mirror centroid (lon, lat) into top-level data keys for the flat
# enrichment path (see enrichment_locations).
if centroid is not None:
data["latitude"] = centroid[1]
data["longitude"] = centroid[0]
if not iscurrent:
# Explicit tombstone from upstream. Only emit if we previously observed it.
if guid in observed_before:

View file

@ -171,6 +171,9 @@ class InciWebAdapter(SourceAdapter):
wizard_order = None # Ships disabled
default_cadence_s = 600
# Coords parsed from the narrative, mirrored from Geo.centroid into event.data.
enrichment_locations = [("latitude", "longitude")]
def __init__(
self,
config: AdapterConfig,
@ -461,6 +464,9 @@ class InciWebAdapter(SourceAdapter):
"url": item.get("link", ""),
"guid": guid,
"raw": item,
# Mirror centroid (lon, lat) for the flat enrichment path.
"latitude": centroid[1] if centroid else None,
"longitude": centroid[0] if centroid else None,
},
)

View file

@ -124,6 +124,9 @@ class NWISAdapter(SourceAdapter):
wizard_order = None
default_cadence_s = 900
# Site lat/lon mirrored from Geo.centroid into event.data (see _build_event).
enrichment_locations = [("latitude", "longitude")]
def __init__(
self,
config: AdapterConfig,
@ -372,6 +375,12 @@ class NWISAdapter(SourceAdapter):
"last_modified": props.get("last_modified"),
}
# Mirror centroid (lon, lat) into top-level data keys so the flat
# enrichment path can reach them (see enrichment_locations).
if centroid is not None:
data["latitude"] = centroid[1]
data["longitude"] = centroid[0]
return Event(
id=f"{monitoring_location_id}:{parameter_code}:{time_iso}",
adapter=self.name,

View file

@ -212,6 +212,9 @@ class NWSAdapter(SourceAdapter):
wizard_order = 1
default_cadence_s = 60
# Alerts cover forecast zones/counties (polygons), not a single point.
enrichment_locations = []
def __init__(
self,
config: AdapterConfig,

View file

@ -41,6 +41,9 @@ class SWPCAlertsAdapter(SourceAdapter):
wizard_order = None
default_cadence_s = 300
# Space weather — no geographic coordinate to enrich.
enrichment_locations = []
def __init__(
self,
config: AdapterConfig,

View file

@ -41,6 +41,9 @@ class SWPCKindexAdapter(SourceAdapter):
wizard_order = None
default_cadence_s = 600
# Space weather — no geographic coordinate to enrich.
enrichment_locations = []
def __init__(
self,
config: AdapterConfig,

View file

@ -40,6 +40,9 @@ class SWPCProtonsAdapter(SourceAdapter):
wizard_order = None
default_cadence_s = 600
# Space weather — no geographic coordinate to enrich.
enrichment_locations = []
def __init__(
self,
config: AdapterConfig,

View file

@ -79,6 +79,9 @@ class USGSQuakeAdapter(SourceAdapter):
wizard_order = 3
default_cadence_s = 60
# Epicenter lat/lon are top-level keys in event.data (see _build_event).
enrichment_locations = [("latitude", "longitude")]
def __init__(
self,
config: AdapterConfig,

View file

@ -60,6 +60,9 @@ class WFIGSIncidentsAdapter(SourceAdapter):
wizard_order = None # Not in setup wizard
default_cadence_s = 300
# Incident-point lat/lon mirrored from Geo.centroid into event.data.
enrichment_locations = [("latitude", "longitude")]
def __init__(
self,
config: AdapterConfig,
@ -326,6 +329,9 @@ class WFIGSIncidentsAdapter(SourceAdapter):
"POOState_raw": state_raw,
"POOCounty": county,
"raw": props,
# Mirror centroid (lon, lat) for the flat enrichment path.
"latitude": centroid[1] if centroid else None,
"longitude": centroid[0] if centroid else None,
},
)

View file

@ -60,6 +60,9 @@ class WFIGSPerimetersAdapter(SourceAdapter):
wizard_order = None # Not in setup wizard
default_cadence_s = 300
# Perimeters are polygons, not a single point — no coordinate to enrich.
enrichment_locations = []
def __init__(
self,
config: AdapterConfig,