Skip to content

Phase 13 — Event Replay (Live Event Mode, rungs R0 + R1)

Opened 2026-08-27 by owner instruction, from the charter exploration/07-live-risk.md (session 6b amendments in its §12). Serves Goal A (a fourth computable form of hazard — an observed event — through the one unified pipeline) and Goal C (Moment 2 gains steps 8–9). Runs beside Phase 11, backbone-first per Phase 11's investment rule; the UI wave is data-driven, so it can land beside the pack waves rather than after them.

What it is. Run the platform's impact chain against a hazard that happened instead of one that is probable: a real past Greek earthquake (USGS ShakeMap) and a real past Greek fire (NASA FIRMS hotspots), each replayed through the real ingest API as an event snapshot and computed on a new deterministic-event lane that reports event loss, never an annual figure. The answer is assets-at-risk on the portfolio page, ranked, with a screening-grade event loss, the snapshot's observed-at and its source version.

What it is not. No live feeds, no cadence, no second worker, no spread model, no sensors — those are rungs R2–R4 in the charter's §4 ladder and get their own phase. Replay and live share one screen by design, so nothing built here is throwaway.

Design record (decisions made 2026-08-27, verified by two reassess passes)

Decision Choice Why (evidence)
Time dimension on the dataset: hazard_datasets.valid_at + hazard_datasets.live_event_id → live_events one dataset per snapshot ⇒ one surface per snapshot with no writer change (core/surface_writer.py:684-698 reuses by exact dataset_id); /v1/risk/lookup already pins dataset_id (api/v1/risk.py:99-105); the RFC's parked "time on the surface" shape is amended, not contradicted
Event loss storage new RLS'd table event_impact_results (event_loss NOT NULL, impact_per_event, loss_exp_path, provenance, grade note) Result.ead/.aai are NOT NULL and annual and consumed by smoke_e2e, the results API and portfolio_service:111-116; ADR-065 rejected a sentinel frequency; ADR-068/069 make frequency-less rows indicator-only — the lane must never write ead/aai
Compute entry POST /v1/live-events/{id}/compute with the landed exposure selector (exposure_dataset_id | portfolio_id, #1008) /compute/impact keeps refusing frequency-less datasets; the event lane needs validate_impact_refs's ref/tenancy/unit checks without assert_dataset_eai_eligible (core/compute_service.py:176 is unconditional today)
Grade a lane property: EVENT_LANE_GRADE_NOTE + event_result_grade() beside _SCREENING_GRADE_PERILS, never an edit to it HazardType.EARTHQUAKE is already in _SCREENING_GRADE_PERILS with a note bound to ESHM20 return-period grids (core/eai_eligibility.py:156-165); editing it would re-label the annual EAI path
Ingest IngestRequest gains a single-event mode (file_path + event: {label, valid_at}, no return_periods) writing ADR-068's unlabelled event, plus an optional intensity_unit restricted to units the seeded curves declare for that peril mode 1 requires return periods (api/v1/hazards.py:47-116); the slice axis is NetCDF-only; source_format accepts only geotiff/netcdf (worker/ingest/adapters.py:310-313) and INGESTOR_REGISTRY is peril-keyed — a second wildfire ingestor has no slot, so FIRMS is rasterised to GeoTIFF and declared K
Snapshot visibility hidden by default from GET /v1/datasets, lookup precedence and matrix resolution; GET /v1/hazards/{id}/layers and the tile route resolve normally; single-event mode still runs convert_to_cog a 1 km ShakeMap or FIRMS raster outscores ESHM20/JRC in core/dataset_precedence.py and would hijack every citizen pin; the UI overlay needs cog_path + legend (api/v1/hazards.py:885-893)
Dark landing GET /v1/live-events returns 200 {"data": []} when live_mode_enabled=False; write paths refuse with the existing 422 taxonomy; the UI Events section mounts only on a non-empty list the UI has no feature-flag layer and /v1/info carries only version/environment/sha — a data-driven conditional degrades correctly on any stack in any deploy order
UI home /portfolios/:id/events/:eventId, entered from an Events section on /portfolios/:id; no header entry Phase 11 guardrail 4's target is invisible surfaces; /portfolios/:id itself is a nav-less nested route reached by click (ui src/App.tsx:157); the 11-link header has no overflow mechanism
Smoke the event leg runs on a vendored, Greece-clipped ShakeMap GeoTIFF in tests/fixtures/reference_datasets/, staged by sdk-smoke.yml like river_flood.tif the walk never egresses (smoke_e2e.py:93-112), a release gate cannot depend on a third-party host, and ShakeMaps are revised — a live fetch is non-deterministic. USGS terms verified 2026-08-26: USGS-produced data is U.S. public domain, credit "U.S. Geological Survey"
Replay events earthquake 2020-10-30 Samos–İzmir M7.0; wildfire Evia, 3–10 Aug 2021 scripts/historical_catalog_manifest.json names no candidate for either (no earthquake block; wildfire status: "gap") — both get new manifest rows; perils.wildfire.status stays "gap" (an observed footprint is not #412's return-period footprint)
#625 superseded by W3 and closed when W3's issues are filed, its two unique checks carried into the rasteriser issue; #412 gets a scope comment (never an edit) #625's body is W3 almost verbatim; #412 is return-period sourcing and scopes the K mismatch out
Poller deferred to the R2 phase (recommendation recorded in the charter §12: in-worker beat task, adapters under ingest/sources/, service-function calls only) out of scope here

Guardrails — every Phase 13 issue

Inherited from phase-11-showcase.md (1–5), plus:

  1. No sentinel frequency, ever. An event dataset's rows carry rp, exceedance_freq and marginal_freq NULL; the lane passes unit weights to the engine and discards aai_agg, eai_exp and the frequency curve at the adapter. No ead/aai on any event row or payload.
  2. _SCREENING_GRADE_PERILS is byte-identical before and after the phase.
  3. Snapshots are invisible to the probabilistic readers (catalog list, lookup precedence, matrix resolution) unless addressed by live_event_id.
  4. No push, no subscription, no email; the word "alert" does not appear in any user-facing copy. Every live surface renders the line "Not for life-safety or emergency response — consult official civil-protection sources." and the snapshot's observed-at, age at render and source version.
  5. Real data through the real API only (ADR-077): replay content is a real past event fetched by a pack from the primary provider, with a docs/data/volume-budget.md row (licence verbatim) before download and a scripts/historical_catalog_manifest.json row; nothing copied from risk-assessment-tool (exploration/05-rat-convergence.md).
  6. Migration PRs are gated (migrations-gate.yml on migrations/** and models/**) — the one path where NO-PR-CI's "merge on mergeable" does not apply; typed Alembic ops only; new org tables use migration 0082's NULLIF(...) RLS predicate.

Scope — waves and items

Dependency line: W0 → W1 → {W2, W3, W4} → exit. W2/W3/W4 are mutually independent once W1 lands. Sizes are drafts; /phase-split grounds and re-sizes each item against the code.

W0 — Foundations (backbone)

Item Delta (grounding) Acceptance
W0.1 live_mode_enabled flag + SSRF allowlist entries config.py has the default-off pattern (score_bands_enabled:198, geocoder_enabled:555); allowlist at config.py:208-214 lacks earthquake.usgs.gov and firms.modaps.eosdis.nasa.gov (pack hygiene — packs download operator-side; the platform ingests local file_paths) flag default False; both hosts pass core/ssrf.py's validator; unit tests
W0.2 migration 0087: live_events + hazard_datasets.live_event_id/valid_at no live_event/valid_at anywhere in src/ (grep 2026-08-26); latest migration 0086 live_events(id, org_id, portfolio_id nullable FK, haz_type, name, status, started_at, ended_at, source, provenance) — portfolio_id is the book the event is watched against and what GET /v1/live-events?portfolio_id= filters on (added at split time, 2026-08-27) with FORCE RLS via the 0082 predicate; FK ON DELETE CASCADE; index (live_event_id, valid_at); alembic upgrade head from scratch under the gate; RLS coverage test lists the table
W0.3 single-event ingest mode IngestRequest.check_input_mode (api/v1/hazards.py:107-116) requires file_path+return_periods; core/ingest/base.py:1708-1720 always computes marginal freqs; derive_dataset_axis (aggregate_and_commit.py:1150-1172) yields SCENARIO_YEAR only for NetCDF slice layouts file_path + event: {label, valid_at} accepted (exactly one of the three modes); worker emits one ADR-068 unlabelled event with NULL rp/freqs; optional intensity_unit accepted only if in seeded_units_for(haz_type, ...), else 422; convert_to_cog still runs; dataset lands with live_event_id + valid_at when the request names an event
W0.4 snapshot visibility rule HazardRepository.list() filters status only (db/repositories/hazard_repository.py:273-275); lookup_service.select_dataset and compute_batch_service.resolve_matrix_cell_datasets consume dataset_precedence with no live notion default queries add live_event_id IS NULL; ?live_event_id= lists snapshots; /v1/hazards/{id}/layers returns a raster LayerSpec for a snapshot; a lookup pin next to a snapshot still answers from the probabilistic dataset (test)

W1 — Deterministic-event lane (backbone)

Item Delta (grounding) Acceptance
W1.1 event-lane grade + validation core/eai_eligibility.py:136-165 (EaiGrade, _SCREENING_GRADE_PERILS); validate_impact_refs calls assert_dataset_eai_eligible unconditionally (core/compute_service.py:176) EVENT_LANE_GRADE_NOTE + event_result_grade(); validate_event_impact_refs (or require_eai=False) keeps ref/tenancy/unit/exposure-type checks, accepts SCENARIO_YEAR; test: same earthquake dataset yields different grade notes on the two lanes; _SCREENING_GRADE_PERILS unchanged
W1.2 migration 0088 event_impact_results + compute_event_impact task worker/tasks.py:494-531 _event_frequency_vector refuses non-RP; engine_adapter.py:177-181 already fills impact_per_event from at_event table (org_id, job_id UNIQUE, live_event_id, hazard_dataset_id, exposure_dataset_id, impact_function_id, valid_at, event_loss NOT NULL, impact_per_event, loss_exp_path, grade note, provenance); task reuses compute_impact's load steps, passes np.ones(n_events), asserts axis is not RETURN_PERIOD, stores at_event and per-point losses, discards aai_agg/eai_exp/freq curve; routed to compute; excluded from prewarm_result_cache; ADR-078 in docs/DECISIONS.md extending ADR-065/068
W1.3 /v1/live-events API + error codes no such router; core/errors.py is the stable-code registry (sdk-regenerate.yml fires on it) POST /v1/live-events (create), GET /v1/live-events (200 empty when flag off), GET /{id}, GET /{id}/snapshots?as_of=, POST /{id}/compute (202 + job id; selector exposure_dataset_id|portfolio_id; refused with a stable code when flag off), GET /v1/live-events/{id}/results/{job_id} returning event_loss, impact_per_event, per-point losses joined to exposure rows, valid_at, grade note; codes E_LIVE_MODE_DISABLED, E_LIVE_EVENT_NOT_FOUND; SDK errors.py mirrored; docs/CLIMATE_LAMA.md API section

W2 — Earthquake replay (pack + smoke)

Item Delta (grounding) Acceptance
W2.1 ShakeMap replay pack + vendored fixture scripts/packs/ convention (dataset_identity.py, manifest rows); no earthquake block in scripts/historical_catalog_manifest.json; tests/scripts/test_historical_catalog_manifest.py asserts the schema scripts/packs/shakemap_event.py: fetch the archived USGS product for 2020-10-30 Samos–İzmir M7.0 (event id resolved at run time via the FDSN API — the exact query is [UNVERIFIED] and is this item's first step), parse grid.xml PGA, convert %g → g, write GeoTIFF + source_manifest.json; volume-budget row (USGS public domain, credit "U.S. Geological Survey") before download; manifest row; a Greece-clipped, small (<1 MB) tests/fixtures/reference_datasets/earthquake_event_pga.tif + README; an EarthquakeIngestor ingest in single-event mode succeeds locally
W2.2 smoke event leg scripts/smoke_e2e.py:108-138 singular constants, one ingest body at :828-836; sdk-smoke.yml:80-90,159-163 stages river_flood.tif HazardLeg dataclass with the river-flood leg unchanged + an event leg (return_periods=None, SDK_SMOKE_EVENT_TIF); creates a LiveEvent, ingests the fixture, computes on the lane against the walk's exposure, asserts event_loss > 0, non-empty per-point vector, valid_at, grade note == EVENT_LANE_GRADE_NOTE, and "ead"/"aai" absent; sdk-smoke.yml stages the fixture; the existing leg's assertions unchanged

W3 — Wildfire replay (pack) — parallel with W2

Item Delta (grounding) Acceptance
W3.1 FIRMS brightness-temperature pack (supersedes #625) design of record is core/ingest/wildfire.py:33-38's docstring ("ingest wildfire as a real intensity — FIRMS brightness temperature, threshold 295 K"); seeded Lüthi curves in K at 1/4/10 km (impact_function_seeder.py:586-621); no FIRMS code in src/ or scripts/ scripts/packs/firms_bt_event.py: FIRMS archive CSV (VIIRS 375 m, Evia bbox, 2021-08-03..10, MAP_KEY from env) → per-day 1 km EPSG:4326 GeoTIFF of max bright_ti4 (nodata elsewhere); volume-budget row (NASA "full and open", LANCE FIRMS acknowledgement string; no named licence — recorded as such) before download; manifest row with perils.wildfire.status left "gap"; ingest each day in single-event mode with intensity_unit="K", indicator=false under one LiveEvent (one dataset per timestep, ADR-068); the seeded 1 km Lüthi curve auto-resolves and the lane computes; ops check (manual, post-merge): ingested via the unified pipeline, catalogued under the event, tiles render, event loss runs

W4 — UI (climate-lama-ui) — parallel with W2/W3

A backbone coordinator issue carries the contract; children live in the UI repo per the cross-repo convention in plan.md.

Item Delta (grounding, verified 2026-08-26) Acceptance
W4.1 Events section + route + rail PortfolioDetailPage.tsx has Stat and a module-private WorstMembersTable (:107-165) fed by the screening rollup; no live types in src/types/index.ts; hand-written src/api/client.ts Events section on /portfolios/:id mounts only when GET /v1/live-events?portfolio_id= is non-empty (byte-identical page on []); route /portfolios/:id/events/:eventId; rail = event header (name, hazard chip, Replay/Near-real-time chip, source + version, observed-at, age at render), EventLossHeadline (currency from the backbone unit via formatNumber, EaiGradeBadge, 3-tier RAG_COLORS only), inline-accordion "why this number" with ProvenanceLine, RankedAssetsTable lifted from WorstMembersTable (PortfolioDetailPage keeps using it — pure refactor guarded by its test), life-safety line; live i18n namespace; no "alert" string (test)
W4.2 LiveEventMap + layer panel generalisation + SnapshotScrubber LayerKey closed union (types/index.ts:684); LayerPanel literal 3 rows (:84-88); LookupMap/ResultMap remove+re-add the raster source per change; YearRangeSlider is a dual-thumb scenario control MapLibre map with the snapshot COG via hazardRasterTileUrl + /layers legend, exposure points coloured by per-point event loss (existing riskColor ramp), layer rows from a LayerRowDef[] prop; scrubber over the event's snapshots (play/pause, step, ticks, observed/now marker, aria-valuetext), switching layers by visibility/opacity with a ±2 warm window — never remove/add sources; layout = aside at inline-start + Splitter + map + scrubber row (RiskLookupPage.tsx:452-538 anatomy, logical properties, h-[45vh] map below sm, resize test)
W4.3 useIntervalFetch usePoller is one-shot; useNotifications has the right loop hard-wired generic hook extracted, useNotifications refactored onto it (NotificationBell test is the guard); the event view refreshes on it only while an event is open

Issues (split 2026-08-27 — /phase-split, sized by /analyze)

Milestone "Phase 13 -- Event Replay" in both repos, label milestone:phase-13.

Wave Backbone UI (climate-lama-ui)
W0 #1018 flag + allowlist · #1019 migration 0087 · #1020 single-event ingest mode · #1021 intensity_unit override · #1022 snapshot visibility —
W1 #1023 event-lane grade + validator (ADR-078) · #1024 migration 0088 event_impact_results · #1025 compute_event_impact · #1026 /v1/live-events CRUD + codes · #1027 compute + results read —
W2 #1028 ShakeMap pack + fixture · #1029 smoke event leg —
W3 #1030 FIRMS pack (supersedes #625, closed) —
W4 #1031 coordinator (not a build target) ui#240 foundation · ui#241 useIntervalFetch · ui#242 LiveEventMap + LayerPanel · ui#243 SnapshotScrubber · ui#244 RankedAssetsTable + EventLossHeadline · ui#245 Events section + page

Dependency line as filed: #1018/#1019/#1023 roots → #1020 → {#1021, #1022, #1024} → #1025 → #1026 → #1027 → {#1029 (also #1028), #1030}; #1028 hangs off #1020. UI: ui#240 → {ui#242, ui#243, ui#244}; ui#241 independent; ui#245 last. Land with /phase-build phase-13 in each repo (backbone first; ui#240 captures its fixtures from #1027).

Build status (2026-09-03)

Wave Issues → PRs Landed
W0 #1018→#1123, #1019→#1124, #1020→#1125, #1021→#1126, #1022→#1127 2026-08-31
W1 #1023→#1130, #1024→#1129, #1025→#1131, #1026→#1133, #1027→#1134 2026-08-31/09-01
W2 #1028→#1143 (ShakeMap replay pack), #1029→#1144 (event leg in the smoke walk) 2026-09-02
W3 #1030→#1145 (FIRMS brightness-temperature pack), →#1153 (key-leak fix: _silence_transport_url_logging, real extract replaces the synthetic fixture) 2026-09-02
W4 (backbone) #1031 coordinator — closed on the children's evidence, not a /build target 2026-09-02
W4 (UI) ui#240→#288, ui#241→#287+#289, ui#242→#290, ui#243→#291, ui#244→#292, ui#245→#293, ui#294 trunk-gate fix→#295, docs+version→#297 2026-08-31/09-01/02

All fourteen backbone issues (#1018–#1031) and all six UI issues are closed as of 2026-09-03. #1028 closed 2026-09-02 (21:52 UTC) on sdk-smoke.yml run 33597653760 satisfying its stack-proof, and coordinator #1031 closed the same day once both children (its UI child and the smoke evidence) were in. #1030 closed 2026-09-03 once the FIRMS pack ran for real against the live archive and the Evia replay landed on prod (see "Prod replays" below); the ops tracker #1146 closed with it. Released as backbone v0.21.0 (fe05e17) + UI v0.12.0 (c35b1dc), deployed to prod 2026-09-02 06:41 UTC beside climate-lama-climada-sidecar:local (healthy). Live mode has been on prod since 2026-09-02 (.env.prod hand edit adding LIVE_MODE_ENABLED=true, reaching the running api/worker containers) — the Events section stays dark until a live event exists for a portfolio, per the dark-landing design record.

Mid-review: mid-review-phase-13.md — verdict Ready for next phase. Phase 13 CLOSED 2026-09-03 on the owner's walk of Moment 2 steps 8–9 on both replay books (exit criterion 2, the last open one); all three exit criteria met and the showcase markers are live.

Prod replays (2026-09-03)

Both replays ran on prod (v0.21.0, live mode on) as an API-key session for the org admin, every write through the public API, against real LitPop Greece books — no invented assets.

Precondition fixed first: prod's region vocabulary held only GRC | Attica | 1, CYP | Cyprus | 0 and CYP | Larnaca | 1, so an ingest declaring region: "Greece" was refused (E_VALIDATION {"region": "Greece"}) although every live Greek dataset already carried that region. The tracked GADM 4.1 admin-0 fixture (gadm41_GRC_0.json.gz, sha256 b6febf95…, 145,741 B) was loaded with scripts/load_admin_boundaries.py --geojson inside the api container → GRC | Greece | 0 inserted 07:15:36 UTC. Command recorded in RUNBOOK.md, "Country data run on prod".

Samos–İzmir M7.0 (W2) North Evia wildfire (W3)
Event date 2020-10-30 3–10 Aug 2021
Book 9de7a920-3aed-4aa4-b06e-08b46c989068 — 100 LitPop Greece rows in 25.8–27.2 E, 37.3–38.6 N dbe2a065-1f7b-47e5-8ee1-1f9e789a8087 — 216 LitPop Greece rows in 22.8–24.1 E, 38.5–39.1 N
Live event 2e3fd696-b326-442c-b72c-c21363730e5e, earthquake, open, source "USGS ShakeMap, atlas product us7000c7y0 v1" da01cb7a-fa36-4510-8fcb-5357134f2508, wildfire, open, source "NASA FIRMS VIIRS S-NPP 375 m, VIIRS_SNPP_SP (Collection 2)"
Snapshots one — 47564d9e-e13d-404b-b6b2-bd4ca58c13e6, valid_at 2020-10-30T11:51:27Z, unit g (ingest job 3bcbd98b…, 6 s) six, one per acquisition day, all unit K, indicator=false, ~6 s each: 35d9724f… 08-05T00:31Z · 795a32a1… 08-06T00:12Z · ce94fa69… 08-07T01:33Z · 77ab86d9… 08-08T01:15Z · 84de6c15… 08-09T00:56Z · 8db5cecb… 08-10T00:37Z
Curve — impact_function_id omitted on every compute; the seeded Luthi 2021 WF buildings 1km default (b671d3e2…, unit K) auto-resolved
Compute job 76e9ec1d-8d89-45d7-9a08-4473ff6bb2fb, completed in 111 s six jobs, all completed
Event loss (USD) 585,548,574, grade screening; top members 134,898,561 / 78,285,453 / 74,662,151 08-05 926,127 · 08-06 21,451,558 · 08-07 15,095,527 · 08-08 13,491,984 · 08-09 5,470,912 · 08-10 15,714,748 — grade screening; the 08-06 peak matches the fire's worst day
ead/aai absent from the payload absent from every payload
Layers raster, /v1/tiles/cog/47564d9e…/{z}/{x}/{y}.png, legend unit g, domain 0.0024–0.129 g, band max 0.526 g (titiler statistics) raster, tile route, legend unit K

The FIRMS pack itself ran against the live archive: 2 requests (2021-08-03 day_range 5, 2021-08-08 day_range 3), both HTTP 200, 314,404 B raw, 4,092 rows; brightness column pinned as bright_ti4 (Collection 2, version = 2); six per-day GeoTIFFs, 113 × 67 EPSG:4326 float32, 11,606 B total. The vendored fixture is now a real extract (rows[::16] of 2021-08-06), fixture_is_synthetic = false. Volume-budget row MEASURED (raw 0.000314 GB, processed 0.0000116 GB). The pack's first live run printed the MAP_KEY through httpx's INFO request log (it is a URL path segment) — fixed in PR

1153 with a prove-it-fails test; the key reached only a build agent's local

transcript, never a commit, an issue or CI.

Guardrail results (exit criterion 3, verified on trunk 7cc61db)

Guardrail Result
6 — no ead/aai on any event row or payload HOLDS — absent from all seven prod result payloads
7 — _SCREENING_GRADE_PERILS byte-identical HOLDS — unchanged vs v0.20.0; the diff adds only EVENT_LANE_GRADE_NOTE beside it (core/eai_eligibility.py:156 and :192), never an edit to the mapping
8 — snapshots invisible to the probabilistic readers HOLDS on prod — GET /v1/hazards?haz_type=earthquake lists only the two ESHM20 datasets, ?haz_type=wildfire only the two FWI datasets, with seven event snapshots resident
9 — no "alert" in user-facing copy; life-safety line everywhere HOLDS — the UI's live namespace and live components contain no non-ARIA "alert"; the life-safety line is pinned by src/test/LiveEventPage.test.tsx
10 — real data through the real API only HOLDS — both replays fetched from the primary provider, volume-budget and manifest rows present, fixture a real extract
— perils.wildfire.status still "gap" HOLDS — an observed footprint is not #412's return-period footprint

Exit criteria:

# State Evidence
1 — Smoke MET sdk-smoke.yml run 33597653760 green on trunk fe05e17 (2026-09-02 06:09 UTC), the release gate for v0.21.0: OK [EVT_GRC] event_loss=49450.52513357252 USD across 3 asset(s), valid_at=2020-10-30T11:51:27Z, grade=screening and the line E2E_READY_EVT_GRC live_event_id=a19306b9-0767-4a19-8655-4672870409d7 snapshot_id=d139b6e2-173a-4685-9283-0fe9f9f55c33 exposure_id=72d459e3-ef98-444c-bc6c-4face2f40a77 job_id=cd5acd99-6ced-4ec3-8a58-2580b74c0bda event_loss=49450.5251 assets=3 valid_at=2020-10-30T11:51:27Z elapsed=10.5s
2 — Walk MET Both replay books exist on prod with computed losses and rendered tile routes (see "Prod replays" above); the ops tracker #1146 closed 2026-09-03 on that evidence, and the owner walked Moment 2 steps 8–9 in the browser the same day — /portfolios/9de7a920… opened the Samos card to its header, life-safety line, the 585,548,574 USD screening-grade loss, the ranked table and the ShakeMap raster; /portfolios/dbe2a065… played the six-tick Evia scrubber with observed-at and loss changing per day and peaking on 6 August; a book without an event showed no Events heading; a page search for "alert" found nothing. The showcase markers are live
3 — Guardrails MET Every guardrail re-verified 2026-09-03 on trunk 7cc61db and against prod — see the guardrail table above. _SCREENING_GRADE_PERILS byte-identical vs v0.20.0, no non-ARIA "alert" in the UI's live namespace or components, the life-safety line pinned by src/test/LiveEventPage.test.tsx, snapshots hidden from the prod catalog, perils.wildfire.status still "gap"

Conflict hotspots (files touched by >1 item)

src/climate_lama/api/v1/hazards.py (W0.3, W0.4), src/climate_lama/models/hazard.py (W0.2, W0.3), src/climate_lama/config.py (W0.1), src/climate_lama/core/errors.py + sdk/python/climate_lama_client/errors.py (W1.3), src/climate_lama/worker/tasks.py (W1.2), scripts/historical_catalog_manifest.json + its test (W2.1, W3.1), docs/data/volume-budget.md (W2.1, W3.1), scripts/smoke_e2e.py + .github/workflows/sdk-smoke.yml (W2.2); UI: src/types/index.ts, src/api/client.ts, src/App.tsx, src/components/LayerPanel.tsx, src/i18n/index.ts (W4.1–W4.3).

Exit criteria

  1. Smoke: sdk-smoke.yml green on trunk running scripts/smoke_e2e.py with the event leg — the vendored ShakeMap fixture ingested in single-event mode, computed on the lane, event_loss > 0, per-point vector non-empty, valid_at present, grade note == EVENT_LANE_GRADE_NOTE, no ead/aai. Deliberately narrower than the charter's §11 wording (reproducing a ranked list from a stored snapshot series): a series is rung R2; the single- snapshot assertion is the correctly scoped form for R0+R1.
  2. Walk: the owner walks showcase-script Moment 2 steps 8–9 on a compose stack (UI_TAG=sha-<short>) for both replays — Samos 2020 and Evia 2021 — and flips the markers.
  3. Guardrails hold: _SCREENING_GRADE_PERILS unchanged; grep -rn "alert" over the UI's live namespace and live components returns 0; every live surface renders the life-safety line (UI test); perils.wildfire.status still "gap".

Out of scope (deliberate)

Feeds and cadence (FIRMS/EFFIS/USGS polling, live queue, second worker, Redis lock, snapshot retention/pin rule), ELMFIRE/WindNinja/weather, sensors and drones, uncertainty ranges, notifications/triggers, a fourth showcase moment, positioning/customers — all in the charter's ladder or Parked ledger.

Design canvas

Owner's artifact "Live Event Mode" (5 artboards: earthquake replay = leading candidate, wildfire near-real-time, two direction sketches, phone layout; all figures sample values): https://claude.ai/code/artifact/a5e73a24-ca16-49f6-bf83-770ec7504573