Skip to content

Phase 8 — Mid-Review

Date: 2026-08-02 (opened, second desk pass, live probe, and owner walkthrough — all same day) Reviewer: Claude (desk half + live probe) and the owner (walkthrough + triage) Status: closed — verdict issued, Phase 8 exits under option A Artifact to walk: the live production stackhttps://climate-lama.online (UI) and https://api.climate-lama.online (API), against the MVP slice in CLAUDE.md and the six Showcase moments in phase-8-foundations-orsa.md. The documented docker compose --profile fullstack up path is unavailable — no Docker on the owner's machine — and is not a prod path anyway (docs/DEPLOYMENT.md § fullstack profile). Framework: per ~/.claude/collaboration-framework.md, Cluster 1 ritual. Evidence pack: phase-8-walkthrough-readiness.md.

Status of this document

Complete. Three passes in one day: a desk audit, a read-only probe of the live stack, and the owner's walkthrough against production after the UI release (F-5) landed. Every check has a result.

# Check State
1 Artifact health ✅ services, data and flags verified
2 Happy path (six moments) 🟥 1 of 6 passed
3 Unhappy paths 🟨 the one exercised behaved honestly
4 UI/UX presentability 🟨 no cosmetic embarrassments; one blank page (F-11)
5 User-path completeness 🟥 no catch-all route (F-11)
6 Promised-but-unbuilt ✅ complete

Verdict

Phase 8 exits under option (A) — amend and exit — decided by the owner 2026-08-02 after the walkthrough.

The walkthrough ran. It did not go well, and the honest record is that one of six moments passed. The desk pass predicted moments 1, 4 and 6 would pass and 2, 3 and 5 would not; the walk inverted that — 1, 4 and 6 failed too, for reasons the desk pass had already identified but underweighted.

What the walk established:

  1. F-5 is fixed. climate-lama-ui v0.2.0 was cut from main at 1879c31 and rolled onto prod. /lookup, /matrix, /agreement and the composed report button all exist on the live site now. The nav carries them.
  2. F-6 is worse than "the map layer is dead". With no COG anywhere, the score card cannot answer either: moment 1 returns "The hazard dataset has no readable raster yet" for river_flood at an Athens pin. The one peril with data reports as unavailable. Re-scoped accordingly.
  3. A new blocking defect, F-10: every scenario-matrix cell fails because the matrix path skips the centroid preflight the single-run path performs. This cascaded into moment 6 — no completed cells, no report.
  4. F-2 held up under observation. The dataless perils rendered exactly as traced: "No dataset is catalogued for this hazard", no bands, no zeros. The drawer showed scheme, surface and dataset provenance for the unavailable peril. The out-of-footprint (France) pin behaved honestly too.

Why exit anyway. Every failure is a data-plane or wiring defect, not a missing feature: the surfaces are built, the routes exist, the data is ingested, and each defect has a named owner issue. F-6, F-8 and F-10 are precisely the work Phase 9 exists to do — holding Phase 8 open for them inverts the dependency, exactly as the §Risks escape hatch anticipated for F-1. The alternative, option (B), would have absorbed Phase 9's first stream into Phase 8 under a different name.

What exiting costs, stated plainly: the product is not currently demonstrable end to end. The Showcase claim must not be made to an audience until F-6 and F-10 close. That is a Phase 9 exit condition now, not a Phase 8 one.

Live probe — what production actually is (2026-08-02, read-only)

Surface Finding
API https://api.climate-lama.online/healthhealthy; /v1/info0.5.0, git_sha 1f1aa2a, environment prod; /docs reachable (200)
API routes Live OpenAPI carries /v1/risk/lookup, /v1/compute/impact/matrix, /v1/compute/impact/matrix/{batch_id}/report, /v1/compute/impact/comparethe backbone half of moments 1, 4, 5, 6 is deployed
UI https://climate-lama.online → 200, serving UI_TAG=v0.1.0
Containers up api, worker, beat, titiler, ui, martin, redis, postgres, plus the shared caddy. No nominatim (by decision, #534); no climada-sidecar (F-9)
Host 38 GB volume at 65%, 13 GB free — the 78%/8.2 GB figure in earlier notes is stale
Flags SCORE_BANDS_ENABLED=true (so bands will render), CL_SEED_DEMO=true, CL_DEMO_USER_PASSWORD set, APP_ENV=prod. No CLIMADA_SIDECAR_URL
.env.prod No duplicate keys remain — #535 appears fixed on the host ahead of its issue being closed

Production database:

haz_type    | scenario  | has_cog | has_grid | count
river_flood | baseline  | f       | f        | 2
river_flood | ssp2-4.5  | f       | f        | 3
river_flood | ssp5-8.5  | f       | f        | 3

assets=5  portfolio_assets=5  exposures=7631
org_risk_surfaces=1  reference_risk_surfaces=1
users=2  orgs=3  portfolios=1

Two readings matter:

  • The Aqueduct scenario pack is ingested. Two scenarios × three horizons of scenario-conditioned river flood exist, so moment 4 has real data — it was the moment most at risk of being unexercisable, and it is not.
  • Not one dataset has a COG or a grid transform. cog_path IS NULL and grid_transform IS NULL across all eight. That is F-6, and it also confirms the Phase 9 audit's claim that #442's grid-membership assignment is dead code in production.

Check 6 — Promised-but-unbuilt ✅

Full evidence in phase-8-walkthrough-readiness.md. Summary: everything the phase promised to build, it built — the defects below are in wiring and release, not in missing implementations.

F-1 · Moment 1 promises "multi-hazard"; Greece has one peril of data — [CRITICAL] (resolved by decision)

The Goal section's moment 1 reads "instant multi-hazard score card." For the Greece-scoped catalogs, river_flood is ingested; wildfire and storm_europe are both status: gap with evaluated sources and reasons recorded inline. tropical_cyclone is absent by design (out of region), not missing.

This was not an implementation failure. 8.9 evaluated the sources, found no public scenario-conditioned or return-period footprints for the two perils, and recorded that verdict in the manifests. The Goal text simply was never amended to match.

Decided 2026-08-02 (owner): ship with fewer perils and say so, per the phase doc's own §Risks escape hatch. No new sourcing work enters Phase 8. Amendment recorded in phase-8-foundations-orsa.md §Goal.

F-2 · Score bands for dataless perils — RESOLVED, re-tagged [ENHANCE-NEXT]

The feared failure is structurally impossible. Traced end to end:

  • The backbone always returns all four perils and marks the dataless ones explicitly: core/lookup_service.py:647HazardUnavailable.NO_DATASET, surfaced as available: false with a machine-readable reason (:828, :835-837). Never omitted, never an error, never a zero — the contract is documented on the endpoint itself (api/v1/risk.py:105-110).
  • A band cannot be fabricated: core/scoring.py:333-339 raises ScoreMetricUnavailableError rather than substituting an input ("no cross-source fallback"), and BandedScore requires metric_value as a frozen-dataclass field (scoring.py:220-222).
  • The UI renders the miss as a designed empty state: ScoreCardPanel.tsx:63-93 returns early with a dashed grey card and a "Not available" pill; the RAG chip is gated on {band && …} at :102.
  • Both sides have negative tests: tests/test_api/test_risk_lookup.py:244-245 and RiskLookupPage.test.tsx:196-202, the latter asserting no /Score/ and no /\b0\b/ appears.

What the walker will see for wildfire / storm_europe / tropical_cyclone: three dashed grey cards reading "No dataset is catalogued for this hazard."

Residual, minor: the out-of-footprint case takes the cog_read_failed path (the lookup's dataset picker has no bbox filter), so a point outside Greece is described as "The raster read failed — try again later." — a transient-sounding message for a permanent coverage boundary. Still no misleading zero. Filed with two other copy defects as climate-lama-ui#97.

F-3 · Wildfire is not computable even once footprints are sourced — [BACKLOG]

Independent of the data gap, WildfireGeoTIFFIngestor declares an FWI intensity unit that no published damage function can consume; no FWI-calibrated vulnerability curve exists. Established across two separate investigations and recorded when #481 was closed as not deliverable.

Recommended action: none in Phase 8 — correctly Phase 9 scope. The route to computable wildfire (brightness temperature / FRP against the already-seeded Lüthi curves, needing no new coefficients) is noted on #412. Two stale assertions that an FWI curve exists should be corrected when that work starts: docs/plan/phase-9-data-foundation.md line ~134 and the e.g. Finney 2005, Riley 2013 pointer in src/climate_lama/core/ingest/wildfire.py's docstring. Neither is sourced.

F-4 · Moments 2 and 3 verified at phase level only — RESOLVED into F-6, F-7, F-8

The suspicion was justified. Reading the implementations found three concrete defects, each now its own issue. F-4 is closed as a finding.

F-5 · Production serves a UI that predates every Phase 8 surface — [CRITICAL]

#547. UI_TAG=v0.1.0 was built 2026-06-11. Grepping the deployed bundle (/assets/index-BZqoj8lF.js, the only JS asset) for route literals: "/portfolios" ×1 and "/datasets" ×1, but "/lookup", "/matrix", "/agreement", v1/risk/lookup, v1/compute/impact/matrix and impact/compareall zero.

So moments 1, 4, 5 and 6 have no UI on the live site. Cause: the UI's publish.yml fires only on v* tags or dispatch, v0.1.0 is the repo's only tag ever, and the last publish run was 2026-06-14. :latest is no help — its enable=${{ github.ref == 'refs/heads/main' }} condition can never be satisfied by a tag push.

Action: cut a UI release from main (CI green 2026-08-01), bump UI_TAG on the host, re-verify the bundle. deploy-prod.yml cannot do this today (#497), so it is a manual host operation. Nothing else in this review can be walked until this lands.

F-6 · No ingest path sets raw_raster_key, so no COG is ever built — [CRITICAL]

#548. cog_path is written only by convert_to_cog (worker/tasks.py:1455), enqueued only under if raw_raster_key: (worker/tasks.py:938-944) — and none of the five producers of ingest_hazard params sets it (api/v1/hazards.py:155-176 and :382-398, worker/dataset_ingest.py:135-146, scripts/seed_demo.py:277-289, scripts/ingest_scenario_hazards.py:429-447). Confirmed against the live DB above: 8/8 datasets have cog_path IS NULL.

Two consequences, and the first is a credibility problem of the same family F-2 was hunting: the Hazard layer toggle renders enabled, with a raster gradient legend, and does nothing at all when clicked — because LayerPanel.tsx:144 treats a non-null spec as available while the spec's source_url is empty (models/layer_spec.py:145). No error, no 404, no toast. The second: the point lookup loses its COG fallback (core/lookup_service.py:649), leaving the score card dependent on the single seeded risk surface.

F-7 · The exposure map layer cannot render, for two independent reasons — [ENHANCE-NEXT]

#549. layer_specs_for_exposure hardcodes "source_url": "" for all three exposure types (models/layer_spec.py:70-95), and the UI requests the layer by dataset_id against a route keyed on primary key (MapView.tsx:89 vs api/v1/exposures.py:336), which 404s and is swallowed at MapView.tsx:93-95. The toggle at least renders disabled — honest, unlike F-6.

Net for moment 2: of the three advertised layers, only Impact (result GeoJSON, not tiles) actually draws.

F-8 · Uploaded portfolio CSVs never reach the scoring plane — [CRITICAL]

#550. Upload writes exposures + portfolio_exposures; the rollup reads portfolio_assets JOIN assets (db/repositories/asset_repository.py:112-175). The two are disjoint, and no API surface creates Asset rows at all — the only Asset(...) construction in the repo is the demo seeder. Production confirms it: assets=5, portfolio_assets=5 (the seeded Attica demo) against exposures=7631.

The demo-hostile shape: uploading into the seeded portfolio shows an unchanged 5-asset rollup that reads as success unless you check the IDs. Uploading into a new portfolio shows coverage: 0 of 0. Two related traps are recorded on the issue — a duplicate-name upload reports "Added 0" as a green success, and the UI nginx caps uploads at 1 MB against the API's declared 50 MB.

F-9 · The CLIMADA sidecar is not deployed, so moment 5 cannot be walked — [ENHANCE-NEXT]

climada-sidecar sits behind the opt-in climada profile (docker-compose.prod.yml:235-240) and is build:-only, while prod never builds from source (ADR-038). CLIMADA_SIDECAR_URL is absent from the live .env.prod, and registry.py:229 gates engine availability on exactly that value. Expect 400 E_ENGINE_UNAVAILABLE.

Standing it up needs a source checkout and a container build on the host. Reasonable to defer: this is the moment the phase doc itself flagged as the hardest, and the agreement view degrades to a single column when only one engine is registered.

F-10 · Every scenario-matrix cell fails; the matrix path skips the centroid preflight — [CRITICAL]

#558. Found in the walk, not the desk pass. All six cells failed with:

No exposure rows found with assigned centroids for this hazard dataset.
Run centroid assignment first.

Dashboard.tsx:7,351-353 runs useCentroidPreflight before a single compute and shows it as "Assigning centroids…". MatrixRunPage.tsx contains no reference to centroids at all, and the backend batch path does not assign either — so the matrix fails on any dataset that has never been through a Dashboard run.

Production state confirms it: each of the seven Aqueduct datasets holds 1,026,000 hazard centroids and zero exposure-centroid assignments. Only JRC River Flood Greece (demo) — the seeder's dataset — has any (68).

This cascaded into moment 6: no completed cells, so the composed report returned "Report failed — please try again."

F-11 · No catch-all route; unmatched paths render a blank page — [ENHANCE-NOW]

ui#98. /dashboard renders header, nav and nothing else. The Dashboard is mounted at / (App.tsx:147) and the route table has no path="*" fallback. During the walk this was initially recorded as "moment 2 is empty" — a blank page is indistinguishable from a crash, and the moment was nearly written off before the URL was the actual problem.

Check 1 — Artifact health ✅

Services, flags, host and database verified under Live probe above; all expected containers up and healthy, the two absentees (nominatim, climada-sidecar) accounted for by decision and by F-9. The UI released during the review (v0.2.0) serves correctly and the nav carries every Phase 8 surface. No crash, no failed boot, no service flapping during the walk.

Check 2 — Happy path: the six moments 🟥 — 1 of 6 passed

Walked 2026-08-02 against https://climate-lama.online on UI v0.2.0 / backbone v0.5.0.

# Moment Result What happened
1 Pin-drop → score card + drawer 🟥 fail Pin at 38.01694, 23.72062 (Athens). River Flood → "Not available — The hazard dataset has no readable raster yet." The other three perils correctly read "No dataset is catalogued for this hazard." Drawer opened and showed scheme, surface (not_built_here / no_cell / plane reference / surface_id null) and dataset attribution (JRC River Flood Greece, CC-BY-4.0). The card works; it has nothing to say. F-6
2 Map → hazard/exposure/result layers not walked /dashboard was tried and is not a route (F-11). The map lives at /. Desk expectation stands: only Impact draws (F-6, F-7)
3 Portfolio CSV → per-asset scores + rollup 🟨 partial Portfolio list renders; a second portfolio was created successfully. The Exposures / Total EAD / Scenario columns are hardcoded em-dashes. Upload → rollup half not exercised; F-8 predicts it will not score
4 Scenario matrix (2 × 3) 🟥 fail Batch dispatched cleanly and all six cells ran — then all six failed. F-10
5 Two engines → agreement view not walked Sidecar not deployed (F-9); the nav entry exists
6 ORSA PDF report pack 🟥 fail "Report failed — please try again" — downstream of F-10, no completed cells to compose

The one unambiguous pass is the honesty of the score card. That is not nothing — it was the finding this review opened most worried about — but it is not a Showcase.

Check 3 — Unhappy paths 🟨

1. Pin outside the Greek data footprint (Paris, 48.78758 2.26679)
   -> "River Flood — Not available. The hazard dataset has no readable raster yet."
      Honest, no zero, no band. BUT: byte-identical to the Athens result, so the
      card cannot currently distinguish "outside coverage" from "no raster built".
      Once F-6 lands, the two must read differently — see ui#97.
2. Malformed portfolio CSV  -> not exercised
3. Peril with status: gap   -> "No dataset is catalogued for this hazard."
      Exactly as F-2 traced. Confirmed on all three gap perils.

Check 4 — UI/UX presentability 🟨

Nothing embarrassing in what rendered: consistent typography, clean nav, sensible empty states, legible failure toasts. No broken images or placeholder text. The two blemishes are structural rather than cosmetic — the blank unmatched route (F-11), and tables whose columns are permanent em-dashes (portfolio list). Not walked: mobile layout.

Check 5 — User-path completeness 🟥

Post-release, all six moments are reachable from the nav (Lookup, Datasets, Portfolios, Scenarios, Measures, Matrix Run, Engine Agreement, Settings, API Keys) — F-5's complaint is resolved. But the map view has no nav entry at all: it is reachable only via the logo or by typing /, and the obvious guess /dashboard renders blank (F-11). That is a genuine completeness gap.

Exit criteria tracking

From phase-8-foundations-orsa.md §Exit criteria:

  • 🟥 Showcase walkthrough runs end-to-end in one sitting, all six moments — not met. Walked 2026-08-02; 1 of 6 passed. Waived under option (A): every failure has a named owner issue and lands in Phase 9's scope. The Showcase claim is not to be made to an audience until F-6 and F-10 close.
  • ✅ Surfaces accumulate from runs with visible size accounting; reference surfaces exist for ≥1 hazard × scenario × horizon triple — 1 org surface + 1 reference surface present in prod.
  • ✅ Zero public-release steps taken (repos private; nothing published).

Findings summary

ID Finding Tag State
F-1 Moment 1 promises multi-hazard; one peril has data [CRITICAL] Resolved by decision — ship fewer perils, say so
F-2 Bands seeded for perils with status: gap [ENHANCE-NEXT] Resolved — naked bands are unconstructible; copy residue → ui#97
F-3 Wildfire not computable (FWI unit mismatch) [BACKLOG] Phase 9 / #412
F-4 Moments 2 & 3 desk-verified at phase level only Resolved into F-6, F-7, F-8
F-5 Prod UI predates every Phase 8 surface [CRITICAL] Fixedui v0.2.0 cut and deployed, #547
F-6 No COG is ever built → hazard layer and score card dead [CRITICAL] #548 — confirmed live in moment 1
F-7 Exposure layer: empty source_url + wrong id [ENHANCE-NEXT] #549
F-8 Uploaded CSV rows never scored [CRITICAL] #550
F-9 CLIMADA sidecar not deployed; moment 5 unwalkable [ENHANCE-NEXT] Deferred — needs an on-host build
F-10 Every matrix cell fails; no centroid preflight on that path [CRITICAL] #558 — cascades into moment 6
F-11 No catch-all route; unmatched paths render blank [ENHANCE-NOW] ui#98

Carried into Phase 9

The four that must close before the Showcase claim can be made, in dependency order:

  1. #558 (F-10) — cheapest and highest-value: it unblocks moments 4 and 6 together, and the immediate ops unblock is six assign-centroids calls.
  2. #548 (F-6) — unblocks moment 1's score card and moment 2's hazard layer. Stream B work.
  3. #550 (F-8) — needs a product decision first; unblocks moment 3. Stream A adjacent.
  4. #549 (F-7) — completes moment 2.

Plus the two UI-repo items (ui#97, ui#98) and F-9's sidecar deployment, none of which gate the phase.