Skip to content

Exploration 07 — Live Event Mode (observed-event risk)

Status: session 6 (charter, 2026-08-26) + session 6b (promotion, 2026-08-27) + session 6c (rung R2 promoted, 2026-09-08). Rungs R0 + R1 are Phase 13 — Event Replay (closed 2026-09-03). Rung R2 — feeds + cadence — plus R4's first row (a pushed raster observation) is Phase 15 — Live Risk, defined by owner instruction on 2026-09-08 with a new Live surface and a fourth showcase moment (the §13 "Moment 4" parked item, promoted). Rung R3 and the rest of R4 remain this track's ladder (§4). The §11 gate's condition 1 was waived by the 6b instruction; §12 records the decisions and the reassess corrections that came with promotion; Phase 15's own design record lives in its phase doc. Owner: Giorgos + Claude sessions. Inputs: owner's brief (live data from drones / IoT sensors / satellites → live risk assessment during a wildfire, assets in danger, prioritisation); 00-frame.md (this idea is already parked there twice — see §1); 04-answer-layer-rfc.md (parked "live/forecast surfaces"); two adversarial verification passes (fast + deep, 11 agents over the backbone, the UI repo, the plan docs and the external sources); two product scans (functional + UI/UX). Protocol: one memo per session; every memo ends with a Decided / Parked / Killed ledger. Facts below are file-grounded (path:line) or marked [UNVERIFIED].


0. One-paragraph summary

The owner's idea — run the platform's impact chain against a hazard that is happening instead of one that is probable — fits the platform's formula exactly: impact = hazard ∘ exposure ∘ vulnerability is indifferent to where the hazard came from. What does not fit is the platform's vocabulary: every loss path today is annual (return periods → ead/aai), and ADR-065/ADR-068 make a frequency-less event indicator-only by construction (src/climate_lama/worker/tasks.py:494-531, core/eai_eligibility.py:365). So the first thing to build is not a feed, a spread model or a screen — it is a deterministic-event lane that stores event loss, never labelled as annual. Everything else stacks on that. The cheapest first artifact is a replay of a real past Greek earthquake (a USGS ShakeMap through the real ingest API), which doubles as the historical-event validation feature the frame doc already names as the #1 stakeholder credibility ask. Wildfire runs beside it at the ingest layer (FIRMS hotspots → Kelvin raster → the seeded Lüthi curves, screening-grade) and ahead of it in business value; spread forecasting (ELMFIRE) and sensors/drones are later rungs, each with a named gate.

1. Why this memo exists — and what it re-opens

The idea is not new to the record. It must be reconciled, not restarted:

  • 00-frame.md lists "Forecast-driven alerting — realistic first rung of the live-risk idea: open forecast feeds (EFAS/GloFAS) × portfolio footprints → alerts … IoT/live telemetry becomes rung two, later", and its Parked ledger carries "IoT / live-telemetry risk — parked behind forecast-driven alerting".
  • 04-answer-layer-rfc.md Parked: "Live/forecast surfaces (EFAS-driven alerting) — same RiskSurface contract with a time dimension; design later, don't preclude now."
  • 00-frame.md also lists an unexplored candidate, "Historical-event validation as a feature — observed-events layer beside modelled risk ('here's what the model said, here's what happened'). Model credibility is the #1 open point raised by every stakeholder conversation to date."

This memo overrides the recorded first rung (EFAS/GloFAS river-flood forecast) with an observed-footprint first rung (earthquake ShakeMap, wildfire hotspots). Reason: an observed footprint is simpler than a forecast (no lead-time axis, no ensemble), earthquake is already a computable peril with seeded curves (ADR-060), and wildfire is the business driver in Greece. The override is recorded in the ledger (§13), not applied silently.

Promotion trigger under phase-parking-lot.md's rule: (c) the context shifted materially — Phase 10 closed with a scripted, machine-checkable walk, which is the precondition any live ingest needs as a regression guard.

2. What the platform is today (the constraints that decide the design)

All verified against trunk 5c67b3d+ on 2026-08-26.

Fact Where Consequence for live mode
Every loss path integrates over return periods; a dataset whose events carry no rp is refused at submission (422 E_HAZARD_DATASET_NO_EVENT_FREQUENCIES) and again in the worker core/compute_service.py:219-235, worker/tasks.py:494-531 (called from both the single lane :704 and the batch lane :1575), ADR-069 No frequency-less compute lane exists. A new lane is the prerequisite for everything.
ADR-065 explicitly rejected the sentinel marginal_freq = 1.0 ("looks defensible, is the worse of the two"); ADR-068's ck_hazard_events_event_shape makes a frequency-less row structurally indicator-only docs/DECISIONS.md ADR-065 (~:5375), ADR-068 (~:5905) The live lane must never write a sentinel frequency and never label its output ead/aai. It needs its own result table and its own ADR extending 065/068 to observed events.
Result.ead/.aai are NOT NULL and annual; impact_per_event JSONB already exists and is filled from the engine's at_event models/result.py:42-48, worker/models/engine_adapter.py:177-181 Do not relax the NOT NULL (smoke_e2e :456-462, the results API and portfolio_service all consume it); do not duplicate impact_per_event.
The engine takes any frequency vector and exposes at_event; aai_agg is defined as Σ frequency·at_event engine spec CLIMATE_LAMA_ENGINE.md :45,:48,:151-152,:240 Engine needs no change. The backbone adapter must discard aai_agg/eai_exp/freq-curve on the event path so no annual-labelled number leaves engine_adapter.py.
Surfaces are keyed hazard × region × scenario × horizon × engine × version × dataset; scenario_label/horizon_year NOT NULL; no valid-at column; find_ready_surface already prefers newest and /v1/risk/lookup already accepts a dataset_id pin core/surface_writer.py:223, models/risk_surface.py:113-114,232-233, db/repositories/risk_surface_repository.py:256-301,803-877, api/v1/risk.py:99-126 One dataset per snapshot ⇒ one surface per snapshot with no writer change. Time belongs on the dataset, not the surface.
Mode-1 raster ingest requires return_periods (non-empty, positive); the frequency-less "slice" axis is reachable only through the NetCDF adapter api/v1/hazards.py:47-116, core/ingest/base.py:1708-1720, worker/ingest/aggregate_and_commit.py:977-984,1150-1172 A single-event GeoTIFF mode must be added to IngestRequest.
ADR-068's SINGLE_FIELD_INDEX admits one unlabelled event per dataset models/hazard.py:212-265 A multi-timestep forecast is one dataset per timestep, not one multi-band dataset.
Wildfire is an indicator peril today (FWI, E_INDICATOR_DATASET_NO_EAI), but three Lüthi 2021 curves are seeded with intensity_unit='K' (x from 295 K; i_half 295.01/409.4/484.4 for 1/4/10 km MODIS aggregates), buildings-only core/ingest/wildfire.py:33-70, core/impact_function_seeder.py:70-106,586-621, DOI 10.5194/gmd-14-7175-2021 A brightness-temperature raster in K is computable against seeded curves — screening-grade — if aggregated to the 1 km grid the curve is calibrated for. Native 375 m VIIRS is outside the calibrated set.
Earthquake curves are ESRM20 in g (PGA); no MMI curve exists core/ingest/earthquake.py:72-80, seeder :432,:645-672, ADR-060 Ingest ShakeMap PGA in g (convert from %g / cm·s⁻²); do not accept MMI until an MMI curve is seeded (that would be new science).
One worker, one compute queue, prefetch 1, every task routed there incl. 6 h ingest chords; no lock/singleton code; beat and a weekly poll_dataset_sources already exist worker/celery_app.py:56,65-93,98, docker-compose.yml:311,350,357, docker-compose.hetzner.yml:33-47 (--concurrency=1) The live tick is a cadence and routing change (second worker on a live queue + a Redis lock), not new machinery — but ingest chords stay on compute.
Outbound fetches go through an SSRF allowlist of six hosts; none is nasa.gov, usgs.gov, ecmwf.int or open-meteo.com; adding one is a code+PR change (ADR-036) or a per-org row config.py:201-215, core/ssrf.py:9-41, worker/download_task.py:36,67-69 Every live feed is unreachable as designed until the allowlist is extended. Sequence this first.
Compose profiles never deploy: deploy-prod.yml passes no --profile, release.yml builds only api + worker images; the climada and geocoding profiled services have never started in prod .github/workflows/deploy-prod.yml:343-376, release.yml:37,104, docker-compose.prod.yml:251-254,288 A profile-gated sidecar or live worker is local-only. Prod needs an un-profiled service or a deploy-pipeline change.
No feature-flag for live; the established pattern is a default-off master switch (score_bands_enabled, surface_run_save_enabled, geocoder_enabled, …); this is a NO-PR-CI repo config.py:198,463,487,555, CLAUDE.md Land everything behind live_mode_enabled: bool = False so every intermediate PR keeps trunk walkable.
No time-control or HTTP-replay seam in tests (no freezegun/time-machine, no respx/responses/vcr); ADR-077 removed the fixture escape hatch pyproject.toml:112-116, tests/conftest.py:53-326, ADR-077 Add an injected clock (the select_eviction_candidates(now=…) idiom, core/surface_service.py:354) and a recorded-response transport for feed fixtures — test data, not product demo data.
Retention evicts on age + LRU only; nothing pins a surface a saved result still references; Result.hazard_dataset_id is a hard FK core/surface_service.py:354-394, config.py:463-467, models/result.py:36 A swept snapshot orphans past results and breaks replay. Pin rule required.
RLS has no public/cross-org lane; the RLS predicate was rewritten in migration 0082 to the NULLIF(...) form — copying 0070's helper reintroduces the empty-GUC raise (#834) db/rls.py:3-32, migrations/versions/0082_*:52-89 One owning org for the PoC; any new table uses the 0082 predicate.
The screening-grade posture is real and enforced: EaiGrade + _SCREENING_GRADE_PERILS ("use the figure to rank and triage, not as a loss estimate") reaches the UI pill and is test-asserted in both repos; but no liability / life-safety / disclaimer decision exists anywhere core/eai_eligibility.py:136-155, ui ScoreCardPanel.tsx:88, grep of docs/ Extend the existing mechanism to events; add the one line that does not exist today (§9).
UI stack: MapLibre GL (maplibre-gl ^4.7.1), imperative, not react-leaflet (CLAUDE.md's "react-leaflet" is stale); English-only i18n (supportedLngs: ['en']); no feature flags; header nav is a non-wrapping row of 11 links; light/dark/high-contrast themes exist but severity colours are hardcoded Tailwind classes (inert outside light) ui package.json, src/i18n/index.ts:31-44, src/App.tsx:60-148, src/branding/themes.ts, ScoreCardPanel.tsx:81-85 See §8.

3. The capability, defined

Live Event Mode (working name; "Event Mode" is equally accurate — the first rungs are replays) is a hazard-agnostic capability with five parts:

  1. LiveEvent — an org-owned incident: id, org_id, haz_type, name, status (open/closed), started_at, ended_at, source, provenance.
  2. Snapshots — each hazard observation/forecast step is an ordinary HazardDataset with two new columns, live_event_id (nullable FK, cascade) and valid_at (timestamptz), ingested through the real ingest API on a new single-event GeoTIFF mode. Exactly one snapshot per event is active; the previous one is marked superseded in the same transaction. Snapshots are invisible to every non-live resolver (GET /v1/datasets, lookup precedence, matrix resolution) unless live_event_id= is passed — otherwise a 1 km ShakeMap or 375 m FIRMS raster outscores ESHM20/JRC on resolution and hijacks every citizen pin (core/dataset_precedence.py:1-60).
  3. Deterministic-event compute lane — POST /v1/live-events/{id}/compute (202 + job id). The worker reuses compute_impact's load steps, replaces _event_frequency_vector with unit weights, asserts the dataset axis is not return-period (the inverse gate), and the adapter returns only at_event and the per-exposure-point loss vector. Results go to a new RLS'd table event_impact_results (event_loss NOT NULL, impact_per_event, loss_exp_path with the same alignment contract as eai_exp_path, provenance) — impact_results stays annual-only, portfolio_service and the surface writer never see event rows. Surface saves for live results use a metric_key other than eai (eai is a reserved namespace, docs/CLIMATE_LAMA.md:568). No band — ADR-075's design-RP match cannot resolve on an RP-less dataset; the UI renders the existing "values only" chip.
  4. Exposure layers — not a new request shape. A "layer" is an exposure dataset in a portfolio; portfolio_exposures.weight already exists (models/portfolio.py:37-54), and Phase 11's compute-on-portfolio (batch expansion = one job per portfolio_exposures row × scenario × horizon, cells keyed by exposure_dataset_id) is the mechanism that makes layers computable. Live pairs with it: one event cell per layer, event loss per layer, weighted sum read-side. Criticality = a per-layer weight keyed on the existing exposure_type taxonomy, with the per-asset properties JSON as an optional override — not a new per-asset column.
  5. Answer — ranked assets-at-risk from the per-point loss vector (not from an H3 rollup), event loss with its screening-grade label, the snapshot's valid_at, its age at render, and the source version (e.g. "ShakeMap v3"). Replay and live are the same screen; only the time source differs.

Time resolution lives at the event level: GET /v1/live-events/{id}/snapshots?as_of= → newest snapshot with valid_at ≤ as_of → pinned through the existing dataset_id param on /v1/risk/lookup. Surfaces are untouched.

Design split to decide (both file-grounded, both viable): the deep pass's two backbone refuters keep live_events + dataset-level valid_at (above); the product refuter, reading 04-answer-layer-rfc.md's parked entry ("same RiskSurface contract with a time dimension"), argues for observed_at on the surface tables and no new table. Recommendation: the dataset-level design — it needs zero surface/writer changes, reuses the existing dataset_id pin, and still honours the RFC's intent (a snapshot is an ordinary surface). The RFC entry should be amended to say so. Owner decision.

4. The ladder (rungs, each with its gate)

Rung Deliverable Needs Gate to the next rung
R0 — Replay / validation One real past Greek earthquake (archived USGS ShakeMap, PGA in g) ingested through POST /v1/hazards/ingest in single-event mode, computed on the deterministic lane against an existing exposure dataset; assets-at-risk + event loss rendered on the Moment-2 variant screen D1 lane, D6 single-event ingest mode, live_mode_enabled, allowlist entry for earthquake.usgs.gov, event-grade screening label smoke_e2e-shaped script passes (§11); owner walks it
R1 — Wildfire observed footprint Replay of a real Greek fire (Evia 2021 / Rhodes 2023 / Mati 2018) from archived FIRMS VIIRS detections: hotspot raster in K, assets within the footprint (+ optional downwind buffer), exposed count + value; € loss screening-grade via the seeded Lüthi 1 km curve FIRMS pre-pipeline rasteriser + WildfireBrightnessIngestor (unit K), allowlist entry, EFFIS recency palette in the UI same script on the fire; curve-pairing assumption written down
R2 — Feeds + cadence The same two events as they happen: beat-driven FIRMS/EFFIS poll (Greece bbox, ≤ every 15 min, well inside FIRMS's 5000 tx/10 min cap) and USGS FDSN event poll with ShakeMap revision re-ingest; live queue + second worker + Redis lock; snapshot lifecycle + pin rule D4, D5 retention, un-profiled prod service, licence ledger rows replay of a stored series reproduces the same ranked list (reproducibility, not uptime)
R3 — Dynamics + forward risk ELMFIRE sidecar (arrival time / burn probability GeoTIFFs), WindNinja wind fields, ECMWF open-data weather; Greek fuel-model mapping (CORINE/EFFIS → fuel classes) as provenance-recorded science work; "forward-risk precompute" — nightly ensemble over tomorrow's weather scored per asset HazardDynamicsInterface (not ModelInterface), object-store transport, ADR-026 licence review, ADR-042 shape, deploy story ensemble members as N snapshot datasets; forecast/observed distinction in the UI
R4 — Sensors + drones Drone orthomosaics as time-stamped GeoTIFFs through the existing raster ingest; IoT point observations in a new PostGIS table, interpolated to a field; crew reports as polygons point-observation model, interpolation step, upload path real device data available (external dependency)

Earthquake and wildfire run side by side at the ingest layer (both are event-footprint ingestors on the same single-event mode). Loss output is earthquake-first because its curves are full-grade; wildfire's is screening-grade from day one and says so.

5. Backbone design — verified item by item

Each row is the corrected design after two independent refuters; the original proposal is noted where it was wrong.

# Component Corrected design Was wrong because
D1 Deterministic lane New task compute_event_impact on the live queue; own endpoint under /v1/live-events/{id}/compute (keeps validate_impact_refs refusing frequency-less datasets on /compute/impact); new table event_impact_results; adapter path discards annual quantities; ADR extending 065/068 "engine occurrence path works unchanged" (refused at the gate); "relax ead NOT NULL" (breaks portfolio_service:111-116, smoke_e2e, and re-creates the ADR-065 failure class); "add event_loss_per_event" (duplicates impact_per_event)
D2 Multi-exposure layers Dropped as a separate item. Compute-on-portfolio (Phase 11) is the layer mechanism: POST /v1/compute/batch takes portfolio_id XOR exposure_dataset_id; expansion per portfolio_exposures row; weight from PortfolioExposure.weight; cap n_layers × n_scenarios × n_horizons ≤ MAX_MATRIX_CELLS (=100, compute_batch_service.py:31) with a named error "a bounded new batch axis" duplicated portfolio_exposures.weight and collided with Phase 11's one committed backbone feature; #367 is per-asset multi-peril from surfaces, orthogonal
D3 Time axis No writer change, no as_of on surfaces; resolve at the event/snapshot level and pin via the existing dataset_id "always create_surface + as_of on created_at" — find_surface_by_key already matches dataset_id exactly, and created_at is build time, not observation time
D4 Live queue live-worker service in all three compose files (--queues=live --concurrency=1), REDIS_EXPORTER_CHECK_SINGLE_KEYS: "0=compute,1=live", redis.set(f"live:tick:{id}", nx=True, ex=2×interval), apply_async(expires=interval); ingest chords stay on compute (accepted contention) the "memory budget per hazard" mechanism cited earlier could not be found under that name — dropped; the hetzner overlay's --concurrency=1 comment is the real RAM constraint
D5 Snapshot identity live_events table (0082 NULLIF predicate) + hazard_datasets.live_event_id/valid_at; hide from catalog/precedence by default; supersede previous; skip convert_to_cog unless tiles are requested; exclude from prewarm_result_cache/sweeps; close → purge policy temporal_coverage_* are Integer years (unusable); no dataset-level retention exists at all today
D6 ShakeMap ingest IngestRequest single-event mode (file_path + event: {label, valid_at}, no return_periods); pre-stage converter %g/cm·s⁻² → g; EarthquakeIngestor unchanged; each revision = new snapshot; design against grid.xml as the guaranteed artifact, GeoTIFF optional "ingest on the slice axis" — the slice axis is NetCDF-only today
D7 FIRMS ingest Pre-pipeline rasteriser (core/ingest/firms_points.py, max bright_ti4 per 1 km cell) → WildfireBrightnessIngestor(BaseGeoTIFFIngestor) with intensity_unit='K', source_format='firms_bt', indicator: false; pin the 1 km Lüthi curve; ELMFIRE outputs are not K and need their own mapping the chord requires a raster on disk (plan_chunks.py:346-350); #412 is not the tracker (it is RP historical footprints and lists the K mismatch as out of scope) — the right issue to un-park is #625 "wildfire intensity pack — BT/FRP footprints" (open, parked)
D8 ELMFIRE sidecar docker/elmfire-sidecar/ with a stdlib HTTP server; transport by MinIO keys ({input_keys, output_prefix, params} → output keys + {elmfire_version, run_id}); a separate HazardDynamicsInterface, never in worker/models/registry.py (/agreement dispatches every registered engine for impact, api/v1/compute.py:823-831); provenance stamps dynamics_engine; new stable codes E_DYNAMICS_* (touches errors.py → sdk-regenerate gate fires); ADR-026 licence allowlist review first "follow ADR-042 as an engine" — the CLIMADA sidecar's JSON/CSR transport is sized for sparse hazard, not dense rasters, and ModelInterface is impact-only

Plus the critic's gates (all verified as absent today): SSRF allowlist entries (earthquake.usgs.gov, firms.modaps.eosdis.nasa.gov, later data.ecmwf.int), an un-profiled prod service, live_mode_enabled, an injected clock + recorded transport for tests, a pin rule (a snapshot referenced by a saved result, scenario or replay timeline is never eviction-eligible), and a volume-budget row per source before download.

6. Data sources — what was actually verified

Source Verified facts Terms / caveats Status
NASA FIRMS (VIIRS/MODIS active fire) Free MAP_KEY; hard cap 5000 transactions / 10 min; latency tiers: URT ~5 min (US/Canada only), RT ~30 min (regional), NRT 1–3 h for Greece; 375 m VIIRS pixel centres, not fire perimeters NASA states "full and open sharing" and asks for the LANCE FIRMS acknowledgement; no named licence, nothing on commercial use — get it in writing before any commercial redistribution verified (firms.modaps.eosdis.nasa.gov/api/area, earthdata.nasa.gov FAQ)
Copernicus EFFIS OGC WMS (fire danger FWI/KBDI/…, Corine, protected areas) and WFS (burnt areas ≥ ~30 ha; active fires ~6×/day, 2–3 h after acquisition); TIME param required; historic extracts via request form CC BY 4.0 for EU-owned content only — the active-fire layer is NASA FIRMS, so routing through EFFIS does not cure FIRMS's licence position verified
USGS ShakeMap Greek events do get USGS ShakeMaps (e.g. 2024-03-29 Filiatra, 2025-05-13 Fry, seen via JRC RapidN); products are versioned and revised for hours–days; grid.xml is the reliable artifact Terms verified 2026-08-26: USGS-produced data is U.S. public domain, credit "U.S. Geological Survey" requested (usgs.gov Copyrights and Credits). [UNVERIFIED]: the automatic magnitude threshold for non-US events, whether raster.zip/GeoTIFF is emitted for every foreign event, and whether NOA / INGV / EMSC publish any gridded PGA/MMI product for Greece (assume not until proven). The FDSN query producttype=shakemap over a Greece bbox 404'd — re-derive it partially verified
ECMWF open data CC-BY-4.0 incl. commercial, 0.25°, 10 m u/v wind, 2 m T and dew point, 4 runs/day, 3-hourly to 144 h, ~2–3 days retained; Python client ecmwf-opendata attribution verified
Open-Meteo Hourly wind/T/RH for Greece; free tier 10k calls/day free tier is non-commercial only — fine for the showcase, paid the moment the product is commercial verified
Copernicus CDS ERA5T reanalysis, ~5-day latency not a nowcast source — dropped —
HNMS / meteo.gr — [UNVERIFIED] — not checked either way —
ELMFIRE EPL-2.0, Fortran, ships Dockerfile + compose, actively maintained (drives Pyrecast), GeoTIFF in/out, no Python bindings, Rothermel/CFFDRS, no Mediterranean fuel calibration copyleft-lite: keep strictly out-of-process verified (github.com/lautenberger/elmfire)
Cell2Fire GPL-3.0, C++/Python, Boost/Eigen build, Canadian FBP, "research use only, no warranty", upstream points at the C2FK fork rejected as first sidecar (licence + posture + build) verified
WindNinja public domain (17 USC 105) + BSD-2-Clause, C++/CMake, Dockerfile in repo the only one with no copyleft friction verified
Static context DEM, CORINE / EFFIS fuel maps, GADM (already ingested) licence rows per docs/data/volume-budget.md —
Tier 1 (later) Drone orthomosaics = GeoTIFF (existing raster ingest, time-stamped); IoT points (LoRaWAN-class gas/T/RH sensors) = new point table; crew reports = polygons redistribution terms for third-party imagery unstated — record before use design only

Every source enters through this repo's own pipeline from the primary provider, with its own provenance row and a volume-budget.md line quoting the licence as the provider states it — no artifact, config or processed output from risk-assessment-tool may be copied for the same event, Storm Daniel EMSR697 included (05-rat-convergence.md, rules 1–2 and the Killed ledger). Rule 3 (knowledge flows, deliverables don't) is what lets Greek event knowledge transfer.

7. Science posture

  • Impact half: reuse. Earthquake = ESRM20 PGA curves (full grade, ADR-060). Wildfire = seeded Lüthi 2021 brightness-temperature curves, screening-grade, 1 km aggregation, buildings only — the pairing "FIRMS brightness temperature → building damage" is what the source calibrates, but the source calibrates on MODIS aggregates, so the 375 m→1 km step is a recorded modelling assumption. Wildfire's first output is therefore two numbers: exposed count + exposed value (assumption-free) and € loss (screening-grade).
  • Hazard dynamics: consume, don't build. ELMFIRE first (R3), in-house cellular automaton only as a plumbing stand-in, never as a claimed model. The real science work is the Greek fuel-model mapping — a provenance-recorded dataset in its own right, and the natural place for external scientific collaboration.
  • Uncertainty: a deterministic replay has no range of its own. Honest states are (i) point at_event + screening-grade label (R0/R1); (ii) the ShakeMap's own uncertainty grid, if published for the event [UNVERIFIED]; (iii) an ensemble of N spread runs as N snapshot datasets (R3). Note phase-parking-lot.md parks "frequency-curve uncertainty bands (#L) — gated on engine uncertainty quantification", but engine#10 (bootstrap CIs on EAD) is closed — that gate may already be open for the annual side; it does not apply to a single event.
  • Never: a sentinel frequency; an annual label on an event number; MMI without an MMI curve; a per-person "safe/unsafe" verdict.

8. UI fit — verified against climate-lama-ui

Where it lives. Not a 12th nav entry (the header is a non-wrapping flex gap-3 row of 11 links, App.tsx:60-148). Live mode is a variant of Moment 2: from /portfolios/:id, an "Events" section opens /portfolios/:id/events/:eventId. The /compare precedent (route without a nav entry, App.tsx:158) covers landing it before it is ready.

Anatomy — mirror the house pattern, not a generic COP. Resizable <aside> at the inline-start (RiskLookupPage.tsx:452-535 — flex-1 flex flex-col sm:flex-row overflow-hidden; aside w-full flex-1 min-h-0 border-e-0 sm:border-e … sm:w-auto sm:flex-none, width from lookupSidebarWidth.ts 320–560 px) + the existing Splitter (components/lookup/Splitter.tsx, 6 px, bg-gray-200, keyboard-operable) + map at flex-1 min-w-0; the snapshot scrubber is a shrink-0 row below the map (the StatsBar slot, ScenarioComparePage.tsx:203-208). Logical properties only (border-e, ms-, start-) — the shell is RTL-aware (App.tsx:31-34). Below sm the whole surface stacks: rail above, map h-[45vh], scrubber reachable; splitter disabled via useViewportWidth; ship a resize test (RiskLookupPage.test.tsx:1637-1739 pattern).

Rail contents (top→bottom): event header (name, hazard chip, Replay / Live chip, source + version, valid_at, age at render); headline event loss with the existing screening-grade pill (EaiGradeBadge) and the 3-tier RAG_COLORS vocabulary (green/amber/red, ScoreCardPanel.tsx:81-85) — not a fourth PAGER tier, and never the 1–10 score slot; an inline accordion "why this number" (the HazardDetail disclosure pattern that replaced the drawer in ui#192), with ProvenanceLine; assets at risk grouped by layer with counts, then a ranked table; the one line that exists nowhere today: "Not for life-safety or emergency response — consult official civil-protection sources."

Components — reuse vs. build (verified):

Need Verdict Detail
Map with two hazard layers (observed + forecast) build components/live/LiveEventMap.tsx LayerKey is a closed union 'exposure'\|'hazard'\|'impact' (types/index.ts:684); ResultMap/LookupMap remove+re-add the raster source on every change (LookupMap.tsx:167-190) — a scrubber must instead pre-create sources and switch with setLayoutProperty('visibility')/opacity, keep a ±2 window warm, prefetch the next step; drop the hasResult gate (MapView.tsx:118); key layer-state on the event id, not the snapshot id
Layer panel generalise LayerPanel rows to a LayerRowDef[] prop today a literal 3-row array (LayerPanel.tsx:84-88); visual stays (absolute bottom-8 right-3 bg-white/90 backdrop-blur rounded shadow-md p-3 w-52 text-xs)
Timeline scrubber build components/live/SnapshotScrubber.tsx YearRangeSlider is a dual-thumb scenario control in the settings namespace with tests; copy only its index-over-sorted-array + tick-row idioms; Windy/Zoom-Earth conventions: play/pause, discrete ticks, now-marker separating observed from forecast, arrow keys, time echoed in every panel
Ranked assets lift WorstMembersTable → exported RankedAssetsTable today module-private in PortfolioDetailPage.tsx:107-165 and fed by the screening rollup (combined_score, no money); the live instance is fed from the per-point loss vector; columns asset / location / value / event loss / criticality / status
Headline build EventLossHeadline no headline component is exported anywhere; StatsBar.fmt hardcodes € and "present day" — do not copy; use the backbone's unit + formatNumber; mirror into sr-only aria-live="polite"
Refresh extract useIntervalFetch from useNotifications usePoller is one-shot (breaks on terminal status and on first error); useNotifications has the right loop (interval, document.hidden skip, visibilitychange catch-up, keep-polling-on-error) but is hard-wired; per-snapshot compute jobs keep useJobPoller
Band-less state reuse bucketHazards → valuesOnly → values-only-badge chip is a first-class state, never an error
i18n add live namespace English only today (no Greek bundle exists); t(..., {defaultValue}) chain for backbone reason codes
Theme use var(--brand-*) hardcoded Tailwind severity classes are inert in dark/high-contrast (grep dark: = 0 hits) — pre-existing debt; do not deepen it
Fixtures capture from real backbone payloads ui#116/#117: an invented fixture field shipped a contract bug

Plan-doc consequence (must precede any UI split): Live mode appears in neither showcase-script.md nor phase-11-showcase.md. Both product refuters converge: do not mint a fourth moment (Goal C in plan.md and CLAUDE.md enumerate three by name — editing that is an owner decision); append steps to Moment 2 instead — "8. Switch the book to an event: same asset map and worst-N table with an observed footprint in place of the return-period layer — target: live-risk track" and "9. Read the event loss as a screening-grade figure with the snapshot's observed-at and version". The responder/official is a lens of the already-defined P3 decision-maker persona (02-personas.md), not a new persona.

9. Product landscape — what to borrow (and what is commoditised)

Two scans, condensed. One URL each; [UNVERIFIED] where the scan could not open the source.

Wildfire - Technosylva FireSight / Wildfire Analyst — simulation-centric; consequence metrics per run (acres, buildings destroyed, population); nightly forward-risk precompute over the whole territory scored per asset → our batch engine repointed at a 5-day horizon. https://technosylva.com/products/wildfire-analyst/firesight/ - Watch Duty — full-bleed map + one verified alert feed, every item pinned to a map object; avoid layer sprawl with no default-on set. https://www.watchduty.org/ - Pano AI — 60 s panoramas, incident triangulation, human confirmation stamp before alert; raw AI detections never at the same visual weight as confirmed ones. https://www.pano.ai/ - Genasys Protect (Zonehaven) — named zones as the shared unit between officials and public; colour never alone. https://protect.genasys.com - Dryad Silvanet — in-forest LoRaWAN gas sensors, smouldering-phase detection; the Tier-1 sensor class. https://www.dryad.net/wildfiresensor - EFFIS Current Situation Viewer — day-range picker; active fires coloured by recency (red ≤24 h, orange ≤7 d, blue ≤90 d, green older) → encode data age in the symbol. https://forest-fire.emergency.copernicus.eu/apps/effis.csv/ - NASA FIRMS Fire Map — states the latency budget in the UI; a hotspot is a pixel centre, not a fire. https://firms.modaps.eosdis.nasa.gov/map/ - Pyrecast / Pyregence — publishing an ELMFIRE ensemble spread forecast to non-experts at all. https://pyrecast.org/ - WIFIRE Firemap (UCSD) — click-to-ignite scenario runs [UNVERIFIED]. https://firemap.sdsc.edu/ - NOA BEYOND FireHub (Greece) — 5-min MSG SEVIRI monitoring feeding the Hellenic Fire Service; stops at hazard + burnt area. The incumbent and the natural partner. https://beyond-eocenter.eu/firehub.html - Hellenic wildfire CubeSats (OroraTech, ~Sept 2026) — detection in Greece is funded and solved; compete downstream. https://www.ctif.org/news/greece-deploys-world-first-wildfire-detection-satellites-ai-system-begins-sending-real-time - Esri Emergency Management Operations — "Understand Impact" tab organised by lifeline/sector; operator vs public views. https://doc.arcgis.com/en/arcgis-solutions/latest/reference/introduction-to-emergency-management-operations.htm - Also: Perimeter, ALERTCalifornia, CAL FIRE incident cards (acreage / containment %), InciWeb, Zoom Earth / Windy (the reference timeline scrubber) — all [UNVERIFIED] in detail.

Earthquake - USGS PAGER onesheet — two alerts side by side (fatalities, economic loss), the higher becomes the summary; histogram of probability per loss band; MMI-over-population map; exposure table by MMI; three historical analogues; explicit uncertainty note. Borrow the whole pattern. https://earthquake.usgs.gov/data/pager/onepager.php - GEM OpenQuake + RISE "European Rapid Earthquake Loss Assessment" — the closest open precedent: ShakeMap webhook → fetch exposure → auto-run → publish. https://www.rise-eu.org/dissemination/good-practices/European-rapid-earthquake-loss-assessment/ - EMSC LastQuake — felt reports via 12 cartoons; crowdsourced ground truth as a correction signal. https://www.emsc-csem.org/lastquake/information_channels/lastquake_app/ - GDACS — green/orange/red, hazard × population × coping capacity; bold what changed, blur events older than 4 days. https://www.gdacs.org/ - JMA / Taiwan CWA intensity maps — station-level dots on a light-cyan→yellow→red-violet ramp, not the alert palette [UNVERIFIED].

Flood / multi-hazard / COP - Google Flood Hub — return-period-anchored severity words (warning ≈ 2-yr, danger ≈ 5-yr, extreme ≈ 20-yr); honest "no inundation map available" state. https://support.google.com/flood-hub/answer/15636998 - JBA Flood Foresight — 3-hourly footprint × portfolio → impact counts + trigger thresholds for early action. https://jbagr.com/digital-tools/flood-foresight/ - Fathom Event Response — every live view exports a citable footprint artifact; analyst-authored bulletin as the paid wrapper. https://www.fathom.global/product/event-response/ - One Concern — downtime as the loss unit; and the cautionary tale: never present modelled impact as observation. https://www.oneconcern.com/en/about/ - Juvare WebEOC — saveable per-role dashboard layouts. https://www.juvare.com/webeoc/ - Copernicus EMS Rapid Mapping — damage grading schema = a ready vocabulary for post-event reconciliation (authorised users only). https://documentation.dataspace.copernicus.eu/Data/CopernicusServices/CEMS.html - GloFAS/EFAS (lead-time slider), NOAA nowCOAST (layer time controls), Ushahidi, Sahana Eden, Everbridge, HOT Tasking Manager — [UNVERIFIED].

Commoditised (do not build): hazard detection and forecast (FIRMS, EFFIS, GloFAS, ShakeMap, FireHub, the CubeSats); fire-spread physics (ELMFIRE, GridFire, Technosylva). Underserved in the EU/Greece: the exposure join (a live footprint → these named assets, this value, this owner); rapid loss outside earthquake ("no PAGER for wildfire"); continuity between batch and live on one consistent chain; and non-state readers of the same picture.

Recurring UI patterns to adopt: map + persistent rail; freshness encoded in colour/opacity not just text; stable named zones/assets as vocabulary; the scrubber conventions above; banded severity + probability histogram rather than a point; return-period-anchored severity words; assets grouped by lifeline then ranked; curated default-on layers; exportable footprint + shareable view URL. Functional ideas not previously considered: a delta-first feed (what changed since the last snapshot); trigger thresholds ("notify when >50 residential units enter the 6 h footprint" — later, and still not a push); a counterfactual second track on the scrubber; an analogue events panel; picture-based ground-truth capture. Palette discipline: one meaning for red per map — EFFIS's recency palette collides with any severity ramp; keep severity in the existing 3-tier RAG and put age in opacity.

10. Risks and guardrails

Risk Disposition
Liability in a life-safety context — nothing in docs/ or either codebase mentions liability, disclaimer, life-safety or evacuation Extend the existing mechanism, not free text: every live output is EaiGrade.SCREENING by construction with its own registry entry and citation-bearing note; the snapshot's valid_at, age at render and source version are mandatory fields; no push, no subscription, no email, and the word "alert" does not appear in copy — the user pulls a view of a stored snapshot; plus the one new line on every live surface (§8). Recorded as an ADR when R0 lands.
"Live" overstates 1–3 h NRT latency for Greece Copy says near-real-time; the UI shows the latency budget (FIRMS pattern); never "minutes-fresh" for Greece
Licence position of FIRMS for a commercial product Showcase-safe; before any commercial use get NASA/LANCE's written position; EFFIS does not launder it
Wildfire loss looks authoritative Two numbers always: exposed count/value (assumption-free) and € (screening-grade, pill + note); MODIS-calibration caveat recorded
ShakeMap revisions change yesterday's answer Revision = new snapshot; the card names the version; results are never rewritten
Retention deletes replay history Pin rule (§5); per-event max-snapshots on close; volume-budget row per event series
Snapshots pollute the catalog and hijack lookup precedence Hidden by default from GET /v1/datasets, precedence and matrix resolution (§3)
Half-built live mode on trunk in a NO-PR-CI repo live_mode_enabled=False master switch gating route, queue and writer; route without nav entry in the UI
Ops: tick piles up behind a 6 h ingest chord; second worker on a 15 GiB box live queue + live-worker --concurrency=1 + Redis lock; ingest stays on compute — accepted; RAM is an owner call
Profiles never deploy Live worker is an un-profiled service; the ELMFIRE sidecar needs a release.yml build job and a deploy-prod.yml change before the science work
Attention: Phase 11 is unstarted and the framework's own diagnosis is front-loaded planning This track runs at strategy-session cadence and consumes no build capacity until Phase 11 exits, except the R0 spike if the owner chooses to pull it forward
Circuit-breaker / weekly metric Unchanged. The breaker is scoped "inside a phase" and is dormant for a track; the weekly metric's "moment improved" half is served by the Moment-2 steps; freshness goes into exit criteria as reproducibility, never into the metric as an uptime number
Scope creep into a Phase-2-sized bundle (51 issues) The ladder (§4) with a gate per rung; nothing past R0 is split until R0 walks

11. Gate to become a phase — and its exit criterion

Superseded 2026-08-27 (session 6b). The owner instructed promotion without the R0 spike: condition 1 below is waived, conditions 2 and 3 are met (showcase-script Moment 2 steps 8–9; trigger (c) in §1). The phase is phase-13-event-replay.md; its exit criterion is deliberately narrower than the one written here (single snapshot, not a series — a series is rung R2) and says so.

The track becomes a phase (taking the next free number then, not "13" now) when all three hold:

  1. R0 spike walked: an archived real Greek ShakeMap ingested in single-event mode through POST /v1/hazards/ingest, computed on the deterministic lane against an existing exposure dataset, rendered on the Moment-2 variant screen; a smoke_e2e-shaped script asserts a saved event_impact_results row with event_loss > 0, a non-empty per-point loss vector, the snapshot's valid_at, and the screening-grade label — plus a written finding on how the event was addressed in the scenario × horizon vocabulary (scenario_label/horizon_year NOT NULL on surfaces; the heatwave scenario=NULL matrix-unaddressable incident is the precedent).
  2. Showcase-script amended with the Moment-2 steps (§8) as target steps.
  3. Promotion trigger recorded in this ledger (it is (c), §1).

Phase exit criterion (when it is a phase) — machine-checkable, offline: scripts/smoke_e2e.py gains a live step that ingests a recorded snapshot series, computes on the deterministic lane, and asserts (a) the ranked assets-at-risk list is reproduced from stored snapshots, (b) every card states valid_at and age at render, (c) no ead/aai is emitted for an event result. Runs under sdk-smoke.yml at the phase boundary like today's walk.

12. Open points — resolved 2026-08-27 (session 6b)

Decided autonomously under the owner's instruction ("provide your own recommendations … verify with reassess … proceed"), each verified by a fast reassess (1 refuter + 1 critic, file-grounded):

  1. Time-dimension home → the dataset: hazard_datasets.valid_at + live_event_id → live_events. One dataset per snapshot already yields one surface per snapshot with no writer change, and /v1/risk/lookup already pins dataset_id. The RFC's parked "same RiskSurface contract with a time dimension" is honoured in spirit and amended to say so.
  2. Poller home → deferred to the R2 phase (out of Phase 13's scope). Recommendation on record: in-worker beat task; feed adapters under ingest/sources/; the tick calls only service functions the public API also calls, so a later extraction to a peer deployable is moving files.
  3. R0 pulled forward → yes, with R1: Phase 13 = both replays (earthquake ShakeMap + wildfire FIRMS), beside Phase 11, backbone-first.
  4. USGS terms verified (usgs.gov "Copyrights and Credits", 2026-08-26): USGS-produced data is U.S. public domain; credit "U.S. Geological Survey" requested — so a clipped ShakeMap fixture can be vendored. Still [UNVERIFIED]: the automatic magnitude threshold for Greek events, raster.zip availability, NOA/INGV/EMSC gridded products (assume none).
  5. FIRMS commercial position; HNMS data — still [UNVERIFIED]; neither is needed for a replay.
  6. CLAUDE.md never named react-leaflet (it was plan.md, fixed by #1007).
  7. #625 is superseded by Phase 13 W3 and closed when W3's issues are filed, its two unique checks carried forward; #412 gets a scope comment, stays return-period scoped, and perils.wildfire.status stays "gap".

Corrections the promotion reassess added to §5's designs (all now in the phase doc's design record):

  • Grade is a lane property, not a peril property. Do not touch _SCREENING_GRADE_PERILS (HazardType.EARTHQUAKE is already there with a note bound to ESHM20 return-period grids); add EVENT_LANE_GRADE_NOTE + event_result_grade() beside it.
  • The EAI gate must be bypassed explicitly: validate_impact_refs calls assert_dataset_eai_eligible unconditionally (core/compute_service.py:176) and every single-event snapshot is SCENARIO_YEAR; the lane gets a sibling validator that keeps every other check.
  • No second wildfire ingestor. source_format accepts only geotiff/netcdf and INGESTOR_REGISTRY is peril-keyed; FIRMS is rasterised to GeoTIFF and ingested with an optional intensity_unit override restricted to seeded units (K is seeded for wildfire).
  • Snapshots stay tile-able: hidden from catalog/lookup/matrix only; /v1/hazards/{id}/layers and the tile route resolve them and single-event mode still runs convert_to_cog.
  • Dark landing without a UI flag: GET /v1/live-events returns 200 [] when live_mode_enabled is off; the UI Events section mounts only on a non-empty list — so the UI wave need not land last.
  • Smoke runs on a vendored fixture, never a live USGS fetch (the walk is a release gate; ShakeMaps are revised).
  • W2 ∥ W3: the wildfire wave depends on W0/W1 only.

13. Decided / Parked / Killed (session 6)

Decided (ratified 2026-08-27 by owner instruction — session 6b; the "consumes no build capacity until Phase 11 exits" clause is superseded by the promotion in §12) - Live Event Mode is an exploration track with a gate (§11), not a numbered phase; this memo is its charter. It consumes no build capacity until Phase 11 exits, except an owner-pulled R0 spike. - The recorded first rung (EFAS/GloFAS forecast alerting, 00-frame.md) is overridden by an observed-footprint first rung — earthquake ShakeMap and wildfire hotspots side by side at the ingest layer, earthquake first for loss. Reason in §1. - The first deliverable is framed as historical-event validation / replay (a real past Greek event through the real ingest API — ADR-077-compliant), not as "live"; replay and live share one screen. - No sentinel frequency, ever; event loss lives in its own table and is never labelled ead/aai; every event output is screening-grade with valid_at, age and version; no push/alerting; the life-safety line on every live surface. - Multi-exposure layers are not a new request shape: a layer is a portfolio exposure; compute-on-portfolio (Phase 11) is the mechanism; weights/criticality live on portfolio_exposures. - No fourth showcase moment now; two target steps appended to Moment 2; responder = a lens of the P3 decision-maker persona. - ELMFIRE (not Cell2Fire) as the first dynamics sidecar, as a HazardDynamicsInterface outside the engine registry, transport by object-store keys; only after R2. - Sequencing gates: SSRF allowlist → live_mode_enabled → D1 + D6 → R0 → R1 → R2 → R3 → R4.

Parked - Positioning and customers ("PAGER for Greek wildfire"; insurers / utilities / banks / municipalities vs civil-protection tendering) — reopens plan.md's parked row "Pilot customer timing and pricing"; decide in the post-showcase strategy session, not here. What survives as design constraints: downstream of detection; the deliverable is the exposure join + loss range + ranked list; decision support with a stated grade. - A standalone Moment 4 (responder/official) — promotable under parking-lot rule (a) if a real official asks; requires a one-line edit to Goal C. - Trigger thresholds / notifications on footprint entry (ADR-037 gives them a home; push webhooks stay parked). - Surface-level observed_at (the RFC's shape) — kept as the alternative to open point 1. - Uncertainty bands on event loss beyond the screening label — until a ShakeMap uncertainty grid or an ensemble exists. - Tier 1 sensors/drones (R4) — behind R3 and real device data. - A near-real-time flood depth footprint feed — the #1284 spike (2026-09-17) found that none exists from a primary provider for Greece: every free standing NRT feed (Copernicus GFM, NASA LANCE, GloFAS) gives extent, water class or discharge, never depth; the only open depth product (JRC Global River Flood Hazard v2.1, 90 m, depth in metres, unrestricted licence) is static return-period; CEMS Rapid Mapping's EXFLOS depth layer is activation-only and class-binned; ICEYE is the sole NRT depth source and is contract-gated. Evidence and per-candidate verdicts: the closing comment on #1284. A flood lane may still ship as JRC depth for the scenario grid plus GFM extent as an event trigger — but never as observed depth.

Killed - "The engine's occurrence-frequency path works without backbone changes" — refused at the gate; the lane must be built. - "Relax Result.ead NOT NULL" / "add event_loss_per_event" — breaks consumers; duplicates impact_per_event. - "Multi-exposure batch axis as a Phase 11 pretext" — double-counts compute-on-portfolio. - "Always create_surface + as_of on created_at" — dataset-keyed surfaces already give one row per snapshot. - "Cell2Fire first" — GPL-3 + research-only + unmaintained upstream. - "ERA5T as a nowcast source" — 5-day reanalysis latency. - "FIRMS URT (~5 min) for Greece" — US/Canada only. - "Register the dynamics engine in worker/models/registry.py" — /agreement would dispatch it for impact. - "Any react-leaflet-shaped UI plan" — the stack is MapLibre GL. - "A memory-budget-per-hazard mechanism constrains a second worker" — could not be found under that name; the constraint is the hetzner --concurrency=1 comment.

14. Sources

Repo evidence: paths cited inline (backbone src/climate_lama/…, UI climate-lama-ui/src/…, docs/DECISIONS.md ADR-024/036/037/040/042/060/ 065/067/068/069/075/076/077, docs/plan/exploration/00–06, phase-parking-lot.md, ~/.claude/collaboration-framework.md). External: FIRMS API + FAQ pages; EFFIS data-and-services + data-licence pages; USGS ShakeMap + RapidN event pages; ECMWF open-data page; Open-Meteo terms; ELMFIRE / Cell2Fire / WindNinja repositories; PAGER onesheet; RISE ERELA; GDACS; Google Flood Hub help; JBA; Fathom; Esri EMO docs; Juvare; product pages listed in §9. Issue state via gh on 2026-08-26 (#367 open/deferred, #412 open/parked, #625 open/parked, engine#10 closed, no live/FIRMS/ ShakeMap issue filed in either repo).