Phase 9 — the Full-Catalog walkthrough (owner-run, phase exit)¶
What this is. Phase 9's verifiable goal, from
phase-9-data-foundation.md§ Goal. The phase does not close on a merge count — it closes when these six steps pass in one sitting, manually, through the real UI and API, against a stack pointed at the Hetzner object store.Companion:
phase-9-wave-0-walkthrough.md— the six-moment Showcase walkthrough. Run wave-0 first; it is the cheaper gate and it shares most of its preconditions with this one.
Sitting 2026-08-25 — all six steps passed¶
Phase 9 is closed on this sitting. The owner ran the walkthrough end to end against
production — backbone v0.17.0 (a5a8404), UI v0.7.0, Alembic 0086 — and reports
all six steps passed. Everything below this section is the preparation record that led
here; it is kept as the dated account of how the sitting was set up, not as pending work.
This retires the "closed by supersession" framing recorded in plan.md's status log on 2026-08-24. The phase exits on its stated criterion met, which is what the If a step fails rule at the bottom of this file was written to protect: Phase 8 exited once on a waived walkthrough criterion and carried four defects forward, and Phase 9 did not repeat it.
Step 4b — the coastal half, walked for the first time. The reinstated two-hazard form of
step 4 (see Step 4's two-hazard form was reinstated on 2026-08-19) completed its grid in
production. Six scenario cells ran at 12:12 UTC, seconds apart; the baseline column is
served by the year-invariant baseline dataset, whose result dates from the 2026-08-19 seed
run. Read against exposure LitPop Greece in northlane:
| Scenario | Horizon | AAI | Job id |
|---|---|---|---|
ssp2-4.5 |
2030 | €29.06 M | 4d4f6570-91b5-45e1-84f1-791ca2293a9e |
ssp2-4.5 |
2050 | €34.04 M | 26233eb8-5fe9-4067-a469-67f8d107c753 |
ssp2-4.5 |
2080 | €44.99 M | 4de7814c-c61b-433e-9bea-cc2178950b7f |
ssp5-8.5 |
2030 | €29.88 M | 70cd5a66-94b0-4d44-b41c-8639109fb9d5 |
ssp5-8.5 |
2050 | €35.58 M | f6ed164b-5845-4fdc-bd0f-82656a3a18f7 |
ssp5-8.5 |
2080 | €50.37 M | 29c4aadc-fb58-4757-a781-9e470e861a8e |
baseline |
1980 | €22.9 M | seeded 2026-08-19 |
The values reproduce the 2026-08-19 seed run cell for cell, which is the expected result for the same (dataset, exposure, curve) triples and is itself a reproducibility check. Eleven river-flood results were written the same day, covering step 4a's grid and step 3's compute.
Per-org curve seeding verified. northlane holds 15 org-scoped impact-function rows —
river flood ×3, coastal flood ×3, earthquake ×4, wildfire ×3, storm_europe ×1, tropical
cyclone ×1 — so the coastal curves shipped by #833/ADR-076
are present in the org that ran the grid.
Together those two facts discharge #970 in full, which was the only Phase 9 item carried into Phase 10.
One criterion closed as dispositioned, not executed — the live
polling→review→approve→ingest cycle. It is recorded honestly in
phase-9-data-foundation.md § Exit criteria: the prod queue
still holds the single jrc_flood row stuck at approved from 2026-08-12, and no
end-to-end auto-ingest has ever committed a dataset. What the sitting does prove about
pipeline unity is step 6's audit.
The three post-pass actions from After it passes are done: the phase-exit box in phase-9-build-plan.md is ticked, and plan.md's Phase status row and status log record the close. The third — the standing rule that nothing publishes publicly before the licensing verdict — is unchanged; licensing is parked with the End-State Vision.
Read this before you book the sitting — both blockers are now CLEARED¶
Status 2026-08-10: book the sitting. The heading here used to read "one hard blocker remains", and the paragraph under it said "This walkthrough is not runnable end-to-end today, and the remaining blocker is not a code defect on trunk. It is work that has not been done yet." Both are kept above as the record and both are now false — the ops work they referred to has been done. Blocker 1 (UI) cleared 2026-08-07; blocker 2 (catalog contents) cleared 2026-08-10, when the last three packs landed in production and the ≥6-hazard-type condition was measured passing on both the DB and the API. Details in each blocker's own section below.
~~Blocker 1 — the UI half of steps 2 and 5 does not exist~~ — CLEARED 2026-08-07¶
| Step | Needs | State |
|---|---|---|
| 2 — pick between ≥2 datasets for the same peril | ui#100 dataset picker per hazard | CLOSED, deployed in ui v0.5.0 |
| 5 — every lookup answer names its dataset | ui#101 dataset provenance chip | CLOSED, deployed in ui v0.5.0 |
Both shipped in climate-lama-ui v0.5.0 and are live on prod as of 2026-08-07 12:23 UTC
(UI_TAG=v0.5.0; deploy run 31177920318). Verified beyond the deploy's own health gate —
which only asserts HTTP 200 and would pass on a stale container — by grepping the served
bundle for the new strings (Auto (recommended), Indicator (not lossable),
Screening-grade EAI) and the dataset_id param. See
phase-9-build-plan.md § UI shipped separately as v0.5.0 for
the full evidence table.
Steps 2 and 5 can therefore be walked through the real UI, as the goal requires — the old "API-only PASS / UI PENDING" fallback no longer applies. ~~Note this clears the surface, not the data: with the catalog still single-source (blocker 2), the picker has nothing to pick between, so step 2 only becomes meaningful once blocker 2 is done.~~ Superseded 2026-08-10 — the catalog is no longer single-source, so the picker now has something to pick between; see blocker 2.
~~Blocker 2 — the catalog is empty of real packs~~ — CLEARED 2026-08-10¶
All ten volume-ledger rows now read ingested, and prod carries 6 distinct hazard
types. This supersedes the text that stood here, kept verbatim: "The ops half —
licence acceptances, downloads, prod ingest — is owner work and has partly run (resynced
2026-08-10): licences accepted 2026-08-07, and 7 of the 10 volume-ledger rows now read
ingested. Step 1 needs ≥6 hazard types actually in the catalog, and the 3 rows still
outstanding (#614/#615/#616, the NetCDF packs) are exactly the ones that would supply the
missing types — so this walkthrough remains gated on the ops track finishing."
Those three rows landed later the same day. Two of them are partial by the ledger's own
vocabulary, and neither partial is visible in this walkthrough: #614's C3S projections
half awaits rotated-pole reprojection (its EWDS half is ingested and is what supplies
wildfire), and #615's WISC synthetic half was not retrieved at all because that CDS
product's process schema publishes no spatial input to scope it with (its C3S half supplies
storm_europe). The ≥6 distinct haz_type condition is measured passing — see the
OPS-5e table in the next section.
The goal's second half — ≥2 distinct source values for at least two hazard
types — is also measured passing (prod, 2026-08-10). It was originally recorded here
as a derivation from the ledger, because the OPS-5e run counted haz_type only; it has
since been measured directly:
haz_type |
distinct source |
the sources |
|---|---|---|
river_flood |
2 | JRC River Flood Hazard Maps v3.1.1 · WRI Aqueduct Floods v2 |
heatwave |
2 | ECDE / ERA5 heat wave days · EURO-CORDEX heat wave days (health) |
coastal_flood |
1 | WRI Aqueduct Floods v2 — coastal |
earthquake |
1 | ESHM20 |
storm_europe |
1 | C3S windstorm tracks + footprints |
wildfire |
1 | CEMS GEFF/ERA5 FWI historical (cems-fire-historical-v1) |
Exactly two hazard types carry two sources each, which satisfies "at least two." Note
how little headroom that is: if either river_flood's JRC row or one of heatwave's two
retrievals were to drop out of active, this half of the goal would fail while the
≥6-type half kept passing. Worth re-running the query on the day you walk this rather than
trusting this table.
There is no "raw-zone upload" step, and there never was. Per
ADR-070 the raw zone is transient by design: packs stream to local disk,
clip, ingest, delete. Phase 9's exit criterion 3 is amended accordingly, so step 6 audits
the per-dataset source manifest (hazards/{dataset_id}/source_manifest.json, ADR-066;
staging/{ingest_job_id}/manifest.json for pre-ADR-066 datasets) — not a raw/ copy.
Do not fail a dataset for having no raw-zone object; none of the ten packs has one.
Back-filling the pre-ADR-066 datasets. Measured 2026-08-10: only 5 of 21 active
datasets have the dataset-scoped key; the other 16 still have their provenance at the
legacy staging/{ingest_job_id}/manifest.json. scripts/backfill_source_manifests.py
moves them onto the dataset-scoped key (verbatim — the captured checksums survive), and
reconstructs a manifest from the pack definition plus the catalog row for any dataset whose
staging record is gone. A reconstructed manifest is stamped capture_mode: "reconstructed"
and leaves checksum_sha256/volume_bytes null — when auditing step 6, read that flag:
it distinguishes a record captured at ingest from one assembled after the fact.
Check where you stand before booking time:
curl -fsS -H "Cookie: <session>" \
"https://api.climate-lama.online/v1/hazards?limit=200" \
| jq -r '.data[] | "\(.haz_type)\t\(.source)\t\(.name)"' | sort | uniq -c
You need ≥6 distinct haz_type values, and ≥2 distinct source values for at
least two of them. ~~As of 2026-08-07 prod had 8 datasets, all river_flood, all one
org — so this returns one row. That is the gap.~~ SUPERSEDED — the ≥6-type half of that
gap is CLOSED, measured on prod 2026-08-10 (OPS-5e), and both sides agree exactly:
haz_type |
count |
|---|---|
coastal_flood |
7 |
earthquake |
1 |
heatwave |
3 |
river_flood |
8 |
storm_europe |
1 |
wildfire |
1 |
6 distinct types, 21 active datasets. That is the DB read (via a psql
superuser connection — note the trap: the app connection returns zero rows here
under RLS, so a bare app-side query looks like an empty catalog when it is nothing of the
kind). The API read agrees exactly: GET /v1/hazards with status=active returns 21 rows
across the same 6 types with an identical breakdown, unfiltered returns 31 rows across the
same 6 types, and a per-type ?haz_type= round-trip returns 200 with matching counts for
all six. #735 was closed on
this evidence. Re-run the curl above to confirm against the state of the day you walk
this — but expect it to pass, not to show the old one-row gap.
~~Also note #706's consequence: the CDS/EWDS packs still need their licence acceptances.~~
Both halves of that are superseded (2026-08-10). Licence acceptances were completed
by the owner 2026-08-07 — do not re-ask; a real authenticated EWDS retrieval succeeded on
2026-08-09 (jobID fd5475dd-c034-4ff5-a696-83e8314a3952), which an unaccepted licence would
have refused. And #706 is only the first of six NetCDF blockers, all now closed
(#706/#761-code/#782/#791/#793/#797). ~~What actually holds #614/#615/#616 today is the
assert_netcdf_adapter_wired evidence gate — no NetCDF ingest has ever run end to end —
tracked on the still-open #761.~~ SUPERSEDED later on 2026-08-10, and this correction
matters for anyone walking this doc: NOTHING holds #614/#615/#616 out of prod any more.
The first end-to-end NetCDF ingest ran that day, PR
#825 (42f2a35) deleted the
evidence-gate layer of the guard along with its --first-end-to-end-run escape, and #761
is CLOSED. All three packs then landed in production: heatwave (3 datasets), windstorm
(1), wildfire (1). Do not tell the owner a NetCDF pack is blocked — none is. The only
residue is inside #614's C3S half (rotated-pole reprojection, not yet built) and #615's WISC
half (not retrieved, because its CDS process schema publishes no spatial input), and neither
is visible in the walkthrough. Read NETCDF_INGEST_BLOCKERS in
scripts/packs/_wildfire_fwi_common.py on trunk for live status; never quote a blocker
number from this doc.
Execution runbook and the per-dataset evidence: phase-9-ops-plan.md.
Preconditions¶
Re-assert each one; do not inherit them from this table. The lesson behind this is #638 — a config change was once applied to an image that could not support it, and every health check still looked green.
| Thing | How to check | Expected |
|---|---|---|
| Deployed backbone contains the code you mean to test | curl -fsS https://api.climate-lama.online/v1/info \| jq -r .data.version,.data.git_sha |
0.16.0 / a70cc3e… or later (measured 2026-08-19) |
| That SHA really contains a given fix | git merge-base --is-ancestor <fix-sha> <deployed-sha> |
exit 0 |
| The deploy actually swapped the containers | scripts/verify-deploy.sh v0.16.0 |
identity check passes. A liveness probe cannot prove this — a healthy old container passes /health/live exactly as well as the new one |
| UI tag | docker ps on the host |
ghcr.io/cortomaltese3/climate-lama-ui:v0.6.2 or later (measured 2026-08-19; it served bundle /assets/index-DuDXBjbE.js) |
| Alembic at head | ./dc exec -T api alembic current |
matches alembic heads on that tag — 0086 on v0.16.0 (measured 2026-08-19) |
| Object store reachable | a tile request returns 200, not 502 | see step 5 — measured 200 for one COG of all six haz_types, 2026-08-12; not re-measured on 2026-08-19 |
No RISK_LOOKUP_COG_TIMEOUT_SECONDS override is present |
docker exec climate-lama-api-1 printenv RISK_LOOKUP_COG_TIMEOUT_SECONDS |
empty — #901 removed the override and it was still unset on 2026-08-19; see the superseded finding below. The code default budget applies and step 5's heatwave lookups pass on it |
| Disk | df -h / on the host |
measure it — it has moved fast before |
Dry-run findings, 2026-08-10 — read before you sit down¶
Every step below was rehearsed against production (then v0.12.0 = 27e99da) via the
API before the sitting was booked, so you are not the one discovering these. Four
defects were found and filed; three are fixed and now live — they shipped in
v0.13.0 (596a6e5, Alembic 0082), deployed 2026-08-10 — and one is a data gap
that still needs a decision (step 4).
The "fixed on trunk" wording in the table below therefore means fixed and deployed; re-verify against the running system rather than trusting either phrasing.
| Step | Rehearsal result | Notes |
|---|---|---|
| 1 | was FAIL → fixed and live; criterion clarified | All 21 active datasets had blank upstream_version and native_resolution; wildfire also had no provider. Fixed by #841, which stamps the fields at ingest and ships scripts/backfill_dataset_identity.py for existing rows; #827/PR #852 then restored the two EURO-CORDEX heatwave scenario labels. Re-verified 2026-08-11: three honest blanks remain on three rows, and nothing else — the wording of step 1 was tightened to name them, because ADR-067 deliberately leaves scenario NULL for a scenario-less source. See Honest blanks. |
| 2 | PASS | Verified end to end, not merely counted — see the worked example below. |
| 3 | PASS | The honest-framing rule holds: wildfire refuses with 422 E_INDICATOR_DATASET_NO_EAI, storm_europe with 422 E_HAZARD_DATASET_NO_EVENT_FREQUENCIES (ADR-069 — refuse rather than integrate a NaN). No fabricated EAI anywhere. One wart: heatwave refuses with a bare 422 E_VALIDATION and an empty detail, so it declines without saying why — acceptable but weaker than the step demands. |
| 4 | was BLOCKED → amended → river-flood half VERIFIED 9/9 on v0.14.0; two-hazard form REINSTATED 2026-08-19, coastal half NOT YET WALKED |
The old "≥2 scenario-conditioned computable hazards" could not be met while coastal_flood held no curve, so the step ran river flood's full 3 × 3 grid alone — see Step 4 was amended. #833 then seeded a coastal curve (option A, ADR-076), which is the recorded reinstatement condition: step 4 is two hazards again — see Step 4's two-hazard form was reinstated. The river-flood half stays verified (9/9 on v0.14.0); the coastal grid has still never been submitted and is not claimed — but its two preconditions are now met (ADR-076 deployed in v0.16.0, and northlane seeded — 15 impact-function rows, 3 coastal), and seven standalone coastal impact jobs completed on prod 2026-08-19. See Re-check, 2026-08-19 and 4b. Do not re-propose earthquake or wildfire — both are ruled out by physics, not by backlog. River-flood rehearsal detail — see Step 4 rehearsal below: it scored 6/9 before #861 and 9/9 after. |
Why no second hazard qualified (prod, re-verified 2026-08-11) — and why coastal flood
now does, as of 2026-08-19. Every other row still holds; only the coastal_flood row
changed, and it changed because the curve claim was wrong, not because new data arrived:
| haz_type | active | scenario axis | curves | verdict |
|---|---|---|---|---|
river_flood |
8 | baseline, ssp2-4.5, ssp5-8.5 × 2030/50/80 | 3 (Huizinga, m) |
✅ the one that works |
coastal_flood |
7 | identical structure | 3 (JRC/Huizinga, m, screening-grade) |
✅ the second one that works, as of #833. The 2026-08-11 reading of this row — "Huizinga reuse is explicitly forbidden, fluvial calibration ≠ surge/wave" — was refuted against the source: JRC105688 §1.2 states the functions are "not associated with specific type of floods" and its headline validation case is a storm surge (§4.3.3.1, Sandy). See ADR-076. Curves are per-org rows: an org must run POST /v1/impact-functions/seed before its coastal grid resolves |
earthquake |
1 | baseline only |
4 (ESRM20, g) |
curve ✓, axis ✗ — ESHM20 is time-invariant; seismicity is not climate-conditioned, so no scenario axis exists to ingest. A matrix over it is one cell |
tropical_cyclone |
0 | — | 2 (Emanuel/Eberenz, m/s) |
curve ✓, data ✗ — synthetic track sets do not cover the Mediterranean basin. Verified at the raw-data level 2026-08-12 (#624 comment): the CLIMADA Data API does return country_iso3alpha=GRC-tagged tropical_cyclone records (17 active — IBTrACS random-walk 150as historical + RCP26/45/60/85, and STORM-based 300as baseline + 4 CMIP6-HighResMIP RCP8.5 variants), but 3 sampled HDF5 files spanning both methodologies (..._GRC_1980_2020, ..._rcp85_GRC_2060, TC_GRC_0300as_STORM) all have an empty intensity/data array — thousands of events and centroids over Greece, zero non-zero wind intensity anywhere. The API's country tag is a metadata-layer false positive; no track's footprint reaches Greek territory in any of these products. Ingesting them would add an all-zero layer, not a computable hazard |
wildfire |
1 | NULL | 6 (Lüthi, K) |
unit mismatch: dataset is FWI, curve is Kelvin. #481 searched and closed it — FWI is a fire-danger index, not a hazard intensity, so no FWI damage curve can exist in principle (corroborated independently by OS-Climate physrisk). The computable form (FIRMS brightness temperature — sourced by Phase 13 W3, not #412, which stays return-period scoped) is satellite-observed and therefore historical-only — it would land at baseline, exactly like earthquake |
storm_europe |
1 | NULL | 2 (Klawa Ulbrich, m/s) |
units match, but historical footprints only and no event frequencies (ADR-069 refuses) |
heatwave |
3 | NULL in column, ssp* in the name (#827) |
0 — indicator by design | both barriers |
| 5 | was FAIL → fixed on trunk | Every tile 502'd, for all 21 datasets — the proxy called a titiler route that no longer exists, and never sent a band selector (#831, fixed). Separately, the point lookup read all 100 bands of the heatwave COG and blew its 2 s budget, so heatwave reported cog_read_failed at every point (#838). Provenance itself is excellent — every answer names its dataset, source, licence, citation and selection rule. |
||
| 6 | PASS, with a caveat | No side doors: every dataset traces to a pack. Only 5 of 21 carried a source_manifest.json, but per ADR-066 the other 16 kept their original provenance at staging/{ingest_job_id}/manifest.json — so scripts/backfill_source_manifests.py relocates the real captured records (real checksums) rather than reconstructing them. |
Two more defects found while rehearsing, both fixed on trunk, neither visible as a step until you hit it:
- No impact calculation could complete at all (#834). The first compute for any (hazard, exposure) pair died with
invalid input syntax for type uuid: "". The RLS org GUC is transaction-local and reverts to the empty string after the preflight's mid-task commit, and the policy cast''::uuidraises. Note the trap: the second attempt always succeeds, because the failed first run leaves the centroids assigned — so it reads as a flake rather than a defect. - Production had no platform admin at all (#828), so every
/v1/admin/*route 403'd for everyone and the weekly poll's review queue could not be drained by anybody.
A point where step 2 actually demonstrates something¶
Most Greek coordinates return nulls from both river-flood datasets, which makes for a flat demo. Verified working side-by-side:
Thessaloniki / Axios delta — lat=40.60, lon=22.80
curl -fsS -H "Cookie: <session>" \
"https://api.climate-lama.online/v1/risk/lookup?lat=40.60&lon=22.80&dataset_id=<id>"
dataset_id |
Dataset | Non-null return periods |
|---|---|---|
67854ef5-… |
JRC river flood Greece (national, historical baseline) | 9 of 9 (1.63 m → 2.19 m) |
a085c0b9-… |
Aqueduct river flood Greece — historical baseline 1980 (WATCH) | 3 of 3 |
Both answer, the values differ, and provenance.dataset.name/.source names whichever
you picked — which is exactly what step 2 asks you to show. Athens centre
(37.98, 23.73) returns nulls from both and is a poor choice for this step, though it
is a good one for earthquake (5 of 5 bands) and wildfire.
Re-check, 2026-08-12 — prod is v0.14.1 (0704467), UI v0.5.0¶
Everything above was written against v0.12.0–v0.14.0. It was re-measured against the
running production stack on 2026-08-12 (DB read via a psql superuser connection,
tiles via a direct titiler call from inside the api container). The substance holds; six
things below it are now stale enough to mislead someone walking the sitting.
Confirmed still true. 21 active datasets across 6 haz_type values, in exactly the
counts the tables above give (coastal_flood 7, earthquake 1, heatwave 3, river_flood
8, storm_europe 1, wildfire 1). Two source values each on river_flood and
heatwave, one on the rest — so the "at least two" half of the goal still passes with no
headroom. The honest-blanks table is exact. Every one of the 21 has a cog_path, and one
COG per haz_type rendered a 200 PNG from titiler at z7/x72/y49 (over Greece). Impact
functions per peril are unchanged: river_flood 3, earthquake 4, wildfire 6,
storm_europe 2, tropical_cyclone 2, and zero for coastal_flood and heatwave.
Six corrections.
GET /v1/hazardsno longer defaults to every status. #873 / PR #876, shipped inv0.14.1, hidesstatus='superseded'when?status=is omitted. The "unfiltered returns 31 rows" claim above is dead: the table now holds 34 rows (21 active, 12 superseded, 1 NULL), and an unfiltered browse returns 22.- That 22nd row is the demo fixture, and it is visible.
JRC River Flood Greece (demo)(d67dbbb7-69af-4f4e-a775-89708199fc1f, sourceClimate-Lama reference fixture) carriesstatus = NULL, which #876 deliberately treats as active — so it appears in the catalog browse, in the dashboard's River Flood group, and as a 9th option in the lookup's river-flood dataset picker. It has blankprovider,upstream_version,native_resolutionandcitation, none of which are in the honest-blanks table. It is demoted, not banned (models/hazard.py::eligible_candidates), so it cannot win a lookup (#810) or a matrix cell (#631) against real catalog data. Consequence for the sitting: step 1 must be counted with?status=active, or those four blanks read as a FAIL that isn't one; and step 6 should record the fixture as present but demoted rather than as a side door. - Step 4's stated check is not observable where the step implies.
provider_fallbackis computed incore/compute_batch_service.pyand then discarded — it is never persisted on the batch row and no endpoint returns it (it appears only in that module, its unit tests, and this file). Per-cellhazard_dataset_idis not on the batch response either:GET /v1/compute/impact/matrix/{id}cells carry onlyscenario_label,horizon_year,status,job_id,result_id,error_message, and the UI's matrix table shows scenario / horizon / status / EAD / AAI. The observable proxy is the result:GET /v1/results/{result_id}carrieshazard_dataset_id. See the run sheet's step 4. - Which dataset you seed the matrix with decides the outcome, and no step says so. The
base dataset's ADR-048
provideris what #591/#861 rank the per-cell family against. Every verified 9/9 batch seeded witha085c0b9-…— Aqueduct river flood Greece — historical baseline 1980 (WATCH), providerWorld Resources Institute. Seeding with the JRC row instead makes the baseline column prefer JRC while the six SSP cells fall back across providers: precisely the #861 shape that scored 6/9. - "9/9 verified" was verified against the demo exposure. Both the
v0.14.0batch (547c2833…) and a fresh one run today onv0.14.1(f90d23f5-7ad0-4a70-be69-0c9616609eb8, 9/9completed, all nine inside the Aqueduct family, baseline resolving the WATCH 1980 row at all three horizons) usedexposure_dataset_id = b0a0cb68-…=LitPop Greece demo, 10 rows. A run against a real exposure (e.g.LitPop — Greece default asset-value exposure (150 arcsec, 2018), 7,896 rows) is not covered by that evidence and will be much slower cold. Use the demo exposure for the sitting unless you deliberately want the bigger claim. - Superseded by #901 (2026-08-14) — step 5's heatwave leg no longer needs a hand-set env var.
#885 and
#901 are both closed. The root
cause was pixel-interleaved deep-stack COGs causing ~100x read amplification, not an
underpowered timeout budget; #901 fixed it by rebuilding the COGs band-interleaved.
Post-rebuild reads measure 27.7–568.2 ms (vs 2374–5989 ms before). The
RISK_LOOKUP_COG_TIMEOUT_SECONDSoverride was removed from.env.prod(backup:.env.prod.bak.20260814_pre901) and the code default budget now applies cleanly.
Two smaller notes: the honest-blanks table is three rows but four blank fields
(windstorm-c3s-footprints-hist has two), and step 3's "(and TC if the stretch landed)"
should be read as struck — tropical_cyclone has 0 active datasets and was ruled out at
the raw-data level on 2026-08-12.
Re-check, 2026-08-19 — prod is v0.16.0 (a70cc3e), UI v0.6.2¶
Five releases landed between the re-check above and this one, and several of them move
numbers this file quotes. Everything in this section was measured against the running
production stack on 2026-08-19; nothing in it is inferred from a merged PR. The section
above is kept as the dated record of what was true on v0.14.1, not as current guidance.
What is deployed. Backbone v0.16.0 =
a70cc3ecbc7da9621ef83a098d732fb385bd28ef (deploy-prod.yml run 32245322810), confirmed
by scripts/verify-deploy.sh v0.16.0 — the identity check, which is the proof that matters,
because a healthy old container passes a liveness curl just as well. UI v0.6.2, serving bundle
/assets/index-DuDXBjbE.js. Alembic head 0086. RISK_LOOKUP_COG_TIMEOUT_SECONDS is still
unset on the api container, as #901 left it.
What shipped since 2026-08-12 and is now live — the three releases that move numbers this file quotes, out of the five tagged in that window.
| Release | Change | Why the sitting cares |
|---|---|---|
v0.15.0 |
#934 / ADR-073 — every address-level band reads the point; a resolved cube slice is a band input | Point-banded ladders that could never fire now can |
v0.15.0 |
#935 / ADR-074 — an absent cell of a count-indicator peril reads as zero (heatwave only) | A heatwave point that used to answer no_bandable_reading can now band at the bottom of the ladder; the card gains intensity.band_value_source naming which read produced the band |
v0.15.0 |
#932 / ADR-075 — a run-backed surface carries its design-return-period intensity into each cell's metrics |
Rollup banding, previously dead for all nine seeded schemes, is alive |
v0.15.1 |
#925 — migration 0086 recalibrates the climatological heatwave band edges |
Any heatwave band label recorded in this file before 2026-08-16 is not comparable |
v0.15.1 |
#942 — earthquake cards band at the design return period, RP 476 | Earthquake band values predating it are not comparable |
v0.15.1 |
#943 — resave_run_surfaces, so stored surfaces can be rebuilt from their own results |
The mechanism behind proof 1 below |
v0.16.0 |
#833 / ADR-076 — coastal flood is computable, screening-grade | Step 4's second hazard exists and is live; see 4b |
v0.16.0 |
#856 — heatwave posture decided (deliberately deferred monetisation, not absent literature) | Step 3's honest-framing wording |
v0.16.0 |
#891 — pack scripts take --org-slug |
Ops only; no step depends on it |
Prod proofs run 2026-08-19 — you may rely on these.
- Surfaces carry their metric.
resave_run_surfaces --applyonnorthlanere-saved 24 surfaces. Their cells now carryflood_depth_mon 85,574 cells andpga_gon 10,000 — the ADR-075metricskeys the rollup ladders band from. - The admin-unit rollup answers. Attica (GADM 4.1 level 1, boundary
e7f12fe5-4c95-4058-9560-14df355f53f0— the only GRC boundary loaded, so it is the rollup you can demo) returns a bandedriver_floodscore of 2 "Very low" at0.0657 m, aggregationpoints_weighted_cell_mean, over 28 cells from 2 surfaces;earthquake's headline score is 6. - Athens earthquake point lookup (
37.9838, 23.7275) → band score 6 "Elevated",band_return_period 476.0,band_value 0.2477 g. The RP is #942's design-RP override, so a pre-v0.15.1reading of the same pin is not comparable to this one. - Coastal flood is seeded, and it computes.
northlaneholds 15 impact-function rows, org-scoped, three of them coastal (JRC coastal flood Europe buildingsis the default, unitm). Do not read that against the 2026-08-12 per-peril counts above and conclude rows vanished: those were DB-wide across both orgs, this one isnorthlanealone, and 15 is exactly_BUILTIN_FUNCTIONS' post-#833 size. All seven active Aqueduct coastal datasets reportindicator: false, eai_grade: screening. Seven impact jobs completed, one per coastal dataset, against exposure69e03607-…(LitPop):
| Scenario | Horizon | AAI |
|---|---|---|
baseline |
— | €22.9 M |
ssp2-4.5 |
2030 / 2050 / 2080 | €29.1 M / €34.0 M / €45.0 M |
ssp5-8.5 |
2030 / 2050 / 2080 | €29.9 M / €35.6 M / €50.4 M |
Seven coastal surfaces were written, carrying coastal_flood_depth_m on 500 cells.
Two coastal caveats to carry into the sitting. Record them; do not chase them mid-sitting.
- The Attica rollup for
coastal_floodreportsno_cells_in_scopeunder the defaultbaseline/ horizon0key. The seven coastal surfaces exist (proof 4); the default rollup key does not select them. River flood and earthquake both roll up fine on the same boundary. - A Piraeus point lookup (
37.94, 23.64) isavailable: truebutno_bandable_reading. Aqueduct's coastal inundation pixels are sparse — the same class as the river-flood sparsity note above, where most Greek coordinates return nulls from both river-flood datasets. So for step 5's coastal leg, pick a coordinate that is actually inundated in the COG you selected rather than an arbitrary waterfront point, or you will demo a blank card and read it as a defect.
What this re-check did not measure — re-assert, do not inherit. The 21-active-dataset
count and the per-type counts were not re-counted beyond coastal_flood = 7. No tiles were
re-rendered. The two cosmetic UI defects listed in the run sheet were recorded against UI
v0.5.0 and have not been re-checked on v0.6.2. And no heatwave band was read on
v0.15.1+ — see steps 3 and 5.
The six steps¶
Each row is one step of the phase goal. Record PASS/FAIL per row with evidence. The per-step UI procedure — which page, which option, what you should see — is in The UI run sheet below.
| # | Step | Do this | Expected result |
|---|---|---|---|
| 1 | Browse ≥6 hazard types | Open the catalog in the UI; cross-check GET /v1/hazards?status=active&limit=200 — status=active is required since #876; without it the demo fixture joins the list |
≥6 distinct haz_type. Every entry carries source, license, citation, version, resolution and scenario metadata — no blanks except the three the sources genuinely do not publish, listed under "Honest blanks" below. A blank outside that list is a FAIL; an invented value for anything on it is a worse FAIL |
| 2 | ≥2 datasets for the same peril, ×2 perils | For two perils (e.g. river flood: JRC vs Aqueduct; heatwave: EURO-CORDEX vs ERA5-HEAT), pick each alternative and run the same lookup | The result changes with the choice, and the provenance shown names the dataset you picked. Walk this in the UI — the per-hazard dataset picker shipped in ui v0.5.0. The API path (GET /v1/hazards?haz_type=<t>&source=<s>, compare by dataset_id) is still the fallback if the UI misbehaves |
| 3 | Compute on every computable peril; indicators for the rest | Run an impact computation for RF / WS / EQ (~~and TC if the stretch landed~~ — TC has 0 active datasets, do not attempt). For heatwave, drought and wildfire-via-FWI, open the score/indicator layer | Computable perils return EAI/EP. Indicator perils show a score layer and are excluded from EAI — and the UI says so explicitly. A fabricated EAI on an indicator peril is a FAIL, not a nicety |
| 4 | Full scenario matrix on two hazards — amended 2026-08-11 to one hazard, reinstated 2026-08-19, see below | Run the matrix over the complete baseline / ssp2-4.5 / ssp5-8.5 × 2030 / 2050 / 2080 grid twice: once on river flood, once on coastal flood. Coastal flood needs POST /v1/impact-functions/seed to have run in the org first (curves are per-org rows) |
All 9 cells return values on each hazard; no cell reports missing centroids, and no cell silently replays another cell's dataset — check the per-cell hazard_dataset_id differs across scenarios. That id is not on the batch response or in the UI table (re-check note 3): read it off each cell's result, GET /v1/results/{result_id}. Expect 7 distinct datasets over 9 cells on river flood — the baseline row repeats across the three horizons by design. The coastal results carry the amber "Screening-grade EAI" pill (ADR-076) — an unlabelled coastal EAI is a FAIL. See wave-0 trap 2 — the first run per (hazard, exposure) pair is slow because it assigns centroids on demand |
| 5 | Tiles + attributed lookups | For every dataset on the map, pan/zoom to confirm tiles render; run a point lookup on each | Tiles 200, not 502/404. Every lookup answer names the dataset it came from. The provenance chip shipped in ui v0.5.0, so check it in the UI; the API field is also available |
| 6 | One pipeline, no side doors | For each dataset, confirm it arrived via the ingest pipeline from a documented manifest | Every dataset traces to a re-runnable source manifest. No script wrote to the DB directly. scripts/seed_demo.py output does not count as catalog content — if a seeded demo row is answering real queries, that is a FAIL (this is exactly what #631 was) |
Step 4 was amended on 2026-08-11 — from "≥2 hazards" to one full grid¶
This is an amendment with a recorded rationale, not a waiver. The distinction matters, because this file's own closing rule forbids waiving a step, and Phase 8 exited once by doing exactly that.
What changed. Step 4 read "Run the matrix over scenario-conditioned data for two different hazards." It now runs the complete 3 × 3 grid on river flood — nine cells, every scenario against every horizon — instead of a thinner slice across two perils.
Why the ≥2-hazard clause was dropped. It was testing two different things at once, and only one of them belongs in step 4:
- Does the scenario-matrix machinery work end to end? — "all cells return values, no cell reports missing centroids." One hazard proves this completely, and the full 3 × 3 grid proves it harder than two perils × two cells ever did. Nine cells, three distinct scenario datasets, three horizons each.
- Is the catalog broad? — that is a breadth claim, and steps 1, 2, 3 and 5 already carry it: ≥6 hazard types browsed, two providers compared for the same peril, every computable peril computed and every indicator peril honestly refused, all 21 datasets tiled and attributed. Asserting it a fifth time inside step 4 was redundant.
So the amendment removes a redundant breadth assertion from the wrong step. The claim under test in step 4 — that the matrix machinery works — is not weakened at all.
Why not just fix the data instead. Because no second hazard can qualify without new science, not new engineering. The per-peril table in the dry-run findings above is the evidence; the two candidates that keep looking promising are both ruled out by physics:
- Earthquake is computable now (engine#30 closed, 4 ESRM20 curves), but ESHM20 is time-invariant — seismicity is not climate-conditioned, so there is no scenario axis to ingest. A matrix over it is one cell.
- Wildfire cannot take a curve at all on FWI: #481
ran that search and closed it — FWI is a fire-danger index, not a hazard intensity
(corroborated independently by OS-Climate
physrisk). Its computable form (FIRMS brightness temperature, #412) is satellite-observed, so it would land atbaseline— the same one-cell shape as earthquake.
Coastal flood is the one real path back, and it is a single missing artifact: seven
active datasets already carry the identical baseline / ssp2-4.5 / ssp5-8.5 × 2030 /
2050 / 2080 structure, and only the damage curve is absent.
#833 owns that, and it is
explicitly not satisfiable by reusing the fluvial Huizinga curve — #621 adjudicated
that and rejected it, because surge and wave are a different physical process.
Reinstatement condition. When #833 lands a defensible coastal curve, restore the original two-hazard wording and re-walk step 4 across river flood and coastal flood. Until then this amendment stands, and the reason travels with it.
Superseded on 2026-08-19 — the condition above was met. This section is kept verbatim as the dated record of the amendment, not as current guidance. Two of its claims are now known to be wrong and are corrected in the section immediately below: "no second hazard can qualify without new science" (coastal flood qualified, and it took no new science — only a corrected reading of a source already in the tree), and "#833 is explicitly not satisfiable by reusing the fluvial Huizinga curve" (the publication scopes its own functions to generic inundation, JRC105688 §1.2). Read Step 4's two-hazard form was reinstated for what step 4 actually requires today.
Step 4's two-hazard form was reinstated on 2026-08-19¶
The condition above was met. #833 landed a coastal-flood depth-damage curve (owner decision, option A — ADR-076), so step 4's criterion in the table above is back to its original two-hazard wording and is walked across river flood and coastal flood.
What the curve is, and why the earlier "explicitly forbidden" bar fell. It is the same
published JRC/Huizinga (2017) Europe depth-damage function the river-flood curves already
carry, seeded under coastal_flood at intensity_unit = "m". That is not the Huizinga reuse
the amendment above forbade — it is the reuse the source itself authorises: "Because the
data used in this study comprise both fluvial and marine flooding, the damage functions
constructed are not associated with specific type of floods" (JRC105688 §1.2, p.3), and the
report's headline validation case is a storm surge (§4.3.3.1, Hurricane Sandy). The
"fluvial calibration ≠ surge/wave" claim this file recorded on 2026-08-11 came from ADR-059,
whose keyword sweep searched for coastal / storm surge / salt while the report says
"marine flooding" and "storm Sandy". ADR-058 and ADR-059 both carry superseding notes.
The EAI is screening-grade (EaiGrade.SCREENING, ADR-076) — depth is the only flood
parameter these functions model, for any flood type — so every coastal result must render the
amber pill, exactly as earthquake's does. That label is part of the step's pass condition.
What is not claimed here. The coastal 3 × 3 grid has still never been submitted. Three things stood between this note and a PASS when it was written, and none of them was code; two are now done. Status measured on prod 2026-08-19:
- ~~Deploy. The curve is on trunk; prod picks it up on the next release.~~ Done —
shipped in
v0.16.0(a70cc3e), verified byscripts/verify-deploy.sh. - ~~Seed the org.~~ Done —
northlaneholds 15impact_functionsrows, three of them coastal, and all seven active coastal datasets reporteai_grade: screening. The rows are per-organization, so the empty legacydefaultorg is still deliberately not seeded. Before the seed, a coastal matrix submission is refused with the generic seeding-gapE_VALIDATION— not the old422 E_INDICATOR_DATASET_NO_EAI, which no longer describes this peril. - Run the grid. Still outstanding, and it is the one that carries the PASS. Seven
standalone impact jobs — one per coastal dataset — completed on 2026-08-19 with the AAIs
in 4b, and seven coastal surfaces were written (
coastal_flood_depth_m, 500 cells). That proves the curve resolves and computes; it does not prove the per-cell matrix resolver picks the right dataset in each of the nine cells, which is the only thing step 4 is actually testing. Walk it.
Expect the same traps the river-flood half hit: the first run per (hazard, exposure) pair is
slow because it assigns centroids on demand, and a provider_fallback = true anywhere in the
grid is a regression, not a wart.
Step 4 rehearsal: 6/9, then 9/9¶
Amending the criterion was not enough — the amended step still had to be run. Doing that on production surfaced a defect that resolution alone could never have shown, and the lesson generalises well beyond step 4.
A cell resolving tells you nothing about whether it computes. The first
rehearsal resolved cleanly — 9 of 9 cells found a dataset, provider_fallback
duly reported on three of them — so it looked fine. Running the actual batch:
baseline 2030/2050/2080 FAILED <- the three cross-provider cells
ssp2-4.5 2030/2050/2080 completed
ssp5-8.5 2030/2050/2080 completed
all three with Every assigned exposure row sits outside this hazard dataset's
grid or on a zero-hazard cell — a message about exposure, when the actual
cause was that the matrix had silently swapped provider on one column.
Resolution is a catalog lookup; compute is a grid ∩ exposure intersection, and
they can disagree. #861
fixed the swap; #871
tracks the misleading message that remains when a fallback does occur.
Verified after the fix, on v0.14.0 (472a8d4), batch
547c2833-2c16-47ea-aa21-a2f14d9a99b6: 9/9 cells completed, every one with a
result, and all nine resolved inside the Aqueduct family — the baseline column
no longer substitutes JRC.
So when you walk step 4, expect all nine cells green and every cell's dataset to
be an Aqueduct row. A provider_fallback = true anywhere in the grid is now a
regression, not a wart.
Two practical notes for the sitting:
- The first run for a given (hazard, exposure) pair is slow — it assigns centroids on demand. A warm pair settles in about 30 seconds; a cold one takes minutes. Don't read the cold-run latency as a fault.
GET /v1/compute/impact/matrix/{id}returns per-cell result references, not EAI values.eai: nullthere is by design — fetch the result to see the number.
Honest blanks — the three step 1 must tolerate¶
Step 1 says "no blanks", and read literally that is now too strict: ADR-067 and #808
deliberately leave scenario NULL for a source that declares no scenario axis, because
inventing a label there is the falsification the platform exists to avoid. Demanding a value
would be demanding the platform violate its own ADR.
Verified against production 2026-08-11 — across all 21 active datasets, exactly three fields on three rows are blank, and every one is honest:
| Row | Blank field | Why it is honest |
|---|---|---|
heatwave-ecde-europe |
scenario |
ERA5-derived observational reanalysis. It is not a scenario; it is what happened |
wildfire-fwi-historical-greece-20230801 |
scenario |
Historical FWI. Same reason |
windstorm-c3s-footprints-hist |
scenario, upstream_version |
Historical footprints; and the C3S reanalysis retrieval declares no version input at all |
Everything else — source, license, citation, upstream_version, native_resolution,
provider — is populated on all 21 rows. So the check to run is: any blank not in this
table is a FAIL, and any value that appears where this table says one should not exist is a
worse FAIL — it means something fabricated a label.
Note that the two EURO-CORDEX heatwave rows are no longer in this list.
#827 (PR #852) restored their
archive-member labels, so they now carry ssp2-4.5 and ssp5-8.5 — the label was known and
was being discarded, which is a different thing from a source that never had one.
Honest framing you must not soften in step 3¶
From phase-9-data-foundation.md: RF / WS / TC / EQ have machine-readable damage curves
→ full EAI/EP. Add CF to that list as of 2026-08-19 (#833, ADR-076): coastal flood is
computable on the same published JRC/Huizinga functions river flood uses, and — like
earthquake — at screening grade, so its EAI must carry the grade label. Those two are the
screening-grade pair; RF / WS / TC are not.
Heatwave and drought have no published structural-damage curves — they
are hazard-indicator perils: ingested, tiled, scored, shown on the score card, and
excluded from EAI. Amended 2026-08-19 (#856): that exclusion is a deliberately
deferred monetisation, not an absent literature — heat has published health and
labour-productivity functions (physrisk; Stalhandske et al. 2022, NHESS, on CLIMADA) and
drought has crop-yield ones; what heat has no curve for is structural damage. Say it that
way, not as "no published curve at all". Wildfire ships as an indicator
peril on the FWI packs (#481): FWI is a weather-only fire-danger index that no damage
function can consume in principle. Computable wildfire is gated on intensity-grade
footprints under #412 (stretch). Earthquake ships screening-grade via ESHM20.
Earthquake carried a cross-repo gate — now tripped and satisfied.
climate-lama-ui#109 (render eai_grade / eai_grade_note) had to land **before
617's earthquake data reached prod**, or the UI would present screening-grade EAI as¶
if it were computed-grade. Both happened in the safe order: ui#109 closed 2026-08-07, ESHM20 reached prod 2026-08-10. Still check the grade label renders when you walk step 3 on EQ — that is what the gate was protecting, and it is now the only part of it you can still get wrong.
The UI run sheet¶
Written against prod as it stood on 2026-08-12 — backbone v0.14.1 (0704467), UI
v0.5.0, 21 active datasets. Every dataset id, option label and expected count below was
read off the running system, not off an earlier version of this file. Re-assert the
preconditions table first; if the version moved, re-read the counts before trusting the
numbers here.
Partially re-measured on 2026-08-19 against backbone v0.16.0 (a70cc3e) and UI
v0.6.2. Steps 3, 4b and 5 below carry the new readings inline; everything else in this run
sheet still dates from 2026-08-12. Read
Re-check, 2026-08-19 for what was and
was not re-measured — in particular, no heatwave or earthquake band label from before
2026-08-16 is comparable to what you will read on the day.
URLs. UI https://climate-lama.online · API https://api.climate-lama.online.
Log in at /login — three fields:
| Field | Value |
|---|---|
| Organization slug | northlane |
develama@protonmail.com |
|
| Password | the owner's |
Do not use demo@example.com. That account belongs to acme-demo, which was
soft-deleted 2026-08-11; all 34 hazard rows live in northlane. develama is
org_admin and platform admin, so the /admin nav entry is reachable — that is the
account the admin-gating side-check needs too.
Two cosmetic defects recorded on UI v0.5.0. Both are unverified on v0.6.2 — check
whether they are still there rather than expecting them, and either way record and move on.
- The provenance chip doubled the "v" —
vv3.1.1,vv2,vESHM20— because most rows'upstream_versionalready carries the provider's ownv.climate-lama-ui#116was fixed on UImain(PR #117, merged 2026-08-12) and prod now runsv0.6.2, which is after that merge — so this is very likely already gone. Confirm on the day; it was not re-measured on 2026-08-19. heatwaveandearthquakerendered as raw enum strings everywhere in the UI. Neither had an entry in the lookup or glossary label maps; the other four types did. Not a step failure — the metadata is present, only the display name is missing — but it was the most visible presentability gap in the whole walk, so it is worth a deliberate look onv0.6.2.
One timing rule that applies to steps 3 and 4. The first compute for any (hazard dataset, exposure) pair assigns centroids on demand: the map shows "Assigning centroids…" and a cold pair takes minutes; a warm one settles in about 30 seconds. Do not read cold-run latency as a fault, and do not start a matrix on a pair you have never computed before if you are short on time.
Step 1 — Browse ≥6 hazard types¶
Page: /datasets → Hazards tab (the default).
Do:
- Leave every filter on "All …". Read the count in the top-right of the filter bar.
- Open the Hazard filter and enumerate its options.
- Click through one row per hazard type and read its attribution chip (the grey pill under
each list entry). Hover it — the citation is the tooltip, and it is a real link when the
row has a
source_url. - Cross-check the API — with
status=active:
Expect:
- The Hazard filter offers 6 options: River Flood, Coastal Flood,
earthquake,heatwave, Storm (Europe), Wildfire (the middle two unlabelled, see above). - The unfiltered list reads 22 datasets, not 21 — the extra is
JRC River Flood Greece (demo), the seeded fixture (status = NULL, source Climate-Lama reference fixture). That is expected, not a finding. It is demoted out of every real resolution path; see step 6. - The API cross-check returns 21 rows over 6 types:
coastal_flood7,earthquake1,heatwave3,river_flood8,storm_europe1,wildfire1 — and two distinctsourcevalues forriver_flood(JRC v3.1.1 · WRI Aqueduct v2) and forheatwave(ECDE/ERA5 · EURO-CORDEX), one for the rest. - Every chip shows
source · license.
What the UI cannot show you, so do it against the API. The /datasets page renders
name, created date, hazard type, region and the source/license chip — it does not
render upstream_version, native_resolution or scenario. Those three are part of
step 1's criterion, so verify them here:
curl -fsS -H "Cookie: <session>" \
"https://api.climate-lama.online/v1/hazards?status=active&limit=200" \
| jq -r '.data[] | [.name,.provider,.upstream_version,.native_resolution,.scenario]|@tsv'
Expect exactly four blanks, on three rows — heatwave-ecde-europe.scenario,
wildfire-fwi-historical-greece-20230801.scenario, and
windstorm-c3s-footprints-hist.scenario and .upstream_version. Every other cell
populated on all 21. A blank outside that set is a FAIL; a value where that table says
there should be none is a worse FAIL. (Partial UI coverage does exist: the lookup's
provenance chip shows version, and its dataset-picker option labels show provider,
resolution and scenario — see step 2.)
Verdict: PASS if 6 types, 21 active rows, the source split above, and only the four honest blanks.
Step 2 — ≥2 datasets for the same peril, on two perils¶
Page: /lookup.
Do — peril 1, river flood:
- Drop a pin at Thessaloniki / Axios delta,
lat 40.60, lon 22.80(address search, or click the map). Athens centre returns nulls from both river-flood datasets and makes a flat demo — use it for earthquake and wildfire instead. - On the River flood card, open the Dataset select (it appears only where the peril
has more than one catalogued dataset). It defaults to "Auto (recommended)", which
sends no
dataset_idat all. - Pick
JRC river flood Greece (national, historical baseline)— id67854ef5-8052-435d-9828-1a1015146654. Read the values table and the provenance chip. - Switch the same select to
Aqueduct river flood Greece — historical baseline 1980 (WATCH)— ida085c0b9-09fc-40fd-aa80-6fa0f9451ac4. Read them again.
Expect: JRC answers 9 of 9 return periods (≈1.63 m → 2.19 m); Aqueduct answers
3 of 3. The numbers differ, and the provenance chip changes to name whichever you
picked — JRC… · Copernicus EMS/JRC · vv3.1.1 versus
Aqueduct… · World Resources Institute · vv2 — or v3.1.1 / v2 with a single v if
ui#116 shipped in v0.6.2, which is likely; see the run-sheet defect note above. Either
spelling passes this step — it is testing that the chip renames itself, not how it spells
the version.
Do — peril 2, heatwave: same pin, the heatwave card, whose Dataset select offers
three:
| Option | id | What it is |
|---|---|---|
heatwave-ecde-europe |
32fcd25e-… |
ERA5 reanalysis, no scenario |
heatwave-euro-cordex-europe-ssp2-4.5 |
a1f804a6-… |
projection, SSP2-4.5 |
heatwave-euro-cordex-europe-ssp5-8.5 |
c1f3c898-… |
projection, SSP5-8.5 |
Expect: each answers with a different value, the chip renames itself, and every one of them also carries the "Indicator — no EAI" badge — which is step 3's evidence, visible here.
The picker will list 9 river-flood options, not 8 — the demo fixture is the ninth. Picking it deliberately is fine (it is a demotion, not a ban, so an explicit pin is honoured); it just is not what step 2 is asking you to compare.
Fallback if the picker misbehaves: GET /v1/hazards?haz_type=river_flood, then
GET /v1/risk/lookup?lat=40.60&lon=22.80&dataset_id=<id> per dataset, comparing
provenance.dataset.
Verdict: PASS if, on both perils, the answer changes with the choice and the provenance names the dataset you picked.
Step 3 — Compute on every computable peril, indicators for the rest¶
Two surfaces. Compute is on the dashboard; the indicator half is on the lookup.
3a — Compute. Page: / (dashboard). Left panel, top to bottom: Exposure → Hazard →
Impact function → Measures → Scenario / Time horizon → Run (pinned at the bottom).
Run it four times — row 4 is new as of v0.16.0:
| # | Hazard section | Dataset | Impact function | Exposure |
|---|---|---|---|---|
| 1 | River Flood | Aqueduct river flood Greece — historical baseline 1980 (WATCH) |
JRC flood Europe buildings (the default) |
LitPop Greece demo |
| 2 | earthquake |
ESHM20 earthquake hazard — Greece (mean PGA…) |
any of the 4 ESRM20 curves | LitPop Greece demo |
| 3 | Storm (Europe) | windstorm-c3s-footprints-hist |
Klawa Ulbrich 2003 WS buildings EUR |
LitPop Greece demo |
| 4 | Coastal Flood | the baseline member of the Aqueduct coastal family (GET /v1/hazards?haz_type=coastal_flood&status=active lists the seven) |
JRC coastal flood Europe buildings (the default) |
LitPop Greece demo |
Expect: runs 1 and 2 complete and return EAD/AAI with an impact layer on the map;
run 2 additionally shows the amber "Screening-grade EAI" pill (that is the ui#109 gate
this step exists to protect — check it renders). Run 3 is expected to be refused:
ADR-069 declines a hazard whose events carry no annual frequency
(E_HAZARD_DATASET_NO_EVENT_FREQUENCIES) rather than integrating a NaN. A storm_europe
run that returns a number is a FAIL, not a success.
Coastal flood became runnable on 2026-08-19, and is live — 7 active datasets, and since
#833 three seeded JRC/Huizinga
curves in m (ADR-076), shipped in v0.16.0. It is now a computable, screening-grade
peril: run it like river flood — row 4 of the table above — and expect an EAD/AAI plus the
amber "Screening-grade EAI" pill.
Precondition — already satisfied on northlane, but re-assert it. Impact-function rows
are per-organization, so POST /v1/impact-functions/seed has to have run in the org after the
deploy carrying #833. It has: measured 2026-08-19, northlane holds 15 impact-function rows,
three of them coastal, with JRC coastal flood Europe buildings as the default. Confirm with
GET /v1/impact-functions?haz_type=coastal_flood. If the select is empty and Run is refused
with "No impact functions registered for hazard type 'coastal_flood'", that is the seeding
gap, not the old indicator refusal, and the fix is the seed call.
3b — Indicators. Page: /lookup, any Greek coordinate (Athens centre 37.98, 23.73
works well — 5 of 5 earthquake bands, and wildfire answers).
Expect: the heatwave and wildfire cards each render values plus the grey
"Indicator — no EAI" badge, and the earthquake card renders the amber "Screening-grade
EAI" pill whose tooltip carries the backbone's own eai_grade_note. Click "Why this
score?" on each to see the scheme / surface / dataset provenance sections. That is the
step's criterion and it has not moved — what a card says has.
The band each card shows is produced differently since v0.15.0/v0.15.1. Do not carry a
number forward from this file's older sections.
- Heatwave. Three changes stack on this one card: ADR-073 (#934) made every address-level
band read the point; ADR-074 (#935) made an absent cell of this count-indicator peril
read as zero rather than as a gap, so a pin that used to answer
no_bandable_readingcan now band at the bottom of the ladder, and the card carriesintensity.band_value_sourcenaming which read produced it; and migration0086(#925) recalibrated the climatological band edges outright. Av0.14.1heatwave reading is therefore not comparable on the value, on the band, or on whether a band appears at all. Measure it on the day and record what you see. What has not changed: heatwave is still excluded from EAI and must still carry the "Indicator — no EAI" badge. - Earthquake. #942 made the card band at the design return period, RP 476. Measured
on prod 2026-08-19 at
37.9838, 23.7275(Athens centre): band score 6 "Elevated",band_return_period 476.0,band_value 0.2477 g. Expect that shape; an RP other than 476 on an earthquake card is the thing to flag.
Known wart, already recorded: attempting a heatwave compute refuses with a bare
422 E_VALIDATION and an empty detail — it declines without saying why. Acceptable,
weaker than the step asks. (Recorded on v0.14.1; not re-measured on 2026-08-19.)
Verdict: PASS if every computable peril computes, every indicator peril is labelled as one, storm_europe refuses honestly, and no fabricated EAI appears anywhere.
Step 4 — Full scenario matrix on two hazards¶
Page: /matrix. Two grids: 4a river flood (below, verified) and 4b coastal flood
(after it, never yet walked).
4a — River flood¶
Do:
- Hazard dataset:
Aqueduct river flood Greece — historical baseline 1980 (WATCH)(a085c0b9-09fc-40fd-aa80-6fa0f9451ac4). This choice is load-bearing — the base dataset'sprovideris what the per-cell resolver ranks families against, so seeding with the JRC row instead reproduces #861's cross-provider split. Do not improvise here. - Impact function:
JRC flood Europe buildings(the default). - Exposure:
LitPop Greece demo— the exposure both verified 9/9 batches used. - Scenarios: tick Baseline, SSP2-4.5, SSP5-8.5 (SSP2-4.5 is pre-ticked; untick nothing else).
- Horizons: tick 2030, 2050, 2080 (2050 is pre-ticked).
- Submit. The page switches to a 9-badge progress grid, then to a results table.
Expect: all 9 badges green (completed) — no red failed, no amber unresolved —
and a results table with 9 rows each carrying an EAD and an AAI. Baseline rows will share
a value across the three horizons; that is correct, they resolve the same 1980 WATCH row.
The dataset check the UI cannot do. Neither the batch response nor the results table
carries the per-cell hazard_dataset_id, and provider_fallback is never returned by any
endpoint. Read it off the results:
curl -fsS -H "Cookie: <session>" \
"https://api.climate-lama.online/v1/compute/impact/matrix/<batch_id>" \
| jq -r '.data.cells[] | "\(.scenario_label)\t\(.horizon_year)\t\(.status)\t\(.result_id)"'
# then, per result_id:
curl -fsS -H "Cookie: <session>" \
"https://api.climate-lama.online/v1/results/<result_id>" | jq -r .data.hazard_dataset_id
Expect 7 distinct dataset ids over the 9 cells — one Aqueduct baseline repeated across the three horizons, plus three SSP2-4.5 rows and three SSP5-8.5 rows — and every one of them an Aqueduct row. A JRC id anywhere in the grid is the #861 regression.
Reference run, prod v0.14.1, 2026-08-12: batch
f90d23f5-7ad0-4a70-be69-0c9616609eb8 — 9/9 completed, baseline column resolving
Aqueduct … historical baseline 1980 (WATCH) at 2030/2050/2080, SSP columns resolving
their own year-matched rows.
Verdict (4a): PASS if 9/9 completed and all nine resolved inside the Aqueduct family.
4b — Coastal flood¶
Reinstated on 2026-08-19 by #833
(ADR-076), and live on prod since v0.16.0. The 9-cell matrix itself has still never been
submitted — treat the procedure below as unwalked. What has been run, on prod 2026-08-19,
is the layer underneath it: seven standalone impact jobs, one per coastal dataset. See the
reference numbers below.
Precondition — already satisfied on northlane; re-assert, do not assume. Impact-function
rows are per organization, so an analyst on the org must have called
POST /v1/impact-functions/seed after the deploy carrying #833. Measured 2026-08-19:
northlane holds 15 impact-function rows, three of them coastal, and all seven active
Aqueduct coastal datasets report indicator: false, eai_grade: screening. Confirm with
GET /v1/impact-functions?haz_type=coastal_flood — expect the three JRC coastal flood
Europe … rows, JRC coastal flood Europe buildings being the default. Without the seed the
Impact-function select is empty and the submission is refused with the generic
"No impact functions registered for hazard type 'coastal_flood'".
Do:
- Hazard dataset: an Aqueduct coastal row (
GET /v1/hazards?haz_type=coastal_flood&status=activelists the seven). Pick thebaselinemember of the family, for the same reason 4a does: the base dataset'sprovideris what the per-cell resolver ranks families against. - Impact function:
JRC coastal flood Europe buildings(the default). - Exposure:
LitPop Greece demo— the same exposure 4a used. - Scenarios: tick Baseline, SSP2-4.5, SSP5-8.5.
- Horizons: tick 2030, 2050, 2080.
- Submit.
Expect: 9 green badges and 9 rows carrying an EAD and an AAI, each labelled with the amber
"Screening-grade EAI" pill (ADR-076 — an unlabelled coastal EAI is a FAIL, the same bar
ui#109 set for earthquake). Read the per-cell hazard_dataset_id off each result exactly as in
4a; every one should be an Aqueduct coastal row, and a provider_fallback is a regression.
If cells fail rather than refuse, expect the same failure modes 4a hit and not new ones: centroid assignment on the first run per (hazard, exposure) pair, and the exposure-shaped message that actually means a grid ∩ exposure miss (#871).
Reference numbers from the layer underneath, prod 2026-08-19. The grid was not submitted,
but seven impact jobs completed, one per coastal dataset, against exposure
69e03607-… (LitPop) — which is the same computation each matrix cell performs, minus the
per-cell resolution the matrix adds. Seven coastal surfaces were written, carrying
coastal_flood_depth_m on 500 cells.
| Scenario | Horizon | AAI |
|---|---|---|
baseline |
— | €22.9 M |
ssp2-4.5 |
2030 / 2050 / 2080 | €29.1 M / €34.0 M / €45.0 M |
ssp5-8.5 |
2030 / 2050 / 2080 | €29.9 M / €35.6 M / €50.4 M |
Use these as a shape check, not as an equality check — a different exposure gives
different absolute numbers. What should hold in the grid: every cell returns a number, the
baseline row repeats across its three horizons, and AAI rises monotonically with horizon
within each SSP column and is higher under ssp5-8.5 than ssp2-4.5 at the same horizon. A
cell that inverts that ordering is worth a look before you record a PASS.
Verdict (4b): PASS if 9/9 completed, all nine resolved inside one Aqueduct coastal family, and every result renders the screening-grade label.
Verdict (step 4): PASS only when both 4a and 4b pass.
Step 5 — Tiles render, and every lookup answer names its dataset¶
5a — Tiles. Page: / (dashboard). The map shows one hazard layer at a time — the
dataset currently selected in the Hazard section — so "every dataset" means stepping the
selection through all 21. The Layers panel sits bottom-right: a Hazard toggle that is
disabled and greyed out whenever the dataset has no renderable layer, plus an opacity
slider once it is on.
Do: for each hazard type, select a dataset, confirm the Hazard toggle is enabled, turn
it on, and pan/zoom over Greece until tiles paint. Watch the browser network tab: requests
go to /v1/hazards/{id}/tiles/{z}/{x}/{y}.png.
Expect: 200s, not 502/404. The route that made every tile 502 (#831) is fixed
and deployed; one COG per haz_type was measured rendering a 200 PNG on 2026-08-12. Two
caveats worth knowing before you call something broken: heatwave and storm_europe are
Europe-wide grids at 0.25°/0.1°, so at low zoom their tiles are legitimately near-empty
(they still return 200); and the demo fixture's COG lives under the retired org's key
prefix by design, so its tile URL looks wrong and is not.
5b — Attributed lookups. Page: /lookup. Drop pins at 40.60, 22.80 (Thessaloniki)
and 37.98, 23.73 (Athens).
Expect: every hazard card — including the "Not built here" ones — carries a provenance chip naming its dataset, provider and version, with license and citation in the tooltip. "Why this score?" opens the drawer with the full attribution and a real citation link. That is the step's criterion, and attribution is what it tests — not the band values, which have moved.
Choose the coastal pin deliberately. Coastal flood was always in this step — 7 of the 21
datasets, all tiling — what v0.16.0 changed is that its cards now carry the screening-grade
pill. What has not changed is that Aqueduct's coastal inundation pixels are sparse in exactly
the way the river-flood raster is. Measured 2026-08-19: a Piraeus waterfront pin at
37.94, 23.64 comes back available: true but no_bandable_reading — the dataset answered,
the pixel is dry. That is the documented sparsity, not a defect: the same trap as
the step 2 note above, where most
Greek coordinates return nulls from both river-flood datasets. Pick a
coordinate that is actually inundated in the coastal COG you selected before you walk this
leg, or you will demo a blank card. The provenance chip must still be present and correct on
a no_bandable_reading card — that, not a number, is what step 5 is asserting.
Band values recorded before 2026-08-16 are not comparable. Migration 0086 (#925)
recalibrated the heatwave band edges, #942 moved earthquake onto the RP-476 design return
period, and ADR-074 (#935) made an absent heatwave cell read as zero. Two prod readings
measured 2026-08-19 you can anchor on: Athens 37.9838, 23.7275 earthquake → band score
6 "Elevated", band_return_period 476.0, band_value 0.2477 g; and the Attica admin-unit
rollup (boundary e7f12fe5-4c95-4058-9560-14df355f53f0) → river_flood band 2 "Very low"
at 0.0657 m over 28 cells from 2 surfaces, earthquake headline 6. No heatwave band was
read on v0.15.1+ — measure that one on the day rather than expecting a label.
The heatwave leg runs on the code default budget — no env override needed. #885 and
901 are both closed (2026-08-14): the original ~4 s heatwave COG point read was 100x read¶
amplification from pixel-interleaved deep-stack COGs, not an underpowered timeout. #901
rebuilt them band-interleaved, and reads now measure 27.7–568.2 ms, comfortably inside the
code default budget. RISK_LOOKUP_COG_TIMEOUT_SECONDS should not be set on the api
container — its absence is the healthy state. If heatwave misses everywhere with
cog_read_failed, that points at a genuine regression, not a missing override — file it.
Verdict: PASS if all 21 datasets tile and every lookup answer is attributed.
Step 6 — One pipeline, no side doors¶
Not a UI step — it is an audit. Do it against the object store and the catalog.
Do: for each of the 21 active datasets, confirm a source manifest exists at
hazards/{dataset_id}/source_manifest.json (ADR-066), or at the legacy
staging/{ingest_job_id}/manifest.json for the pre-ADR-066 rows.
scripts/backfill_source_manifests.py relocates the legacy ones onto the dataset-scoped
key verbatim — real captured checksums survive. Read the capture_mode flag: a
"reconstructed" manifest was assembled after the fact and leaves
checksum_sha256/volume_bytes null.
Expect: every dataset traces to a re-runnable pack. No raw/ copy exists for any of
them, and none should — ADR-070 makes the raw zone transient by design.
About the demo fixture, explicitly. JRC River Flood Greece (demo) is written by
scripts/seed_demo.py and it is in the catalog. Under #631's rule that would read as a
FAIL, so record it precisely: it is present and visible, but demoted out of every
resolution path — models/hazard.py::eligible_candidates removes seeded fixtures from
any group that also holds catalog data, in both the point lookup (#810) and the scenario
matrix (#631). The FAIL condition #631 named is a seeded row answering a real query, and
that cannot happen while real river-flood data exists. Verify rather than assume: confirm
the step-2 river-flood lookup on "Auto (recommended)" resolves a real dataset, and that the
step-4 batch's nine cells all resolved Aqueduct ids.
Verdict: PASS if every dataset traces to a manifest and nothing seeded is answering.
After it passes¶
- Tick the phase-exit box in
phase-9-build-plan.md. - Flip the Current Phase banner and the Phase status row in
plan.md. - Only then may the Full-Catalog claim be made to an audience — and the standing licensing rule still holds: nothing publishes publicly before the licensing verdict.
If a step fails¶
Capture the failing job id, the request_id from the API response, and
./dc logs --tail=200 worker on the host. Open an issue against the phase and link it
here. Do not waive a step to close the phase — Phase 8 exited once on a waived
walkthrough criterion and carried four defects forward into Phase 9; that is the cost
this file exists to avoid repeating.