Skip to content

MIP-0034 tasks — the INMET alert arm

Ordered delivery of MIP-0034 §5.1/§5.2 only, the inbound operational-alert path, as stacked PRs (.claude/skills/mip-tasks/SKILL.md, scripts/stack.sh). Branch mip-0034/<k>-<slug>, each based on the previous; merge bottom-up, scripts/stack.sh restack after each squash-merge.

MIP-0034 is explicitly "three separable things, deliberately not one pipeline". The other two, §5.3/§5.4 the reading queue, §5.6 the outbound Atom feed, ship alone, touch none of the files below, and get their own task list when someone wants them. Nothing here blocks them.

# slug delivers tests (must exist before the PR) depends on
1 cap-parser core/alerts/: MarineAlert, AlertArea, AlertSeverity (CAP's Moderate/Severe/Extreme, with INMET's Portuguese label kept verbatim for display), CapParser (CAP 1.2 XML → MarineAlert), Geo.contains (ray-casting point-in-polygon, ~20 lines, no dependency). Fixtures core/src/test/resources/feeds/inmet-cap-55649.xml (the real 291-point document) and inmet-avisos.xml. No network, no HTTP, no wiring CapParserSpec: 55649 parses: event, both severity vocabularies, onset/expires with their -03:00 offset, 291 polygon points, web URL; a CAP doc with no <polygon> yields an empty area and never matches, rather than throwing. GeoSpec: inside, outside, on a vertex, on an edge, and a point whose latitude exactly equals a vertex's (ray-casting off-by-one); a Joaquina coordinate is NOT covered by 55649's real Amazonas polygon and a coordinate inside it is, the inverted-lat/lon catcher –
2 inmet-client core/alerts/InmetAlertClient: RSS index → per-item CAP fetch → List[MarineAlert]. Sends an explicit User-Agent (§4.1: INMET returns no HTTP response to curl's default, which reads as an outage). Event allow-list (Ventos Costeiros, Vendaval, Tempestade, Chuvas Intensas; drops Baixa Umidade, Geada, Declínio de Temperatura, 36 of 89 live items). Expiry filter on [onset, expires]. Fails soft: a fetch error yields Nil, never an exception up the pipeline InmetAlertClientSpec against the saved fixtures via the existing HTTP test seam (no live call in just test): the allow-list drops the three non-marine events, an expired alert is absent at now, an alert active at now is present, a 500 on one item's CAP does not lose the other items. One E2E-tagged live test, excluded from just test, in just e2e 1
3 cli-surface Report/Main: the alert block of MIP-0034 §3 above the summary: severity, window, INMET's own text, areaDesc, and the Source: URL the licence requires. The explicit ⚠ INMET — no active warning for this area line when there are none, so absence is legible rather than ambiguous. Not in the LLM prompt (MIP §5.2) ReportSpec: an alert renders with its source URL and its area description; no alerts renders the explicit no-warning line; the block is byte-identical with the LLM disabled (proves nothing on this path is model output) 2
4 board-schema core/site/Board: optional alerts array via Board.alertJson (beside waterJson/trailJson), Board.SchemaVersion → 2, site/board.schema.json gains the optional array BoardSpec: a board with alerts round-trips through JsonValue.parse and conforms to the schema; a board with no alerts key still deserialises (the trails precedent from MIP-0030); the existing schema check passes with and without 3
5 map-ui site/static/: the alert control, a collapsed row per active alert, ordered by severity then onset, hover reveals INMET's full text and dates, and hovering a row draws that alert's polygon on the map. Same idiom and icon vocabulary as the ranked beach list; icons come from MIP-0046's set, no new emoji scripts/site_check.js (a just quality-other gate): a board fixture with two alerts renders two rows, highest severity first; hovering a row adds exactly one polygon layer and leaving removes it; a board with no alerts key renders no control and logs no error. Manual at 390 px and 1280 px, MIP-0046's standard, the stub harness is not sufficient 4
6 backfill scripts/inmet_backfill.py, stdlib-only with --self-test, wired into quality-other: walks ids downward from a given start, writes one JSON per alert, resumable, skips ids already on disk. Conservative by default (bounded --count, a sleep between fetches) because no rate limit is documented; see the MIP's "Not checked". Offline path only: not in CI, not on any request path inmet_backfill.py --self-test: id walking stops at --count; a 500 is recorded and skipped rather than ending the walk (ids have gaps, only five were sampled); an existing output file is not re-fetched; the CAP→record shaping matches task 1's parser on the same fixture 1
7 docs-status docs/2-Building-marola/ARCHITECTURE.md §5 gains the alert integration row; docs/4-Research-and-plans/FUTURE-WORK.md §9.2's proactive-hazard sketch points here for what was built and what was not; MIP-0034 §5.1/§5.2 → Implemented with the PR links, other arms left Draft just build && just test && just quality green; just mip-graph --check clean 5, 6

Decisions taken so the tasks above are unambiguous (all from MIP-0034 §4.1's 2026-09-08 revision):

  1. Geometry, not names. Matching is point-in-polygon against each alert's CAP <polygon>. AlertAreas.byArea, the hand-written mesoregion→areas.json table in the first draft, is not built. This is what makes the path work for all of Brazil: a new area in areas.json needs no new mapping row.
  2. Two severity vocabularies, both kept. Order by CAP's Moderate/Severe/Extreme; display INMET's Perigo Potencial/Perigo/Grande Perigo verbatim. Never translate INMET's words.
  3. RSS is the index, CAP is the content. The <description> HTML table is a fallback for fields CAP lacks, not the parse path.
  4. /avisos/ativos JSON is not used in these tasks. It is richer (GeoJSON, IBGE codes, aviso_cor) but undocumented; adopting it is a later, separate change that must degrade to RSS+CAP when it moves.
  5. No scoring change. MIP-0034 §6 and the 2026-09-08 re-probe reached this separately: Swimability.score is untouched, and an alert suppresses nothing. Whether Grande Perigo / Extreme should suppress a recommendation is a human decision and needs its own MIP.
  6. No LLM on this path, per §5.2. The alert text is INMET's, shown verbatim with its source.

Restack notes: tasks 1→5 form a single line, each adding to what the previous created and never rewriting it (1 adds core/alerts/, 2 adds one file in it, 3 touches Report, 4 touches Board + the schema, 5 touches site/static/ + site_check.js). Task 6 depends only on task 1's parser and touches scripts/ alone, so it can be developed in parallel and merged in any order relative to 3–5, but scripts/stack.sh wants a line, so it is stacked after 5 unless someone splits it out deliberately. Task 7 is docs-only and must merge last, since it flips the MIP's status.

Open before task 5: the map control's exact shape is the one thing here not pinned by the MIP: "a list/hover like the ranked beaches" is the requirement, and MIP-0046's icon set is the constraint. Worth a look at the rendered page before writing the CSS, not after.