Phase 2 PR2: Mobility v1 population (airports/airspace/reroute/NOTAM)#2976
Conversation
Phase 2 PR2 of the Regional Intelligence Model. Replaces the Phase 0
empty `mobility` stub with a real MobilityState built from existing
Redis inputs — no new seeders, no new API keys, no new dependencies.
## New module: scripts/regional-snapshot/mobility.mjs
Single public entry point:
buildMobilityState(regionId, sources) -> MobilityState
Pure function. Never throws. Wrapped in a defensive try/catch that
returns the empty shape on any unexpected builder bug so snapshot
persist can never be blocked by mobility data issues.
## Data sources (all already in the repo)
- aviation:delays:faa:v1 — US airports (FAA ASWS, Vercel seeder)
- aviation:delays:intl:v3 — ~51 non-US airports across Europe/APAC/
MENA/Africa/Americas (AviationStack,
Railway ais-relay, 30min refresh)
- aviation:notam:closures:v2 — global ICAO NOTAM closures (FAA-side
seeder hits ICAO API with monitored
ICAO list spanning all regions)
- intelligence:gpsjam:v2 — global GPS jamming hexes (Wingbits,
fetch-gpsjam.mjs)
- military:flights:v1 — global military ADSB tracks (OpenSky,
seed-military-flights.mjs)
## Per-field output mapping
| Field | Source | Coverage |
|-----------------------|--------------------------------|---------------|
| airspace[] | GPS-jam hexes aggregated 1:region | All regions |
| flight_corridors[] | (empty in v1) | - |
| airports[] | FAA + intl delays, severity ≥ MAJOR | All regions |
| reroute_intensity | clip(military count / 50, 0, 1) | All regions |
| notam_closures[] | NOTAM reasons for region airports | All regions |
## Region classification helpers
Three pure classifiers handle the three different region conventions
the source data uses:
airportToSnapshotRegion(alert) — splits AviationStack AIRPORT_REGION_*
by country so americas → {north-america, latam} and apac →
{south-asia, east-asia}
gpsjamRegionToSnapshotRegion(label) — maps the 17 fetch-gpsjam.mjs
classifyRegion labels (iran-iraq, levant, ukraine-russia, etc.) to
the 7 snapshot regions. Turkey/Caucasus intentionally lands in MENA
per the geography.js WB override.
latLonToSnapshotRegion(lat, lon) — bbox classifier for military
flights. Coarse but shippable; v2 could use h3/geometry.
## Airspace aggregation (P2 design decision)
v1 emits ONE AirspaceStatus per region instead of one per GPS-jam hex.
Emitting per-hex would flood the UI (current MENA data has 40+ hexes).
The aggregated entry's `reason` lists sub-region names and high/med/low
counts so the analyst can still drill into the underlying data via the
evidence chain.
## Reroute intensity caveat
Military flight count is a CRUDE proxy for civil rerouting pressure.
A dedicated civil ADSB track-diversion pipeline would be more rigorous,
but this gets us a defensible 0-1 scalar today. Documented inline.
## Freshness registry
Added 5 new input keys to FRESHNESS_REGISTRY so the snapshot's
missing_inputs / stale_inputs classification tracks mobility sources.
Per-key maxAgeMin set to 2x the source's cron cadence:
aviation:delays:faa:v1 — 60 min (30min cron)
aviation:delays:intl:v3 — 90 min (30min cron + Railway jitter)
aviation:notam:closures:v2 — 120 min (60min cron)
intelligence:gpsjam:v2 — 240 min (120min cron)
military:flights:v1 — 30 min (10min cron)
## seed-regional-snapshots.mjs Step 8
Replaced the hardcoded empty stub with a single call:
const mobility = buildMobilityState(regionId, sources);
No reordering of the compute flow. Pure data, no async.
## Tests — 35 new unit tests
tests/regional-snapshot-mobility.test.mjs:
airportToSnapshotRegion (5):
- US/CA/MX → north-america
- Other Americas → latam
- IN/PK/BD → south-asia, rest of APAC → east-asia
- europe/mena/africa direct
- lowercase labels accepted
- null/unknown safety
gpsjamRegionToSnapshotRegion (4): 17 labels + "other" + undefined
latLonToSnapshotRegion (3): major cities + oceans + invalid inputs
buildAirports (5): empty sources, region filter, severity filter,
severity → status (disrupted vs closed), reason passthrough
buildAirspace (4): empty, region-scoped aggregation, no-match,
low-level still restricted
buildRerouteIntensity (4): zero, region-scoped count, saturation,
missing lat/lon
buildNotamClosures (4): empty, region attribution via airport alerts,
unattributable ICAOs skipped, long-reason truncation
buildMobilityState (4): full shape, all-empty sources, malformed
inputs don't throw, region isolation across fields
## Verification
- node --test tests/regional-snapshot-mobility.test.mjs: 35/35 pass
- npm run test:data: 4537/4537 pass
- npm run typecheck: clean
- npm run typecheck:api: clean
- scripts/jsconfig.json typecheck: clean on touched files
- biome lint: clean on touched files
## Deferred to future iterations
- Direct civil flight track diversion counts per corridor
- flight_corridors[] stress level + rerouted_flights_24h
- LLM-assisted NOTAM structured parse
- h3 geometry for smarter hex-to-region attribution
- Mobility-based alert triggers in the diff engine
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR introduces
Confidence Score: 4/5Safe to merge after fixing the null-coordinate misattribution in buildRerouteIntensity; all other findings are non-blocking. One P1 defect exists: null lat/lon values in the military flights feed can inflate reroute_intensity for sub-saharan-africa due to Number(null)===0 bypassing the coordinate guard. The fix is a one-liner null-check. Remaining findings are P2 (dead code, docstring inconsistency) and do not affect correctness. scripts/regional-snapshot/mobility.mjs — specifically buildRerouteIntensity (line 314–316) and the dead JAM_LEVEL_RANK constant (line 239). Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
S[("Redis sources\n(fetched once)")] --> BA[buildAirports]
S --> BS[buildAirspace]
S --> BR[buildRerouteIntensity]
S --> BN[buildNotamClosures]
BA -->|"aviation:delays:faa:v1\naviation:delays:intl:v3"| R1["filter: severity ≥ MAJOR\nairportToSnapshotRegion()"]
BS -->|"intelligence:gpsjam:v2"| R2["aggregate hexes per region\ngpsjamRegionToSnapshotRegion()"]
BR -->|"military:flights:v1"| R3["count flights in region bbox\nlatLonToSnapshotRegion()\n⚠️ null→0 misattribution"]
BN -->|"aviation:notam:closures:v2\n+ both delay feeds"| R4["cross-ref icaoToRegion\n(all severity levels)\ntruncate reason @200"]
R1 --> MS["MobilityState\n{ airports[], airspace[],\n reroute_intensity,\n notam_closures[],\n flight_corridors[] }"]
R2 --> MS
R3 --> MS
R4 --> MS
MS --> SS["RegionalSnapshot\n(persisted to Redis)"]
Reviews (1): Last reviewed commit: "feat(intelligence): Mobility v1 populati..." | Re-trigger Greptile |
P2 #1 — Mexico bbox mismatch between airport and flight classifiers airportToSnapshotRegion() routes Mexico to north-america by country, but latLonToSnapshotRegion()'s NA bbox started at lat 20°N. Mexico City (19.43°N), Guadalajara (20.67°N), and most Mexican airspace fell into latam, so NA's reroute_intensity understated its actual military pressure and the two classifiers disagreed on the same country. Fix: lower the NA bbox southern edge to lat 16.0°N. That captures every major Mexican city and state capital (Tuxtla Gutiérrez at 16.75°N is the southernmost) while still routing Guatemala City (14.6°N), San Salvador, Belize, and Honduras to latam. Tiny airports at lat ~14.9°N (Tapachula in southern Chiapas) route to latam — acceptable because they're not in the monitored set and don't carry meaningful military traffic. Added 8 regression tests asserting classifier parity across major Mexican cities and correct routing of Central American capitals. P2 #2 — seed-meta freshness path for undated payloads classifyInputs() was treating undated payloads as "fresh" via its fallback branch, so FAA/NOTAM/AviationStack/GPS-jam stalls would never drag down snapshot_confidence. None of those four payloads carry a top-level fetchedAt field; the seeders track freshness in companion seed-meta:* keys only. Fix: added an optional `metaKey` field to FreshnessRegistry entries. classifyInputs() now prefers the metaKey companion's fetchedAt when the spec declares one, falling back to the primary payload's top-level timestamp (existing behavior) and finally to "fresh" (existing fallback for legacy undated payloads). Wired through the full pipeline: - freshness.mjs: added metaKey type, ALL_META_KEYS export, and metaPayloads parameter to classifyInputs() - snapshot-meta.mjs: buildPreMeta() forwards metaSources - seed-regional-snapshots.mjs: readAllInputs() now fetches both ALL_INPUT_KEYS and ALL_META_KEYS in a single pipeline and returns { sources, metaSources }; main() destructures and passes metaSources through computeSnapshot → buildPreMeta - computeSnapshot() signature gained an optional metaSources arg (defaults to {} for back-compat with existing tests) Registered metaKey for each mobility input: aviation:delays:faa:v1 → seed-meta:aviation:faa aviation:delays:intl:v3 → seed-meta:aviation:intl aviation:notam:closures:v2 → seed-meta:aviation:notam intelligence:gpsjam:v2 → seed-meta:intelligence:gpsjam military:flights:v1 does NOT declare a metaKey because its payload already carries top-level fetchedAt — this is asserted in a test. Added 11 regression tests covering: - every undated mobility input has a metaKey - military:flights:v1 does NOT need a metaKey - ALL_META_KEYS aggregation - undated payload + fresh meta → fresh - undated payload + STALE meta → stale (the core bug fix) - undated payload + missing meta → falls back to fresh - missing payload → missing regardless of meta - legacy payloads with top-level timestamp still work - meta without fetchedAt falls through to payload timestamp ## Verification - node --test tests/regional-snapshot-mobility.test.mjs: 54/54 pass (35 original + 19 new regression tests) - npm run test:data: 4556/4556 pass - npm run typecheck: clean - biome lint on touched files: clean
Summary
Phase 2 PR2 of the Regional Intelligence Model. Replaces the Phase 0 empty `mobility` stub with a real `MobilityState` built entirely from existing Redis inputs. No new seeders, no new API keys, no new dependencies.
What landed
New module: `scripts/regional-snapshot/mobility.mjs`
Single public entry point: `buildMobilityState(regionId, sources) -> MobilityState`. Pure, never throws, wrapped in a defensive try/catch so snapshot persist can never be blocked by a mobility builder bug.
Data sources (all already in the repo)
Per-field output mapping
Region classification helpers
Three pure classifiers handle the three different region conventions the source data uses:
Key design decisions
Airspace aggregation: v1 emits ONE `AirspaceStatus` per region instead of one per GPS-jam hex. Emitting per-hex would flood the UI (MENA currently has 40+ hexes). The aggregated entry's `reason` lists sub-region names and high/med/low counts so analysts can still drill into the underlying data via the evidence chain.
Reroute intensity proxy: Military flight count is crude but defensible as a civil-rerouting pressure signal. A dedicated civil ADSB track-diversion pipeline would be more rigorous, but this gets us a defensible 0-1 scalar today. Documented inline.
Airport severity filter: Only surfaces alerts at MAJOR or SEVERE to keep the list focused on actionable state. SEVERE maps to `closed`, MAJOR maps to `disrupted`.
Freshness registry
Added 5 new input keys to `FRESHNESS_REGISTRY` so the snapshot's `missing_inputs` / `stale_inputs` classification tracks mobility sources. Per-key `maxAgeMin` set to 2x each source's cron cadence.
Testing
35 new unit tests in `tests/regional-snapshot-mobility.test.mjs`:
`airportToSnapshotRegion` (5): US/CA/MX → NA, LatAm countries → latam, APAC split by country, europe/mena/africa direct, case insensitivity, null safety
`gpsjamRegionToSnapshotRegion` (4): 17 labels mapped correctly, "other"/unknown → null
`latLonToSnapshotRegion` (3): major cities classified correctly, oceans → null, invalid inputs → null
`buildAirports` (5): empty sources, region filter, severity filter, severity → status mapping, reason passthrough
`buildAirspace` (4): empty, region-scoped aggregation, no-match returns empty, low-level still restricted
`buildRerouteIntensity` (4): zero, region-scoped count, saturation at 1.0, missing coords ignored
`buildNotamClosures` (4): empty, region attribution via airport alerts, unattributable ICAOs skipped, long-reason truncation
`buildMobilityState` (4): full shape matches proto, all-empty safe, malformed inputs don't throw, region isolation across all fields
`npm run test:data`: 4537/4537 pass
`npm run typecheck` + `typecheck:api`: clean
Scripts JSDoc typecheck: clean on touched files
`biome lint` on touched files: clean
Deferred to future iterations
Post-Deploy Monitoring & Validation
PR sequence