Phase 9 — build execution plan (waves over #583–#625)¶
Companion to
docs/plan/phase-9-data-foundation.md. The split (2026-08-03) produced 43 backbone issues + ui#100/#101. This file is the operating plan for landing them: what runs in parallel, what serializes and why, and the per-wave mechanics. Update the wave checkboxes as waves land; each wave-session starts by reading this file.
Ground rules (every wave)¶
- FAST MODE (owner decision, 2026-08-04). The phase is built unattended,
back-to-back, with verification deferred: sessions run under the
--ghostprotocol, builders never wait on CI, and the full test/CI pass happens once multiple/all waves are on main (see "End-of-phase verification"). This consciously overrides, for Phase 9 only and by owner decision, the CLAUDE.md rule that each migration merge warrants its ownci.ymldispatch — an unattended session must NOT "correct" the cadence back. Prod is protected regardless: releases stay gated on a SHA-exact green run (ADR-046), so nothing unverified can ship. - Sessions run back-to-back, one at a time. A wave boundary is a session/limit
boundary, nothing more. Start every session with the state check (
git fetch origin main, open-PR list,gh issue list --label milestone:phase-9 --state open,git worktree list) — several sessions share this repo. Never run two orchestrating sessions concurrently; they will fight over the migration head and hotspot files. - Cap concurrency at ~4 builders. Session limits were hit twice on 2026-08-03; four parallel worktrees is the sweet spot between throughput and burn. Schedule the Opus issues (#589 #593 #594 #595 #596 #598 #600 #604 #609, #621=max) early in a fresh limit window.
- Migrations serialize at merge, always. Every migration-bearing issue conflicts on the Alembic head chain. Build in parallel worktrees if files are otherwise disjoint, but merge migration PRs one at a time, rebasing the revision id onto the new head. Typed Alembic ops only — in fast mode a migration reaches main with NOTHING having executed it (no local Postgres, no CI until the checkpoint), so the 2026-08-01 raw-SQL lesson applies double.
- CI cadence: two dispatches for the whole phase. (a) One migration checkpoint
ci.ymldispatch after the last migration-bearing frontier lands (end of wave 3) — migrations chain linearly and are CI-only-verifiable, so a broken one discovered at phase end would poison every revision built on top of it; one ~1-minute run bounds that blast radius and no builder waits on it. (b) The end-of-phase verification pass. Nothing else — no per-wave, no per-merge dispatches. - Targeted tests only, in-flight. Builders run each issue's targeted pytest per its acceptance checks — never the full ~1264-test suite mid-build. The full suite runs once, at end-of-phase verification.
- Stretch stays out. #606, #607, #623, #624, #625 keep their
stretchlabel and are excluded from waves 1–5. Pulling one in is an explicit owner decision. - Ops is a separate track, not a build. Pack issues have a buildable half (tooling, manifests, fixtures) and a manual ops half (real downloads → Hetzner raw zone → prod ingest). Ghost sessions never mutate prod — ops runs happen owner-side (WSL→ssh) as each pack's code merges. Start CDS/EWDS license acceptances early; the queues are the long pole of the phase.
- Four perils are enum-only until wave 3 —
tests/hazard_wiring.pyis the register. #599 addedcoastal_flood,heatwave,earthquakeanddroughttohazard_type_enumin wave 1, but their wiring is wave 3.UNWIRED_HAZARD_TYPESintests/hazard_wiring.pynames each one beside the issue that wires it (#602 / #603 / #604 / #606), and every "each hazard type is covered" test derives its expectation from that one map. The guard stays live: the wired set is the derived complement, so a newHazardTypemember is held to full coverage automatically. If a wave ever adds another enum member ahead of its wiring, add it to that map with its issue number — never weaken or skip a coverage test instead.tests/test_hazard_wiring.pyfails if a listed peril turns out to be wired, so deleting your own entry is part of wiring the peril. (#657 → PR #658.)
Wave 0 — owner, no code (can run any time, gates the phase exit)¶
- [x] Six-moment Showcase walkthrough on prod — PASSED, on the owner's report (2026-08-10).
#550 is closed on that report, not on any agent-side verification: no session
observed
coverage.considered == N. Prod at the time ran v0.11.0 =339f2cd(GET /v1/info, checked 2026-08-10). Thebaseline-column caution below is discharged — #631's fix (cdcc6f4, PR #644) is contained in v0.8.0 and every tag since, so the 2×3 SSP grid's baseline column is safe to read. Superseded detail, kept for the record: the box originally read "prod runs v0.7.0 backbone + ui v0.4.0 as of 2026-08-04" and warned not to read abaselinecolumn until #631 landed. Moment 4 drives the 2×3 SSP grid (ssp2-4.5 / ssp5-8.5 × 2030/2050/2080). - #558 no longer rides this walkthrough — but check that was intended. It was closed
2026-08-04T15:14:45ZbyCortoMaltese3with no closing comment and no linked commit, 3.5 minutes after its own last status comment recorded acceptance items 3–4 as still riding this walkthrough ("no matrix run has ever executed against production"). The close may be deliberate; nothing in the record says so. If the walkthrough's matrix moment fails, reopen #558 rather than filing a new issue.
Wave 1 — foundations (7 issues)¶
Parallel batch (disjoint files): #583 (identity columns, migration), #586 (storage keys + ADR-049), #597 (real DatasetSource URLs — needs outbound network for fixtures), #598 (CDS/EWDS client, L), #605 (OpenQuake spike, docs), #609 (SDK risk-API backfill, L).
Serial tail: #599 (enum members — merge after #583 lands the head), then #592
(IngestAdapter ABC — after #586; both touch aggregate_and_commit.py).
- [x] Wave 1 landed 2026-08-05 — all 8 issues merged: #583→PR #646, #609→#647,
#605→#648, #599→#649, #598→#650, #586→#651, #597→#654, #592→#655; trunk
2295369. Two follow-ups off this wave, both landed: #652/#653 (RLS org GUC dropped aftercompute_impact's mid-task commit) → PR #656, and #657 (14 coverage tests red — #599's enum members outran their wiring) → PR #658, trunk358674b, suite back to 0 failed. See ground rule 9 before touching a hazard-coverage test.
Wave 2 — second-order contracts + mechanism (6 issues)¶
#584 (exposure_datasets ←583), #585 (taxonomy ←583), #589 (dataset_id into surfaces, L), #600 (indicator-peril mechanism, L), #595 (out-of-core aggregation, L ←592 file-wise), #593 (NetCDF ←592).
Constraints: #589 and #600 both touch lookup_service.py → serialize those two
(build both, merge #589 first, rebase #600). Migration merge order: #584 → #585 → #589.
- [x] Wave 2 landed 2026-08-05/06 — all 6 issues merged: #595→PR #663, #584→#664,
#593→#665, #585→#666, #589→#667, #600→#670. The first
ci.ymldispatch covering wave 2 came back RED: two defects in migration0070(#584/#664), both invisible to lint/mypy/the local non-DB suite —func.min(exposures.org_id)(Postgres has nomin(uuid)), fixed by PR #672, and a new FK with no parent-row writer, filed as #673 and fixed by PR #679. Further fixes landed as #682, plus #684 as residue.
Wave 3 — consumers (10 issues, two sub-batches)¶
3a parallel: #587 (manifests ←586), #588 (catalog producer ←583/584), #590 (risk selector ←589/583), #594 (raster→exposure ←592/584), #596 (retire centroid rows ←595),
601 (WF-FWI indicator ←600).¶
3b serial chain (shared impact_function_seeder.py + layer_spec.py + migrations +
tests/hazard_wiring.py): #602 (coastal) → #603 (heatwave) → #604 (earthquake, L) →
621 (Huizinga re-derivation, Opus·max — license-boundary judgment). These were run as¶
a stacked chain so the PRs did not fight over the seeder. (Historical note: this line
originally called /build-chain 602 603 604 621. That skill no longer exists — it was
deleted and its batch behaviour folded into /analyze. Serialise a hotspot chain with
successive /build N calls instead.)
Per ground rule 9, #602, #603 and #604 each delete their own UNWIRED_HAZARD_TYPES
entry from tests/hazard_wiring.py as part of wiring — that file is a fourth hotspot
for this chain, which is one more reason to stack rather than parallelise. #606 (drought,
stretch) owns the remaining entry whenever it is pulled in; until then the register is
what keeps the coverage guards green without switching them off. #601 (WF-FWI) has no
entry — wildfire was already wired before #599.
- [x] Wave 3 landed · MIGRATION CHECKPOINT:
ci.ymldispatch ran. 3a: #594→PR #668, #588→#669, #587→#671, #601→#674, #590→#675; #596 closed on reconcile with no closing-PR reference (its work landed in PR #681, which lacked aResolvesfooter). 3b serial chain: #602→#676, #603→#688, #621→#689, #604→#691.
Wave 4 — surface + resolver (4 issues)¶
#591 (matrix pinning ←590), #608 (catalog browse ←583/584/585), #610 (SDK selector ←609/590/608), and file the climate-lama-engine issue for the CF/EQ hazard codes if wave 3 didn't (referenced by #599/#602/#604). ui#100/#101 unblock after this wave — they build in the UI repo, not here.
- [x] Wave 4 landed — all 3 issues merged: #608→PR #693, #610→#694, #591→#695.
Wave 5 — pack code halves (11 issues)¶
#611 first (creates the volume-budget ledger every other pack appends to), then
parallel: #612, #613, #614, #615, #616, #617, #618, #619, #620; #622 last (←611).
docs/data/volume-budget.md is a shared file — trivial merge conflicts, just rebase.
- [x] Wave 5 landed 2026-08-06/07 — all 11 issues merged: #611→PR #700, #612→#701,
#613→#705, #614→#712, #615→#707, #616→#709, #617→#714, #618→#702, #619→#704,
#620→#703, #622→#713; plus the deferred ledger Totals row →#715. No Alembic
migration in the whole of wave 5 — head stayed
0078. The plan's "#611 first, then all nine in parallel" was wrong in an instructive way: the five hazard-pack issues (#611/#612/#618/#619/#620) each listedsrc/climate_lama/ingest/sources/in their Files line, implying a 5-deep serial chain on that directory, butCopernicusCdsSourceis driven by an operator-declared retrieval registry parsed from settings/env — registering a CDS/EWDS retrieval is CONFIG, not code, andSOURCE_CLASSESwas never touched by any wave-5 pack. Separately,docs/data/volume-budget.md's pre-stubbed rows sit on consecutive lines, so concurrent builders editing different rows still conflicted on git's 3-line diff context (#616 and #614 both had to rebase) — the doc's "trivial merge conflicts, just rebase" claim was accurate in effect but understated the frequency.
Known gaps at wave-5 exit¶
- #706 — NetCDF ingest built but unreachable from the dispatch path. CLOSED (PR #717,
per-job
source_formatoverride). It was the first of six successive blockers on the OPS half of #614/#615/#616 (all CDS-only, hence NetCDF-only); all six are now closed — #706, #761's two worker-side gaps (ADR-064), #782 (PR #794), #791 (PR #798, ADR-067), #793 (PR #800, ADR-068), #797 (PR #804, ADR-069). SUPERSEDED 2026-08-10 — the paragraph that followed is kept verbatim because it is the record of what was believed, but every clause of it is now false: "Those ops halves are nevertheless still blocked, now by an evidence gate rather than a code gap: no NetCDF ingest has ever run end to end, soassert_netcdf_adapter_wiredrefuses every dispatch and #761 stays open to track it." What is true instead: the first end-to-end NetCDF ingest ran against prod on 2026-08-10 (hazard_datasetsc83be4f5-5bb1-4bef-82d8-a100061163cc), which satisfied the gate; PR #825 (42f2a35) then deleted layer 2 ofassert_netcdf_adapter_wired, its one-shot--first-end-to-end-runescape and theADAPTER_GAP_ISSUE_URLconstant, and #761 is CLOSED. The surviving guard is the plan-time adapter-resolution check only and refuses nothing that resolves to the right reader. All three ops halves then ran: #615 and #616 are fully ingested, #614's EWDS half is ingested and its C3S half needs only rotated-pole reprojection (the ZIP-per-experiment blocker is fixed by ADR-071/PR #822; PR #819 added the grid guard). See phase-9-ops-plan.md's OPS-4 block and volume-budget.md for the dataset ids and measured volumes. ReadNETCDF_INGEST_BLOCKERSinscripts/packs/_wildfire_fwi_common.pyon trunk — never a remembered issue number. GeoTIFF packs are unaffected. - #708 — LISCOAST turned out to be irregularly-spaced coastal transect point CSVs, not a raster; no ingest path takes scattered points, so #613 shipped its Aqueduct-coastal half only.
- #550 — CLOSED 2026-08-10 on the owner's report that the wave-0 walkthrough passed
against prod (see Wave 0 above). It was never a code gap: every engineering
acceptance item landed in PR #570 (ADR-047, union rollup) plus ui#99, and all three are
asserted by DB-backed tests in
tests/test_api/test_risk_portfolio_uploads.py(coverage.considered == N, uploaded rows inworst_memberswithmember_kind == exposure, and the stated-partial case), green in CI run31128301400. The wave-0 prod walkthrough was the only thing outstanding on it, and it has now passed. Do not route this issue to a builder; there is nothing left to build. - Stretch set still excluded: #412, #606, #607, #623, #624, #625.
Ops track (owner, interleaved from wave 3 onward)¶
- [x] CDS + EWDS per-dataset license acceptances (portal, manual) — DONE by the owner
2026-08-07. Recorded in phase-9-ops-plan.md (header), which
supersedes both this box and the "still need their licence acceptances" line in
phase-9-full-catalog-walkthrough.md.
Corroborated by a real authenticated EWDS retrieval succeeding on 2026-08-09:
cems-fire-historical-v1, jobIDfd5475dd-c034-4ff5-a696-83e8314a3952,201→ completed,application/netcdf(see #802 and the #614 row in volume-budget.md) — an unaccepted licence would have been rejected at submission. Do not re-ask the owner about Copernicus licences. - [ ] One live polling→review→approve→ingest cycle documented (#597 manual check — exit criterion)
- [ ] Pack downloads → raw zone → prod ingest, per pack as its code merges (budget row first)
- [ ] Storage bill vs volume-budget ledger check (exit criterion)
Execution runbook for the items above: docs/plan/phase-9-ops-plan.md (supersedes ground rule 8 for the pack-ops waves — see that doc).
Mechanics per session (fast mode)¶
Invocation: /phase-build phase-9 --ghost --rr — phase-build owns partitioning,
worktrees, and landing; --ghost makes the session unattended (no mid-run questions:
reversible calls are made and logged, owner-only steps deferred to a "Run these
yourself" block); --rr is subsumed by ghost's reassess rule. /forge phase-9 --ghost
routes to the identical command (issues exist, so forge skips its planner) — use forge
as the front door for new things discovered mid-phase instead: /forge "bug: <what
broke>" --ghost turns a wave's fallout into a routed reproduce-first /fix without
ceremony, and keeps discoveries out of the wave issues' scope.
- One invocation per session; ~one wave per session. Do NOT attempt the whole phase in one run — context and limits won't hold it. phase-build keeps building as deps merge, so a run that clears its wave and rolls into the next frontier is fine — free speed — EXCEPT at the wave-3 boundary, which is HARD: the session covering wave 3b must stop after the 602→603→604→621 chain lands, dispatch the migration-checkpoint ci.yml, and not start wave-4 issues (write this into that session's prompt). The next session checks the checkpoint result first and fixes forward if red. Waves 4 and 5 may fuse into one session if capacity allows — no checkpoint sits between them.
- Bound each ghost run deterministically with
--max-runtimesized to its wave — nobody is watching to stop an unattended run manually. - If a session dies mid-wave, the next rerun reconciles half-states (open PRs, dirty worktrees) before building anything new.
- Ghost rule: subagents inherit nothing — the orchestrating session must carry rules 4/6 (migration merge order, targeted-tests-only) into every builder prompt it writes.
- Escape hatch for hotspot chains: successive single
/build Ncalls, serialised — one lands before the next starts, so they stack instead of colliding on the shared file. (The/build-chainskill this line used to name has been deleted; do not try to invoke it./analyze N1 N2 …sizes a batch, but it does not build or stack anything.) A single/build Nalso remains the tool for a straggler. - NEVER add the
cilabel to any PR; land-wait merges onmergeable(NO-PR-CI).
End-of-phase verification (the deferred bill, one session)¶
- [x]
ruff+mypygreen on8be5770(2026-08-07).uvx ruff@0.16.1 check→ "All checks passed!";uvx ruff@0.16.1 format --check→ "467 files already formatted";mypy→ "Success: no issues found in 213 source files". Both ruff commands run at CI's exact scope (src/ tests/ sdk/python/— notscripts/) and CI's pinned version. The two other cheap CI steps also pass locally:cmp LICENSE sdk/python/LICENSEanduv lock --check. - [x] Full local suite run on
8be5770—3 failed, 2123 passed, 45 skipped, 684 errors in 596s. The 684 errors are the documented no-Postgres baseline, not failures. All 3 failures are local-environment artifacts and each PASSES in CI on this same code (verified by name in the run-31128301400job log, not inferred):tests/scripts/test_windstorm_c3s_wisc_pack.py::test_register_netcdf_layouts_is_idempotent— a real forward-compatibility defect, filed separately.NetCDFDatasetSpecis a@dataclass(frozen=True)whosenodataisNaNfor the WISC pack, so idempotent re-registration depends onnan == nanbeing true. It is, on CI's Python 3.12, because dataclass__eq__compared field tuples and CPython's tuple compare short-circuits on identity. Python 3.14 compares fields directly and loses that shortcut. Measured locally:astuple(a) == astuple(b)isTruewhilea == bisFalse, witha.nodata is b.nodataalsoTrue.tests/test_core/test_config_security.py::test_dev_allows_placeholders— not hermetic. The repo's gitignored.envsetsAPP_SECRET_KEY=dev-only-not-a-secret, whichSettingsreads; the test asserts the built-in placeholder. CI has no.env.tests/scripts/test_assert_deployed_contains.py::test_exits_nonzero_and_distinct_when_git_sha_unknown—subprocess.TimeoutExpiredat 30s shelling out toscripts/assert_deployed_contains.shthroughC:\Program Files\Git\bin\bash.exe. Windows Git-Bash process startup under CPU contention (a second agent's suite was running); no equivalent cost on Linux. Standing caveat: the local suite is not a substitute for CI. It reaches ~72% of the suite (2171 non-error local items vs 2748 in CI) because every DB-backed test errors out. The CI run below is the authoritative gate.
- [x]
gh workflow run ci.yml --ref maingreen (executes every migration from scratch) — run31128301400on trunk146f384completed success, 17 steps (full lint + format + mypy + the complete 2748-test suite, executing every Alembic migration from scratch). Verified at job level, per ADR-046's rule that a run of skipped jobs is not a tested run:Checksiscompleted/successwith 17 steps;Slow tests (memory-bound, opt-in)isskippedby design. Every commit since is docs-only (146f384..8be5770touchesdocs/plan/alone), and no test readsdocs/— the only test that reads a markdown file istests/test_changelog.py, and it readsCHANGELOG.mdonly. Confirming dispatch on the phase-exit HEAD: run31128819605one8ebbc0—completed/success,Checksjob 17 steps (2026-08-07). That HEAD also contains #717, the concurrent session's #706 fix, which landed mid-pass. Only this line's own record commit sits abovee8ebbc0, and it is docs-only for the reason stated above, so no further dispatch was spent on it. -
[x]
sdk-smoke.ymlgreen — DISCHARGED 2026-08-07. Run31149358628ond4f46d1,End-to-end smokesuccess, 19 steps (genuinely ran, not skipped) — see the Release state table below. Both defects the dispatches uncovered are closed: #721 (PR #726, national GADM admin-0 Greece boundary) and #729 (closed 2026-08-07). Read the rest of this bullet as a historical trail, not as current status — it was written top-down as the dispatches happened, so its earlier paragraphs say "still RED" and "blocks the v0.9.0 release". Neither holds; v0.9.0 shipped, and prod has since moved on twice more (v0.11.0, below).Historical trail (2026-08-07) — superseded
At the time this was genuinely outstanding, and the gap was #610 specifically. The newest green smoke was run
31008099833on5617168(2026-08-05), which does contain #609 (PR #647) but not #610 (PR #694, the wave-4 SDK selector) — confirmed withgit merge-base --is-ancestor. So the selector surface has never been smoke-tested. That run is also real, not vacuous: itsEnd-to-end smokejob iscompleted/success, not skipped, so theSDK_SMOKE_*credentials were present (the #470 fail-closed split). One dispatch on the phase-exit HEAD settles this; this is the plan-doc-authorized exception to CLAUDE.md's "smoke is a release gate only" rule. Dispatched 2026-08-07 one8ebbc0— run31128820087came back RED, and it caught a real regression rather than a flake.Detect smoke credentialspassed (so the smoke genuinely ran);End-to-end smokefailed atPOST /v1/hazards/ingest -> 422 E_VALIDATION: Unknown region 'Greece', which madescripts/demo.py --seed-onlyexit 1. Cause: #666 (9797f0c) added region validation againstadmin_boundaries, andseed_demoseeds only the Attica boundary while dispatching its ingest with region "Greece" — the seeder arms a guard that then rejects its own next call.9797f0cis not an ancestor of5617168, which is precisely why the previous smoke was green. Filed as #721 with the fix options; it wants a decision (seed a national boundary vs. relabel the demo surface), so it was not patched blind.#721 is FIXED and the fix is verified working (PR #726 — seeds the national GADM admin-0 Greece boundary, so the region the seeder ingests under is one it loads). Smoke run
31130897341shows step [2] nowOK ... completed in 5.4s; the 422 is gone.The smoke is still RED, on a second and different defect that #721's fix merely made reachable — the run had never got past step 2 before. It now fails at step [7]:
GET /v1/impact-functions?haz_type=river_floodreturns an empty list immediately afterPOST /v1/impact-functions/seedlogged "Seeded 12 built-in impact functions", sodemo.py --seed-onlydies onfunctions[0]→IndexError. Filed as #729, with #689 (Huizinga curve re-derivation, migration0077) and #710 (refuse built-in curves aimed at the wrong exposure type) as the named suspects — both landed after5617168, the last fully green smoke. #729 was then the only thing between trunk and a green phase-exit gate, and it blocked the v0.9.0 release under ADR-046. It was fixed and closed the same night; the smoke went green ond4f46d1and v0.9.0 shipped. - [ ] If red: fix-forward on main, newest-suspect-first;git revertper the CLAUDE.md protocol only if a fix isn't quick. Budget a real session for this — it is the price of the deferral, paid once instead of 5+ times. - [ ] Then the Full-Catalog walkthrough (phase exit) and, when releasing,scripts/verify_ci_green.sh --sha $(git rev-parse HEAD)before any tag (ADR-046).
Release state — prod runs v0.11.0 = 339f2cd (checked 2026-08-10)¶
Current, verified this run: curl -fsS https://api.climate-lama.online/v1/info →
{"version":"0.11.0","environment":"prod","git_sha":"339f2cdf2f2598be7a0245779cb59b37181f82c3"}.
Prod is BEHIND trunk, deliberately — do not read this as "prod matches main". Trunk is
d4b2cc5 and 339f2cd is an ancestor of it (git merge-base --is-ancestor exit 0), with
7 commits on main not deployed: PRs #798, #800, #803, #804, #806, #807, #809 — i.e. the
whole #791/#793/#797/#802/#803/#805 NetCDF-and-baseline fix run. Anything testing
that surface against prod is testing code that is not deployed; re-check /v1/info and
git merge-base --is-ancestor <fix-sha> <deployed-sha> before assuming otherwise.
v0.9.0 — shipped and deployed 2026-08-07 05:29 UTC (superseded by v0.10.0, then v0.11.0)¶
v0.9.0 = d4f46d1 carried the whole Phase 9 code half. Its gates, kept as the release-gate
worked example:
| Gate | Evidence |
|---|---|
ci.yml |
run 31149330231 on d4f46d1 — Checks success, 17 steps |
sdk-smoke.yml |
run 31149358628 on d4f46d1 — End-to-end smoke success, 19 steps (genuinely ran, not skipped) |
| Local pre-tag gate | scripts/verify_ci_green.sh --sha d4f46d1 → PASS, 0 billed minutes |
release.yml |
run 31150185847 — verify-ci + both image builds success |
deploy-prod.yml |
run 31150397303 — every step green, incl. alembic upgrade head (0070–0078) and the at-head assertion |
UI shipped separately as v0.5.0 on 2026-08-07 12:23 UTC, closing the gap the backbone
deploy left open (that deploy passed ui_tag empty, so the UI stayed on v0.4.0 and
ui#100/#101 were absent — this is the note that used to record it, and the Full-Catalog
runbook's blocker 1 no longer applies).
| Gate | Evidence |
|---|---|
UI ci.yml |
run 31177206148 on 0589dfb — build success (job present, not skipped) |
| API contract | every field the new UI reads (provenance, eai_grade, eai_grade_note, dataset, dataset_id, provider, upstream_version, indicator) is emitted by the deployed backbone at d4f46d1 — checked in core/lookup_service.py and core/rollup_service.py; dataset_id is accepted on all four /v1/risk rungs |
UI publish.yml |
run 31177798315 on tag v0.5.0 — GHCR image built and pushed |
deploy-prod.yml |
run 31177920318 — UI-only; backbone/migrate step skipped, backbone untouched at v0.9.0 |
| Live | docker ps → climate-lama-ui:v0.5.0, UI_TAG=v0.5.0; served /assets/index-*.js contains Screening-grade EAI, Auto (recommended), Indicator (not lossable), eai_grade, dataset_id |
v0.5.0 carries ui#100 (dataset picker per hazard), ui#101 (dataset provenance chip),
ui#109 (eai_grade screening-grade label) and ui#107 (tour behind TOUR_ENABLED).
Rollback: re-dispatch deploy-prod.yml with ui_tag=v0.4.0 — the prune step keeps the
previous image on the host, so no re-pull is needed.
A UI-only deploy verifies far less than a backbone one.
Verify deployed identityis gated onbackbone_tagand skips, and theHealth gateonly asserts HTTP 200 — which a healthy old container also passes. A green run therefore does not prove the UI container swapped. Confirm withdocker psplus a grep of the served bundle, as in the last row above. Also note the UI'spublish.ymlhas noverify-cijob: unlike ADR-046 on the backbone, nothing mechanically stops a red UI commit being tagged, so the green-CI-on-this-exact-SHA check is manual discipline.
deploy-prod.yml takes its own backup (keeps 10), migrates, health-gates, verifies
deployed identity and prunes superseded images — the manual pre-migration pg_dump from
the older deploy recipe is not needed. This repo cuts no GitHub Release objects (only
v0.1.0/v0.4.0 have one), so the tag + GHCR images + CHANGELOG are the release; GitHub
consequently still badges v0.4.0 as "Latest".
Carry-forward risk: #729 is closed but was never root-caused, and it is INTERMITTENT. (2026-08-10: still the standing advice. #729 remains closed and has not recurred; the
sdk-smokegate has separately flaked once since, on #799 — re-run the same job on the same SHA before calling either a regression.)git diff b23b236 d4f46d1oversrc/andscripts/is empty — the identical product code failed the smoke, then passed it. The failure breaksscripts/demo.py --seed-only, which is the seeded-demo path the wave-0 walkthrough depends on. If a walkthrough comes up with no seeded data, suspect this first and reopen #729 rather than filing a fresh issue. PR #732 added the guarding tests, so a recurrence should now surface inci.ymlrather than only in a smoke run.
Phase-exit status — resynced 2026-08-10 against trunk d4b2cc5¶
NOT closed. The build is done; one walkthrough and part of the ops track are not.
All 43 wave issues are merged, and the code half of Phase 9 is finished. #706 closed (PR #717) and #550 closed 2026-08-10 on the owner's report of the wave-0 walkthrough.
The two "hard blockers" this section used to list are both discharged:
- ~~
sdk-smokeis RED on trunk (#721).~~ Discharged. #721 closed (PR #726), and the follow-on #729 closed too; the smoke went green ond4f46d1(run31149358628). - ~~Prod is 46 commits behind trunk with none of waves 4–5 (
v0.8.0,5617168).~~ Discharged. Waves 4–5 shipped in v0.9.0 and prod has since moved to v0.11.0 =339f2cd(verified 2026-08-10). Standing, smaller version of the same caution: prod is still 7 commits behind trunk — see Release state above. That gap is the NetCDF / baseline fix run, not the catalog/pack surface the Full-Catalog walkthrough exercises. (Retained finding: v0.8.0 already contained the #631 fix —cdcc6f4/PR #644 — so the wave-0baseline-column warning was discharged then and remains so.)
| Exit criterion | State |
|---|---|
| Wave-0 six-moment Showcase walkthrough on prod | PASSED 2026-08-10 — on the owner's report; closed #550 |
| Full-Catalog walkthrough (the phase's verifiable goal) | outstanding — owner-only; owner running it separately. All six steps are now walkable: step 4's criterion was amended 2026-08-11 (≥2 hazards → river flood's full 3 × 3 grid) because no second hazard can qualify without new science — rationale, evidence and reinstatement condition in the walkthrough doc; closed #836. Step 1's "no blanks" was clarified to name the three honest blanks ADR-067 deliberately produces |
| One live polling→review→approve→ingest cycle (#597) | Re-run live on v0.14.0 2026-08-12. #830 is FIXED and proven; the blocker moved to #865. poll ✅ an on-demand dispatch queued 4 rows, 3 of them copernicus_cds items the poll discovered on its own. review ✅ GET /v1/admin/pending-ingests → 200. approve ✅ → approved, handoff: dispatched. ingest ⚠️ now reaches real work — stage_source staged 9 rasters (2.67 GiB) in 200 s and validate_source passed them — then plan_chunks refused with E_INGEST_PLAN_REJECTED: the JRC source is full-continent (110162 × 51992 = 5.7 × 10⁹ cells against a 10⁹ ceiling). Typed refusal, no chunk rows, no partial dataset. Clipping to the Greece bbox gives ~1.03 × 10⁸ cells (~1.8% of the ceiling), so #865 alone unblocks this — #446's out-of-core aggregation is not required. Note the earlier 22-row queue is gone: all were rejected on 2026-08-11 as correct triage (nothing was ingestable pre-#830), and a rejection is absorbing — see #870 |
| Storage bill vs volume-budget ledger check | DONE 2026-08-08 (OPS-5d): bucket 0.325 GB vs the ledger's ≈24.6 GB transient raw estimate — expected, and ratified by ADR-070. Re-measured 2026-08-10 after OPS-4 and the trash/ purge: 158 objects / 359.87 MB vs a recomputed ledger raw sum of ≈24.87 GB — same gap, same reason |
| CDS/EWDS licence acceptances → pack downloads → prod ingest | DONE 2026-08-10 — supersedes the "partial … 7 of 10 ledger rows are ingested; the 3 remaining are blocked on the assert_netcdf_adapter_wired evidence gate (#761, open) and on OPS-4b's CDS-form inputs" that stood here. Licences accepted 2026-08-07; all 10 ledger rows now read ingested, two of them partially by the ledger's own vocabulary — #614 (EWDS half; C3S half needs rotated-pole reprojection) and #615 (C3S half; WISC not retrieved, because its process schema publishes no spatial input at all). Both cited blockers are gone: #761 is CLOSED and the evidence gate was deleted by PR #825, and OPS-4b's CDS-form values were resolved and its retrievals delivered |
Runbook and live status for the ops rows: phase-9-ops-plan.md.
The earthquake cross-repo gate is TRIPPED AND SATISFIED — no longer outstanding.
#617's earthquake data must not reach prod before climate-lama-ui#109 (render
eai_grade / eai_grade_note), or the UI presents screening-grade EAI as if it were
computed-grade. Both halves have now happened, in the safe order: ui#109 closed
2026-08-07, and the ESHM20 dataset reached prod on 2026-08-10 (1 active row,
scenario = baseline, 4 ESRM20 curves). Nothing further is owed here — do not re-raise
it as a blocker.
Do not flip the plan.md banner to "Phase 9 closed" on the strength of the merge
count. The phase's stated goal is a walkthrough, not a milestone tally; Phase 8
already exited once on a waived walkthrough criterion and carried four defects forward.