Phase 17 — Revamp: one workspace¶
DEFINED 2026-09-26 by owner instruction (planning session of 2026-09-26) from the owner's
annotated market scan (Business investigation\Market scan (formatted).docx, 93 comments, v.
2026-09-25), the four pace-setting products the owner named (Moody's Intelligent Risk Platform,
Swiss Re CatNet, Munich Re Location Risk Intelligence, Climate X Spectra) and two /reassess fast
passes (three agents each; records below — the second validated the prototype against the comments). Direction: locked by the owner on 2026-09-27 on the
Direction board of the Phase 17 prototype (claude.ai/artifact/1TXwPDAiGY3Pj1tqB9bBN5); the picks and the
owner's two layout notes are folded in below (one reversal: Oasis LMF joins in Phase 17, D19).
Runs beside the last Phase 15 tracker (#1300,
the 24 h poll-health read) with no file overlap. Serves Goal C (the showcase UI reads as one
product) and, through the contracts in wave A and D–E, Goal B.
Status 2026-10-01: built and released. Waves B1–B28 landed every build issue in both milestones (exit
criterion 1; trackers route by Blocker:). The closeout R1 (#1500, ui#567) wrote the plan.md banner
and Phase status row, the amended Goal C (D1), the derivation row settled as one base platform with
role lenses (D2) and the Phase 18 charter stub (D19–D21, D18) —
exit criterion 7's banner and stub. Released 2026-10-01: backbone v0.27.0 at 433e12f5
(ci.yml 36840997605, sdk-smoke.yml 36841001249) and UI v0.17.0 at 0640a63 (ci.yml
36841005142, e2e-smoke.yml 36841008822), deployed by deploy-prod.yml 36844235147. O1 (#1498)
is closed: prod reports assistant: true. O2 (#1499) brought the Oasis sidecar up (deploy
36846154952, oasis in available_engines() in the api and the worker); its three-engine agreement
run and the loaded docker stats reading ride the owner walk. What remains is R2,
#1501: the owner walk (criterion 2)
and mid-review-phase-17.md.
Owner walk 2026-10-01: failed. The walk on prod failed Moments 1 to 4, while Oasis passed its
three-engine agreement (exit 0, +0.3965 %). Every finding, its cause and the change it needs is in
Phase 17b. This phase stays open and closes with Phase 17b's
closing walk and mid-review-phase-17.md.
What it is. The revamp the Phase 14 charter stub deferred, re-cut by the market scan: one workspace instead of three analysis pages, a dense "instrument panel" visual system with a real dark theme, maps that stay smooth at millions of points, a place page for citizens and insurers, adaptation measures, a report studio, charts that explain time and chance, and an assistant that acts only through the product's own actions. Every surface answers from the real APIs; the prototype's figures are illustrative and never enter the product (ADR-077).
What it is not. No new country, peril or billing, and no engine beyond Oasis LMF: those are the Phase 18 charter stub (D21, D19, D20). Property-level results are a spike here, a build there (D18).
Inputs¶
| Input | What it contributes |
|---|---|
| Owner remarks, 2026-09-26 | Pace-setters (Moody's, CatNet, Munich Re LRI, Climate X Spectra); little whitespace; large cards and icons; AI that runs queries and scenarios; phones; maps with zoom-driven aggregates, rich popups, layers, 3D, custom palettes, millions of points; adaptation measures; graphs; property level (investigate) |
| Market-scan comments (docx) | Per-screen likes and dislikes — cited as C-ids below (C6 roles … C112 scenario explainer) |
| ui-revamp.md | 44 products, 25 recurring patterns graded against UI v0.16.0 (this doc supersedes its "Open questions") |
| Prototype artifact (claude.ai/artifact/1TXwPDAiGY3Pj1tqB9bBN5) | Every view designed and clickable, three Explore layouts, two Home and two Lookup layouts, a working assistant (named actions), and the Direction board that records the owner's picks |
| Backbone / UI inventories (2026-09-26) | Ground truth used for every Grounding: line below (backbone 6c00faab, UI v0.16.1 71e9402) |
The frame — what each surface becomes¶
Today (UI v0.16.1) |
Phase 17 | Owner evidence |
|---|---|---|
| Home — card grid, no map, no search | Home — one screen per role lens: headline cards, map, events, recent activity (history), key insights, search | C71, C85, C15, C86 |
Assess /assess (Dashboard.tsx, 2,785 lines), portfolio analysis on /portfolios/:id, Live map /live/:eventId |
Explore — ONE workspace driven by a scope picker (multi-country · country · region · municipality · drawn area · pins · portfolio · asset · event · live); modes Analyse · Compare · Events · Adapt (amended 2026-10-02, Phase 17b EX-16 and K13: the Scope tab picks the exposure first, a portfolio, catalogue exposure datasets of one unit, saved pins or one asset, then an optional Where clip, country, region, municipality or drawn area; event and live stay modes; phase-17b-run-model.md § 7) | C65, C64, C18, C22, C70, C87 |
| Lookup — side rail of prose cards over a raster | Lookup — a place page: headline + hazard groups, hazard tabs, this-year/15/30-year chances, horizon ruler, past events, key insights, community view, place report | C46, C96, C101–C104, C106, C108 |
| Portfolios — table; detail is one long scrolling column | Portfolios — cards with a score-per-horizon ruler; detail = map left, synopsis right, directory bottom; upload with templates, addresses or pins | C84, C86, C28, C72, C73, C13 |
/measures (hidden, 0 measures on prod) + CostBenefitPanel |
Adapt — measure library with sketches and own images, editor, apply to asset/portfolio/area, adapted vs unadapted map, money cards, evolution chart | C62, C66, C68, C81, C82 |
| PDF/CSV export on Assess, ORSA pack per batch | Reports — studio: framework templates, widgets from saved analyses, notes, assistant draft, coverage checklist | C16, C41, C46, C63, C20 |
| Datasets — filter bar, list + large empty inspector | Data — dense list, retractable drawer, full page, upload templates, "what can be computed where" matrix | C12, C73, C76, C13, C72, C51 |
| Runs — one table | Analyses — visual run cards, running strip, one-look run summary, reproduce bundle, compare tray | C38, C57, C19, C59 |
| Live | Folded into Explore → Events (behind live_mode), plus a Home card |
C10, C34, C65 |
| Settings, Admin | Settings — role lens, appearance (themes, palettes, basemaps), alerts, assistant, API keys. Admin — health, engines registry, feeds, pipeline, usage, audit | C67, C11, C74, C79, C92, C58 |
Direction lock (owner picks on the Direction board)¶
An unpicked item follows the recommendation. Confirmed is filled from the board's shared store
before /phase-split (the session that splits reads the picks with ArtifactData list picks).
| Id | Decision | Recommended | Confirmed |
|---|---|---|---|
| V1 | Home layout | A — command centre | A (2026-09-27) |
| V2 | Explore layout | A — instrument (docks + strip) | A, with an owner note: both docks retract and resize; on phones the map chrome never blocks the map (B-U3) |
| V3 | Lookup layout | A — place report | A, with an owner note: the right panel resizes to give the map more room (C-U2) |
| D1–D17 | Direction decisions (board items; they map to D1–D13, D15, D18–D20 below) | as the design record below | all as recommended except board item 14 (engines, D19): Oasis LMF moves into Phase 17 |
Design record (D1–D22 — one line each: the decision, then the constraint that binds a builder)¶
- D1 — One Explore workspace, staged — wave B merges Assess and the portfolio analysis; wave C folds Live in as Events mode behind
live_mode. Redirects re-target directly (never chained):/assess,/dashboard→/explore?…(scenario/horizon/batch params carried);/matrix?batchId→/explore?batch=;/portfolios/:idanalysis →/explore?scope=portfolio&id=(the list stays in Portfolios) (amended 2026-10-02, Phase 17b EX-16: the target becomes/explore?exposure=portfolio:<id>, the oldscope=portfolio&id=is rewritten to it, and the events redirect carries the book the same way; see phase-17b-run-model.md § 7);/live/:eventIdand/portfolios/:id/events/:eventId→/explore?mode=events&event=;/runs→/analyses;/runs/compareand/compare→/explore?mode=compare&a=&b=(amended at the split, 2026-09-28: the prototype has no Analyses compare view — ui#523);/scenarios(today →/runs, App.tsx:278) →/analyses;/datasets→/data;/measures→/adapt.NAV_ITEMSand its test change in the same issue as the route. plan.md Goal C wording ("showcase, not the do-it-all platform") is amended in the closeout. - D2 — Role lens, not permission — Insurer · Scientist (plan.md's "researcher") · Decision-maker · Citizen shape Home, nav order and wording only; stored as
UserPreferences.persona+OrgSettings.default_persona(schema-only — both models areextra="allow", no migration). Un-parks plan.md's "Client-UI derivation model" as "one base platform with role lenses" (C6, C18). Never read byrequire_role. - D3 — Dense instrument-panel system — viewport-exact shell (retractable icon rail, top bar with centred search, bottom tabs on phones), no page scroll at 1440×900 on Home/Explore/Lookup/Portfolios detail/Adapt/Reports/Data, big numerals, one hazard icon set, dark key-figure strip over maps, a density preference (comfortable · compact) on the same tokens. Built as semantic Tailwind colours on CSS variables (extending
branding/themes.ts::applyTheme),lucide-react(ISC) + custom hazard glyphs, fonts self-hosted (@fontsource-variable/archivo, JetBrains Mono — OFL; the product is offline-capable). A guard test counts hard-codedgray-N/bg-white/hex classes: the count may only fall, migrated components are at zero. - D4 — Aggregates as vector tiles — result and exposure aggregates are computed server-side and served by the API as MVT (
ST_AsMVT): H3 cells per resolution chosen from zoom (reusingcore/surface_writer.py's H3 pattern, res ≤ 8 as the DB check allows), GADM admin levels with aggregates, and asset points from z 11. The old martin path was retired for having no consumer (#562, migration 0067) — this one ships with its consumer. The GeoJSON admin path (/results/{id}/geojson) stays as fallback. deck.gl is adopted only if the revived performance suite fails. - D5 — MapLibre 6 + one MapKit — upgrade
maplibre-gl4.7.1 → 6.x (ESM; Vite bundles it) in wave A; every map in the app goes through oneMapKitcomponent (basemaps, layer catalogue grouped by hazard family, palettes + legend, rich popup card with hazard icons, timeline player, swipe and side-by-side compare, 3D extrusion; terrain only when a DEM source is configured). Today 12 files construct maps; each surface issue deletes its old map when it moves to MapKit. A-U5 extends the shared jsdom map mock that already exists atsrc/test/helpers/maplibreMock.ts(ui#421, used by six suites; the rest still mockmaplibre-glper file — corrected at the split, 2026-09-28) and each surface issue moves its tests onto it. The multi-country scope may use MapLibre 6's globe projection (a MapKit toggle, as in the prototype). - D6 — One scale — the backbone's banded 1–10 score and its band labels are the only scale; headline = worst banded hazard + "n hazards not covered" (counted from D14's coverage contract, never a literal) + a hazard-group level (flood · earthquake · fire & heat · storm, C40); when
score_bands_enabledis false no headline shows — the bands-disabled notice and the worst hazard by value do. A–F letters stay an open owner option (board D6), never a second scale. - D7 — Indicator hazards count, badged — heat and wildfire (indicator-only today) count in the headline; a headline driven by an indicator carries an "indicator" badge (board D7).
- D8 — Switch contract — return period and metric re-cut the loaded result (re-query
/results/{id},/freq-curve, tiles; never POST compute); scenario/horizon POST/v1/compute/impactwith the same exposure/impact function/engine —200 cache_hitswaps theresult_idsilently,202shows "this cell is not computed yet" with an explicit Compute button and never the previous cell's figures (strip, result layers, charts and table all show the not-computed state until the compute lands); portfolio scopes prefetch the scenario × horizon grid once via/compute/impact/matrix. Acceptance: switching to a precomputed cell makes zero202responses. No new backbone endpoint. - D9 — Adapt v1 — backbone: the adapter passes the scenario/horizon hazard dataset as the future hazard (
worker/models/engine_adapter.pylines ~315–318 reuse the present hazard today);PATCH /v1/measures/{id}; measure images stored in MinIO; a seeded reference catalogue throughPOST /v1/measures/seed(thePOST /v1/impact-functions/seedpattern — cited reference parameters, not demo data under ADR-077); the cost-benefit response returns the stored present/future risk per measure. UI: Adapt absorbsMeasuresPageandCostBenefitPanel. No benefit–cost ratio renders before the future-hazard fix is on trunk (feature flag). CLIMADA cost-benefit stays deferred (ADR-042). - D10 — Report documents — a report-document model (org, name, template, scope, ordered widgets referencing result/batch/analysis ids, notes) with CRUD, rendered by the existing WeasyPrint pipeline (
core/reports/); templates: ORSA (exists) + portfolio + place + post-event + framework-aligned (CSRD/ESRS E1 physical risk, EU Taxonomy climate-risk assessment, IFRS S2) whose requirement wording is marked to verify until read from the published texts; a coverage checklist computed from what the report contains. No regulatory claim or figure ships unverified. - D11 — Named actions first, assistant bounded — one action registry in the UI (URL state, controls, Ctrl K and the assistant call the same functions; a guard test fails when an Explore or Lookup component writes URL state outside
src/actions/); backendPOST /v1/assistant/messagesbehindcore/assistant/whose adapter is the only file importing an LLM SDK (architecture rule 3); tools = the registry manifest + read-only result queries; the UI executes proposed actions visibly (trace), the assistant never states a figure a tool did not return, every action is an audit event, the feature is off withoutASSISTANT_ENABLED+ a key (credential = human item). Second use: CSV column-mapping suggestions on upload (the user confirms). The prototype's assistant demonstrates this contract. - D12 — Phones — rail → bottom tabs, side panels → bottom sheets; every view readable at 390 px without horizontal overflow; Home, Lookup and Events fully usable; Explore/Portfolios editing from 768 px; on phones the map chrome (context chips, level switch, legend, timeline) collapses so it never blocks the map (owner note on V2, 2026-09-27). One 390-px e2e check per surface.
- D13 — Themes and palettes reach the map — light (default), dark and high-contrast themes on tokens; palettes Ember (default, colour-blind-safe, not green-amber-red — C92), Aurora, Viridis, Cividis, Greyscale (C79), Crimson, Depth, Traffic light (flagged not colour-blind-safe); the palette drives MapLibre paint, raster
colormap_name, legends and charts; per-user default in preferences, per-layer override; the saved theme is re-applied on reload. - D14 — Coverage contract —
GET /v1/catalog/coveragegains per-cell computability (modelled | screening | indicator | none, fromcore/dataset_computability.py) and the admin levels available per country; every scope option, card, chart and measure reads it and renders an explicit "not available for X" state — never a zero, never demo data. - D15 — Offline acceptance — with every external provider unset (basemap style URL, satellite imagery, DEM terrain, LLM, geocoder) every view renders: a self-hosted vector basemap (Natural Earth — public domain — plus GADM boundaries), bundled fonts, satellite and terrain shown as disabled "connect a provider" entries that open Settings → Appearance → Map providers (selectable once a provider is configured), assistant hidden while Ctrl K still runs the registry, geocoding falling back to admin-boundary search (D16).
- D16 — Admin-boundary endpoint —
GET /v1/admin-boundaries(list, search, simplified geometry by level and bbox) reusingdb/repositories/admin_boundary_repository.py; before shipping simplified geometry the GADM redistribution terms (ADR-031) are checked, with geoBoundaries (CC-BY 4.0) recorded as the fallback source. - D17 — Pins become exposures — pins dropped on the Explore map are scored instantly and saved as a portfolio through the real
POST /v1/exposures(a CSV of points; ADR-077). Polygons and corridors (C42) are Phase 18. - D18 — Property level is a spike — a written feasibility note: building footprints (OSM, Overture, EUBUCCO) are ODbL-gated (
docs/DATA_SOURCES.html), hazard grids are ~90 m at best, exposure as ingested is point/grid (LitPop 150″ ≈ 4.6 km percore/display_names.py; GHSL's ingested resolution to be verified before quoting). The note names the licence outcome, minimum hazard resolution and exposure source that would make per-building results defensible; the build is Phase 18. - D19 — Engines (owner pick 2026-09-27: Oasis LMF in Phase 17) — Explore Compare refreshes engine comparison (swipe/side by side, agreement) over the roster from
GET /v1/engines. Oasis LMF joins as the third engine through ADR-042's pattern: its own container (docker/oasis-sidecar/, the only code importingoasislmf/ktools) and an HTTP adapter registered under anoasisselector (C-B2, with an ADR extending ADR-042). Phase 17 delivers ground-up loss built from the platform's own return-period hazards and impact functions (C-B3), plus the existing flat deductible/cover mapped to one uniform OED location term, proven on Oasis's reference model and compared like for like. A minimal shared results vocabulary lands first (C-B1): curve basis OEP/AEP,aalfor the aggregate, and a basis-labelled PML 1-in-200. AEP for return-period maps waits for stochastic event sets (D21). Per-location/account OED policy terms, layers and reinsurance, Delft-FIAT, OS-Climate physrisk, OpenQuake and uncertainty ranges stay in Phase 18. - D20 — Tiers parked — plan.md keeps the business model out of focus: a design note in ui-revamp.md lists what a tier would gate (location cap, map interactivity, analysis, AI — C24, C25, C97); no flags, no billing code.
- D21 — Charter deferrals move — stochastic event sets, GeoTIFF exposure upload through the API and per-asset impact-function binding move to Phase 18 (plan.md row edited in the closeout PR); Phase 17's event viewer uses the existing event lane (replays, FIRMS, ShakeMap).
- D22 — Phase shape — one milestone per repo, ordered waves A–E; cut line: if the escape rate trips after wave C, waves D–E move to Phase 18 (reversed by the owner). Every UI issue names one component in its acceptance (mid-review-16 F-3); before a UI issue is written its backbone route is grepped on trunk (F-4).
Guardrails — every Phase 17 issue¶
Phase 14 guardrails 2, 4, 8 and 9 apply verbatim (nothing removed without its walk; contracts
tested, not assumed; dependencies flow downward; feat:/refactor: for planned work, fix: only
for rework of Phase 17 code; breaker at 25 % via scripts/rework_rate.py --follow-ups). Plus:
- No demo data (ADR-077): a surface renders real API data or an explicit empty/unavailable state (D14).
- One component per UI issue; a UI issue that needs a new backbone field carries a fully-qualified
Deps:and greps the route on trunk before building. - Every map through MapKit, every chart through the chart kit, every colour through tokens (D3, D5, D13); the colour guard count never rises.
- Layout is measured: the no-page-scroll assertion at 1440×900 and the 390-px check are Playwright specs on the smoke stack; a prove-it-fails run is recorded for each new assertion.
- Offline acceptance (D15) on every provider-backed feature; the PR body records the resolved value of every container- or workflow-consumed key.
- Named actions: every state change on Explore and Lookup goes through the registry (D11).
- Regulatory wording stays "to verify" until verified against the published text; no figure from a legal text without its source.
- CLIMADA isolation unchanged and extended to Oasis: only
docker/oasis-sidecar/importsoasislmf/ktools (an import-ban test like CLIMADA's); the assistant adapter is the only LLM SDK importer.
Scope — waves and items¶
Dependency line: A (backbone contracts ∥ UI foundation) → B (map delivery + Explore stage 1)
→ C (surfaces + Events mode) → D (Adapt ∥ Reports) → E (assistant), with the spike and the ops
rows free-floating and the closeout last. Sizes are drafts; /phase-split grounds and re-sizes
each item with /analyze. Ids are stable handles for the split; the issue numbers follow each id (#N backbone, ui#N = CortoMaltese3/climate-lama-ui#N), added at the split on 2026-09-28.
A — Foundations¶
Backbone:
| Id | Title | D | Files (seam / hotspot) | Grounding |
|---|---|---|---|---|
| A-B1 → #1466 | feat(api): persona on user preferences and default_persona on org settings |
D2 | schemas/preferences.py, tests/test_api/test_preferences.py, docs/CLIMATE_LAMA.md (hotspot) |
PARTIAL — both models extra="allow"; typed fields missing |
| A-B2 → #1467 | feat(api): computability and admin levels on GET /v1/catalog/coverage |
D14 | api/v1/catalog.py, core/catalog_coverage.py, core/dataset_computability.py, tests/test_api/test_catalog_coverage.py, docs/CLIMATE_LAMA.md (hotspot) |
PARTIAL — coverage grid + indicator flag exist (Phase 16 W0.7) |
| A-B3 → #1468, #1469 | feat(api): GET /v1/admin-boundaries — list, search, simplified geometry by level and bbox |
D16 | api/v1/admin_boundaries.py (new), main.py (hotspot), db/repositories/admin_boundary_repository.py, schemas/admin_boundaries.py (new), tests/test_api/test_admin_boundaries.py, docs/CLIMATE_LAMA.md (hotspot), docs/DATA_SOURCES.html (licence note) |
PARTIAL — models/admin_boundary.py, migration 0031_admin_boundaries.py and AdminBoundaryRepository (level + bbox filtering _level_bbox_filter, as_payload()) exist; only the router and schemas are missing (checked api/v1/, api/v1/admin/) |
| A-B4 → #1470, #1471 | feat(worker): H3 aggregates of per-asset result impacts at write time + ADR |
D4 | migrations/versions/0095_result_h3_cells.py (new), models/result_h3_cell.py (new), core/surface_writer.py (pattern), worker/tasks.py (hotspot), tests/worker/test_result_h3_cells.py, docs/DECISIONS.md (hotspot) |
MISSING — H3 exists for risk surfaces only (ADR-040) |
| A-B5 → #1472, #1473 | feat(api): result vector tiles — GET /v1/tiles/results/{result_id}/{z}/{x}/{y}.mvt |
D4 | api/v1/tiles.py (hotspot), core/vector_tiles.py (new), tests/test_api/test_result_mvt.py, docs/CLIMATE_LAMA.md (hotspot) |
MISSING — tiles route serves COG PNG only |
| A-B6 → #1474 | feat(api): exposure vector tiles — GET /v1/tiles/exposures/{dataset_id}/{z}/{x}/{y}.mvt |
D4 | api/v1/tiles.py (hotspot), core/vector_tiles.py, tests/test_api/test_exposure_mvt.py |
MISSING |
| A-B7 → #1475 | feat(api): classed score tiles and legend accept a palette ramp (added at the split, refuter K6 — D13's backbone gap; UI consumer A-U6c) |
D13 | api/v1/tiles.py (hotspot), core/tile_render.py, api/v1/hazards.py, core/score_classes.py, tests, docs/CLIMATE_LAMA.md (hotspot) |
PARTIAL — classed + colormap_name is a 422 (tiles.py:163); class colours minted by classed_colormap (core/tile_render.py:237) |
UI:
| Id | Title | D | Files (hotspot) | Grounding |
|---|---|---|---|---|
| A-U0 → ui#492 | refactor(ui): split api/client.ts into per-domain modules behind the same api object |
— | src/api/client.ts (hotspot), src/api/*.ts (new), tests/e2e/smoke/contract-map.ts (hotspot) |
EXISTS as one 2,056-line file — every later consumer would co-edit it (principle 6) |
| A-U1 → ui#493, ui#494, ui#495 | feat(ui): design tokens — semantic colours; dark and high-contrast themes that reach every surface; theme re-applied on reload; colour guard test |
D3, D13 | tailwind.config.js, src/index.css, src/branding/themes.ts, src/branding/BrandProvider.tsx, tests/colour_guard.test.ts (new) |
PARTIAL — 7 --brand-* vars, only brand-primary used; 1,184 gray-N classes |
| A-U2 → ui#496, ui#497 | feat(ui): primitives, icons and fonts — Card, KpiCard, Chip, Pill, ScoreBadge, HazardIcon, Segmented, Tabs, Drawer/Sheet (retractable and resizable, width persisted), Toast system; lucide-react; self-hosted fonts |
D3 | package.json (hotspot), package-lock.json (hotspot), src/ui/*.tsx (new), src/test/ui/*.test.tsx |
MISSING — no icon library, no toast system |
| A-U3 → ui#498, ui#499 | feat(ui): app shell — retractable icon rail, top bar with search slot, phone bottom tabs, role-lens nav order |
D1, D2, D12 | src/App.tsx (hotspot), src/shell/*.tsx (new), src/test/App.test.tsx (hotspot), src/i18n/locales/en/common.json, tests/e2e/smoke/responsive.spec.ts |
PARTIAL — one-row header truncates on phones |
| A-U4 → ui#500, ui#501 | feat(ui): named-action registry, URL state and Ctrl K command palette |
D11 | src/actions/*.ts (new), src/components/CommandPalette.tsx (new), src/run/assessAddress.ts (hotspot), tests |
MISSING — no palette or actions layer |
| A-U5 → ui#502, ui#503, ui#504 | feat(ui): MapLibre 6 and MapKit core — camera, basemaps incl. the offline default and provider entries, globe projection, resize, popup card, shared map mock |
D5, D15 | package.json (hotspot), src/map/MapKit.tsx (new), src/map/basemaps.ts (new), src/map/styles/base.ts, src/test/helpers/maplibreMock.ts (exists, ui#421 — extended; replaces the per-file vi.mock('maplibre-gl')), public/basemap/* (new, offline data) |
PARTIAL — maplibre 4.7.1, 12 files construct maps; shared map mock exists but only six suites use it |
| A-U6 → ui#505, ui#506, ui#507 | feat(ui): MapKit layers — hazard-family layer catalogue, palettes and legend, raster colormap, event footprints |
D5, D13 | src/map/layers/*.ts (new), src/map/legend.ts, tests |
PARTIAL — LayerPanel is three fixed rows |
| A-U7 → ui#508, ui#509 | feat(ui): MapKit compare (swipe, side by side), timeline player and 3D extrusion |
D5 | src/map/compare.tsx (new), src/map/timeline.tsx (new), tests |
PARTIAL — synced maps exist in ScenarioComparePage; no swipe, no timeline player |
| A-U8 → ui#510 | feat(ui): chart kit — line/area with crosshair, loss curve on log return periods, columns, bars, donut, gauge, grade ruler, distribution bar |
D13 | src/charts/*.tsx (new), tests |
PARTIAL — recharts LineChart in 4 files |
B — Map delivery and Explore, stage 1¶
| Id | Title | D | Deps (draft) | Files | Grounding |
|---|---|---|---|---|---|
| B-U1 → ui#511 | feat(ui): results as vector tiles — zoom-driven levels (regions → units → municipalities → hexagons → assets), level switch, cluster labels |
D4 | A-B5, A-U5, A-U6 | src/map/layers/results.ts, tests |
PARTIAL — ResultMap.tsx already switches between two zoom tiers (admin choropleth vs centroid circles, map/styles/results.ts:6-9,41-79) fetched as GeoJSON per bounds (ResultMap.tsx:54 onBoundsChange), not vector tiles. A prior martin-served result MVT source existed and was deleted as dead code because VITE_MARTIN_TILE_URL was never set in any deployment (map/styles/mvt.ts:3-16, issue #319). Municipality/hexagon/asset tiers, cluster labels and real MVT delivery are new, gated on A-B5/A-B4 (both MISSING). |
| B-U2 → ui#512 | feat(ui): performance suite against the MVT path — 10M fixture, numbers recorded |
D4 | B-U1 | tests/e2e/performance/*, docs/architecture/performance-budgets.md, .github/workflows/performance.yml (seam) |
EXISTS as an orphaned harness — tests/e2e/performance/{fps,memory,tile-latency}.spec.ts, a fixture generator/server and .github/workflows/performance.yml already benchmark synthetic MVT tiles at 10M scale, plus docs/architecture/performance-budgets.md records budgets. But by the fixture's own header comment it "no longer mirror[s] a result-map codepath" since issue #319 deleted the martin branch it used to mimic (fixtures/generate-fixture.ts:12-17, fixtures/server.ts:14-18) — it measures generic MapLibre MVT rendering, not the app's result path. This is the "revived performance suite" the reassess record (L254) refers to: repoint it at A-B5's real endpoint, don't build fresh. |
| B-U3 → ui#513, ui#514, ui#515 | feat(ui): Explore route, layout and scope picker; redirects from /assess, /dashboard, /matrix |
D1 | A-U3, A-U5, A-B3 | src/App.tsx (hotspot), src/pages/ExplorePage.tsx (new), src/explore/ScopePicker.tsx (new), src/test/App.test.tsx (hotspot), tests/e2e/smoke/compute.spec.ts, tests/e2e/smoke/scenario-matrix.spec.ts |
PARTIAL — /assess → <Dashboard/>, and /dashboard and /matrix already redirect there with query preserved (App.tsx:271,277,314, MatrixBatchRedirect at App.tsx:103-112) — exactly the redirect pattern this item extends to /explore. No /explore route or src/explore/ directory exists yet (confirmed empty). |
| B-U4 → ui#516, ui#517, ui#518 | feat(ui): Explore analysis dock and the switch contract (cache-hit swap, opt-in compute, matrix prefetch) |
D8 | B-U3 | src/explore/AnalysisDock.tsx (new), src/run/RunProvider.tsx, tests |
PARTIAL — src/run/RunProvider.tsx exists and already tracks an in-flight run across navigation, wrapping the whole shell so leaving /assess doesn't unmount it (App.tsx:205, issue #434). src/explore/AnalysisDock.tsx and the cache-hit-swap/matrix-prefetch contract are net new — no src/explore/ directory exists. |
| B-U5 → ui#519, ui#520, ui#521 | feat(ui): Explore insight panel, key-figure strip and table drawer — charts and table filter the map |
D1 | B-U3, A-U8 | src/explore/InsightPanel.tsx, src/explore/TableDrawer.tsx, tests |
MISSING — no src/explore/ directory exists; the chart kit it needs is itself only PARTIAL (A-U8: recharts LineChart in 4 files, no shared kit). No cross-filtering (chart/table selection driving the map) precedent was found in the current components searched (RankedAssetsTable.tsx, StatsBar.tsx are static displays). |
| B-U6 → ui#522 | feat(ui): portfolio scope in Explore; /portfolios/:id analysis redirects |
D1 | B-U3 | src/explore/*, src/pages/PortfolioDetailPage.tsx, tests/e2e/smoke/portfolio.spec.ts, tests/e2e/smoke/portfolio-map.spec.ts |
PARTIAL — PortfolioDetailPage.tsx already combines a map, a hazard rollup and a paginated member table on one screen (see C-U5 below); tests/e2e/smoke/portfolio.spec.ts and portfolio-map.spec.ts already exist. The Explore-scope wrapper and the /portfolios/:id analysis redirect are net new — no /explore route exists. |
| B-U7 → ui#523, ui#524 | feat(ui): Explore Compare mode — scenarios and engines, swipe or side by side; /runs/compare redirects |
D1, D19 | A-U7, B-U3 | src/explore/CompareMode.tsx, src/pages/ScenarioComparePage.tsx, tests/e2e/smoke/compare.spec.ts, tests/e2e/smoke/agreement.spec.ts |
PARTIAL — ScenarioComparePage.tsx (routed at /runs/compare, App.tsx:304) plus components/assess/{AgreementView,EngineComparisonPanel,EnginePicker}.tsx already implement scenario/engine comparison, consistent with A-U7's own grounding ("synced maps exist in ScenarioComparePage; no swipe, no timeline player"); tests/e2e/smoke/{compare,agreement}.spec.ts already exist. src/explore/CompareMode.tsx wrapping this under Explore is net new. |
| B-U8 → ui#525 | feat(ui): pins on the Explore map — instant scores, save as a portfolio through POST /v1/exposures |
D17 | B-U3 | src/explore/Pins.tsx (new), tests |
PARTIAL — the backend target already exists (api/v1/exposures.py), and single-point "instant score" already exists in Lookup (components/lookup/{CoordinateEntry,AddressSearch,ScoreCardPanel}.tsx). Dropping pins directly on the Explore map and a save-to-portfolio action from there are new UI wiring onto an existing endpoint, not a new contract. |
Acceptance boxes carried by wave B (reassess 2026-09-26, second pass):
- B-U4 — on a
202no figure of the previous cell is shown as the new cell: strip, result layers, charts and table show "not computed yet" with Compute until the compute lands (D8). Save analysis calls the existing save-as-scenario path and the saved run is listed in Analyses, so step 6 of the CLAUDE.md slice ("Save the result") keeps working through Explore. - B-U5 — the loaded result exports as CSV and PDF through the existing export endpoints, as
/assessdoes today, until D-U3's report studio replaces them (guardrail: nothing removed without its walk). - B-U1 — the prototype's "Scale test" (Explore → Layers, 1,000,000 synthetic points) is a design demo only; the measured budget is B-U2 on the MVT path.
Acceptance boxes from the owner's direction lock (2026-09-27):
- B-U3 — layout A with both docks retractable and resizable: a drag handle between dock and map, min/max widths, the width persisted per user; collapsing or narrowing a dock gives the map the space. On phones (390×844) the map chrome never blocks the map: the context chips collapse into one summary pill that opens a bottom sheet, the level switch, legend and timeline sit behind toggles, and a Playwright check asserts the visible map area with the chrome at rest (the threshold is recorded in the spec with a prove-it-fails run). The resizable dock is the A-U2
Drawer/Sheetprimitive, not per-view code. - B-U7 — the engine list comes from
GET /v1/engines, never hard-coded, so a third engine appears without UI changes; an unconfigured engine shows an explicit unavailable state (D14 pattern).
C — Surfaces, and the Oasis engine lane (backbone)¶
| Id | Title | D | Deps (draft) | Files (hotspot) | Grounding |
|---|---|---|---|---|---|
| C-U1 → ui#537, ui#538, ui#539 | feat(ui): Home — role-lens command centre, recent activity, events, insights; one screen at 1440×900 |
D2, D3 | A-U3, A-U5, A-B1 | src/pages/HomePage.tsx, src/components/home/{CoverageCard,LiveNowCard,NotificationsCard,PortfoliosCard,RecentRunsCard,StartHereCard}.tsx, src/test/HomePage.test.tsx, src/i18n/locales/en/home.json |
PARTIAL — Home already ships a card grid covering coverage, live events, notifications, portfolios and recent runs (components/home/*.tsx, src/test/home/CoverageCard.test.tsx). Role-lens ordering is new: no persona/role-lens string exists anywhere in src/ (repo-wide grep), and A-B1's persona is schema-only. The 1440×900 one-screen constraint is unverified today. |
| C-U2 → ui#540, ui#541, ui#542, ui#543 | feat(ui): Lookup place page — headline and hazard groups, hazard tabs, 30-year chances (non-stationary), horizon ruler |
D6, D7 | A-U5, A-U8, A-B2 | src/pages/RiskLookupPage.tsx, src/components/lookup/{ScoreCardPanel,HazardDetail,HazardAvailableYears,RiskOverTimeChart}.tsx (pattern), src/lookup/HazardTabs.tsx (new), tests |
PARTIAL — RiskLookupPage.tsx plus components/lookup/{ScoreCardPanel,HazardDetail,RiskOverTimeChart}.tsx already render a headline score and a per-hazard time series, and ScenarioHorizonControl.tsx exists as a reusable horizon control. No hazard-tab/hazard-group UI and no "30-year chances" wording exist anywhere in src/ (grep for chance/30-year/non-stationary found nothing relevant) — matches the reassess record's own note that the chances formula was newly decided this session (L258-259). |
| C-U3 → ui#544, ui#545, ui#546 | feat(ui): Lookup past events and community view (admin-unit rollup), place report action |
— | C-U2 | src/components/lookup/PastEvents.tsx (new), src/components/lookup/CommunityView.tsx (new), tests |
MISSING — no "past event" or "community" concept exists on the Lookup page (grep of RiskLookupPage.tsx found nothing). The nearest analogues are result-side: the choropleth admin rollup tier (map/styles/results.ts:6-9) and Live's admin aggregation — both different surfaces. A "place report" action would need a place-scoped report; today's only report endpoint is result-scoped (request_result_report, api/v1/reports.py:101). |
| C-U4 → ui#547 | feat(ui): Portfolios list — cards with score-per-horizon ruler |
— | A-U2 | src/pages/PortfoliosPage.tsx, src/components/PortfolioCard.tsx (new), src/components/ScenarioHorizonControl.tsx (pattern), tests |
PARTIAL — PortfoliosPage.tsx lists portfolios as a plain <table> (PortfoliosPage.tsx:273), not cards, and has no horizon-ruler UI; ScenarioHorizonControl.tsx exists elsewhere in the app as a reusable horizon picker to build the ruler from. |
| C-U5 → ui#548, ui#549 | feat(ui): Portfolio detail — map, synopsis and directory in one screen |
— | A-U5, B-U1 | src/pages/PortfolioDetailPage.tsx, src/components/{PortfolioMap,RankedAssetsTable,TablePager}.tsx |
EXISTS in substance — PortfolioDetailPage.tsx already lays out a map (PortfolioMap, L1239) beside a hazard-rollup summary (rollup.hazards, L524) and a paginated member directory (<table> + TablePager, L1259-1313) in a two-column grid on one screen (L1217-1226, issue #406). The remaining work is porting this onto MapKit/vector tiles (B-U1) and new tokens, not building the layout. |
| C-U6 → ui#550 | feat(ui): Portfolio upload — CSV with template, addresses, pins |
D17 | A-U2 | src/components/portfolio/AddAssetsPanel.tsx, src/components/datasets/ExposureUploadModal.tsx |
PARTIAL — CSV upload already works: AddAssetsPanel.tsx:586 (uploadCsv) reuses ExposureUploadModal.tsx, which already accepts .csv/.xlsx/.h5/.hdf5 (ExposureUploadModal.tsx:50). No downloadable CSV template and no address→pin geocoding on upload were found in either file. |
| C-U7 → ui#551, ui#552 | feat(ui): Data list with retractable drawer and full page; upload templates |
— | A-U2 | src/pages/DatasetsPage.tsx, src/components/datasets/DatasetInspector.tsx (pattern) |
PARTIAL — DatasetsPage.tsx is already a full-page dataset <table> (L273) with a separate DatasetInspector.tsx detail view. A retractable-drawer variant and downloadable upload templates were not found. |
| C-U8 → ui#553 | feat(ui): Data coverage matrix — what can be computed where |
D14 | A-B2 | src/pages/CoveragePage.tsx (new), src/components/home/CoverageCard.tsx (pattern) |
PARTIAL — components/home/CoverageCard.tsx already renders a country × hazard-type coverage grid off the catalog-coverage API (types.ts CatalogCoverage* types, fixture at src/test/fixtures/catalogCoverage.ts), so the data plumbing exists in miniature on Home. A dedicated full matrix surface with admin-level/computability detail is new and depends on A-B2 (itself only PARTIAL). Don't confuse with the unrelated scenario-compute matrix (components/assess/Matrix{HeatmapTable,Strip,GridCell}.tsx) — same word, different feature. |
| C-U9 → ui#554, ui#555, ui#556 | feat(ui): Analyses — run cards, running strip, run summary, reproduce, compare tray |
— | A-U8 | src/pages/RunsPage.tsx |
PARTIAL — RunsPage.tsx already lists every saved scenario/matrix batch/engine comparison in one <table> and gates a Compare action on a valid two-row selection (comparePair/handleCompare, RunsPage.tsx:452-517, compareHint L465-479). Cards, a live "running" strip and a "reproduce" action were not found — this is a table-hub today, not a card gallery. |
| C-U10 → ui#526, ui#527 | feat(ui): Explore Events mode — event list, footprints, impact panel; /live redirects |
D1 | B-U3, A-U7 | src/App.tsx (hotspot), src/test/App.test.tsx (hotspot), src/pages/{LivePage,LiveEventPage}.tsx, src/components/live/*.tsx |
PARTIAL — /live and /live/:eventId already render a full event-list + footprint-map + impact-panel experience (LivePage.tsx, LiveEventPage.tsx, components/live/{LiveEventMap,Inspector,DeltaPanel,EventHeader,WatchListRail}.tsx). The work is folding this into an Explore mode switch and leaving /live as a redirect — App.tsx already uses exactly this pattern for /dashboard, /scenarios and /compare (App.tsx:277-282). Gated behind live_mode (App.tsx:65). |
| C-U11 → ui#528, ui#529 | feat(ui): Events timeline player and watch creation on MapKit |
— | C-U10 | src/components/live/SnapshotScrubber.tsx, src/components/live/CreateWatchDialog.tsx |
EXISTS already for the current Live surface — SnapshotScrubber.tsx is a full play/pause/step replay timeline over sorted snapshots (SnapshotScrubber.tsx:9,31-40), and CreateWatchDialog.tsx already creates watches. The work is re-hosting both on the new MapKit primitive (A-U7, PARTIAL) inside Explore, not building either from scratch. |
| C-U12 → ui#530, ui#531, ui#532 | feat(ui): Settings — role lens, appearance (themes, density, palettes, basemaps, map providers), alerts, assistant, API keys |
D2, D3, D13 | A-U1, A-B1 | src/pages/Settings.tsx |
PARTIAL — Settings.tsx already has a tab shell (preferences\|organization\|apiKeys, L26) with a live theme switch (THEME_OPTIONS = ['light','dark','high-contrast'], L37; applyThemeLive, L135-137) and a URL-backed API-keys tab (L470-586). Role lens, palette/basemap pickers, alerts and assistant tabs do not exist — no "role-lens" or "persona" string anywhere in src/. |
| C-U13 → ui#533, ui#534, ui#535, ui#536 | feat(ui): Admin — health, engines registry, feeds, pipeline, usage, audit |
D19 | A-U2, C-B2 | src/admin/AdminLayout.tsx, src/admin/{health,engines,feeds,pipeline,usage}/*.tsx (new) |
PARTIAL — AdminLayout.tsx has a working tab shell with real users, orgs, datasets, pending-ingests, audit panes (L16-29). Note jobs, system and branding tabs were deliberately deleted in ui#310 as dead "coming soon" stubs (L11-14) — a documented caution against re-adding health/engines/feeds/pipeline/usage tabs before their panes are real. |
| C-B1 → #1476 | feat(core): shared results vocabulary — curve basis (OEP/AEP), aal and a basis-labelled PML 1-in-200 on every engine's result |
D19 | — | models/result.py, worker/models/base.py, worker/models/{engine_adapter,climada_adapter}.py, result response schemas, scripts/verify_engine_agreement.py, migrations/versions/<next>_result_curve_basis.py (new, if the basis is persisted), tests |
PARTIAL — models/result.py:57-58 stores freq_curve as bare [{return_period, loss}] with no basis; both engines' curves are per-event exceedance, i.e. OEP basis (docker/climada-sidecar/server.py:283-287); RP 200 is already in the pinned set (climada_adapter.py:58); the aggregate is ead/aai, while EAI is the per-point quantity (server.py:298-301) |
| C-B2 → #1477, #1478, #1479 | feat(worker): Oasis LMF sidecar skeleton, oasis engine selector and reference-model conformance + ADR extending ADR-042 |
D19 | S3 | docker/oasis-sidecar/{Dockerfile,server.py} (new — the only code importing oasislmf/ktools), worker/models/oasis_adapter.py (new, HTTP client), worker/models/registry.py (hotspot), core/engines.py (hotspot), config.py, docker-compose*.yml (opt-in oasis profile), tests/test_worker/test_oasis_adapter.py (new), tests/test_worker/test_oasis_sidecar_server.py (new), an oasislmf/ktools import-ban test (new), CLAUDE.md (isolation section extended), docs/DECISIONS.md (hotspot) |
MISSING — no oasis string in src/, docker/ or pyproject.toml. The seam is ready: registry.py:20-26 treats an out-of-process engine as an ordinary entry, and the CLIMADA block (registry.py:284-343, core/engines.py:32,46-48, config.py:169) is the pattern to mirror |
| C-B3 → #1480, #1481 | feat(worker): Oasis model builder and ground-up run — hazard and impact functions to footprint, vulnerability and occurrence; exposure to OED; flat deductible/cover |
D19 | C-B2, C-B1 | docker/oasis-sidecar/*, worker/models/oasis_adapter.py, scripts/verify_engine_agreement.py, tests |
MISSING — the input already exists: the CLIMADA sidecar decodes CSR intensity, exposure and impact functions (docker/climada-sidecar/server.py:119-147,174-235); only RETURN_PERIOD-axis datasets carry annual frequencies (models/enums.py:14-43, models/hazard.py:480-490,522-525); flat deductible/cover and insured_loss exist (models/result.py:60-64, worker/models/base.py:30) |
Scenario explainer (C93, C112 — SSP → warming by 2081–2100 from IPCC AR6 WGI Table SPM.1) and the chances wording ("this year / next 15 / next 30 years", C101) are acceptance boxes on C-U2 and B-U4. Further acceptance boxes (reassess 2026-09-26, second pass):
- C-U2 — the "n hazards not covered" count is read from
GET /v1/catalog/coverage(A-B2) for the place's country, never a literal (D6). - C-U3 — key insights (C106): rule-based sentences built only from the place's
/resultsand coverage fields, each showing the value it came from; an unavailable hazard reads "not available", never a figure (ADR-077, D14). Critical infrastructure is excluded (Phase 18, Out of scope); the prototype shows it only as a labelled Phase 18 placeholder. - C-U9 —
/runsand/scenariosre-target to/analysesdirectly (D1); saved analyses from B-U4 appear here. - C-U11 — watch creation follows the prototype's Explore → Events → "Create a watch" dialog: area = current scope, hazards per live feed (FIRMS, ShakeMap, replays, pushed observations), trigger, in-app channel; e-mail later.
- C-U12 — the density preference (D3) and the map-provider entries of D15 live under Appearance.
Acceptance boxes from the owner's direction lock (2026-09-27, decision-protocol refuter):
- C-U2 — layout A with a resizable right panel (the A-U2 primitive: drag handle, min/max widths, width persisted per user); the map takes the reclaimed width.
- C-U13, C-U9 — engines come from
GET /v1/engines; Oasis shows its availability and the conformance result, or an explicit unavailable state where its sidecar is not configured. - C-B1 — climate-lama-engine and CLIMADA report
curve_basis: "OEP";aalaliases the aggregateead/aai(never the per-point EAI); PML is read at the pinned RP 200, never interpolated, and is labelled with its basis ("PML 1-in-200 (OEP)"). The agreement view compares curves only when their bases match and otherwise reads "different curve basis". The basis gets a migration if it is persisted (architecture rule 5). - C-B2 —
X-Engine: oasiswith no URL configured returnsE_ENGINE_UNAVAILABLE; an unreachable sidecar surfaces as one entry indivergence.failed_engines, not a failed comparison. A named workflow or parity job builds theoasisprofile and asserts Oasis's own reference model (picked in S3) against its published outputs. The box records where that job runs, so conformance cannot silently never run. - C-B3 — one area-peril per centroid, keyed by the backbone's own point-to-centroid assignment (no Oasis lookup re-assigns). Intensity bins are fine enough to stay inside the 1 % EAD agreement tolerance (
verify_engine_agreement.py:34,91), and the bin scheme is recorded in the result metadata. Each impact-function curve becomes a damage-bin distribution: PAA on the bin holding MDD and 1 − PAA on zero damage. Events and frequencies come fromRETURN_PERIODdatasets only;SCENARIO_YEARdatasets fail withE_ENGINE_EXECUTION"no annual frequency". AAL and the OEP curve integrate the event loss table with the backbone's marginal frequencies. AEP, PLT and ALT are not reported for return-period maps until stochastic event sets land (D21). The flatdeductible/covermap to one uniform OED location term, soinsured_lossis filled on the same basis as the other engines, or is null with "not supported by this engine" — never a zero. One platform scenario runs through all three engines in the agreement view.
D — Adaptation and reports¶
| Id | Title | D | Deps (draft) | Files (hotspot) | Grounding |
|---|---|---|---|---|---|
| D-B1 → #1482 | feat(worker): climate-adjusted future hazard in cost-benefit |
D9 | — | worker/models/engine_adapter.py, worker/tasks.py (hotspot), tests |
PARTIAL (given) — engine_adapter.py:315-318 reuses the present hazard as the future hazard rather than loading the climate-adjusted one. |
| D-B2 → #1483, #1484 | feat(api): PATCH /v1/measures/{id} and measure images |
D9 | — | api/v1/measures.py, models/measure.py, migrations/versions/0096_*.py (new) |
MISSING — measures.py has only POST/GET/GET-by-id/DELETE (measures.py:62-131), no PATCH; models/measure.py has no image column (repo-wide grep of the file found no "image" hit). An image column/table needs a migration — the first new one after A-B4's 0095, i.e. the candidate for 0096. |
| D-B3 → #1485 | feat(api): seeded reference measure catalogue — POST /v1/measures/seed |
D9 | D-B2 | api/v1/measures.py, core/measure_seeder.py (new, pattern: core/impact_function_seeder.py), tests |
MISSING — no seed endpoint or seed data for measures exists anywhere (seed.*measure grep: 0 hits). core/impact_function_seeder.py plus migrations 0004-0006_seed_{tc,wf,ws}_impact_functions.py are the pattern to copy; Measure's existing source/license/citation/source_url fields (measures.py:33-36) already carry the provenance a reference catalogue needs, so this may not need its own migration. |
| D-B4 → #1486 | feat(api): cost-benefit returns stored present and future risk per measure |
D9 | D-B1 | api/v1/results.py, models/cost_benefit_result.py, db/repositories/ (CostBenefitRepository) |
PARTIAL, further along than the title implies — models/cost_benefit_result.py:45-49 already stores risk_baseline_present, risk_with_measure_present, risk_baseline_future, risk_with_measure_future per measure, and the worker already persists all four (worker/tasks.py:2734). GET /v1/results/{job_id}/cost-benefit (api/v1/results.py:554-621) only ever serializes measure_id/measure_name/npv_benefit/npv_cost/ratio (L602-611) — the four risk columns are stored but never returned. The real gap is the API response shape; also note D-B1's bug means the stored "future" values are currently wrong. |
| D-B5 → #1487, #1488 | feat(db): report documents — model, migration, CRUD + ADR |
D10 | — | models/report_document.py (new), migrations/versions/0097_*.py (new), db/repositories/report_document_repository.py (new), docs/DECISIONS.md (hotspot) |
MISSING — none of the 35 files in src/climate_lama/models/ is a report/document entity. Today a "report" is a transient render keyed by (org_id, job_id, template) (api/v1/reports.py:216-231), not a persisted, listable row. Reusable adjacent infra: core/reports/{composer,renderer,charts,storage}.py and templates/{base,impact_report,orsa_report}.html. |
| D-B6 → #1489, #1490 | feat(core): render report documents — portfolio, place and post-event templates |
D10 | D-B5 | core/reports/composer.py, core/reports/templates/*.html (new) |
PARTIAL — the pipeline is a registered-template mechanism by design: "adding a template means one HTML file plus one entry in composer.TEMPLATES" (core/reports/__init__.py:21-23), and composer.py:57-60 currently registers exactly "impact": "impact_report.html" and "orsa": "orsa_report.html". Portfolio/place/post-event templates are net-new entries on a proven mechanism, not a new mechanism. |
| D-B7 → #1491, #1492 | feat(core): framework-aligned templates (wording to verify) and the coverage checklist |
D10 | D-B6 | core/reports/templates/*.html (new), core/reports/composer.py |
MISSING — no TCFD/CSRD template keys exist (composer.py:57-60 lists only impact/orsa), and no "checklist" concept appears anywhere in src/ or api/ (repo-wide hits are docs/plan files only, e.g. docs/DECISIONS.md, not code). |
| D-U1 → ui#557, ui#558 | feat(ui): Adapt library and editor — sketches, own images, custom measures |
D9 | D-B2, D-B3 | src/pages/MeasuresPage.tsx, src/components/editor/{FreehandMode,ParametricMode,SaveCurveDialog}.tsx (pattern) |
PARTIAL — MeasuresPage.tsx already lists/creates measures against the existing measures API and is kept alive post-nav-removal specifically for this ("D9 owns /measures' deletion", App.tsx:305-311). components/editor/FreehandMode.tsx already implements a freehand-sketch editing mode, but for impact-function damage curves (ImpactFunctionEditor.tsx:7-19), not adaptation measures — a nearby UX pattern, not reusable code for images/custom-measure editing. |
| D-U2 → ui#559, ui#560 | feat(ui): Adapt analysis — money cards, evolution chart, adapted vs unadapted map |
D9 | D-B1, D-B4, D-U1 | src/components/CostBenefitPanel.tsx |
PARTIAL/MISSING — CostBenefitPanel.tsx exists but renders none of the present/future risk breakdown (present\|future grep: 0 hits in the file), mirroring the API gap in D-B4. An evolution chart and an adapted-vs-unadapted map do not exist. |
| D-U3 → ui#561, ui#562, ui#563 | feat(ui): Report studio — templates, widgets from analyses, basket, notes, export |
D10 | D-B5, D-B6 | src/components/assess/ComposedReportButton.tsx (pattern), src/components/ExportButton.tsx (pattern), src/reports/ (new) |
PARTIAL — the render→poll→download plumbing already exists twice: ComposedReportButton.tsx explicitly reuses "the exact same POST -> 202 job_id -> poll -> GET contract" that ExportButton's single-result PDF path uses (ComposedReportButton.tsx:13-18). A "studio" that assembles a basket of widgets/notes across analyses into a persisted, template-picked document is new and waits on D-B5's document model. |
| D-U4 → ui#564 | feat(ui): Report coverage checklist and framework templates |
D10 | D-B7, D-U3 | src/reports/ (new) |
MISSING — depends on D-B7 (MISSING) and D-U3 (PARTIAL, not yet built); no checklist or framework-template UI exists. |
Acceptance box (reassess 2026-09-26, second pass): D-U3/D-U4 — the ORSA template tile reads "Available today"; every framework tile reads "Wording to verify" until its text is verified (D10, guardrail 7).
E — Assistant¶
| Id | Title | D | Deps (draft) | Files (hotspot) | Grounding |
|---|---|---|---|---|---|
| E-B1 → #1493, #1494, #1495 | feat(core): assistant service — LLM adapter, tool manifest from named actions, POST /v1/assistant/messages, feature flag + ADR |
D11 | A-U4 (manifest shape) | core/assistant_service.py (new), api/v1/assistant.py (new), main.py (hotspot), docs/DECISIONS.md (hotspot) |
MISSING — a repo-wide, case-insensitive search of anthropic\|openai\|llm_adapter\|LLMAdapter\|assistant in src/ returns zero hits. main.py's router-mount hotspot is explicitly shared with A-B3 per the plan's own Conflict-hotspots note. |
| E-B2 → #1496 | feat(core): assistant audit events, org scoping and rate limit |
D11 | E-B1 | core/audit.py (pattern), models/audit_event.py (pattern) |
PARTIAL — generic audit infrastructure to extend already exists (core/audit.py, models/audit_event.py), and a rate-limit pattern already exists elsewhere in the codebase (core/errors.py, core/live_feeds.py) to copy for org scoping. Nothing assistant-specific exists, and the feature it audits (E-B1) is itself MISSING — likely reuses the generic audit table rather than needing a new migration. |
| E-B3 → #1497 | feat(api): upload column-mapping suggestions (heuristic without a model) |
D11 | — | api/v1/exposure_datasets.py or api/v1/exposures.py, core/exposure_service.py |
MISSING — no column-mapping/suggestion code exists in any upload path. The only "heuristic" hits in src/ are unrelated: dataset-identity backfill logic that ADR-048 explicitly rejected (models/exposure.py:65-69, admin_boundary_repository.py:188-191). |
| E-U1 → ui#565 | feat(ui): assistant panel — trace of named actions, hidden when disabled |
D11 | E-B1, A-U4 | src/assistant/AssistantPanel.tsx (new) |
MISSING — no "assistant" string anywhere in climate-lama-ui/src (repo-wide search). Depends on both A-U4 (named-action registry, MISSING) and E-B1 (MISSING backend). |
| E-U2 → ui#566 | feat(ui): upload mapping assist in Portfolio and Data uploads |
D11 | E-B3, C-U6 | src/components/portfolio/AddAssetsPanel.tsx, src/components/datasets/ExposureUploadModal.tsx |
MISSING — the upload surfaces it attaches to already exist and handle CSV/XLSX (AddAssetsPanel.tsx:586, ExposureUploadModal.tsx:50), but no mapping-assist UI exists and its backend (E-B3) is MISSING. |
Spikes, operations and closeout¶
| Id | What | Kind / Blocker |
|---|---|---|
| S1 → #1463 | Property-level feasibility note → docs/plan/exploration/property-level.md |
spike · Blocker: agent |
| S2 → #1464 | GADM redistribution terms for the admin-boundary endpoint (ADR-031) with the geoBoundaries fallback recorded | spike · Blocker: agent (feeds A-B3) |
| S3 → #1465 | Oasis engine note → docs/plan/exploration/oasis-engine.md: (a) the oasislmf/ktools version pin and licences (BSD-3 is recorded for oasislmf in ui-revamp.md; ktools to verify); (b) whether the pinned version accepts event rates or needs an occurrence file; (c) the hazard, vulnerability and exposure mapping of C-B3; (d) the agreement tolerance the chosen bins reach; (e) the reference model the conformance job runs |
spike · Blocker: agent (feeds C-B2, C-B3) |
| O1 → #1498 | ANTHROPIC_API_KEY (or the configured provider key) on prod; ASSISTANT_ENABLED=true |
ops · Blocker: credential:ANTHROPIC_API_KEY |
| O2 → #1499 | Oasis sidecar on prod: the oasis compose profile built on the host (ADR-042's private-image deviation), OASIS_SIDECAR_URL in .env.prod, the engine roster shows oasis available |
ops · Blocker: agent |
| R1 → #1500, ui#567 | docs: Phase 17 closeout — plan.md banner and rows (Phase 17 row, Goal C wording, parked derivation row decided, Phase 18 charter stub with D19–D21), showcase-script routes and new steps, CLAUDE.md nav sentence and slice wording, climate-lama-ui/docs/quickstart/first-scenario.md (saved results are listed in Analyses, not "Scenarios"), ui-revamp.md status and tier note |
build (docs) |
| R2 → #1501 | Release (both repos, one coordinated bump), the one phase-end owner walk on prod, mid-review-phase-17.md |
walk · Blocker: walk |
Split readiness (for /phase-split phase-17)¶
- Milestones:
Phase 17 -- RevampinCortoMaltese3/climate-lama(A-B, C-B, D-B, E-B, S, O, R) andCortoMaltese3/climate-lama-ui(A-U, B-U, C-U, D-U, E-U). Cross-repo deps are writtenCortoMaltese3/climate-lama#N; UI issues that consume a new backbone field depend on it. - Direction picks: read on 2026-09-27 (
ArtifactData list picks, 20 of 20 picked) and folded in — the Direction lock table, D12, D19, the wave B and C acceptance boxes, S3, O2 and C-B1..C-B3. Re-read them only if the board changes after this date. - Found-in drain: none open on 2026-09-26 (
found-in:phase-14/15/16all 0 in both repos). - Mocks: every UI item's mock is the matching prototype view — Home (
#home, layout per V1), Explore (#explore,#explore.compare,#explore.events, layout per V2), Lookup (#lookup, per V3), Portfolios (#portfolios,#portfolios.hpb,#portfolios.upload), Adapt (#adapt), Reports (#reports), Data (#data,#data.<id>, coverage toggle), Analyses (#analyses), Settings (#settings), Admin (#admin), Direction board (#board). The prototype's data is illustrative; its layouts, states and copy are the target, its numbers are not. The artifact is private to the owner until shared from its page. - Not split targets: Lookup's "Edit home" dialog and the community view's infrastructure block are labelled Phase 18 previews (D18, Out of scope); Explore → Layers → "Scale test" is a design demo of the millions-of-points feel, measured for real by B-U2.
- Sizing hints: A-B4, A-U5, B-U3 and C-B3 are the likeliest L items; split at the seams named in
their titles if
/analyzereturns L. The Oasis lane (S3 → C-B2, C-B1 → C-B3) is backbone-only and runs beside the wave C UI items, before the D22 cut line.
Split record (2026-09-28)¶
/phase-split phase-17 cut 115 issues — 39 backbone, 76 UI — into the two Phase 17 -- Revamp
milestones (backbone #17, UI #8). Trackers (not build targets): S1 → #1463, S2 → #1464, S3 → #1465, O2 → #1499,
R2 → #1501; O1 → #1498 is filed without the milestone (a credential item). Every build target was sized by
/analyze (109 of 109 succeeded): 104 at tier 2 (Opus · high), 2 at tier 1, and 3 at tier 3
(Opus · xhigh → builder-max: #1470, #1480 and #1487 — each an L residue marked split-candidate, the migration
or protocol unit that cannot be cut further). No Fable candidates.
Split decisions (decision protocol, one refuter; the owner then had all six reassessed on 2026-09-28 — two refuters and a completeness critic — and the lines below carry the result):
- B-U4b (ui#517) never POSTs a compute speculatively; the switch contract's 202 path shows "not computed". Reassess confirmed it: the backbone has no scoped read of stored cells, so "known" stays API-returned ids.
/runs/compareand/comparego to/explore?mode=compare(D1 amended above; ui#523 also re-points the two UI docs that describe the old page).- C-B2b (#1478) adds a third path-scoped PR gate,
oasis-conformance.yml. It triggers on every input the gate compares:docker/oasis-sidecar/**, the Oasis parity test, harness and fixture generator,engine_adapter.py, anduv.lock(a lock-only change without an engine-pin bump ends green in a minute). CLAUDE.md, docs/CI.md and README.md must then say three. - A-U5c (ui#504) keeps prod's OSM street tiles through Dockerfile build args set in
publish.yml. Reassess confirmed it: the OSMF tile policy allows it (best effort, attribution required), and the UI has no runtime config. - A-B7 and A-U6c were added so classed score rasters follow the palette (D13).
- The assistant's default model is
claude-opus-5-5atassistant_effortmedium(the owner's call overclaude-sonnet-5), withassistant_max_tokens8192. Prod overrides the default withASSISTANT_MODEL=claude-sonnet-5-5in.env.prod(set with the key at O1, recorded on #1498). The call stays awaited in-request under a 60 s total deadline with no SDK retries (#1493, #1494). The UI nginx gives/v1/assistant/a 90 s read timeout (ui#565), so the 503 envelope reaches the browser. O1 checks the model list and the Caddy timeout. - The old Assess CostBenefitPanel never completes (it waits for a
result_idcost-benefit jobs never set). ui#557 removes its mount when costed measures first become possible; ui#559 rebuilds the flow in Adapt.
Waves: topological by Deps, at most five build targets per session, estimated at ~80 min per serial
step plus a 20-min gate reserve. No D or E item starts before every A–C item has landed (the D22 cut
line). Run each from the backbone checkout; the UI items ride the sibling-repo lane. The three spikes
(S1–S3, Blocker: agent) ride the earliest wave before the first issue they gate: /phase-build runs a
tracker in its orchestrator session, never in a builder, so it costs no builder lane. S2 (GADM terms)
rides B1 ahead of A-B3b in B3. S3 (the Oasis note) rides B3 ahead of C-B2a/C-B2b in B7, with +80 min.
S1 gates nothing and rides B4. The ops and walk trackers follow as their own rows.
| Wave | Command | After | Owner | Budget (min) | Covers |
|---|---|---|---|---|---|
| B1 | /phase-build 1464 1466 1467 CortoMaltese3/climate-lama-ui#492 CortoMaltese3/climate-lama-ui#493 CortoMaltese3/climate-lama-ui#495 --no-pr-ci --max-runtime 180 |
— | agent · orchestrator runs S2 #1464 | 180 | S2, A-B1, A-B2, A-U0, A-U1a, A-U1c |
| B2 | /phase-build 1468 1470 CortoMaltese3/climate-lama-ui#502 CortoMaltese3/climate-lama-ui#494 --no-pr-ci --max-runtime 180 |
B1 lands | agent | 180 | A-B3a, A-B4a, A-U5a, A-U1b |
| B3 | /phase-build 1465 1475 1469 1471 CortoMaltese3/climate-lama-ui#496 CortoMaltese3/climate-lama-ui#497 --no-pr-ci --max-runtime 260 |
B2 lands | agent · orchestrator runs S3 #1465 | 260 | S3, A-B7, A-B3b, A-B4b, A-U2a, A-U2b |
| B4 | /phase-build 1463 1472 1473 CortoMaltese3/climate-lama-ui#503 CortoMaltese3/climate-lama-ui#510 CortoMaltese3/climate-lama-ui#498 --no-pr-ci --max-runtime 180 |
B3 lands | agent · orchestrator runs S1 #1463 | 180 | S1, A-B5a, A-B5b, A-U5b, A-U8, A-U3a |
| B5 | /phase-build 1474 CortoMaltese3/climate-lama-ui#504 CortoMaltese3/climate-lama-ui#505 CortoMaltese3/climate-lama-ui#508 CortoMaltese3/climate-lama-ui#500 --no-pr-ci --max-runtime 180 |
B4 lands | agent | 180 | A-B6, A-U5c, A-U6a, A-U7a, A-U4a |
| B6 | /phase-build 1476 CortoMaltese3/climate-lama-ui#509 CortoMaltese3/climate-lama-ui#499 CortoMaltese3/climate-lama-ui#507 CortoMaltese3/climate-lama-ui#501 --no-pr-ci --max-runtime 180 |
B5 lands | agent | 180 | C-B1, A-U7b, A-U3b, A-U6c, A-U4b |
| B7 | /phase-build 1477 1478 CortoMaltese3/climate-lama-ui#533 CortoMaltese3/climate-lama-ui#506 CortoMaltese3/climate-lama-ui#513 --no-pr-ci --max-runtime 180 |
B6 lands | agent | 180 | C-B2a, C-B2b, C-U13a, A-U6b, B-U3a |
| B8 | /phase-build 1479 1480 CortoMaltese3/climate-lama-ui#535 CortoMaltese3/climate-lama-ui#514 CortoMaltese3/climate-lama-ui#516 --no-pr-ci --max-runtime 180 |
B7 lands | agent | 180 | C-B2c, C-B3a, C-U13c, B-U3b, B-U4a |
| B9 | /phase-build CortoMaltese3/climate-lama-ui#536 1481 CortoMaltese3/climate-lama-ui#540 CortoMaltese3/climate-lama-ui#519 CortoMaltese3/climate-lama-ui#511 --no-pr-ci --max-runtime 180 |
B8 lands | agent | 180 | C-U13d, C-B3b, C-U2a, B-U5a, B-U1 |
| B10 | /phase-build CortoMaltese3/climate-lama-ui#530 CortoMaltese3/climate-lama-ui#531 CortoMaltese3/climate-lama-ui#517 CortoMaltese3/climate-lama-ui#518 CortoMaltese3/climate-lama-ui#512 --no-pr-ci --max-runtime 180 |
B9 lands | agent | 180 | C-U12a, C-U12b, B-U4b, B-U4c, B-U2 |
| B11 | /phase-build CortoMaltese3/climate-lama-ui#532 CortoMaltese3/climate-lama-ui#520 CortoMaltese3/climate-lama-ui#521 CortoMaltese3/climate-lama-ui#541 CortoMaltese3/climate-lama-ui#542 --no-pr-ci --max-runtime 180 |
B10 lands | agent | 180 | C-U12c, B-U5b, B-U5c, C-U2b, C-U2c |
| B12 | /phase-build CortoMaltese3/climate-lama-ui#525 CortoMaltese3/climate-lama-ui#522 CortoMaltese3/climate-lama-ui#543 CortoMaltese3/climate-lama-ui#544 --no-pr-ci --max-runtime 180 |
B11 lands | agent | 180 | B-U8, B-U6, C-U2d, C-U3a |
| B13 | /phase-build CortoMaltese3/climate-lama-ui#523 CortoMaltese3/climate-lama-ui#548 CortoMaltese3/climate-lama-ui#550 CortoMaltese3/climate-lama-ui#524 --no-pr-ci --max-runtime 180 |
B12 lands | agent | 180 | B-U7a, C-U5a, C-U6, B-U7b |
| B14 | /phase-build CortoMaltese3/climate-lama-ui#526 CortoMaltese3/climate-lama-ui#545 CortoMaltese3/climate-lama-ui#515 CortoMaltese3/climate-lama-ui#534 --no-pr-ci --max-runtime 180 |
B13 lands | agent | 180 | C-U10a, C-U3b, B-U3c, C-U13b |
| B15 | /phase-build CortoMaltese3/climate-lama-ui#546 CortoMaltese3/climate-lama-ui#547 CortoMaltese3/climate-lama-ui#527 CortoMaltese3/climate-lama-ui#528 --no-pr-ci --max-runtime 180 |
B14 lands | agent | 180 | C-U3c, C-U4, C-U10b, C-U11a |
| B16 | /phase-build CortoMaltese3/climate-lama-ui#549 CortoMaltese3/climate-lama-ui#537 CortoMaltese3/climate-lama-ui#529 --no-pr-ci --max-runtime 180 |
B15 lands | agent | 180 | C-U5b, C-U1a, C-U11b |
| B17 | /phase-build CortoMaltese3/climate-lama-ui#551 CortoMaltese3/climate-lama-ui#554 --no-pr-ci --max-runtime 180 |
B16 lands | agent | 180 | C-U7a, C-U9a |
| B18 | /phase-build CortoMaltese3/climate-lama-ui#538 CortoMaltese3/climate-lama-ui#552 CortoMaltese3/climate-lama-ui#555 CortoMaltese3/climate-lama-ui#556 --no-pr-ci --max-runtime 180 |
B17 lands | agent | 180 | C-U1b, C-U7b, C-U9b, C-U9c |
| B19 | /phase-build CortoMaltese3/climate-lama-ui#539 CortoMaltese3/climate-lama-ui#553 --no-pr-ci --max-runtime 180 |
B18 lands | agent | 180 | C-U1c, C-U8 |
| B20 | /phase-build 1483 1487 --no-pr-ci --max-runtime 180 |
B19 lands; D22 cut-line check (escape rate after wave C) | agent | 180 | D-B2a, D-B5a |
| B21 | /phase-build 1482 1484 --no-pr-ci --max-runtime 180 |
B20 lands | agent | 180 | D-B1, D-B2b |
| B22 | /phase-build 1488 1485 --no-pr-ci --max-runtime 180 |
B21 lands | agent | 180 | D-B5b, D-B3 |
| B23 | /phase-build 1486 1489 CortoMaltese3/climate-lama-ui#557 CortoMaltese3/climate-lama-ui#558 --no-pr-ci --max-runtime 180 |
B22 lands | agent | 180 | D-B4, D-B6a, D-U1a, D-U1b |
| B24 | /phase-build 1490 1491 CortoMaltese3/climate-lama-ui#561 CortoMaltese3/climate-lama-ui#562 --no-pr-ci --max-runtime 180 |
B23 lands | agent | 180 | D-B6b, D-B7a, D-U3a, D-U3b |
| B25 | /phase-build 1497 1492 CortoMaltese3/climate-lama-ui#559 CortoMaltese3/climate-lama-ui#560 CortoMaltese3/climate-lama-ui#563 --no-pr-ci --max-runtime 180 |
B24 lands | agent | 180 | E-B3, D-B7b, D-U2a, D-U2b, D-U3c |
| B26 | /phase-build 1493 CortoMaltese3/climate-lama-ui#564 1494 CortoMaltese3/climate-lama-ui#566 --no-pr-ci --max-runtime 180 |
B25 lands | agent | 180 | E-B1a, D-U4, E-B1b, E-U2 |
| B27 | /phase-build 1495 1496 --no-pr-ci --max-runtime 180 |
B26 lands | agent | 180 | E-B1c, E-B2 |
| B28 | /phase-build CortoMaltese3/climate-lama-ui#565 1500 CortoMaltese3/climate-lama-ui#567 --no-pr-ci --max-runtime 180 |
B27 lands | agent | 180 | E-U1, R1, R1-ui |
| O2 | /phase-build 1499 --no-pr-ci --max-runtime 120 |
B9 lands, then a backbone release carries #1479 and #1481 | agent · orchestrator (prod SSH) | 120 | O2 — Oasis sidecar on prod |
| O1 | owner: create the key, then follow the Run: line on #1498 |
B27 lands (#1494, #1496) | owner · credential ANTHROPIC_API_KEY |
— | O1 — assistant on prod |
| R2 | /tag on both repos → gh workflow run deploy-prod.yml -R CortoMaltese3/climate-lama --ref main → owner walk (exit criterion 2) |
B28 lands | agent + owner walk | — | R2 — release, walk, mid-review |
Showcase moments this phase advances¶
- Moment 1 (citizen) — the place page: hazard tabs, 30-year chances, past events, a place report, phone-first.
- Moment 2 (insurer) — Explore with a portfolio scope: vector-tile aggregates, strip, cross-filtering charts, report studio; upload with templates.
- Moment 3 (scientist) — Compare mode (scenarios and three engines — climate-lama-engine, CLIMADA, Oasis LMF — swipe/side by side, curves labelled by basis), Analyses cards with reproduce bundles.
- Moment 4 (decision-maker) — Events mode with the timeline player; a new step: pick adaptation measures and read avoided loss (after D-B1).
- Across moments — "Ask" runs the same named actions (when a key is configured).
Exit criteria¶
- Build issues at 0 open on
Phase 17 -- Revampin both repos (trackers route byBlocker:;credential:rows leave for the standingopslist). - One phase-end walk, by the owner on prod after the release, on prod data only: every view in its locked layout, the revised steps of Moments 1–4, and the CLAUDE.md demoable slice (bootstrap → log in → ingest → configure → run → results → save) through the new surfaces; findings tagged and fixed in one closing wave or filed
found-in:phase-17. - Measured: the performance suite on the MVT path records ≥ 30 FPS (5th percentile) on the 10M fixture, p95 first tile < 500 ms at z 4–6 and < 500 MB; the no-page-scroll assertions at 1440×900 and the 390-px checks are green with recorded prove-it-fails; the colour guard count is below its baseline and zero for migrated components.
- Contracts: persona preference; coverage computability + admin levels; admin-boundary endpoint; result and exposure MVT; measures PATCH, seed and images; cost-benefit stored risk; report documents CRUD and render; assistant endpoint (flagged); curve basis and
aalon every result; theoasisengine (reference-model conformance job green, one platform scenario through three engines) — each covered bytests/e2e/smoke/contract-map.tsor the named conformance job. - Offline acceptance: a stack with every external provider unset renders every view (scripted check).
- Guardrails hold and the escape rate is ≤ 25 % (target 0.25 escape / 0.15 partial per the 2026-09-25 amendment).
- Record:
mid-review-phase-17.md, plan.md banner, Phase 18 charter stub, ADRs for D4, D10, D11 and the Oasis sidecar (C-B2).
Conflict hotspots¶
UI: src/App.tsx + src/test/App.test.tsx (A-U3 → B-U3 → C-U10/C-U11 redirects — serial);
package.json + lock (A-U2, A-U5 — serial); src/api/* after A-U0 (one module per domain, one owner
at a time) and tests/e2e/smoke/contract-map.ts (every consumer — serial); src/test/helpers/maplibreMock.ts
(extended by A-U5; surface issues then move their tests onto it one at a time); src/run/assessAddress.ts (A-U4 then B-U4); per-surface i18n files (one owner each).
Backbone: main.py router mounts (A-B3, E-B1 — serial); api/v1/tiles.py + core/vector_tiles.py
(A-B5 then A-B6); worker/tasks.py (A-B4, D-B1); worker/models/engine_adapter.py (D-B1 only);
migrations/versions head chain (A-B4, C-B1, D-B2, D-B5 — serial in merge order; the numbers in
Files cells are indicative); worker/models/registry.py + core/engines.py + config.py + compose
files and docker/oasis-sidecar/* (C-B2 then C-B3); docs/CLIMATE_LAMA.md and docs/DECISIONS.md
(one region per issue).
Reassess record (2026-09-26, fast — 2 refuters + completeness critic)¶
Confirmed: the role lens; the dense shell; phones; themes and palettes; showcase/e2e migration;
the place page; vector tiles with the revived performance suite; MapLibre 6 + MapKit; the adapter
future-hazard fix, measures PATCH/seed/images; report documents on WeasyPrint; the assistant
boundary; schema-only persona storage; the admin-boundary endpoint. Corrected: the IA (Adapt
absorbs /measures; multi-country scope; Live staged behind live_mode; direct redirect table;
plan.md Goal C amended) → D1; the chances formula (non-stationary 1 − Π(1 − p_t) with a named
threshold, interpolated across horizons, "independent years" stated) → C-U2; the headline (bands
flag path; letters an open option; hazard-group level) → D6; Data (list and detail in one view
via a retractable drawer, full page for editing; templates in Data, Portfolios and Adapt) →
C-U7; the switch contract (a scenario/horizon switch is a hazard-dataset change — cache hits
swap, misses ask) → D8; LitPop resolution (150″ ≈ 4.6 km) → D18; tiers (a design note only —
plan.md keeps the business model out of focus) → D20. Added: the coverage contract (D14),
the phase shape and cut line (D22), offline acceptance (D15), pins → exposures (D17), the charter
deferrals' placement (D21). Artifact review: see "Prototype review" below.
Direction lock (2026-09-27, decision protocol — one refuter). The owner's one reversal (Oasis
LMF in Phase 17) was made concrete as four claims; all four came back NEEDS-CHANGE and the corrected
designs are adopted. C1: a sidecar per ADR-042, but a second engine needs its own registry block,
settings and an import-ban test (C-B2). C2: ground-up first is right, but insured loss already
exists as a flat deductible/cover, so Oasis maps that and only OED per-location/account structure moves
to Phase 18. C3: the conversion covers RETURN_PERIOD datasets only, keeps the backbone's
centroid assignment and reports OEP, not AEP, until stochastic event sets exist (C-B3, S3). C4: the
vocabulary needs an explicit curve basis, aal over the aggregate (not per-point EAI) and a
basis-labelled PML (C-B1). Added: a conformance job that cannot silently skip (C-B2) and the UI's
dependency on the engine roster (B-U7, C-U13).
Prototype review (artifact reassess)¶
2026-09-26, fast — two refuters (owner fidelity per view; direction and plan consistency) and a completeness critic, over the owner's request, all 93 comments, the prototype source and desktop, phone and dark renders of every view; one claim settled inline. Of 26 claims, 17 were confirmed as built, 8 corrected and 1 dropped; the critic added 3 items.
- Confirmed: Home command centre with the role lens (C15, C71, C85, C86); Explore — scope picker down to event/live, zoom-driven aggregates, hazard-family layers, dark strip, timeline, palettes with greyscale and a non-RAG default, 3D, hazard-icon popups, retractable panels, three layouts (C7–C11, C21, C22, C26–C34, C43, C64, C65, C74–C79, C84, C91–C94, C110); Portfolios (C13, C70, C72, C84, C86, C87); Adapt (C66, C68, C81, C82); Reports (C16, C20, C41, C46, C63); Data (C12, C13, C72, C73, C76); Analyses (C19, C38, C57); Settings (C6, C11, C67, C74, C79, C92); Admin with Oasis and OpenQuake planned (C58); phones at 390 px; the assistant acting through named actions with a trace (C14); density (C15, C86, C107); every owner-named view navigable, with the Direction board; graphs across views; honesty labels (Q1); scenario and engine compare, swipe and side by side (Q5); maps at scale with a measured exit (Q10).
- Corrected in the prototype: Lookup "Edit home" property preview, labelled Phase 18 (P3; C98,
C105); every Explore control through the registry —
set_scopegains area, pins, asset, event and level-1 regions, newset_engine, palette and level viaset_map(Q2); the switch contract — an uncomputed cell shows no figures in strip, panel, table or result layers until Compute,set_analysisreportscached, newcompute_cell, the assistant asks first (Q3); satellite and terrain entries lead to Settings → Appearance (Q4); "n hazards not covered" computed from the catalogue on Home, Lookup and Reports (Q6); Explore → Events → "Create a watch" dialog and the infrastructure block labelled Phase 18 (Q9); the million-point GPU layer reachable from Explore → Layers → "Scale test", with an 8-bit point-to-asset index fixed (inline S1); a dark-theme capture stall on Analyses (dense SVG thumbnails plus an infinite animation) fixed by the view's builder. - Corrected in this plan: D1
/scenarios; D3 density; D5 shared map mock is new, globe projection; D6 count read from coverage; D8 never the previous cell's figures; D11 registry guard test; D15 provider entries; A-U5 title and files; A-B3 grounding (the data layer exists); Files and Grounding on all 37 rows of waves B–E (D-B4's storage already exists — a response-shape change; C-U5's one-screen layout and C-U11's watch dialog largely exist today); acceptance boxes under waves B, C and D. - Added (completeness critic): step 6 of the demoable slice ("save the result") through Explore — B-U4, C-U9, exit criterion 2, R1; Lookup key insights with an honesty rule — C-U3 (C106); export continuity until the report studio lands — B-U5.
- Dropped: "ORSA is never marked as existing" (Q7) — the template carries
exists: truein the prototype's catalogue (mock.js:565), so its tile reads "Available today"; the plan keeps the acceptance box on D-U3/D-U4.
After the fixes, a scripted functional pass went 17/17 with no page errors: the switch contract, registry routing, the scale test (1,000,000 points drawn in 1.65 s headless), the watch dialog, the edit-home recalculation and the computed coverage wording.
Out of scope (deliberate)¶
defer:engines beyond Oasis (Delft-FIAT, OS-Climate physrisk, OpenQuake after licence review), Oasis OED per-location/account policy terms, layers and reinsurance, AEP on stochastic event sets, and uncertainty ranges → Phase 18 charter stub (D19, D21).defer:property-level build — footprints, editable homes, street-level photos (C98–C100, C105) → S1 spike (exploration/property-level.md: licence outcome, hazard resolution, exposure source, Phase 18 build items), then Phase 18 (D18).defer:polygons and corridors — railways, roads, parks, elevation profiles (C42) → Phase 18 (D17).defer:stochastic event sets, GeoTIFF exposure upload through the API, per-asset impact-function binding → Phase 18 (D21).defer:subscription tiers and billing (C24, C25, C97) → design note in ui-revamp.md (D20).defer:e-mail and push alerts → Phase 18 (ADR-037 unchanged in Phase 17).defer:deck.gl → only if B-U2's measured numbers fail the budget (D4).defer:critical-infrastructure layers for the community view (C108) → needs an open, licensed source (OSM under the ODbL review) → Phase 18.defer:compliance scoring beyond the coverage checklist (C63) → Phase 18.defer:per-flood-type vs combined aggregation selector for EIOPA (C51) → Phase 18 backlog note.guard:satellite imagery and DEM terrain are configured providers only; the offline default stays vector (D15).guard:no persona is ever used for authorization (D2).
Phase state — 2026-10-01 (by /phase-analyze, origin/main 433e12f5)¶
| Wave | Status | Owner | Issues | Deps | Command | Budget | Notes |
|---|---|---|---|---|---|---|---|
| B1 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui492 ✓ui493 ✓ui495 ⊘1464 ✓1466 +1 | — | /phase-build 1464 1466 1467 CortoMaltese3/climate-lama-ui#492 CortoMaltese3/climate-lama-ui#493 CortoMaltese3/climate-lama-ui#495 --no-pr-ci --max-runtime 180 |
180/135 | lama-phase-build-P17-B1 |
| B2 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui502 ✓ui494 ✓1468 ✓1470 | B1 | /phase-build 1468 1470 CortoMaltese3/climate-lama-ui#502 CortoMaltese3/climate-lama-ui#494 --no-pr-ci --max-runtime 180 |
180/77 | UI half · backbone half ran as BE1 |
| B3 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui496 ✓ui497 ⊘1465 ✓1475 ✓1469 +1 | B2 | /phase-build 1465 1475 1469 1471 CortoMaltese3/climate-lama-ui#496 CortoMaltese3/climate-lama-ui#497 --no-pr-ci --max-runtime 260 |
260/99 | lama-ui-phase-build-P17-B3 |
| B4 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui503 ✓ui510 ✓ui498 ⊘1463 ✓1472 +1 | B3 | /phase-build 1463 1472 1473 CortoMaltese3/climate-lama-ui#503 CortoMaltese3/climate-lama-ui#510 CortoMaltese3/climate-lama-ui#498 --no-pr-ci --max-runtime 180 |
180/180 | lama-ui-phase-build-P17-B4 |
| B5 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui504 ✓ui505 ✓ui508 ✓ui500 ✓1474 | B4 | /phase-build 1474 CortoMaltese3/climate-lama-ui#504 CortoMaltese3/climate-lama-ui#505 CortoMaltese3/climate-lama-ui#508 CortoMaltese3/climate-lama-ui#500 --no-pr-ci --max-runtime 180 |
180/171 | partial at closeout, closed since |
| B6 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui509 ✓ui499 ✓ui507 ✓ui501 ✓1476 | B5 | /phase-build 1476 CortoMaltese3/climate-lama-ui#509 CortoMaltese3/climate-lama-ui#499 CortoMaltese3/climate-lama-ui#507 CortoMaltese3/climate-lama-ui#501 --no-pr-ci --max-runtime 180 |
180/155 | lama-ui-phase-build-P17-B6 |
| B7 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui533 ✓ui506 ✓ui513 ✓1477 ✓1478 | B6 | /phase-build 1477 1478 CortoMaltese3/climate-lama-ui#533 CortoMaltese3/climate-lama-ui#506 CortoMaltese3/climate-lama-ui#513 --no-pr-ci --max-runtime 180 |
180/143 | lama-ui-phase-build-P17-B7 |
| B8 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui535 ✓ui514 ✓ui516 ✓1479 ✓1480 | B7 | /phase-build 1479 1480 CortoMaltese3/climate-lama-ui#535 CortoMaltese3/climate-lama-ui#514 CortoMaltese3/climate-lama-ui#516 --no-pr-ci --max-runtime 180 |
180/159 | lama-ui-phase-build-P17-B8 |
| B9 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui536 ✓ui540 ✓ui519 ✓ui511 ✓1481 | B8 | /phase-build CortoMaltese3/climate-lama-ui#536 1481 CortoMaltese3/climate-lama-ui#540 CortoMaltese3/climate-lama-ui#519 CortoMaltese3/climate-lama-ui#511 --no-pr-ci --max-runtime 180 |
180/176 | lama-ui-phase-build-P17-B9 |
| B10 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui530 ✓ui531 ✓ui517 ✓ui518 ✓ui512 | B9 | /phase-build CortoMaltese3/climate-lama-ui#530 CortoMaltese3/climate-lama-ui#531 CortoMaltese3/climate-lama-ui#517 CortoMaltese3/climate-lama-ui#518 CortoMaltese3/climate-lama-ui#512 --no-pr-ci --max-runtime 180 |
180/214 | over budget · partial at closeout |
| B11 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui532 ✓ui520 ✓ui521 ✓ui541 ✓ui542 | B10 | /phase-build CortoMaltese3/climate-lama-ui#532 CortoMaltese3/climate-lama-ui#520 CortoMaltese3/climate-lama-ui#521 CortoMaltese3/climate-lama-ui#541 CortoMaltese3/climate-lama-ui#542 --no-pr-ci --max-runtime 180 |
180/— | session has no closeout |
| B12 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui525 ✓ui522 ✓ui543 ✓ui544 | B11 | /phase-build CortoMaltese3/climate-lama-ui#525 CortoMaltese3/climate-lama-ui#522 CortoMaltese3/climate-lama-ui#543 CortoMaltese3/climate-lama-ui#544 --no-pr-ci --max-runtime 180 |
180/199 | over budget |
| B13 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui523 ✓ui548 ✓ui550 ✓ui524 | B12 | /phase-build CortoMaltese3/climate-lama-ui#523 CortoMaltese3/climate-lama-ui#548 CortoMaltese3/climate-lama-ui#550 CortoMaltese3/climate-lama-ui#524 --no-pr-ci --max-runtime 180 |
180/169 | lama-ui-phase-build-P17-B13 |
| B14 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui526 ✓ui545 ✓ui515 ✓ui534 | B13 | /phase-build CortoMaltese3/climate-lama-ui#526 CortoMaltese3/climate-lama-ui#545 CortoMaltese3/climate-lama-ui#515 CortoMaltese3/climate-lama-ui#534 --no-pr-ci --max-runtime 180 |
180/118 | partial at closeout, closed since |
| B15 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui546 ✓ui547 ✓ui527 ✓ui528 | B14 | /phase-build CortoMaltese3/climate-lama-ui#546 CortoMaltese3/climate-lama-ui#547 CortoMaltese3/climate-lama-ui#527 CortoMaltese3/climate-lama-ui#528 --no-pr-ci --max-runtime 180 |
180/— | session has no closeout |
| B16 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui549 ✓ui537 ✓ui529 | B15 | /phase-build CortoMaltese3/climate-lama-ui#549 CortoMaltese3/climate-lama-ui#537 CortoMaltese3/climate-lama-ui#529 --no-pr-ci --max-runtime 180 |
180/158 | lama-ui-phase-build-P17-B16 |
| B17 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui551 ✓ui554 | B16 | /phase-build CortoMaltese3/climate-lama-ui#551 CortoMaltese3/climate-lama-ui#554 --no-pr-ci --max-runtime 180 |
180/115 | lama-ui-phase-build-P17-B17 |
| B18 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui538 ✓ui552 ✓ui555 ✓ui556 | B17 | /phase-build CortoMaltese3/climate-lama-ui#538 CortoMaltese3/climate-lama-ui#552 CortoMaltese3/climate-lama-ui#555 CortoMaltese3/climate-lama-ui#556 --no-pr-ci --max-runtime 180 |
180/145 | lama-ui-phase-build-P17-B18 |
| B19 | ✅ done — ui e2e 36841008822 at 0640a63 |
agent (UI) | ✓ui539 ✓ui553 | B18 | /phase-build CortoMaltese3/climate-lama-ui#539 CortoMaltese3/climate-lama-ui#553 --no-pr-ci --max-runtime 180 |
180/~200 | over budget |
| B20 | ✅ done — be 36840997605 at 433e12f5 |
agent | ✓1483 ✓1487 | B19 · D22 cut line | /phase-build 1483 1487 --no-pr-ci --max-runtime 180 |
180/80 | lama-ui-phase-build-P17-B20 |
| B21 | ✅ done — be 36840997605 at 433e12f5 |
agent | ✓1482 ✓1484 | B20 | /phase-build 1482 1484 --no-pr-ci --max-runtime 180 |
180/52 | lama-ui-phase-build-P17-B21 |
| B22 | ✅ done — be 36840997605 at 433e12f5 |
agent | ✓1488 ✓1485 | B21 | /phase-build 1488 1485 --no-pr-ci --max-runtime 180 |
180/40 | lama-ui-phase-build-P17-B22 |
| B23 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui557 ✓ui558 ✓1486 ✓1489 | B22 | /phase-build 1486 1489 CortoMaltese3/climate-lama-ui#557 CortoMaltese3/climate-lama-ui#558 --no-pr-ci --max-runtime 180 |
180/~200 | over budget |
| B24 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui561 ✓ui562 ✓1490 ✓1491 | B23 | /phase-build 1490 1491 CortoMaltese3/climate-lama-ui#561 CortoMaltese3/climate-lama-ui#562 --no-pr-ci --max-runtime 180 |
180/~140 | lama-ui-phase-build-P17-B24 |
| B25 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui559 ✓ui560 ✓ui563 ✓1497 ✓1492 | B24 | /phase-build 1497 1492 CortoMaltese3/climate-lama-ui#559 CortoMaltese3/climate-lama-ui#560 CortoMaltese3/climate-lama-ui#563 --no-pr-ci --max-runtime 180 |
180/116 | lama-ui-phase-build-P17-B25 |
| B26 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui564 ✓ui566 ✓1493 ✓1494 | B25 | /phase-build 1493 CortoMaltese3/climate-lama-ui#564 1494 CortoMaltese3/climate-lama-ui#566 --no-pr-ci --max-runtime 180 |
180/~100 | lama-ui-phase-build-P17-B26 |
| B27 | ✅ done — be 36840997605 at 433e12f5 |
agent | ✓1495 ✓1496 | B26 | /phase-build 1495 1496 --no-pr-ci --max-runtime 180 |
180/86 | lama-ui-phase-build-P17-B27 |
| B28 | ✅ done — be 36840997605 · ui e2e 36841008822 | agent | ✓ui565 ✓ui567 ✓1500 | B27 | /phase-build CortoMaltese3/climate-lama-ui#565 1500 CortoMaltese3/climate-lama-ui#567 --no-pr-ci --max-runtime 180 |
180/~90 | lama-ui-phase-build-P17-B28 |
| O1 | ✅ done — #1498 closed with prod proof | owner | ✓1498 | B27 ✓ | owner: create the key, then follow the Run: line on #1498 |
— | prod model claude-sonnet-5-5 via .env.prod |
| P17-BE1 | ✅ done — be 36840997605 at 433e12f5 |
agent | — | — | backbone lane, no doc command | 300/142 | lama-phase-build-P17-BE1 · no doc row |
| O2 | 🟠 owner: agreement run needs an owner token | agent · owner | ⊘1499 | v0.27.0 deployed ✓ | /phase-build 1499 --no-pr-ci --max-runtime 120 |
120/— | boxes 1–2 ticked · 3–4 ride the walk |
| R2 | 🟠 owner: phase-end walk + mid-review | owner + agent | ⊘1501 | O2 box 4 | /tag on both repos → gh workflow run deploy-prod.yml -R CortoMaltese3/climate-lama --ref main → owner walk |
— | release half done 2026-10-01 |
| P17-B29 | — no closeout · nothing open on gh | agent (UI) | — | — | none recorded | —/— | no doc row |
| # | Wave | Command | Session | After |
|---|---|---|---|---|
| 1 | O2 | python scripts/verify_engine_agreement.py --base-url https://api.climate-lama.online --hazard <id> --exposure <id> --require-engine climada --require-engine oasis |
owner shell | — |
| 2 | R2 | owner walk on prod per #1501, then docs/plan/mid-review-phase-17.md |
lama-release-P17-R2 |
O2 |
- release — climate-lama
v0.27.0at433e12f5(0 behind, pyproject 0.27.0, Alembic 0098, deploy-prod 36844235147 ✓) · climate-lama-uiv0.17.0at0640a63(0 behind, package.json 0.17.0) - unassigned — ✓1507 ✓1544 ✓ui620 (filed in-run) · superseded — none · any-time — none
- holds — none · lanes — 0 builder worktrees / cap 3 · stale worktree — none