Skip to content

fix(weather): era-complete GOES coverage — filter, clamps, fallback, scope keys, size cap#101

Merged
helloiamvu merged 9 commits into
mainfrom
phase28/goes19-era-coverage
Jul 5, 2026
Merged

fix(weather): era-complete GOES coverage — filter, clamps, fallback, scope keys, size cap#101
helloiamvu merged 9 commits into
mainfrom
phase28/goes19-era-coverage

Conversation

@helloiamvu

Copy link
Copy Markdown
Member

Why

The KNYC measurement run exposed that the fleet's satellite request layer was era-blind, and stress-testing the fix surfaced a family of related defects. Observed live: KNYC 2026 backfilled ALL-EMPTY (--satellites goes16; GOES-19 became GOES-East in 2025), and modern ACMC files were 100% rejected by a stale size cap.

What (6 commits)

  1. Era-complete GOES defaults + coverage filter — roster/workflow defaults become goes16,goes17,goes18,goes19; _enumerate_slices gains a GOES coverage filter so the cartesian product no longer fetches archives that can't serve a station.
  2. _AVAILABLE_UNTIL retirement clamp (goes17→2024-01-01, goes16→2026-01-01; ~1yr margins, month-granular, no progress markers written for clamped slices — reversible).
  3. Scope-keyed progress objects ({stations}_{years}_{satsproducts-digest} instead of Batch shard index) — concurrent station-mode jobs (multi-station measurement) no longer clobber each other's GCS markers. Plus a satellites workflow input enabling Asia (Himawari) / Europe (VIIRS) station runs.
  4. East-for-West era fallback — the E/W split is a routing preference, not a coverage boundary: GOES-East genuinely covers the whole Americas, and for 2017→Aug-2018 goes16 was the only ABI platform. West platforms stay excluded for East stations (PACUS truly can't see them).
  5. Full-month, un-margined coverage claims — positive "native covers this month" claims require the FULL month and use a new un-margined _GOES_ACTUAL_DATA_END map (archive-verified), keeping margins fetch-side only.
  6. CONUS size cap 2→16 MiB — modern ACMC files are 4.3–4.6 MB (NOAA added variables ~2021+); the 2 MiB cap zeroed modern-era ACMC coverage fleet-wide.

Review record (loop iterations that CAUGHT things)

  • Codex pass 1 → P2: no era-end bound → fixed (commit 2).
  • Codex pass 2 → clean on commits 1–2.
  • 18-agent adversarial exit panel (findings verified by executing the enumeration code) → P1: Aug 1–26 2018 fetched by NO platform for all 8 West roster stations (partial first-light month wrongly clipped the fallback) + 3 P2s (margined-until-as-coverage; scope key missing product/satellite axes; stale-image warning invisible at dispatch) → all fixed (commit 5 + workflow descriptions).
  • Execute-and-verify round on the final coverage logic → all FIXED, zero new defects, including day-level coverage sweeps 2017–2026 for KSEA/KDEN/KSFO/KLAX and KNYC: zero uncovered days.
  • Codex final stamp on the full branch: pending quota reset (~13:00 local); will be posted as a PR comment before merge.

Operator notes

  • Rebuild the image before dispatching the multi-satellite default (the input descriptions now warn: an old image cartesian-products all four satellites).
  • Old shard-NNNNN progress objects are orphaned (one idempotent re-fetch; slice keys are full-identity so no wrong-skips either way).
  • After merge + rebuild: ACMC 2021+ years need re-dispatch (they produced zero ACMC rows under the old size cap); DSRF ≥ mid-2024 unblocked by fix(weather): unblock full-disk goes16 DSRF + fast single-station year-shard backfill #99 remains valid.

Tests

29 new/updated tests: era-set helper + boundary agreement with the live router; hemisphere/coverage filter (East + West + mixed); until-clamp (6 cases incl. month-granular partial windows); East-for-West fallback (8 cases incl. Aug-2018/Jul-2022 partial-month keeps, margined-until non-claim, explicit-request honored); scope-key disjointness across stations/years/products/satellites + order-insensitivity + multi-station digest; modern-ACMC size admission. Full satellite suite green.

TS Parity

python_only: true — internal satellite backfill enumeration/ops; no mostlyright.* public API change; the TS SDK has no satellite ingest.

🤖 Generated with Claude Code

helloiamvu and others added 6 commits July 5, 2026 11:05
Observed live (station backfill 28734982038): KNYC 2026 completed
ALL-EMPTY because the fleet requests goes16 only, and GOES-16 stopped
being GOES-East in 2025 (goes19 took over). The West side has the mirror
hole: goes18 only reaches back to 2022-07, so West stations were missing
the 2018-2022 goes17 era. And the roster defaults ("goes16","goes18")
carried a latent fleet-cost pathology: _enumerate_slices was a plain
cartesian product, so every East station would fetch the ENTIRE West
archive (and vice versa) — whole-month downloads whose extraction fails
out-of-grid only AFTER the bytes are fetched.

- _sources: add goes_satellites_for_longitude() — East {goes16,goes19} /
  West {goes17,goes18}, keyed on the SAME _GOES_EAST_WEST_SPLIT_LON_DEG
  as the live router (single source of truth; boundary test pins the
  agreement).
- _backfill._enumerate_slices: GOES hemisphere visibility filter — a GOES
  satellite enumerates for a station only on its own side of the split.
  Skips are logged loudly (once per satellite), never silent. Non-GOES
  sources are untouched (they own their coverage).
- __main__._DEFAULT_ROSTER_SATELLITES -> (goes16, goes17, goes18, goes19):
  era-complete; the filter keeps each station on its side pair and the
  available_since clamp skips pre-first-light slices with no I/O.
- run-weather-backfill.yml station+pilot modes pass the full four
  (NOTE: requires an image built after this commit — an old image would
  cartesian-product them).

Transport already supported all four platforms (buckets + available_since
for goes17/goes19 were registered); only the request layer was era-blind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex P2 on the era-coverage branch: goes_satellites_for_longitude
returns every platform that EVER served a side, and the only downstream
clamp was _AVAILABLE_SINCE — so retired platforms (goes17 post-2023
storage, goes16 post-2025 handover) kept enumerating post-retirement
months and burning empty per-hour listing calls, growing every future
fleet year.

- _goes_s3._AVAILABLE_UNTIL: the symmetric counterpart of
  _AVAILABLE_SINCE, RETIRED platforms only (goes17 -> 2024-01-01,
  goes16 -> 2026-01-01). SAFETY over savings: ~1 yr margin past the
  public retirement dates (a too-early date silently skips real
  settlement-adjacent data; a too-late one only wastes cheap listings),
  and the goes16 date is observation-backed (2026 Jan-Jun measured
  ALL-EMPTY live). goes18/goes19 deliberately absent.
- _enumerate_slices: month-granular clamp at ENUMERATION — clamped
  slices are never submitted, write NO progress marker (raising/removing
  a date later backfills them normally), and are logged loudly per
  satellite.
- Tests: 6-case TestGoesOperationalUntilClamp (per-era keep/drop,
  operational platforms never clamped, month-granular partial window);
  hemisphere tests re-anchored to 2022 (in-era for all four platforms);
  the two future-slice resume tests switched goes16->goes19 so the 2099
  window still genuinely exercises resume semantics instead of passing
  vacuously under the clamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enable concurrent multi-station measurement runs (KNYC + Asia + Europe):

- Progress marker objects are now keyed on the RESOLVED (stations,
  year-window) scope instead of the Batch shard index. Shard-index keys
  COLLIDED across concurrent jobs: two station-mode jobs both wrote
  progress/shard-00000/progress.json — a concurrent read-modify-write
  clobber on the same GCS object. The scope key is stable across Spot
  retries (the shard->(station,year) mapping is pure), disjoint across
  array tasks AND across concurrent different-station jobs, and
  resume-stable across re-submissions. Whole-roster scopes (>3 stations,
  e.g. the incremental Cloud Run Job) use a bounded stable digest. Old
  shard-NNNNN objects are orphaned (one idempotent re-fetch; slice keys
  inside the files are full-identity so no wrong-skip either way).
- run-weather-backfill.yml: new `satellites` input for pilot/station
  modes (default: the era-complete GOES set). Enables Asia
  (himawari8,himawari9 + AHI-L2-FLDK-Clouds) and Europe
  (viirs-* + VIIRS-JRR-CloudMask) station measurements; Meteosat backfill
  stays unwired (GRIB reader seam) per the _sources registry.

Tests: scope-key disjointness across stations/year-shards, multi-station
digest bounding, and the C4 wiring test updated to the scope scheme.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex P1 on the hemisphere filter: treating the East/West routing split
as a coverage boundary silently lost the West side's 2017-2018 era. The
sector geometry is ASYMMETRIC: GOES-West PACUS truly cannot see East
stations, but GOES-East CONUS/full-disk genuinely covers the whole
Americas (oblique at the far West) — and for 2017 -> 2018-08-27 (goes17
first light) goes16 was the ONLY ABI platform in existence, so a pure
hemisphere filter left KSEA-class stations with zero 2017-2018 coverage
that the previous defaults DID fetch.

_enumerate_slices now applies direction-correct rules:
- West platforms x East stations: still excluded (true out-of-grid).
- East platforms x West stations: month-granular era FALLBACK — kept
  ONLY for months no REQUESTED native West platform covers (per the same
  since/until month semantics as the child-side clamp). August 2018
  counts as goes17-covered (last day >= first light), so the fallback is
  Jan-Jul 2018 + all of 2017.
- Explicit oblique requests (e.g. --satellites goes16 --stations KSEA,
  no native platform requested) are honored IN FULL — never silently
  narrowed.
All three skip classes (out-of-grid exclusion, retirement clamp,
fallback clip) log loudly.

Tests: 6-case TestGoesEastForWestFallback (2017 kept, 2019 dropped,
month-granular 2018 boundary, explicit-request honored, East exclusion
unchanged, 2024 native-gap handling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…te scope keys

Adversarial exit panel (18 agents, findings verified by EXECUTING the
enumeration code) caught 4 blocking defects in the fallback/scope work:

- P1: _covers() counted a month as native-covered from a SINGLE day of
  data. goes17's Aug 27-31 2018 first-light tail clipped the goes16
  fallback for all of August while the child-side clamp fetched only the
  tail -> Aug 1-26 2018 fetched by NO platform for all 8 West roster
  stations, on the DEFAULT request (and Jul 1-27 2022 under the legacy
  goes16,goes18 pair). The old boundary test enshrined the hole. Positive
  coverage now requires the FULL month (first day >= available_since);
  partial first-light months keep the East fallback (~5 duplicate days
  in separate per-satellite partitions — the safe direction).
- P2: _covers() consumed the MARGINED _AVAILABLE_UNTIL as a positive
  coverage claim, inverting the margin's "too-late only wastes" safety
  contract (goes17 "covered" Feb-Dec 2023 with zero real data ->
  11-month hole for goes16,goes17 requests; margin widening would
  silently widen the suppression window). New _GOES_ACTUAL_DATA_END map
  (un-margined real last-data dates, verified against the live archive)
  is used for claims; the margined map stays fetch-side only.
- P2: the scope-keyed progress object omitted the products/satellites
  axes -> concurrent ACMC vs DSRF jobs for the same station+window still
  clobbered one GCS object (full-map PUTs, last-write-wins). The scope
  now appends a SORTED satellites#products digest (order-insensitive).
- P2: the stale-image danger (all-four satellites x pre-filter image =
  cartesian wrong-hemisphere downloads) was documented only in shell
  comments invisible at dispatch; it is now in the image_tag and
  satellites INPUT descriptions the operator actually sees.

Regressions added for every scenario the panel reproduced: Aug-2018
partial-month keep, Jul-2022 legacy-pair keep, 2023 margined-until
non-claim, default-2023 goes18 clip, and scope disjointness across
products/satellites with order-insensitivity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live-fleet finding (2026-07-05, KNYC station run, tasks 2022/2023): every
modern ABI-L2-ACMC file was rejected "before read (P2-d resource limit)"
— listed sizes 4.3-4.6 MB vs the 2 MiB default cap. The cap was
calibrated on 2017-era 2-variable files (~0.3-1.5 MB); NOAA later added
ACM + Cloud_Probabilities to the product, roughly tripling file sizes,
so the guard zeroed ACMC coverage for the modern era (2021+) fleet-wide.
Fail-safe held (per-file errors block completion markers — no wrong
"done" state), but the affected year-tasks burned full listing time
producing zero ACMC rows. Third instance of the same failure family
today: a guard hardcoded from old-era data (DSRF grid, goes16-only
satellites, now file sizes).

Default cap -> 16 MiB (>3x headroom over observed modern sizes, still
bounds the in-memory BytesIO well below DSRF's 64 MiB). Regression test
pins: modern observed size admitted with margin, cap still < DSRF cap,
DSRF cap unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@helloiamvu helloiamvu requested a review from Tarabcak July 5, 2026 10:30
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

📝 Docs-required check: REMINDER

API-surface change without docs and no opt-out — surfacing reminder.

API-surface files changed:

packages/weather/src/mostlyright/weather/_fetchers/_goes_s3.py
packages/weather/src/mostlyright/weather/satellite/__main__.py
packages/weather/src/mostlyright/weather/satellite/_backfill.py
packages/weather/src/mostlyright/weather/satellite/_sources.py

Docs files changed:

(none)

Docs surfaces to consider

  • CHANGELOG.md — every behavior change goes here. Auto-synced to the docs site on every release.
  • docs/ — hand-authored prose. Lifted into the landing repo as MDX on every release.
  • Per-package READMEs (packages/*/README.md, packages-ts/*/README.md) — front-door copy on PyPI / npm.
  • Docstrings (Python """ ... """ / TypeScript JSDoc /** ... */) — propagate to the auto-generated API reference via Sphinx / TypeDoc.

How to silence this reminder

  • Add a docs change to this PR (any *.md / *.mdx under docs/, or CHANGELOG.md, or any README.md).
  • Apply the docs-not-required label.
  • Add a line to the PR body: docs-not-required: <one-sentence reason>.

This check is advisory — it never blocks the merge.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Parity ticket gate: PASSED

parity-ticket-check: Python-side trigger surface touched; opt-out satisfied (parity ticket, python_only flag, or label).

See CROSS-SDK-SYNC.md §2 for the workflow.

helloiamvu and others added 3 commits July 5, 2026 13:39
…h sides

Codex P2 (final stamp): the West-for-East exclusion was PRODUCT-blind.
PACUS/CONUS-sector products truly cannot see East stations, but
FULL-DISK products (ABI-L2-DSRF, 5424x5424 registry grid) see most of
the Americas from either GOES slot — an explicit goes18+DSRF+KNYC
request was silently dropped to zero slices, with the exclusion log
falsely claiming "the PACUS sector cannot see them".

_enumerate_slices West-for-East branch is now product-aware:
- sector product -> always excluded (unchanged; true out-of-grid);
- full-disk + a requested native covering the station -> skipped as
  redundant (default cost control), logged distinctly;
- full-disk WITHOUT any native in the request (explicit cross-side
  study) -> honored in full.
Full-disk detection reads grid_shape_expected == (5424, 5424) from the
PRODUCTS registry (no hardcoded product list).

Tests: explicit cross-side DSRF honored, sector product still excluded,
default all-four still skips cross-side DSRF, mixed-products explicit
request keeps only the full-disk slices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…k rule

Codex P2 (re-stamp): the cross-side full-disk redundancy check was
REQUEST-level (any native present in the request suppressed the
cross-side platform outright), while the mirror-image East-for-West
fallback was already month-granular. goes16,goes18 + ABI-L2-DSRF + KNYC
in 2026: goes16 is era-clamped (no 2026 data), yet its mere presence in
the request dropped goes18 — zero valid slices remained.

Cross-side full-disk platforms now fall through to the SAME
month-granular fallback machinery as East-for-West (_native_fully_covers
against the requested natives): kept exactly for months no requested
native fully covers, honored in full when no native is requested.
Sector products remain always-excluded cross-side (true out-of-grid).
One unified skip bucket + generalized log wording (both directions).

Tests: era-clamped-native 2026 scenario keeps goes18 (12 months) and
drops goes16; 2023 control still clips goes18 (goes16 fully covers).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex P2 (round 8): the progress scope key was built from RAW CLI
station tokens, but bulk_backfill resolves NWS aliases to canonical
ICAOs (NYC -> KNYC) via _resolve_station_infos — so an alias submission
and a canonical submission of the SAME work got different progress
objects: resume never rehydrated markers across the pair, and
concurrent alias/canonical dispatches double-processed partitions. The
scope now resolves stations to canonical ICAOs, dedupes, and sorts
(order-insensitive) before keying.

Also collapses the SIM102 nested-if in the cross-side branch (lint
slipped through the previous chained run; behavior identical, verified
by the full filter-family suite).

Test: alias (NYC) and canonical (KNYC) submissions share one scope URI;
multi-station order-insensitivity pinned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@helloiamvu helloiamvu merged commit fd7c5d4 into main Jul 5, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant