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 stack —
https://climate-lama.online(UI) andhttps://api.climate-lama.online(API), against the MVP slice in CLAUDE.md and the six Showcase moments in phase-8-foundations-orsa.md. The documenteddocker compose --profile fullstack uppath 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:
- F-5 is fixed.
climate-lama-ui v0.2.0was cut frommainat1879c31and rolled onto prod./lookup,/matrix,/agreementand the composed report button all exist on the live site now. The nav carries them. - 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_floodat an Athens pin. The one peril with data reports as unavailable. Re-scoped accordingly. - 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.
- 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/health → healthy; /v1/info → 0.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/compare — the 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 NULLandgrid_transform IS NULLacross 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:647→HazardUnavailable.NO_DATASET, surfaced asavailable: falsewith 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-339raisesScoreMetricUnavailableErrorrather than substituting an input ("no cross-source fallback"), andBandedScorerequiresmetric_valueas a frozen-dataclass field (scoring.py:220-222). - The UI renders the miss as a designed empty state:
ScoreCardPanel.tsx:63-93returns 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-245andRiskLookupPage.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/compare — all 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] |
Fixed — ui 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:
- #558 (F-10) — cheapest and highest-value: it unblocks moments 4 and 6 together, and the immediate ops unblock is six
assign-centroidscalls. - #548 (F-6) — unblocks moment 1's score card and moment 2's hazard layer. Stream B work.
- #550 (F-8) — needs a product decision first; unblocks moment 3. Stream A adjacent.
- #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.