-
v0.58.0 Stable
released this
2026-09-22 00:31:06 -05:00 | 41 commits to main since this releaseThe weather tenant is complete at nine tools. W2, W3 and W4 all shipped together:
list_stations,get_model_run,get_taf,search_wind_shifts,get_observations,
get_alerts,get_forecast_discussionandget_hourly_forecast, alongside W1's
get_forecast_grid.One release rather than three, deliberately, and the reason is the release rule's own
argument. That rule exists so nothing running in production lacks a version to name it
by. These three phases were built and verified in one session and deploy together behind
one connector reconnect, so three tags would have named three moments that never existed
apart — fictional history rather than a finer-grained record. The entry below is organised
by phase instead.The build overturned three of its own plan's premises, each by measurement. The
Open-Meteo forecast response carries no issuance time at all, so the run time for a
tool namedget_model_runhad to be found in a second document. The configured global
model is three-hourly, not hourly, so its hourly series is interpolated — the grid's
native-versus-filled problem arriving through a different door. And the per-request model
choice could not be built on nulls-outside-the-domain because there are none: a
coordinate outside a model's domain is an explicit HTTP 400.Both of W0's open questions are answered, and one of them without waiting a day. The
plan said the gridpoint reissue cadence needed samples spread across a day; fifteen
offices sampled at one instant gave the same distribution, because offices issue on
staggered schedules. Median issuance age 5.48 hours, maximum 10.36, minimum 0.43 —
so the 3-hour warn line would have fired on 10 of 15 normal gridpoints and a 12-hour
line on none. HRRR's horizon is anchored to its run: the extended 00/06/12/18Z cycles run
48 hours and the hourly cycles between them run 18, so a request gets 42 to 48 hours and
hours=48stands as a default.1805 tests green, 155 of them this tenant's. 40 mutants were run against the new
guards and all 40 were caught — two survived the first pass, and both were the failure
CLAUDE.mdwarns about rather than a gap in the code: one mutant was ineffective, and one
test asserted a true thing the broken code also satisfied. The second is written up under
Fixed.Added
list_stations— every reporting station near a coordinate, nearest first, with
what can be read from each. The phase that proves "anywhere" is real, sincebbox
is the mechanism that works outside the United States and/pointsis the one that does
not. The widening ladder is0.5 → 1 → 2 → 4 → 8degrees, per product, stopping each as
soon as it answers.get_model_run— an hourly model forecast from whichever configured slug covers the
coordinate, with the run's initialisation time, its native step, and how much of the
window actually carried data.get_taf— the terminal forecast for the nearest airfield, with its change groups
read for what they mean rather than as a list of wind samples. One station, never
several;stationis the one argument that overrides the coordinate.search_wind_shifts— a scan over the model run and the TAF, with the raw grid as
corroboration reported alongside and never as the scan surface.get_observations— what the weather is actually doing, from NWS inside the United
States and from the aviation network everywhere else.get_alerts,get_forecast_discussion,get_hourly_forecast— the context
tools. The discussion comes back verbatim and nothing is parsed out of it.- Seven network-free reader modules, each fixture-driven like
calendar/expand.pyand
for the same reason:stations.py,taf.py,model.py,observations.py,alerts.py,
hourly.py,shifts.py. weather/units.py— the conversion layer, moved out ofgrid.pynow that it serves
four endpoint shapes rather than one.- A preflight check per configured model slug, advisory and never fatal, modelled on
_check_espnff_season. The plan specified validating each slug against Open-Meteo's
model list; there is no list —/v1/models404s and the invalid-slug rejection names
the bad value without enumerating the good ones. What replaces it is one real forecast
request per slug at a point inside that slug's own coverage, which validates the string
and that the model answers. Its detail line reports the run state, which is where the
three-hour native step surfaces without anybody going looking for it. envelope.from_epochandenvelope.from_iso— one conversion point for the three
timestamp shapes these upstreams send: an ISO string with an offset, an ISO string with
none, and epoch integers.envelope(deterministic=True)— for a station list or a grid resolution, which
carry no issuance time because they are mappings rather than products. The fields stay
identical so a caller reading the booleans never has to know which kind it holds; only
the sentence differs, because "do not describe it as current" warns about the wrong thing
on a mapping and teaches a reader to discount the note that matters.
Changed
client._classifyno longer calls every 4xx "an argument problem rather than an
outage", and it carries the body out on the exception. Open-Meteo answers both a
coverage boundary and a misspelled slug with a 400, and thereasonstring is the only
thing that distinguishes them — so a fallback that merely caught a 400 would have
routed every CONUS request to the global model forever on a typo, and looked like working
software.WeatherBadRequestcarries up to 2 KiB of the body, read while it arrives and
redacted by coordinate shape, because NWS's own 400 on/alerts/activeechoes the
rejected value and that string reaches a tool response. An unrecognised 400 is never
fallen back from.- The gridpoint staleness line is 12 hours, revised from 3 by use — the first
threshold on this tenant to be revised, which is what the table's own comment invited.
See the measurement above. The 24-hour absurdity ceiling did not move. - The model's staleness thresholds are derived per model from its own document rather
than read fromTHRESHOLDS. Measured availability lags: 1.53 h for the hourly
high-resolution model, 5.51 h forncep_gfs013, 7.12 h forecmwf_ifs025. So a
normal freshest run is ~2.5 hours old on one and ~13 on another, and a single row would
have marked every global-model answer stale. CLAUDE.mdcorrected: the raw gridpoint does not carrygeneratedAt. That section
claimed both/gridpoints/{wfo}/{x},{y}and/forecast/hourlycarry it. Checked across
sixteen offices and then directly on one cell: the raw gridpoint's scalar properties are
@id,@type,updateTime,validTimes,forecastOffice,gridId,gridXand
gridY, andgeneratedAtis not among them. The original measurement was real and came
from the hourly endpoint; the claim about the raw one was inferred and never checked.
The rule is stronger for the correction: on the endpoint that does carry both, the gap
measured ten hours, andgeneratedAtchanged between two requests fourteen minutes
apart on the same unchanged grid.docs/weather-plan.mdis down to four items from a full build plan: the recorded
frontal passage the wind-shift thresholds must be tuned against, the live pass, W5, and
one operator decision. A plan that shrinks to its outstanding items rather than emptying
is that rule working — Canvas's deletion is why, since three live checks were sitting
only in that plan when it went..env.examplesays the global model's native step is three hours, since that is an
operator decision the boot log now prints.
Fixed
- A wind shift that went out and came back reported the return as a remnant.
shifts._collapsetreated a candidate window that began exactly where the previous one
ended as overlapping it, so a wind veering 120 degrees and then backing 120 reported the
veer and a 60-degree fragment of the back. Two rotations that meet at a point are two
events. Found by writing the test, which is the discipline inCLAUDE.mdworking as
advertised. get_observationsreported a mistyped station as an upstream failure. The fallback
to the aviation network was unguarded, so a 204 — which is how that host says "no such
station" — escaped as a transport error and lost the sentence that names the typo. It now
reaches the empty-versus-invalid branch, which asks/stations/{id}and says which of the
two facts it is.- A cap test that could not reach the branch it named. It asserted the error detail was
short, which the final slice guarantees on its own — so it passed against a client with
the read cap removed. It now counts the chunks the upstream was actually asked for: a
body noticed after it has arrived is a body already in memory. This is the a test that
cannot reach a branch is not coverage of it failure, caught by the mutation sweep rather
than by review.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads