Changelog
Mostly Right follows Keep a Changelog and Semantic Versioning. This page mirrors CHANGELOG.md in the SDK repo on every release tag.
The canonical source: CHANGELOG.md in mostlyrightmd/mostlyright-sdk.
[Unreleased]
Section titled “[Unreleased]”[4.1.0] — 2026-08-06
Section titled “[4.1.0] — 2026-08-06”- The flood and CAL FIRE timestamp grammars refuse the midnight-24 form on
every interpreter. Newer CPython’s
fromisoformatacceptsT24:00:00and rolls the date, so the pinned cross-SDK refusal (‘refused in both SDKs’) silently depended on which Python parsed the payload. The hour range now lives in the grammar itself; the two already-pinned refusal tests are the regression pin. - A
__proto__key in an upstream payload no longer vanishes from TypeScript’s raw columns. The shared JSON compactor assembled its output on a plain object, so a__proto__member hit the prototype setter instead of becoming a column byte: the key was silently dropped (and the output’s prototype mutated), while Python kept it — a cross-SDK byte divergence on every JSON hazard column that could carry one, three of which (raw_cap, the tropicalraw_json,wind_radii_nm) shipped in a released version. The compactor now builds on a null-prototype object; the cross-SDK value gate pins the payload.
-
Read wildfire incident records and satellite thermal-anomaly detections in TypeScript.
@mostlyrightmd/weathergains two verbs on its root and on the lean@mostlyrightmd/weather/hazardssubpath:wildfiresandhotspots, writing two new row contracts —schema.hazard.wildfire.v1(one row per incident x capture) andschema.hazard.hotspot.v1(one row per detection). Both row types are codegen output from the same committed JSON schemas the Python columns come from.wildfiresspans both providers on onesource=axis: the national interagency feed throughscope(current/season/history) and the California state feed throughyear/activeOnly. There is no separate California verb — both write the same row and thesourcecolumn says which answered. A past window againstscope: "current"throwsHazardRetentionErrorquoting the publisher’s own fall-off rule rather than returning a season that burned as a season with no fires, and an argument belonging to the other leg is refused with that leg named.hotspotstakes instants and derives the upstream’s own look-back file and day count. The keyless bulk tier is the default, so an install that has never registered for anything gets detections and reads no environment variable; asking for a bounding box opts into a key of your own inMOSTLYRIGHT_FIRMS_MAP_KEY. Every detection row carries the NASA acknowledgment in itsattributioncolumn, andFIRMS_ATTRIBUTIONis exported for a result with no rows.The runtime splits per leg, and each leg declares it.
wildfires()runs in a browser —services3.arcgis.comanswers a cross-origin request withAccess-Control-Allow-Origin: *.wildfires({ source: "calfire" })andhotspots()are Node-only:incidents.fire.ca.govandfirms.modaps.eosdis.nasa.govsend no such header, and both refuse a browser page at call time with a typed error naming the host, the missing header, and the measurement date. No proxy is offered. Seedocs/browser-integration.md. -
Read river gauges, their stage and flow, their rating curves, and USGS continuous water values, in both SDKs. Ask which gauges sit inside a bounding box, read one gauge’s flood-category thresholds and identifier crosswalk, pull the observed and forecast stage-and-flow points a River Forecast Center is publishing right now, convert a stage to a discharge through the official curve, or read a window of USGS readings with the upstream’s own revision stamp on every row.
Python
mostlyright.weather.hazardsgains four verbs —gauges,stage_and_flow,rating_curve, andwater_observations— and TypeScript ships the four camelCase twinsgauges,stageAndFlow,ratingCurve, andwaterObservationson the@mostlyrightmd/weatherroot and on the lean@mostlyrightmd/weather/hazardssubpath. There are four verbs and not five:gaugescovers both the bounding-box scope and the single-gauge scope, and therecord_scopecolumn says which shape a row is.Four row contracts:
schema.hazard.gauge.v1(one row per gauge, inventory or detail),schema.hazard.stage_and_flow.v1(one row per point, discriminated byseries),schema.hazard.rating_curve.v1(one row per curve point, ascending bystage_ft), andschema.hazard.water_observation.v1(one row per series x instant x upstream revision). Three source addresses join the manifest catalog, one per route family:nwps.gauges— the gauge inventory and detail routesnwps.stage_and_flow— the stage-and-flow and rating routesusgs.waterdata_ogc— the continuous and monitoring-location collections
The two NWPS keys deliberately share one URL so the gauge and series routes can be re-pointed independently.
The empty-versus-error split is by scope, not by verb: a bounding box holding no gauges, a gauge with no current forecast, and a quiet observation window each return a valid empty frame, while a named identifier that does not exist raises. “This gauge has no forecast right now” and “this gauge does not exist” call for opposite responses from a caller, so the two are never collapsed.
Three measured traps the adapter closes for you. The stage-and-flow route serves KILO-cubic-feet per second while the threshold and rating routes serve cubic feet per second for the same gauge, so
flow_kcfskeeps the upstream number and onlyflow_cfsis comparable to a*_flow_cfsthreshold. Exactly one forecast issuance is ever served and no parameter asks for a prior one, so an issuance nothing captured is unrecoverable — which is what Python’spersist=is for. USGS marks recent valuesProvisionaland flips them toApproved60 to 90 days later, a flip that can change the VALUE and not only the label, so a revision appends beside the earlier reading under a newerlast_modified_at_utcrather than overwriting it.Both flood hosts answer a cross-origin request with
Access-Control-Allow-Origin: *and need no key, so all four TypeScript verbs run in a browser — there is no Node-only restriction anywhere in this set, unlike the NHC verbs beside them.persistis Python-only, because the vintage store is parquet on disk and pyarrow and filelock have no browser equivalent; a cross-SDK test asserts the keyword is absent from the TypeScript modules and still present in the Python ones, so the claim cannot go stale silently. That same test reads the Python__all__off disk and compares the two verb name sets by set equality in both directions.All four routes are US federal government works — NOAA/NWS and USGS, 17 U.S.C. §105, public domain — read over plain HTTPS with no key. The legacy
waterservices.usgs.govhost is deliberately not used and no catalog entry points at it: it carries no per-row revision stamp, serves local-offset timestamps, and is announced for decommission.No release is cut by this work.
Changed
Section titled “Changed”- The private core module
mostlyright._internationalis nowmostlyright._daily_extremes. It computes station-local daily extremes for Polymarket settlement — the same computation TypeScript already spellsdailyExtremes— so the name now says what the module does rather than where its stations are. It is a private module (leading underscore): no public import path changes, no row bytes change, and no deprecation shim is needed.
[4.0.0] — 2026-08-04
Section titled “[4.0.0] — 2026-08-04”A major, not a minor: the SDK now requires an API key. 3.0.0 shipped a copy of
the manifest catalog inside the wheel and the npm bundle, so a keyless install
could resolve source addresses; 4.0.0 deletes those vendored catalogs and every
resolution goes through the hosted manifest, gated on MOSTLYRIGHT_API_KEY.
Every keyless 3.0.0 integration breaks by design — details under
“Changed (BREAKING)” and “Removed” below.
-
Read NWS hazard alerts, NWS/NHC storm bulletins, NHC active tropical cyclones, and the NHC HURDAT2 best-track reanalysis, in both SDKs. Ask for every hurricane warning issued for a Florida zone last Tuesday, pull the advisory bulletins that carry the storm’s position and intensity, list the storms NHC has in play right now, or read the re-analysed landfall fixes a “hurricane hits X” contract settles against — each as source-identified rows with the verbatim upstream text preserved.
Python
mostlyright.weather.hazardsships nine names:alerts,text_products,post_storm_reports,issuing_offices,tropical_cyclones,best_track,best_track_revisions, pluscapture_historyandpersist_rowsfor the local vintage ledger. TypeScript ships the seven camelCase twins —alerts,textProducts,postStormReports,issuingOffices,tropicalCyclones,bestTrack,bestTrackRevisions— on the@mostlyrightmd/weatherroot and on a lean@mostlyrightmd/weather/hazardssubpath.Four row contracts:
schema.hazard.alert.v1(one row per issuance x UGC zone),schema.hazard.text_product.v1(one row per bulletin,raw_afosverbatim),schema.hazard.tropical_cyclone.v1(one row per active storm), andschema.hazard.best_track.v1(one row per synoptic fix, with HURDAT2’sLlandfall flag as a named column). Every row carriescaptured_at, the vintage stamp: a re-capture of the same identity is a new row, never an overwrite, so what you knew at training time stays recoverable.Retention and the two archive routes:
api.weather.govkeeps exactly 7 days on both/alertsand/products. An older window is answered by the Iowa State archives —alerts(source="iem")reads the VTEC archive (2005 onward) and text products auto-route to the AFOS archive, which is also the only route carrying intermediate advisories and-AAApost-storm amendments.alertsrefuses an out-of-retention window withHazardRetentionErrornamingsource="iem"rather than returning the empty result the live feed would give, which reads as “no hazards occurred”.capture_historyandpersist_rowsare Python-only: both read and write a local parquet vintage ledger, and pyarrow and filelock have no browser analogue.tropicalCyclones,bestTrack, andbestTrackRevisionsare Node-only in TypeScript:www.nhc.noaa.govsends noAccess-Control-Allow-Originheader, so each refuses a browser page with a typedContractErrornaming the host and the remedy instead of surfacing an opaque CORSTypeError. An extension service worker with the host inhost_permissionscan still call them. Both sets are reasoned omissions with their reason stated in the docs, not parity gaps.The settlement-cited host
alerts.weather.govis dead — it is NXDOMAIN, and has been long enough that anything still pointing at it has been failing silently.api.weather.gov/alerts?zone={UGC}replaces it and is what these verbs call. -
mostlyright.economy.release_calendar()— the agency release schedule as a point-in-time frame. Returnsschema.economy.releases.v1rows (agency,release_name,period,release_type,schedule_knowledge_time,release_datetime,time_precision,schedule_state,state_evidence,cancellation_detectable) from BEA’s iCalendar feed, BEA’s release-dates JSON (parsed for cross-check, contributing no row), the curated Federal Reserve meeting table, and the BYO-key FRED/fred/releases/datesleg.as_of=reconstructs the schedule as it was known at a past instant: a reschedule is a new row at a newschedule_knowledge_time, never an overwrite. A release that vanishes from the feed while still in the future becomes an explicitschedule_state='cancelled'row labelledstate_evidence='sdk_inferred_absence'. An uncovered scope raisesReleaseScheduleUnavailableError— never an empty frame. Nodelivery=keyword and no hosted call. Coverage is stated rather than faked:gdpat exact time from BEA,fed_funds/fed_decisionat exact time from the curated table, and the nine BLS-family indicators only through the date-only FRED leg orreleases(), becausewww.bls.govreturns 403 to programmatic fetch andapi.bls.govhas no calendar endpoint. -
ReleaseScheduleUnavailableErrorin both SDKs (aSourceUnavailableErrorsubclass), raised on an empty parse or an uncovered scope.
Changed
Section titled “Changed”- BEHAVIOR CHANGE — a cached release calendar moves settlement anchors
EARLIER.
series,snapshotandtraining_tablewrite a settlement-grade first print’svintage_time,knowledge_timeandrelease_datetimefrom one resolved anchor, and that anchor is now resolved in three tiers: the cached agency calendar, then the curatedreleases()table, then the end-of-day fallback. With no calendar cached the output is byte-identical to before — proven by comparing ahistory()frame built on an empty store, byte for byte, against one captured before the tier existed. Once a calendar has been captured, an anchor that used to fall back to the last microsecond of the ALFREDrealtime_startday moves to the agency’s scheduled instant on that same day: measured against the committed BEA calendar, 21 of 23 GDP anchors move, every one earlier, and the largest movement is 11 hours 29 minutes 59.999999 seconds (the last microsecond of the day minus BEA’s 8:30 Eastern release, 12:30 UTC under daylight time; 10:29:59.999999 under standard time). A calendar row dated off the known print day never wins, adate_onlyrow never displaces an anchor, and any ambiguity falls LATE. To restore the previous anchors, clear the calendar cache (clear_release_calendar_cache(), or delete~/.mostlyright/cache/v1/economy/release_calendar/).
Changed (BREAKING)
Section titled “Changed (BREAKING)”- The SDK requires an API key — without one it resolves nothing. Every
source address — AWC, IEM, GHCNh, the NWS products, Kalshi, Polymarket, the
economy agencies — is looked up in the manifest catalog, and the catalog now
comes only from the hosted manifest endpoint. With no
MOSTLYRIGHT_API_KEYconfigured, resolution raises PythonApiKeyRequiredError/ throws TypeScriptApiKeyRequiredErrorat call time, before any network I/O. In 3.0.0 a keyless install worked because a copy of the catalog shipped inside the wheel and the npm bundle; those vendored catalogs are deleted (see Removed), so every keyless 3.0.0 integration breaks by design. - A rejected key stops resolving immediately — no cache fallback. An HTTP
401, 402, or 403 from the manifest endpoint raises
ManifestUnavailableErroreven when a warm local catalog cache exists: a revoked key or a lapsed subscription stops at its next fetch window instead of coasting on the cache. Only failures that say nothing about the key’s standing — network errors, 5xx, other unexpected statuses, malformed bodies — fall back to a key-holder’s local cache. Verified against a warm, valid cache in both SDKs. fallback=narrows to missing-entry and SSRF-reject. The per-fetcher fallback URL applies only once a catalog is in hand — when the catalog has no entry for the source, or the entry fails the SSRF address check. A missing key or a rejected key never falls through to the fallback, and resolution failures are never memoized.- (internal)
MOSTLYRIGHT_MANIFEST_PATHpoints resolution at a local manifest file — the seam the repo’s test suite uses viatests_support/manifest_fixture.json, which ships in neither the wheel nor the sdist.
Removed
Section titled “Removed”- The vendored manifest catalogs, their checksum sidecars and sync gates,
and
KeylessDecayWarning— from both the Python wheel and the npm bundle. The vendored copy was byte-identical to the hosted catalog (the wheel even carried one extra source) and was refreshed at every release: the paid product was shipping free inside its own SDK. The cross-SDK catalog sync test is now a deletion tripwire — re-adding a catalog at either path fails a named test.
-
A cold keyed manifest resolution runs inside the caller’s transport controls (TypeScript).
resolveSourceUrl/sourceAuthaccept an optional{ signal, fetchImpl }(ResolveOptions), and every call-time resolution site — the four weather fetchers and the markets clients — threads its caller’s ownAbortSignal/ injected fetch through. Aborting a call now aborts the manifest request instead of waiting out the resolver’s retry budget (an aborted attempt is never memoized for later callers), and an injectedfetchImplcarries the manifest request instead of being bypassed.FetchWithRetryOptionsgains afetchImplinjection seam, which the weather fetchers inherit as a caller-facing option. A keyless caller raisesApiKeyRequiredErrorbefore any request (see Changed (BREAKING)). The Python resolver keeps its plain(key, fallback)signature by design — documented in_manifest_resolve.pyand in the resolver’s divergence list. -
TypeScript
./market-dataclients resolve their hosts through the manifest catalog at call time (markets.kalshi,markets.polymarket_gamma,markets.polymarket_clob,markets.polymarket_data), matching the Python clients:KALSHI_API_BASE/GAMMA_API_BASE/CLOB_API_BASE/DATA_API_BASEstay exported as the fallbacks, and an explicitbaseUrl/gammaBaseUrl/clobBaseUrl/dataBaseUrloption wins over the manifest. Previously a manifest catalog entry for a markets source redirected Python requests and silently did nothing in TypeScript. -
The TypeScript
./tradesand./polymarketclients resolve their hosts through the manifest catalog at call time too (markets.kalshi,markets.polymarket_clob,markets.polymarket_gamma), closing the last eight unrouted call sites:fetchCandlesticks,fetchTrades,fetchOrderbook,fetchMarket,polymarketHistory,polymarketSnapshot,fetchEvents, andfetchEventById. Each module keeps its own host constant as the fallback for a catalog with no usable entry for that source, andfetchTradesandfetchEventsresolve once above their pagination loops so every page of a multi-page walk goes to the same resolved address. Each resolution runs inside the caller’s own transport controls — these clients spell the injected fetchfetchFn, mapped onto the resolver’sfetchImplalongsidesignal— so a cold keyed manifest fetch aborts with the caller and travels through an injected fetch. Previously a catalog entry for one of those hosts redirected Python and silently did nothing in these three TypeScript modules.
[3.0.0] — 2026-08-02
Section titled “[3.0.0] — 2026-08-02”A major, not a minor: this release carries three breaking changesets that had
accumulated on main unreleased — the daily_summaries rename, the market-data
surface, and the weather hosted-subpath move. Anything published from this tree
had to be a 3.x; consumers on ^2.1.0 must not auto-upgrade into the removals
listed under “Changed (BREAKING)” and “Removed” below.
-
A market-data grammar on both venues. Seven verbs on
mostlyright.markets.kalshi—series(),events(),markets(),market(url_or_id),candles(),trades(),orderbook()— and six onmostlyright.markets.polymarket(events(),markets(),market(url_or_slug),candles(),trades(),orderbook()) plus a typedpolymarket.series()refusal, because that venue’s hierarchy is Event → Market and has no series tier. Prices are probabilities in[0, 1]with the venue-native magnitude alongside in*_native; volume columns are unit-named (volume_contracts/volume_usd) and there is deliberately no column calledvolume. Each verb accepts a ticker, a slug, or a link pasted from the venue, and parses the link offline rather than fetching it.candles()reads any window length, chunking under Kalshi’s 5,000-bucket per-request ceiling. -
Six row contracts:
schema.markets.series.v1,schema.markets.events.v1,schema.markets.listing.v1,schema.markets.candles.v2,schema.markets.trades.v2,schema.markets.orderbook.v2. A shape test reads the generated JSON Schema documents and fails the build if the Python columns and the TypeScript row keys drift apart. -
UnsupportedResolutionError(carriesinterval/supported/venue) andVenueCapabilityError(carriesvenue/capability/remedy), both extendingContractErrorin Python and its TypeScript twin, so an existing coarsecatchkeeps working. -
discover(query, live=True)andkind="market": an explicit opt-in to a cross-venue live market search, returning the unchanged 17-column discover envelope. The defaultdiscover()stays byte-identically network-free, andkind="market"withoutlive=Truereturns an empty, correctly shaped frame naming the opt-in rather than raising or reaching the network. -
The
@mostlyrightmd/markets/market-dataTypeScript subpath: the same verbs in camelCase with snake_case row keys, nonode:import, and no client-identity header, so every Polymarket verb runs from a page. -
The
markets.polymarket_datamanifest source key. -
docs/market-data.md — the user-facing guide to the verbs, the venue asymmetries, the unit rules, and the browser split; docs/market-data-browser-support.md — the per-verb browser verdict; docs/data-licensing-markets.md — the per-source licensing posture.
-
Satellite station resolution now falls back to the packaged station catalog, so any of the 6,419 catalog stations resolves for extraction rather than only the 94 curated settlement stations, and arbitrary coordinates are supported through a derived site-id identity. ICAO remains a strict subset of the site-id alphabet, so existing partitions, progress keys, and serving routes are unaffected, and an explicit site id cannot claim an existing station’s identifier.
-
satellite()accepts coordinates. Passlat=andlon=instead of a station code to extract over an arbitrary point that exists in no registry and no catalog; its identity is a legible derived site id (40.7789, -73.9692→N4078W07397) at 0.01° precision, which is what selects the cache partition, the object-store key, and the row’sstationvalue.site_id=andtz_override=override the derived identity and timezone. Passing both a station and coordinates, only one oflat/lon, or neither raises before any network call, and an explicitsite_idcannot claim an existing station’s identifier. A station code now resolves against the full packaged catalog, so ordinary non-settlement stations work too. ICAO stays a strict subset of the site-id alphabet, so existing partitions and routes are unaffected. The TypeScript hostedsatellite()gains the same coordinate options, deriving the identical site id in the browser. -
--siteson the satellite backfill CLI.--sites "40.7789,-73.9692;51.5053,0.0553"dispatches a fleet backfill for arbitrary coordinates beside--stations; the two combine as a deduplicated union. -
Meteosat SEVIRI live extraction is wired.
satellite()can now decode the real EUMETSAT Data Store MSG-CLM product: it is GRIB2 on the space-view grid (grid-definition template 3.90), which needs the low-leveleccodesreader —cfgribhas nospace_viewentry in its grid map, so the CGMS navigation keys never reach the dataset and the file opens as a flat array with nothing to navigate by.eccodesand ECMWF’seccodeslibbinary wheel join the[satellite]extra (no system install needed; no Windows wheel exists, so that platform resolves without them and raises a typed error if a decode is attempted). Rows are marked as containing EUMETSAT data, which the collection’s licence requires. -
A backfill pass can persist a crop window beside its derived rows. With the crop tier enabled, each backfill pass also writes a 64x64 window of every registered variable around each site, cut from the file it already has open — so it costs no extra download. Values are stored PACKED at the source’s native on-disk width against
schema.satellite.patch.v1, with each variable’s ownscale_factor,add_offset, fill sentinel, signedness and units carried per row, so a reader can recover the physical value without the source object. A later feature re-derive can read the window instead of re-downloading the archive. Opt-in and operator-side: no public verb, no serving route, and the settlement-feeding derived partitions are byte-identical whether it is on or off. -
MeteosatCredentialsRequiredError— raised when a Meteosat fetch finds no EUMETSAT Data Store credentials. The message names three ways forward: register a free account, use hosted delivery, or ask forsatellite="viirs-n20". -
An expiry-guard test that fails when any economy release-schedule family’s newest date is less than 60 days out. The failure names the family, how many days short it is, and the refresh procedure — including which agency page to read and the rule that every date needs a second independent confirmation.
-
A DST-convention test asserting every schedule row converts back to 08:30
America/New_York(14:00 for the FOMC statement). Both tests ship in Python and TypeScript. -
packages/economy/src/**andpackages-ts/economy/src/**are now cross-SDK parity-gate trigger paths, so an economy public-surface change in one language without its counterpart in the other is flagged on the PR.
Changed
Section titled “Changed”-
_kalshi_client.fetch_marketresolves its address through the manifest; it was the last fetcher in the package still bypassing it. -
The Polymarket client gained bounded retry with a capped
Retry-After, matching the Kalshi client. It previously had no retry hardening at all. -
Gamma listing moved to keyset pagination via
after_cursor. The two obvious spellings (cursor,next_cursor) each re-serve page one with HTTP 200 forever, so a token-only loop never terminated and never errored; the walk now also breaks when a page yields zero new ids. -
docs/browser-integration.md’s CORS table corrected for Kalshi and all three Polymarket hosts.
api.elections.kalshi.comrejects any non-kalshi.comOriginserver-side with 403 (sohost_permissionsdoes not help); every Polymarket host answersAccess-Control-Allow-Origin: *. -
_polymarket_trades.historyand the TypeScriptPolymarketHistoryArgsharmonized tofrom_time/to_timeandfromTime/toTime, matching the window grammar every other verb speaks. -
economy_trades.candlesnow routes through the Kalshi/historicaltier when the live tier answers 404, so a deep window returns real candles instead of a labeled-empty frame.df.attrs["tier"]records which tier answered; thesourcecolumn anddf.attrs["source"]stay"kalshi". -
Satellite extraction now serves every requested station from a single pass over an open file, and Himawari station navigation is computed once per satellite grid rather than once per station per file. A Himawari month over the 64-station roster previously downloaded about 24.6 TB because each station’s slice re-read every file; it is now about 384 GB. Rows are byte-identical either way, and the cached navigation is guarded by the content hash of the geolocation it was built from, so a grid change rebuilds it loudly instead of mis-locating every station.
-
Economy cache persistence is decided by an explicit per-source decision map rather than a denylist. Every tag in
ECONOMY_SOURCESdeclares whether its bytes may be stored, and a row carrying an unregistered or missingsourceraisesUnregisteredEconomySourceErrorinstead of being written; previously such a tag persisted by omission. Behavior for every existing tag is unchanged, including the FRED store/cache/archive carve-out and itsMOSTLYRIGHT_PERSIST_FRED=1opt-in.
Changed (BREAKING)
Section titled “Changed (BREAKING)”-
Europe, Africa and the Indian Ocean now auto-route to Meteosat, not to VIIRS. If you called
satellite()for a station in that band WITHOUT passingsatellite=, you previously receivednoaa_viirsrows; you now receiveeumetsat_meteosatrows, and without an EUMETSAT key you get aMeteosatCredentialsRequiredErrorinstead of data. To keep the old behaviour, passsatellite="viirs-n20"(or"viirs-npp"/"viirs-n21") explicitly.There is deliberately no automatic fallback. Routing is a function of geography alone, so it cannot change depending on whether a key happens to be present in the environment — a key-dependent source swap would train a model on VIIRS and infer on SEVIRI for the same station, which is exactly the train/infer source mismatch this SDK’s source-identity invariants exist to catch.
-
weather.climate()→weather.daily_summaries()(Python) /climate()→dailySummaries()on the@mostlyrightmd/weather/daily-summariessubpath (TS, formerly/climate). The verb now speaks the same vocabulary as thedaily_summary_*columns it emits; “climate” misread as climatological normals. No alias — the old names raise a teachingAttributeError(Python) or fail at the exports map (TS). Public TS types renamedClimate*→DailySummary*(incl. theDailySummaryObservationbarrel alias). -
weather.label.cli()→weather.label.daily_summary(), andtraining_table(label="cli")→label="daily_summary"(Python + TS, same recipe;label="cli"raises a teaching error). The registry idweather.label.cli, the recipe key"cli", andRECIPE_LABEL_COLUMNS["cli"]are wire identities and unchanged. -
weather.climate_gaps()→weather.daily_summary_gaps()(Python); the TSclimateGapsteaching stub is nowdailySummaryGaps(DailySummaryGapsNotImplementedError).availability()output keys renamed:climate_years/first_climate_year/last_climate_year→daily_summary_years/first_daily_summary_year/last_daily_summary_year(TS:dailySummaryYears/firstDailySummaryYear/lastDailySummaryYear). -
Frozen on purpose (wire/product identities, byte-parity): the
schema.settlement.cli.v1schema id,source=values ("iem"/"cli"/"cli.archive"), thecli.archiveframe tag, cache tier paths (.../v1/climate/...),_internal/merge/climate.py+ TSinternal/merge/climate.ts, and the IEM CLI fetch/parse internals.
- The curated economy release-schedule table had run out of forward dates:
jobless claims were already exhausted, and nonfarm payrolls, CPI and PPI ran
out within two weeks. When a family ages out,
economyderives the settlement anchor from an end-of-day-UTC fallback instead of the scheduled release wall-clock. That fallback lands at or after the real print, so it never surfaces a value early, but it costsknowledge_timeprecision on the largest economy contract families. The table now carries real forward dates through the end of 2026 for CPI, PPI, nonfarm payrolls, U3 and jobless claims, and through 2027 for FOMC decisions, in Python and its TypeScript mirror. GDP needed no new row: its newest entry is still about 90 days out and BEA publishes no 2027 dates yet. - Winter release rows are now stored at the Eastern Standard Time offset. Every row in the previous table fell inside daylight time, so the module comment claiming the offset was not load-bearing was untested; a November-to-March row entered at the summer offset would have anchored an hour early.
- The pandas install hint in
economy.series,economy.research_economyandeconomy.snapshottold the reader to runpip install mostlyrightmd-economy[pandas]. That extra was removed in 2.0 — pandas ships as a base dependency — so the command failed. The hint now namespip install pandasand explains that a miss means an incomplete environment. - Himawari satellite ingest read every AHI-L2-FLDK-Clouds object in a slot, though only the cloud phase/type one carries variables the SDK registers: about 178 GB listed a day where 12.4 GB is useful. Listings now keep only the file kinds that carry registered variables, matched on the loose file kind so an upstream product-version bump cannot silently empty a listing, and keyed per satellite so the himawari8 and himawari9 naming eras share one mechanism.
- Himawari extraction rejected every real AHI-L2-FLDK-Clouds file: it required
variables on dims named
('y', 'x')while the files carry data and geolocation on('Rows', 'Columns'). The read is now positional off the variable’s own dims, guarded by the product’s registered grid shape and co-registration with the file’s geolocation rather than by dim names. - An unparseable satellite object name raised a bare
ValueError, which the backfill orchestrator’sSatelliteErrorhandler could not catch, so a single odd name ended the whole task. Name-parse failures now raiseSatelliteFilenameParseErrorand skip that one object.
Removed
Section titled “Removed”economy_trades.PUBLIC_DEPTH_FLOORandeconomy_trades.MAX_PUBLIC_DEPTH_NOTE, and thepublic_depth_limited/depth_noteframe attrs. They encoded a claim that the public Kalshi API surfaces only ~50-90 recent settled markets per series with the oldest around 2026-05, which live probing on 2026-07-30 disproved:/historical/markets?series_ticker=KXHIGHNYpaged back to 2025-11-11 in six pages of 200, and the historical candlestick endpoint served 37 hourly candles for a market that far back. The floor was refusing windows the venue can serve.
Note on persistence: nothing on any market-data path writes to disk.
packages/markets/src/mostlyright/markets/_trades_cache.py remains deliberately
unwired (the v1 surface is no-cache) and now says so in its module docstring;
packages-ts/markets/src/trades/cache.ts is re-exported by the ./trades barrel
but called by no verb.
Added — manifest resolution
Section titled “Added — manifest resolution”-
A default manifest endpoint. A caller with
MOSTLYRIGHT_API_KEYset but noMOSTLYRIGHT_MANIFEST_URLnow resolves source addresses againsthttps://api.mostlyright.md/api/manifestinstead of skipping the fetch and falling back to the catalog vendored into the build. One env var reaches the live catalog; there is no second address to discover. An explicitMOSTLYRIGHT_MANIFEST_URLstill takes precedence, so a self-hosted or staging catalog is unaffected.Python and TypeScript resolve against the same address —
DEFAULT_MANIFEST_URLis declared once per SDK (_internal/_manifest_resolve.py,packages-ts/core/src/manifest/shared.ts) and a test fails the build if the two ever name different URLs. Both the browser and Node TypeScript entries apply it.A keyless caller still issues no request. The
MOSTLYRIGHT_API_KEYcheck runs before the default is read, in both SDKs, pinned by a test in each.
Changed — manifest resolution
Section titled “Changed — manifest resolution”- The keyless-decay warning points at
https://app.mostlyright.md, replacinghttps://mostlyright.md/signup, a path that was never deployed. Both SDKs.
Fixed — release and gate plumbing
Section titled “Fixed — release and gate plumbing”- The hosted-call gate (
scripts/check_no_hosted_calls.py) scanned only Python. It now scanspackages-ts/*/srcas well, including the checked-in generated.jsvalidators that ship exactly like hand-written source, so a hardcoded hosted host in the TypeScript tree fails the build instead of shipping. RULE 1 gained a per-line carve-out for the manifest bootstrap address — the one address that cannot be looked up in the catalog, because it is the catalog’s own address — held to one declaration per SDK. .github/parity-trigger-paths.jsondid not list_internal/_manifest_resolve.py, so a Python-only change to the manifest resolver could merge without the cross-SDK parity gate noticing its TypeScript twin had drifted.@mostlyrightmd/core’s exportedversionconstant read2.0.0while the package shipped2.1.0. It is publicly exported and feeds the default User-Agent, so it misreported the release to consumers and to every server the SDK called. Now asserted againstpackage.jsonby a test rather than kept in lockstep by comment.
[2.1.0] — 2026-07-27
Section titled “[2.1.0] — 2026-07-27”Two review cycles in one release: an issue burn-down of the open queue, then a fix sweep over every bug an adversarially-verified review fleet filed against the SDK. 146 new regression tests ride along.
weather.stations.CITIES(Python) /CITIESon@mostlyrightmd/core(TS): a city-first index over the station catalog — 82 cities covering all 94 stations, with per-venue settlement stations (venue_stations). One codegen artifact feeds both SDKs, so they cannot drift.LiveThrottledError(subclass ofNoLiveDataError, error codeLIVE_THROTTLED, carriesretry_after_seconds) on the live IEM leg, backed by a process-wide pacing gate and an escalating 429 cooldown — persisted across processes in Python, in-memory in TS. Fleet pollers back off instead of re-tripping IEM’s per-IP throttle.SttTranscriber.transcribe(..., word_timestamps=True)surfaces per-word{word, start, end, probability}; per-segment decode stats (avg_logprob,no_speech_prob,compression_ratio,temperature) andlanguage_probabilityare always surfaced. The default engine call is unchanged.observations()accepts a list/tuple of stations (concatenated in input order), andreturn_type="list"now honorsquality_controland carriesknowledge_time.- docs/temporal-safety.md: the canonical statement of what the temporal tools guarantee and what they cannot.
align()picked its join mode (equality vs backward as-of) from the frame it was building up rather than from the spine you passed. A second source declaring the sameevent_time_colname as an earlier one saw that earlier source’s restored event column, silently switched to an equality join keyed on those timestamps, and came back all-NaN — or, where the two event grids intersected, attached values by event-time equality instead of point-in-time — order-dependently, withmr.provenance()still reporting the column as populated. The mode is now read from the caller’s spine columns, snapshotted before the first join. A declared temporal-key name that two sources claim is no longer carried onto the output under the first source’s ownership.- The weekly drift watchdog had captured nothing since the 2.0 grammar cut
(
capture_drift.pystill called the removedresearch()); it now drivesweather.training_table()like the parity gate, andcompare.pyspeaks the current column grammar. - IEM archive fetches: a chunk spanning today splits at the UTC boundary so completed days become permanent cache hits; partial chunks are reused within a TTL and superseded files are pruned — a trailing-window poller no longer re-downloads its full range every call.
- Weather cache LST gates resolve the station’s own timezone: catalog-only
stations (KBUR/KLGB/KSNA) no longer crash cache reads, a documented
timezone=override is honored for non-registry stations, and an unknown zone raises a typedContractError. - Settlement correctness, both SDKs: Polymarket
settle()anchors on the station-local day close and settles catalog-allowlisted stations at US 0.1 °C precision (timezone and country now come from one resolution seam); TSdailyExtremes()no longer re-rounds US rows to whole °C, and its °F labels are HALF_UP integer siblings byte-matched to Python. - Point-in-time windows: observation fetches extend over the leading UTC tail for positive-offset stations (all three fetch strategies agree), NWP per-model columns are windowed to the local standard day, and Open-Meteo partitions enumerate over local days.
- CWOP: covariates read the LST settlement window; the QC context is actually
populated (a detected indoor sensor can no longer score “clean”); a fast PWS
clock stays in the present period and
observed_atnever exceedsknowledge_time. - Economy: keyless BLS/BEA requests reach the print released at the window’s start; keyless PPI is pinned to the seasonally-adjusted series (WPSFD4) both SDKs agree on.
- Finance streaming: a resume never loses or duplicates a same-sequence fact
delta (inclusive replay + consumer-side boundary dedup in both SDKs); the
final speech run flushes at end of stream;
/streamsegments carry an absolutepublished_at. - TS parity: southern-hemisphere LST offsets (Sydney/Melbourne/Auckland were
1 h off), the registered-source union +
forecast.nwp.v1validator entry, and canonical-ICAO station keys frombuildPairsRow. - TS packaging: the weather root bundle no longer re-exports the hosted shim
(
@mostlyrightmd/weather/hostedonly — see the migration notes) and the IEM live path no longer pins the whole core barrel into dependent bundles (weather bundle −13%); all size budgets ratcheted and green.
[2.0.0] — 2026-07-24
Section titled “[2.0.0] — 2026-07-24”The first published release of the 2.0 rebuild. On PyPI and npm this supersedes
1.17.0 directly — the interim 1.9.9 workspace cut was never tagged or
published, and its contents ship here. The 2.0 line is a deliberate cut. The composition grammar collapses to one
noun-first entry point, the econ domain is renamed to economy, and every
deprecated alias, redundant selector, and dead scaffold is removed in a single
release. There is no intermediate deprecation step and no compatibility shim —
update each call against the migration table and re-run. The full
symbol-by-symbol rename/removal table is
in docs/reference/migration-2.0.md.
The byte-parity settlement contract survives the release: the parity gate pins
the same settlement values a 1.x pin produced, now under the 2.0 column grammar —
date → local_standard_date, cli_* → daily_summary_*, obs_* → observed_*.
The full column rename table is in
docs/reference/migration-2.0.md.
Removed
Section titled “Removed”- The composition verbs
research(),dataset(), andpairs()are gone in favor of a singletraining_table()(TypeScript:trainingTable()); hand-composition moves tomostlyright.experimental. - Feature engineering leaves the SDK: the
transformsandpreprocessinghelpers (lag,diff,rolling,calendar_features,wind_chill,heat_index,clip_outliers,spread, …) andfeature_catalogare removed with no replacement — pandas and polars own these operations; apply them to the returned frame. - The composition flags
include_forecast,include_trades,include_satellite, andinclude_cwop, plusdays()and thegranularityargument, are removed. - The redundant DataFrame extras
mostlyrightmd[parquet]andmostlyrightmd-economy[pandas]are removed: pandas is a base dependency now, so a plainpip install mostlyrightmdreturns DataFrames with no extra. The genuinely optional extras ([polars],[nwp],[satellite],[cwop],[polymarket],[trades], finance[transcripts]) are unchanged. - The remaining deprecated aliases and dead scaffolds are cleared: the legacy
root and module import shims, the
TRADEWINDS_CACHE_DIRcache-dir env alias (useMOSTLYRIGHT_CACHE_DIR), and the retired hosted-seam placeholders.
Changed
Section titled “Changed”obs()is renamed toobservations(), and it always returns the merged per-report grain (the rows 1.xgranularity="observation"returned). The 1.x daily default (one aggregated row per LST settlement day) lives intraining_table().- The economic-data domain is renamed from
econtoeconomyat every layer, with no alias: import pathfrom mostlyright import economy, PyPI distributionmostlyrightmd-economy, npm package@mostlyrightmd/economy. - Keyword arguments line up across verbs: dates are
from_date/to_date, times arefrom_time/to_time, and the station argument isstationeverywhere;as_dataframe=becomesreturn_type=. Type renames:MostlyRightResult→ProvenancedFrame,LazySourceSpec→DeferredSource. - Hosted-seam environment variables are namespaced under
MOSTLYRIGHT_(EARNINGS_HOSTED_URL→MOSTLYRIGHT_FINANCE_HOSTED_URL,WEATHER_HOSTED_URL→MOSTLYRIGHT_WEATHER_HOSTED_URL);MOSTLYRIGHT_API_KEYis unchanged. - The SDK ships as six PyPI wheels + six npm packages, installed together via the
mostlyrightmdmeta distribution (pip install mostlyrightmd) and themostlyrightnpm meta package.
- The unpinned
weather.observations()fan-out is coverage-gated per source leg, on both routing strategies: the GHCNh leg is skipped for window slices inside the station’s current LST month (NCEI’s full-year PSV lags real time by days-to-weeks, so the current month is not reliably present in it) and for non-US stations. Previously a same-day unpinned call re-downloaded the entire ~24 MB current-year PSV on every single run for zero contributed rows — cold cache and warm cache alike. Elapsed months, including elapsed months of the current year, still query all three sources identically on either strategy; an explicitsource="ghcnh"pin bypasses the gate. - Transport failures are typed, end to end: a queried IEM or GHCNh archive leg
that fails now raises
HttpError(the class the migration guide tells you to catch) instead of leaking a rawhttpxexception out ofobservations()/training_table(), and the shared download helper now retries transport errors (timeouts, resets) with the same backoff as transient 5xx — previously a single connection blip escaped on the first attempt with zero retries. A queried archive leg never degrades silently: partial data from a broken feed is an error, not a thinner frame. - A fresh install’s first import is warning-free: first-party modules
(
weather.forecasts,weather.label,markets.kalshi,markets.polymarket) no longer consume the one-shotExperimentalFeatureWarningwhen they register their own contracts at import time. The warning now fires only when you call the experimentalregistry.source()/label()/contributor()surface — which also means it is no longer silently swallowed before it can reach you. - The Kalshi daily-temperature settlement universe is corrected to the 20 cities
the exchange actually lists: phantom cities that never had daily-temperature
markets are dropped, three real market cities are added, and the live
KXHIGHT*/KXLOWT*ticker family now resolves. A research run keyed to the old roster settled against data no market resolves on.
[1.17.0] — 2026-07-14 — Registry API redesign: the align / spine / sources ladder + per-domain pairs()
Section titled “[1.17.0] — 2026-07-14 — Registry API redesign: the align / spine / sources ladder + per-domain pairs()”Minor release, dual-SDK (PyPI 1.17.0 + npm 1.17.0 in lockstep). Phase 34 answers
the researcher feedback on the shipped composer (“too much magic — I can’t tell a
feature from the target at a call site; let me build the DataFrame myself”) by
splitting dataset()’s four fused concerns into two orthogonal axes + one
composition operator: SOURCES (mr.<domain>.<table>(entity, window) → X),
LABELS (mr.<domain>.label.<name>(entity, window) → a spine with y), and
mr.align(spine, *sources) — a leakage-guarded as-of join frozen at two
concepts. Every intermediate product is a plain, inspectable pd.DataFrame.
This release is additive-plus-shims. Nothing you run today breaks — the
default research() / dataset() output is byte-identical to 1.16 (live parity
gate 5/5 byte-green), every relocated symbol keeps an importable
DeprecationWarning shim, and all 2.0 removals are DOCUMENTED only (no shim
removed this release; ≥2 minors of warning runway).
Migration guide (where-did-each-kwarg-go table, the ladder, the symbol-move map,
TypeScript-on-1.17, econ-unchanged): docs/reference/migration-2.0.md.
mr.align(spine, *sources)— the frozen composition operator (D-34.01..05). A pure collapse + as-of/equality join + per-source leakage audit so a raisedLeakageErrornames WHICH source leaked and never blames a clean co-aligned source. Signature is exactly(spine, *sources)— all policy (revision authority,pit_fidelity, staleness) lives on the source specs. The bitemporalSourceContract(event_time + knowledge_time,revision_ordertie-break, schema id + version, per-column units, license flag,pit_fidelity,valid_fromcoverage, native frequency,availability py|ts) EXPRESSES the legacy climate dedup (report_type_prioritySTRICT>, first-seen-wins) with no special case insidealign()(Path A — the cheapest falsifier, run first).- The LABEL / SPINE axis + raw spine constructors (D-34.09..12).
mr.spine(df, *, entity, decision_time, y)(the BYO bridge; loudContractErroron a missing mapped column, never guesses);weather.days(stations, from_date, to_date, *, tz_override=None)(the public bare per-settlement-day spine; a list mints a long-format panel byte-equal to the per-station loop);weather.forecasts(entity, ..., source="iem_mos", model=None, models=None)(a registered bitemporal source wrapping the shipped forecast join — the landing site for the four forecast kwargs); and the.label.-registered spine factoriesweather.label.cli()/weather.label.daily_extremes()/markets.*.label.settlement()(y columns byte-identical to the recipes). A spine is the primitive; a label is a spine factory over the same domain constructor, so train and trade share the calendar-minting site (the structural fix for backtest/live settlement skew). - Per-domain
pairs()quickstart (D-34.21).weather.pairs(entity, from_date, to_date, *, label="cli")— capped at ≤5 params, two positional dates, a docstring showing its ownweather.days()→weather.label.cli()→align()desugaring, and a visible graduation error naming spine+align (never a bareTypeError) when you pass an unsupported kwarg.markets.kalshi.pairs/markets.polymarket.pairsare the renamed delegators. - Generalized
discover(text?, **filters)(D-34.13/14). Cross-domain catalog search over the registration metadata (id/kind/domain/columns/units/availability/ license/PIT fidelity) with deterministic field-weighted token scoring (no fuzzy dep), per-domain entity alias resolvers ("nyc"→ KNYC/KLGA, preserving the cross-venue settlement note), a runnable language-tagged{py, ts}usageper row, and anavailabilityruntime filter. Keyword-onlycity=still works. - Cross-SDK
align()conformance fixtures (D-34.14/25). A shared four-axis JSON suite (tie-break authority ⟂ arrival + first-seen tie,<=boundary inclusivity, nanosecond precision, NaN preservation) undertests/fixtures/parity/conformance/, driven as a HARD gate by Python against the real join core and read by the identical fixtures from the TS suite. A registry-generated@mostlyrightmd/weather/cataloglazy static-JSON subpath (byte-deterministic underexport_schemas.py --check), kept OUT of the size-budgeted meta barrel (TS-BUNDLE-01). - Migration guide + error-copy conformance (D-34.24/26).
docs/reference/migration-2.0.md(the 26-kwarg → new-home table, the four forecast kwargs →weather.forecasts(), label’s 5 jobs → 4 call shapes, the three orphan kwargs homed, the namespace-inversion symbol map, TypeScript-on-1.17, the econ-unchanged F-2 follow-up, and PIT-transform-out-of-scope). Every user-facing error string from Phase 34 is audited againstdocs/error-copy-style-guide.md(four required fields: source id, row/entity, both timestamps where temporal, concrete fix) and built through the single_errmsgconstruction path — including the unknown-entitydid you meansuggestion (EntityNotFoundError).
Changed
Section titled “Changed”- Namespace inversion — root now reads as the product pitch (D-34.20). Root =
machinery (
align,spine,discover,provenance,LeakageError/ContractError/NoDataError,registry,contracts,__version__) + domains (weather/markets/econ). A symbol stays top-level only if it is meaningful without naming a domain. - Collapsed error taxonomy (D-34.23).
MostlyRightError→LeakageError/ContractError/NoDataError;LiveStreamErrorre-parented underNoDataError(additive MRO) soissubclass(NoCWOPDataError, NoDataError)is True. The three are importable one deep (except mr.LeakageError:).
Deprecated
Section titled “Deprecated”- Relocated root symbols keep byte-stable importable shims (removal at 2.0).
from mostlyright import research/dataset/Station/CATALOG/StationCatalogandmostlyright.livenow resolve via a once-per-sessionDeprecationWarningshim naming themostlyright.weather.*home (mostlyright.research is mostlyright.weather.research— same object; parity is output bytes, not import path).markets.kalshi.dataset()/markets.polymarket.dataset()→pairs()with a deprecation shim. Removal target: 2.0 (≥2 minors out), documented only. - The D-22 label-default
FutureWarningis GONE (D-34.22). The 2.0 flip ("cli"→None) is cancelled — there is no implicit label default in the new surface, so an OMITTED label stays"cli"byte-stable with no warning, forever.
[1.16.0] — 2026-07-14 — Economic indicators vertical: first published mostlyrightmd-econ / @mostlyrightmd/econ dist
Section titled “[1.16.0] — 2026-07-14 — Economic indicators vertical: first published mostlyrightmd-econ / @mostlyrightmd/econ dist”First release of the econ vertical — CPI, PPI, nonfarm payrolls, unemployment,
GDP, jobless claims, and Fed decisions for Kalshi + Polymarket — as the 4th
published PyPI dist (mostlyrightmd-econ) and 5th npm package
(@mostlyrightmd/econ), in lockstep with core/weather/markets/meta at 1.16.0.
(1.15.0 shipped the general-first re-layering without econ, so econ debuts here.)
- Econ surface conformance to the 1.13/1.14 source-identity contract
(dual-SDK, plan 29-11). The
mostlyright.econvertical now mirrors the cross-verticaldocs/source-identity.mdsurface it was unified under:series()is the canonical read function (history()is a byte-identical, docstring-@deprecatedsilent alias — no runtime warning this release, warning next minor, removal at 2.0);series()/research_econ()gainsource=(contract §1 provenance: accepted{None,"fred","bls","bea","dol", "fed"}, an unknown source OR a valid authority that cannot serve the indicator raisesValueErrorbefore any network call — never a silent fallback) anddelivery=(contract §2:"live"default |"hosted"raisesSourceUnavailableErrornamingECON_HOSTED_URL+MOSTLYRIGHT_API_KEY, the reserved paid-tier seam); and a newsnapshot(indicator, *, as_of=None)returns the settlement-target state — the latestsettlement_grade=Truevintage withvintage_date <= as_of, raisingIndicatorNotYetReleasedErrorwhen nothing is knowable. TS mirrors the whole surface (series/snapshot,@deprecatedhistoryre-export,source/deliveryoptions with the same loud validation). The engine (fetchers/schema/cache/resolver/first-print logic) is untouched — a default call is byte-identical to the pre-conformance output. Core’sdataset()/research()are not modified (aninclude_econcovariate seam is an option-B question deferred to review); the econ↔weather firewall (test_firewall.py) stays green. - Packaging / release. All published TS packages now declare
engines.node >= 20(matching the workspace root); thetest-tsCI runs on Node 20 and 22 via a matrix behind a stable required-check gate; and the npm release path (release-ts.yml,release-ts-preflight.mjs, the changesetsfixedgroup) is wired for@mostlyrightmd/econso it publishes to npm in lockstep (PyPI was already wired in 29-01). Data-source licensing notices completed: the FRED®/BEA required disclaimers plus the BLS “cannot vouch” citation string; the BLS API ToS was verified permissive (no end-use controls).
[1.15.0] — 2026-07-10 — General-first re-layering: the label axis, thin markets delegators, panels, and the contributor registry
Section titled “[1.15.0] — 2026-07-10 — General-first re-layering: the label axis, thin markets delegators, panels, and the contributor registry”Minor release, dual-SDK (PyPI 1.15.0 + npm 1.15.0 in lockstep). The SDK is now
general-first: the venue-free core dataset() becomes an alignment
engine with an explicit label axis
(label="cli" | "daily_extremes" | None | <your own DataFrame>), and all venue
knowledge moves into thin markets delegators
(markets.kalshi.dataset(ticker) / markets.polymarket.dataset(event_id) with
contract-shaped outcome= targets). Everything you run today keeps returning
the same bytes — the default dataset() / research() output is
byte-identical to 1.14.0 and the live parity gate ran 5/5 byte-green — but
one loud deprecation notice starts this release:
⚠️ D-22 bridge — bare
dataset()now emits aFutureWarning. Callingdataset()without an explicitlabel=still returns thecli_*settlement columns byte-identically, but warns once: “dataset() label default changes from ‘cli’ to None in 2.0; pass label=‘cli’ to keep settlement labels or label=None to silence.” The default flips tolabel=None(features-only) at 2.0, after a ≥2-minor bridge window. Say what you mean today:label="cli"keeps settlement labels forever,label=Noneopts into the future default now.research()is unchanged and never warns — the legacy alias keeps its implicitclilabel until it retires at 2.0; its output is byte-stable. Note for-W errorusers: our own CI/suites do not run warnings-as-errors — promoting warnings to errors is a user opt-in; if you do, a baredataset()raises and the fix is the same explicitlabel=. Parity byte-stability is guaranteed by explicitlabel="cli"pins in the parity gate, not by a warnings filter. Seedocs/migration-1.15.md.
dataset(label=...)— the label axis (both SDKs). Four shapes:label="cli"(NWS CLI settlement columns, today’s bytes),label="daily_extremes"(WU/NOAA-WRH-style extremes with nativedaily_extremes_{tmax_c,tmin_c,tmean_c,n_obs,source_tmax,source_tmin}columns + WR-05 low-coverage nulling, row-preserved),label=None(features-only — works for any catalog station worldwide, no market required), and BYO: pass your own label DataFrame and the SDK enforces the leakage discipline for you (settlement-calendar LST bucketing, LEFT-join row preservation, collision guard against reservedobs_/cli_/wu_/noaa_wrh_prefixes, publication discipline vialabels_as_known="settled" | "publication_lag"). Misalignment errors loudly with the typedLabelAlignmentError(reason enum), never silently.research()anddataset()are now distinct thin wrappers over one shared body — the alias split that letsdataset()warn whileresearch()stays silent.- Thin markets delegators (both SDKs).
markets.kalshi.dataset(ticker, from_date, to_date, outcome=...)routes to coredataset(label="cli", ...)(Kalshi NHIGH/NLOW settle on the CLI product);markets.polymarket.dataset(event_id, ..., outcome=...)routes tolabel="daily_extremes"(Polymarket resolves on WU/NOAA-WRH extremes, NOT CLI — today’scli_*frame was the wrong ground truth for Polymarket models).outcome=Trueappends a binarylabel_outcomecolumn via NET-NEW strike parsers: Kalshi-T<strike>thresholds settle inclusive (value >= strike— the strike degree itself is YES, matching “{n}° or above”),-B<lo>-<hi>ranges are inclusive on both endpoints, and both parsers accept negative-temperature strikes (a leading minus binds to the strike integer:T-5,B-5--3). Polymarketabove/below/betweenmirrors the same inclusive rule. Malformed tickers raise typedKalshiTickerError/PolymarketStrikeError. - Multi-station panels:
dataset(stations=[...])(both SDKs). Long-format output (station column first, caller order preserved), per-station settlement calendars and per-station label-recipe resolution (mixed-venue panels OK); rows/columns are exactly the concat of the per-station single calls; nested per-station provenance inattrs["stations"]. - Experimental contributor registry (D-24, both SDKs). The documented
mostlyright.experimental.contributordecorator (TS:@mostlyrightmd/meta/experimentalregisterContributor) registers third-party post-join feature contributors — public but experimental/semver-exempt until 2.0 (first registration warns withExperimentalFeatureWarning). The labels-only firewall is unconditional: contributors can never emit label-reserved columns (obs_/cli_/wu_/noaa_wrh_prefixes andlabel/date/stationexacts). Contributor failure semantics: NaN block +RuntimeWarning, the call succeeds — fail-loud is reserved for missing extras and firewall violations. dataset(features=[...])selector (both SDKs). Replaces the accretinginclude_*flags with one explicit list ("forecasts","satellite","cwop","trades"); built on the same registry as third-party contributors.- Fetch-provenance ledger (D-25 — ships dark). Every re-fetchable
station-pool cache write (obs monthly, climate annual, IEM MOS runtime) now
appends a best-effort JSONL record —
(source, station, window, fetched_at UTC, row_count, order-independent row-window SHA-256)— to$MOSTLYRIGHT_CACHE_DIR/provenance/, per-source shards, filelock-guarded, size-bounded with single-backup rotation. Read it back withmostlyright.provenance.history(station, window=None). Zero read-path impact; a ledger hiccup can never fail a fetch. This is the audit trail Phase 33’sreconcile()will use to attribute upstream restatements. Python-only for now (TS is browser/server-agnostic — PT-3205 tracks the server-side port). - TS subpath surfaces:
@mostlyrightmd/meta/experimental,@mostlyrightmd/meta/panels,@mostlyrightmd/meta/markets-dataset; the D-22 warn latch fires once per process; meta size budget 40→44 KB, weather stays within 27 KB.
Deprecated
Section titled “Deprecated”- Bare
dataset()withoutlabel=—FutureWarning(the D-22 bridge above). Bytes unchanged this release; default flips tolabel=Noneat 2.0.research()never warns. include_forecast/include_satellite/include_cwop/include_trades=— deprecated aliases offeatures=[...](DeprecationWarning, byte-identical, removal no earlier than 2 minors out).contract=/contracts=/include_trades=on coredataset()— venue knowledge is leaving the core (DeprecationWarning, byte-identical below the warning, ≥2 minors); usemarkets.kalshi.dataset/markets.polymarket.dataset.
TS Parity
Section titled “TS Parity”Same-phase parity (32-05): the label axis (label: "cli" | "daily_extremes" | null | LabelRow[]), the alias split + once-per-process warn latch, the
experimental registry with the same unconditional labels-only firewall, panels,
and the markets delegators + inclusive strike parsers all ship in npm 1.15.0.
Known divergence: the fetch-provenance ledger is Python-only (PT-3205,
server-side TS follow-up).
[1.14.0] — 2026-07-09 — Observation-grain dataset(): per-report training rows joined to daily labels
Section titled “[1.14.0] — 2026-07-09 — Observation-grain dataset(): per-report training rows joined to daily labels”Minor release, dual-SDK (PyPI 1.14.0 + npm 1.14.0 in lockstep). Purely
additive: dataset()/research() gain a granularity="observation" mode
that returns the merged per-report METAR/SPECI rows LEFT-joined to every daily
column the same call produces (labels + covariates), and obs() per-report
rows gain a settlement_date/settlementDate column. The default
granularity="daily" output is byte-identical to 1.13.0 — parity untouched.
The one behavioral thing to know is the pseudo-replication guardrail: on an
observation-grain frame, daily labels repeat across every report row of the
same settlement day, so cross-validation MUST group by settlement_date. See
docs/migration-1.14.md.
dataset(granularity="daily"|"observation")+research()alias (both SDKs, D-17/D-18).granularity="observation"returns the per-report observation rows — identical to theobs(granularity="observation")row set, REUSED verbatim through the shared list path (no second copy of the trim/merge/settlement logic) — LEFT-joined by LST settlement day to all daily columns:cli_*(incl.cli_report_type),obs_*,fcst_*wheninclude_forecast, andsat_*/cwop_*when the covariate flags are on. The join is fan-out-proof: the daily side is unique per settlement day by construction, row count is preserved (asserted), and label-less days keep NaN/null daily columns — rows are never dropped or fanned.granularityvalues other than"daily"/"observation"(e.g."hourly") are rejected loudly, up front, before any fetch.granularity="daily"remains the default and is byte-identical to 1.13.0. Python:mostlyright.dataset(..., granularity="observation"); TS:dataset({ granularity: "observation" }).obs()per-report rows carrysettlement_date/settlementDate(both SDKs, D-19). Everyobs(granularity="observation")report row now stamps the ISO LST settlement day (viasnapshot.settlement_date_for/settlementDateFor), never a naiveobserved_at[:10]UTC slice — a UTC-midnight-spanning report is bucketed to the correct local settlement day (KLAX vectors verified byte-for-byte across both SDKs). A new optional field; existing rows are otherwise unchanged.tz_override/tzOverrideis threaded through the settlement machinery on this path.- Pseudo-replication guardrail docs (D-20).
docs/source-identity.mdgains an “observation-grain composer and pseudo-replication” subsection: because each settlement day’s labels repeat across all of that day’s report rows, splitting cross-validation by row leaks the label. The documented pattern isGroupKFold(or equivalent) keyed onsettlement_date.docs/migration-1.14.mdcarries the same headline for anyone adopting the observation grain.
-
TS
obs()station-identifier routing (review r3): IEM now fetched with the NWS code and AWC with the ICAO via registry resolution — ICAO input no longer yields empty/divergent IEM rows at observation grain; unknown stations raise typed errors. -
Dotted source pins now resolve at observation grain in both SDKs (review-loop hardening). A dotted source pin (e.g.
iem.asos) that previously produced a silent empty[]in TS / aValueErrorin Python on the observation path is now correctly resolved and threaded through, and the observation-grain frame’s provenance carries the dotted pin.tz_override/tzOverrideis likewise threaded into the daily side of the observation-grain composer (incl.market_close_utc), so both sides of the LEFT join agree on the settlement day.
TS parity
Section titled “TS parity”- Ships in the same phase (D-21).
dataset({ granularity })+settlementDatemirror the Python semantics:"daily"default returns the unchangedPairsRow[];"observation"returns the per-report rows joined to every dailyPairsRowcolumn bysettlementDate. camelCase where the TS surface already camelCases (settlementDate,tzOverride); daily columns keep their wire-parity snake_casePairsRownames. The composer is a pure object join — browser/MV3-safe, no Node APIs, within existing bundle budgets (no size-limit bumps). Note the existing Phase-30 default divergence persists: Pythonobs()defaults togranularity="daily", TSobs()defaults to"observation"; thedataset()default is"daily"in both.
[1.13.0] — 2026-07-09 — Observation-API unification: dataset(), climate(), covariates, one mode
Section titled “[1.13.0] — 2026-07-09 — Observation-API unification: dataset(), climate(), covariates, one mode”Minor release, dual-SDK (PyPI 1.13.0 + npm 1.13.0 in lockstep). Unifies the
observation API onto a single dataset()/research() surface, ships a
standalone climate() settlement table, adds an opt-in labels-only covariate
seam, and starts the deprecation train for the numbered-mode era. Every
existing entry point is byte-stable at runtime this release; see
docs/migration-1.13.md — headline: migrating a
pinned feature is a retrain event, not a find-replace.
dataset()— the new canonical name for the training-pairs join (both SDKs).research()is retained as an alias bound to the same callable (dataset is research); no deprecation warning onresearch()this release, byte-identical output. Python:mostlyright.dataset(...); TS:import { dataset } from "mostlyright".obs(granularity="observation")— per-report observation grain (Python). Returns the merged per-report METAR/SPECI rows (window-trimmed, no daily bucketing) with per-row source identity preserved andobservation_type/raw_metarsurfaced. Unpinned frames carry the new frame-identity tagmerged.live_v1, accepted only by the newschema.observation.merged.v1— the frozenschema.observation.v1rejects it, so fused frames can never masquerade as single-source. The defaultgranularity="daily"output is byte-identical to 1.12.1. TSobs()gains the same option with a deliberate divergence: its default is"observation"(preserving existing TS per-report behavior);"daily"throws a documented not-yet-ported error (parity ticket filed). Observation-grain frames stampretrieved_atprovenance.climate()— standalone NWS CLI settlement-label table (both SDKs). The dailycli_high_f/cli_low_flabels previously visible only inside thedataset()join are now a first-class, source-identified domain table in the JOIN column vocabulary, window-trimmed (no whole-year leak), frame tagcli.archivewith truthful per-row endpoint tagiem. Python:mostlyright.weather.climate(...); TS:import { climate } from "@mostlyrightmd/weather/climate"(lean subpath — keeps the root barrel inside its size budget). TSclimate()resolves NWS↔ICAO station codes through the station registry so thestationcolumn matches Python cross-SDK.dataset(include_satellite=, include_cwop=)— opt-in covariate composition (Python). LEFT-joins per-LST-settlement-daysat_*/cwop_*covariate columns onto the composed training frame behind flags defaultingFalse. Labels-only firewall: label columns are byte-identical flags-on vs flags-off, row count/order unchanged, missing days are NaN (never dropped), and satellite/CWOP can never become observation sources — the four parity-firewall files are untouched and grep-gated. CWOP covariates are QC-clean-only and read-only (no APRS sockets); satellite covariates count only scans that contribute a usable value (sat_scan_count). Requires the[satellite]/[cwop]extras; missing extras fail loud with a typed error. Adding a covariate to a trained model is itself a retrain event — see the migration guide. TS parity for the covariate flags is tracked as a CROSS-SDK-SYNC ticket (browser runtime cannot run local extraction).docs/source-identity.md— the single cross-vertical kwarg contract page:source=is always provenance,delivery=is always local|hosted, grain is vertical-defined, frame-vs-row identity, the labels-only firewall, and the train/trade symmetry example.
Changed
Section titled “Changed”- Output-knob unification (D-06):
backend=+return_type=is the one output convention.obs()gainsbackend/return_type;return_type="list"added across the dispatch layer. TSreturn_typeaccepts"dataframe"as the canonical value (the historical"frame"remains an alias). Every function’s no-argument default return is byte-stable. - Numbered-mode vocabulary retired across docstrings, runtime strings,
and docs (
source must be one of ...,source-pinned dispatch requested ...); forecast surfaces are now described as IEM MOS vs per-NWP-model. TS neutral namesSOURCE_PINNED_SOURCES/SourcePinnedSource/isSourcePinnedSourceare canonical. - TS weather bundle budget raised 26 → 27 KB (TS-BUNDLE-01) to admit the station-format guard + pinned-AWC fail-loud hardening; measured 26.23 KB.
Deprecated
Section titled “Deprecated”All deprecations warn but behave identically; removal target is ≥2 minor
releases out. Production pipelines running -W error (Python) will surface
these as errors — see the migration guide before tightening warning filters.
research_by_source()→ useobs(source=..., granularity= "observation"). Caveat: this is NOT a drop-in swap for pinned features — the new path returns pre-merge full single-source coverage, a different (typically larger) row composition thanresearch_by_source’s post-merge filter. Retrain/re-backtest before switching production strategies (docs/migration-1.13.md).mostlyright.mode2import path →obs(...)frommostlyright.weatherandassert_source_identityfrommostlyright.core(its new canonical home, re-exported).as_dataframe=ondataset()/research(),research_by_source(), andobs()→return_type=(True→"dataframe",False→"list"; an explicitreturn_type=wins).- TS
MODE2_SOURCES/Mode2Source/isMode2Source→ theSOURCE_PINNED_*names (old identifiers remain as aliases binding the same values; removal at 2.0).
- TS station-format guard on every exported weather network surface (including the hosted satellite client): malformed station input now fails loud before any URL is built or network dispatched.
- TS pinned
source: "awc"queries fail loud on fetch errors instead of silently returning an empty array (unpinned merge path unchanged). - Parity case-4 fixture (KMIA rolling year) re-baselined 2026-07-09:
upstream restated 3 of 365 days after the May capture (mean aggregates
only; observation counts, settlement labels, and extremes byte-identical),
and the v0.14.1 hosted data plane the original fixtures were captured from
has since been decommissioned, making a fresh v0.14.1 re-capture
permanently impossible. The prior v0.14.1-captured fixture is preserved in
git history; provenance documented in
tests/fixtures/parity/README.md. Cases 1/2/3/5 remain v0.14.1-captured and byte-green.
[1.12.1] — 2026-07-08 — #107 follow-ups: cross-SDK consistency + version metadata
Section titled “[1.12.1] — 2026-07-08 — #107 follow-ups: cross-SDK consistency + version metadata”Patch release, dual-SDK. Resolves the P3 follow-ups from the 1.12.0 review loop (#107). No behavior change on any default path.
__version__now derived from dist metadata in all three Python packages viaimportlib.metadata(was hardcoded and stale: core/weather reported0.1.0rc1, markets0.0.1, while dists shipped 1.12.x). Falls back to0.0.0+unknownin metadata-less source trees.DataVersiontokens computed viafor_research()without an explicitsdk_versionnow reflect the true SDK version (opt-in surface; not wired intoresearch(), no cache/settlement impact).- Cross-SDK unknown-source error text unified on the
research()surface: both SDKs now emitresearch(): source must be one of [...], exact-message-pinned in both test suites.researchBySource’sMode 2 source must be one oftext is unchanged. - TS
sources: nullhonors the Phase 21 null-as-absent wire contract (was throwing the v0.3 deferred error for a JSON-round-tripped Pythonsources=None); removed a dead TS mutual-exclusion check whose only reachable firing was that same wire-contract bug, and the mirrored unreachable PythonValueErrorblock. - Pinned-source provenance threads explicitly through the wrapper path
(
wrap_result(source=...), mirroringmode2.research_by_source) — defense-in-depth; unpinned wrapper calls byte-identical.
- TS public barrel now re-exports
RESEARCH_SOURCE_ALIASES,RESEARCH_OBSERVATION_SOURCES,isResearchObservationSource. - Coverage:
research(source=)underas_dataframe=False, polars wrapper (skip-guarded), empty-after-pin attrs; TSsources: nullregression; barrel surface test. - Weather TS bundle growth root-caused (hosted-shim root-barrel re-export, a tree-shaking blocker — not data growth); shrink deferred to its own PR since it contracts the root-barrel API (#107).
[1.12.0] — 2026-07-08 — research(source=) pin + Mode 2 window fix
Section titled “[1.12.0] — 2026-07-08 — research(source=) pin + Mode 2 window fix”Minor release, dual-SDK (PyPI 1.12.0 + npm 1.12.0 in lockstep).
research(source=...)single-source pin (both SDKs). The singularsource=kwarg is now functional (wasNotImplementedErrorsince Phase 10): observation rows are filtered to the requested source’s alias-accept set after the multi-source merge and before dailyobs_*aggregation. Accepts the full Mode 2 vocabulary (iem,iem.archive,iem.live,awc,awc.live,ghcnh,ghcnh.archive) — byte-identical alias tables across Python (mostlyright.mode2._SOURCE_ALIASES) and TS (RESEARCH_SOURCE_ALIASES). Unknown sources raise before any network fetch.cli_*settlement columns are unaffected (NWS CLI is independent of the observation pin); the pinned DataFrame carriesdf.attrs["source"]. Inherited v0.1 limitation documented: the AWC > IEM > GHCNh merge runs before the pin filter, so pinning a lower-priority source yields only rows the merge did not supersede.sources=[...](plural, multi-source subset) remains v0.3; its error message no longer claimssource=is unwired. Thesource=Nonedefault path is byte-identical to 1.11.0 (parity gate green). TSsource: nullfollows the Phase 21 wire contract (null-as-absent). Follow-up nits: #107.
mode2.research_by_sourcereturned whole calendar months instead of the requested window. The month-granular parquet cache flows through_fetch_observations_rangeunfiltered, so aKNYCJan 6–12 query returned all 865 January rows (a Jan 28–Feb 3 query returned two full months) — silent out-of-window leakage into backtest frames, including the README’s owntrain = research_by_source(...)example. Rows are now trimmed to[from_date, to_date]by UTC date-part, inclusive, matching the TS port’s shipped filter exactly (cross-SDK lockstep); the +1-day fetch extension remains as a fetch-width knob only. Every return shape is fixed (DataFrame,as_dataframe=False, wrapper/backends). Callers needing the last LST day’s pre-midnight UTC tail should extendto_dateby one day.- Earnings venue-rule compound-word under-count (D-30). Hyphenated compounds
(e.g.
supply-chainforchain) were dropped from BOTH venue counts by an inverted guard; they now count on both venues per each issuer’s actual rule. Adds the per-occurrencecompound_typeaxis toschema.earnings_fact.v1(standalone/open/hyphenated/closed/affix_derivation— additive, nullable): closed compounds (wildfireforfire) are Kalshi-No + Polymarket human-review candidates, affix derivations (joyfulforjoy) count for neither venue, and an unknown value fails loud in every venue tally, at theFactLedgerdurable-write boundary, and through the catalog read boundary. Live-path parity: the streaming classifier emits one delta per(term, compound_type); its engine-relative timestamp crosses the SSE wire asoffset_seconds(never as the wallclockspoken_at), and the stream consumer fail-louds on any naive/numeric temporal field.
[1.11.0] — 2026-07-02 — Earnings-mention markets engine + SDK surface (Phase 27)
Section titled “[1.11.0] — 2026-07-02 — Earnings-mention markets engine + SDK surface (Phase 27)”Minor release. Adds the earnings-mention markets vertical (Kalshi
KXEARNINGSMENTION{TICKER} + Polymarket “will <company> say <word>”) — both
the [earnings] engine module and the thin SDK/consumer surface. Additive only;
no public API removed or changed.
-
Earnings-mention markets SDK surface (Phase 27). The client/SDK half of the earnings-audio pipeline for Kalshi
KXEARNINGSMENTION{TICKER}and Polymarket “will <company> say <word>” markets. No public API removed or changed; all additive.schema.earnings_transcript.v1+schema.earnings_fact.v1— canonical transcript/fact schemas with the fail-closed Kalshi-count rule (validate_kalshi_counted_occurrence), role-source provenance, the six venue-rule dimensions, and additive live-streaming fields (is_final/spoken_at/stream_seq,resolution_status="provisional").mostlyright.markets.earnings— resolver parsing a market id into the frozen{ticker, target_word, event_date, resolution_rule}tuple (KXEARNINGSMENTION / KXMENTIONEARN / KXBRKEM roots), Polymarket derive with a webcast-host allowlist, provider-fingerprint naming, point-in-time alignment (align_event_date), and leakage guards (assert_earnings_no_leakage, plus the live-stream firewallassert_not_backtest_source/assert_settlement_source).EarningsAdapter(mostlyright.weather.catalog.earnings) — thin consumer withearnings.live/earnings.hostedsources (byte-identical rows) and a livestream()async generator over the hosted SSE feed.- Codegen registries (webcast-provider fingerprints, ticker→HQ-timezone, earnings-calendar seed) exported through the byte-deterministic drift gate.
- TypeScript parity (
@mostlyrightmd/markets,@mostlyrightmd/weather): earnings resolver, hosted-consumer fetcher, PIT helper, and a browser/MV3EventSourcelive consumer. - New
[earnings]optional extra onmostlyrightmd-weathershipping the engine module (mostlyright.weather.earnings): the transcript-anchored role parser + fact builder with the fail-closed, roster-anchored Kalshi-count guarantee (an occurrence is Kalshi-counted only when the speaker is a named participant on the prepared-remarks roster, so a mis-parsed transcript line can never fabricate a countable executive mention), the append-only, audio-free transcript/fact parquet ledger, faster-whisper STT (lazy;av+ CTranslate2), live-HLS capture, the streaming transcriber, and the in-process segment bus.
The hosted earnings backend (deployed capture/STT/serving + SSE) and the operator-gated real-call de-risk spike ship in a later phase;
earnings.liveis BYO-audio andearnings.hostedfills its reserved seam then.
[1.10.1] — 2026-06-29 — CWOP adapter patch (pre-release review fixes)
Section titled “[1.10.1] — 2026-06-29 — CWOP adapter patch (pre-release review fixes)”Patch release. Supersedes 1.10.0, which was published before the CWOP review
loop completed and shipped the defects fixed below. No public API changes;
upgrade is recommended for anyone on 1.10.0.
- Packaging (import break):
mostlyrightmd-weathernow floors its core dependency atmostlyrightmd>=1.10.0,<2.0somostlyright.weather.cwop’sNoCWOPDataErrorresolves on a partial upgrade. Previously a stale-core install couldImportErroratcwopimport time. - APRS parser (
_aprs.py): numeric fields are only coerced whenraw.isascii() and raw.isdigit()(Unicode-digit strings no longer slip throughint()); positionless_weather packets retain their weather payload when the timestamp fails to parse;_closest_torejects future timestamps and applies a 10-minute skew tolerance. - QC (
qc/_cwop.py):qc_reliability_scoreis floored atmax(0.05, mean)so a fully-penalized station never reports an exactly-zero (divide-unsafe) reliability. - Cache (
_cache.py):write_cwop_cachepins an explicit pyarrow schema for byte-stableschema.cwop.v1round-trips. - Scan (
_scan.py): the early-exit station counter counts only placeable in-radius stations, so the cap is applied against the true candidate set. - Docs: corrected the
[1.10.0]CHANGELOG signatures for thescan,nearby,snapshot, andstreamentry points.
[1.10.0] — 2026-06-21 — CWOP adapter (PWS live stream + backtest cache)
Section titled “[1.10.0] — 2026-06-21 — CWOP adapter (PWS live stream + backtest cache)”Adds the Citizen Weather Observer Program (APRS-IS) adapter (mostlyright.weather.cwop) as a standalone live data source for Personal Weather Stations. Deliberately isolated from the parity-critical core — CWOP data never enters research(), merge/observations.py, or live/_sources.py.
mostlyright.weather.cwoppublic surface:nearby(station, *, radius_km=25.0, listen_seconds=60)— resolve an ICAO/NWS station code to coordinates and discovery-listen for nearby CWOP stations (delegates toscan())scan(lat, lon, radius_km=25.0, *, listen_seconds=60, min_reports=1)— discover CWOP stations near a point by listening on APRS-IS with a radius filter (cache-then-augment, early-exit oncemin_reportsstations are seen)stream(station, *, qc=True)— async generator yielding QC-scoredCWOPObservations (deduped by(station_id, observed_at))snapshot(station, *, duration_seconds=60, qc=True, persist=False)— collect a fixed window into aschema.cwop.v1DataFrame;persist=Truealso writes the backtest parquet cachelatest(station)— most-recent observation for a single stationhistory(station, from_date, to_date, *, qc_status)— backtest replay from persisted cache, returnsschema.cwop.v1DataFramepersist_observations(observations)— explicit write path for custom ingestion loops
- 6-layer QC — weighted geometric mean (0.0–1.0): range (0.20), temporal consistency (0.20), indoor detection (0.15), buddy/ASOS check (0.20), solar-radiation bias (0.10), reliability (0.15).
< 0.3→ dropped,0.3–0.7→ flagged,> 0.7→ clean. History-dependent layers (indoor ≥24 h, solar ≥7 d) pass until enough data accumulates. - Persistence — monthly parquet at
$HOME/.mostlyright/cache/cwop/{station}/{year}/{month}.parquet(honorsMOSTLYRIGHT_CACHE_DIR).filelock-guarded read-modify-write merge, dedup by(station_id, observed_at)first-seen-wins. No current-month skip (CWOP is ephemeral — unlike re-fetchable AWC/IEM, the current month is the only retention opportunity). - In-house APRS parser (
weather/_aprs.py) — MIT-licensed, no GPLv2aprslibdependency; pure stdlib (socket+re). NoCWOPDataError— subclass ofNoLiveDataError; raised instead of returning[]/None.schema.cwop.v1— new schema, accepts source tags"cwop.live"and"cwop.cache"via_registered_sources.schema.observation.v1observation_typeenum is untouched (parity firewall).[cwop]extra — pullspandasforsnapshot()/history()DataFrames.
- Parity firewall holds. CWOP is not registered in
research(),merge/observations.py(SOURCE_PRIORITY), orlive/_sources.py. Models access CWOP only throughcwop.history()— a DataFrame the strategy joins itself, never the settlement join. - Python-only release. No public TS API surface change; npm packages bump to 1.10.0 in lockstep with no functional change.
[1.9.0] — 2026-06-20 — Global NATIVE satellite ring (live path)
Section titled “[1.9.0] — 2026-06-20 — Global NATIVE satellite ring (live path)”Extends the Phase 25 GOES extractor to entire-world coverage at native-L2 fidelity on the live/self-parse path — one SDK, no fork. Each instrument family is a distinct, leakage-safe source so a model trained on one never silently reconciles against another. The anonymous ring is landed and routed: GOES + Himawari + VIIRS (no key). EUMETSAT Meteosat SEVIRI landed its CGMS projection + OAuth boundary (golden-vector verified, synthetic-fixture-tested) but its live Data-Store GRIB file reader is a forward seam — so Meteosat is reachable by explicit satellite= and gated out of auto-routing (Europe/Africa falls back to the honest VIIRS fill) until the reader lands. GMGSI mosaic was dropped (lower fidelity, off-brand for a source-identity SDK). Hosted delivery stays a Phase-27 seam.
- Multi-source contract + the native ring. Generalized the GOES-hardcoded Phase-25 surface into a per-source contract: a
schema.satellite.v1source SET{noaa_goes, jma_himawari, noaa_viirs, eumetsat_meteosat}, per-source product mini-registries, an extendedsatellite=enum (goes16/17/18/19,himawari8/9,viirs-npp/n20/n21,meteosat-0deg/iodc), cache-path validation for all sources, and a source→handler dispatch table each adapter self-registers into (so a later-wave halt never strands an earlier adapter). Phase-25noaa_goesbehavior is byte-unchanged.noaa_goesGOES-West (goes18operational PACUS,goes17archive) — config-swap on the existing GOES path, samenoaa_goessource identity (a single model can union East+West). Native Americas/Pacific incl. Hawaii. Anonymous.jma_himawari— Himawari-8/9 AHI L2 single-pixel extraction from the anonymousnoaa-himawari8/9buckets. Native Asia-Pacific. Geostationary fixed-grid.noaa_viirs— VIIRS (SNPP/NOAA-20/21) polar L2 SWATH nearest-neighbor extraction (vectorized haversine / 3D unit-sphere nav, numpy-only, antimeridian- and pole-safe; max-distance gate) from the anonymousnoaa-nesdis-{snpp,n20,n21}-pdsbuckets. Global incl. poles, ~2 overpasses/day. Rows carrysat_lon_used=null(no single sub-satellite point) — the row contract was generalized to makesat_lon_usednullable for swath sources.- Row-contract generalization —
sat_lon_usedis now nullable (geostationary sources populate it; the VIIRS polar swath leaves it null),pixel_row/pixel_coldocumented as native grid OR swath line/sample, and source-genericnotes— so a VIIRS-shaped row and a Phase-25 GOES row both validate. eumetsat_meteosat— Meteosat SEVIRI Cloud Mask (MSG-CLM, Core tier / CC-BY-4.0) single-pixel extraction from the keyed EUMETSAT Data Store (the only keyed source in the ring). Native Europe/Africa/Indian-Ocean. Geostationary via the CGMS LRIT/HRIT projection (integerCFAC/LFAC/COFF/LOFFdecode; “Meteosat sweeps y” — latitude drives the line; sub-sat0°/45.5°E), distinct from GOES scan-angle and Himawari 2D geolocation;sat_lon_usedpopulated. The projection is verified against independent golden navigation vectors (published CGMS constants + a forward↔inverse round-trip, not module self-consistency): sub-satellite(0,0)→(col 1856, line 1856)1-based, EGLL →(col 1866, line 3399), off-disk → loud raise. Single OAuth boundary (M4): one credential precedence (envEUMETSAT_CONSUMER_KEY/_SECRET→eumdacstored creds), one injectable Data-Store client (bearer token via HTTP Basicclient_credentialsatapi.eumetsat.int/token); missing creds raiseSourceUnavailableErroronly when the live path is invoked, with theeumdac set-credentials/ env-var hint. Live file-reading is a forward seam: the real Data-StoreMSG-CLMproduct is GRIB and reading the CGMS nav/pixels off it is not yet wired (Phase 26 ships SEVIRI synthetic-fixture-verified only);_eumetsat_extract._read_navreads the synthetic fixtures’ nav and any standard CGMS attributes, and otherwise raises a precise “live file navigation is not yet wired … forward seam” signal. So Meteosat is gated out of auto-routing (auto_route_target=False) — an explicitsatellite="meteosat-0deg"still dispatches to the handler; the band auto-routes to the VIIRS fill._open_datasetopens withmask_and_scale=False(the extractor is the single decode site) ready for the wired reader.
- Auto-routing when
satellite=is omitted (Wave 5).satellite()now defaultssatellite=/product=toNoneand auto-routes by the first resolved station’s coverage region over the self-registered handlers — GOES for the Americas/Pacific (with a GOES-East/West longitude split), Himawari for Asia-Pacific, VIIRS for the poles and any band no landed geostationary source covers yet. MODEST: one(source, satellite, product)triple per call, paired with the source’s cheap default product. Explicitsatellite=bypasses routing entirely (Phase-25 parity). Unknown station under auto-routing raises a clearValueError. The Europe/Africa/Indian-Ocean band was reserved as a clean Meteosat extension point — Wave 6 landed theeumetsat_meteosathandler +MSG-CLMproduct + projection, but because the live Data-Store GRIB reader is an unwired forward seam, Meteosat is gated out of auto-routing (auto_route_target=False) and that band currently falls back to the honest VIIRS global fill (a key-holding user auto-routing over Europe gets real VIIRS data, not a confusing unwired-reader raise). An explicitsatellite="meteosat-0deg"still dispatches to the Meteosat handler. The routing decision is key-free; flipauto_route_target=Truewhen the live reader lands and the band auto-routes to Meteosat with no router change. - Packaging. The
[satellite]extra now pinsh5py>=3.0(real HDF5 reads) and addseumdac>=3.1(the EUMETSAT Data-Store client, used only on the keyed Meteosat path). - Docs.
docs/satellite.md+ the README satellite section rewritten for the native ring: source-identity table, coverage map, auto-routing (with the honest Meteosat forward-seam note — projection landed, live reader pending, gated out of auto-routing), live-vs-hosted (delivery="hosted"raises a “Phase 27” error — noapi.mostlyright.mdanywhere), and the EUMETSAT key-setup section (EUMETSAT_CONSUMER_KEY/_SECRETenv vars →eumdac set-credentials→ GCP Secret Manager; Core-tier CC-BY-4.0 note).
- Keyless build + tests. GOES/Himawari/VIIRS are anonymous (no key). EUMETSAT is the only keyed source, and its build + unit tests are fully keyless (mocked Data-Store transport + synthetic SEVIRI fixtures + independent golden navigation vectors, ≥80% coverage on the keyless extractor + transport); the real Data-Store fetch is a single CI-excluded
@pytest.mark.livetest. - CI-guard. Every new test module importing the
[satellite]extra is skip-guarded so the base no-extra fast-suite/pandas-3/polars/coverage-gate jobs still pass (new tests SKIP, 0 failures); a base-suite acceptance test asserts no extra is bound at module scope. - Python-only. No public-API change touches the TS surface this release; the npm packages bump to 1.9.0 in lockstep with no functional change.
- Cross-region auto-route warning now fires on a GOES-East/West satellite split. When
satellite=is omitted and a multi-station list straddles the GOES East/West boundary (bothnoaa_goesbut a different platform — e.g. an East station plus a mid-Pacific one), the collapse warning previously compared onlysourceand stayed silent, so a genuinely off-disk station could be dropped with no signal. The guard now compares the full(source, satellite)route and names the divergent target.
Corrections (added later; the released 1.9.0 text above is unchanged)
Section titled “Corrections (added later; the released 1.9.0 text above is unchanged)”Three statements above were true when 1.9.0 shipped and are no longer true. They are corrected here rather than rewritten, so the released history stays byte-frozen:
- “its live Data-Store GRIB file reader is a forward seam” and “Meteosat is gated out of auto-routing” — superseded in 2.2.0. The live SEVIRI reader landed: MSG-CLM is GRIB2 on the space-view grid (grid-definition template 3.90), read through the low-level
eccodesAPI becausecfgribhas nospace_viewentry in its grid map.auto_route_targetis nowTrue, so the Europe/Africa/Indian-Ocean band auto-routes to Meteosat instead of falling back to the VIIRS fill.latlon_to_seviri_pixelis byte-unchanged across that landing. - “Hosted delivery stays a Phase-27 seam” and “
delivery="hosted"raises a ‘Phase 27’ error” — superseded in 2.0.0. Hosted delivery is a live opt-in seam reached viadelivery="hosted"plusMOSTLYRIGHT_WEATHER_HOSTED_URLandMOSTLYRIGHT_API_KEY; hosted rows are byte-identical to the locallivepath. The default path still makes no hosted call. - The env-var names in this section predate 2.0.0. Every hosted-seam env var gained the
MOSTLYRIGHT_prefix in 2.0.0: the current names areMOSTLYRIGHT_WEATHER_HOSTED_URL,MOSTLYRIGHT_FINANCE_HOSTED_URL, andMOSTLYRIGHT_ECONOMY_HOSTED_URL.
[1.8.0] — 2026-06-19 — GOES ABI L2 satellite ingest (free local tier)
Section titled “[1.8.0] — 2026-06-19 — GOES ABI L2 satellite ingest (free local tier)”Minor release: a new optional extra mostlyrightmd-weather[satellite] adds local-first GOES-16/19 ABI Level-2 single-pixel extraction at station locations, mirroring the [nwp] extra and the forecast_nwp() pipeline shape.
weather.satellite()fetcher +[satellite]optional extra (#78, Phase 25). Reads anonymous public NOAA NODD buckets (AWSnoaa-goes16/19, GCP mirror) directly — no hosted backend — and returns a leakage-safe DataFrame of single-pixel ABI L2 derived products extracted at a station’s ICAO lat/lon. Covers GOES-16 and GOES-19 (the GOES-East lineage) across 10 products / 18 variables:ABI-L2-ACMC(cloud/clear-sky mask — the primary CONUS product),LSTC,TPWC,ACHAC,CTPC,AODC,DSIC(stability indices),DSRF(downward shortwave radiation — full-disk, gated behind a one-time warning), and the 3D vertical profilesLVMPC/LVTPC.schema.satellite.v1with source identity"noaa_goes"(shared by live self-extraction and the future paid adapter so a cross-delivery model reconciles), a per-rowdeliverylineage enum{live, hosted}(informational only, NOT source-identity),qc_status(annotate-never-drop:clean/flagged/suspect), ICAO^[A-Z]{4}$station identity, and event-time (scan_start/end) vs knowledge-time (as_of) wiring throughKnowledgeViewso satellite is leakage-safe inresearch()/backtests.- Whole-file S3/GCS transport (single full-object read into memory, not byte-range), a parquet cache tier at
~/.mostlyright/cache/satellite/..., and a fleet-ready backfill CLI (python -m mostlyright.weather.satellite backfill) with crash-safe resume, an AWS/GCP--mirrorselector, and an on-demand throughput probe. - Lazy-imports the heavy deps (
boto3/s3fs/gcsfs/h5netcdf/xarray) inside the public function — the module imports cleanly without the extra and raisesSourceUnavailableErrorwith apip install mostlyrightmd-weather[satellite]hint when it is absent.
- Dual version bump: PyPI
1.8.0(mostlyrightmd,mostlyrightmd-weather,mostlyrightmd-markets) and npmvts-1.8.0(@mostlyrightmd/core,@mostlyrightmd/weather,@mostlyrightmd/markets,mostlyright). This release is Python-only — the TypeScript satellite reader is a deferred parity ticket (futureh5wasmimpl) perCROSS-SDK-SYNC.md; the npm1.8.0carries no satellite surface and bumps for version parity only. - Known follow-ups (tracked, non-blocking — the bulk backfill is the not-yet-run path): wire cached-partition reads into
satellite(cache=True); have the backfill write finalized (schema-shaped) rows; add dead-PID/stale-lock recovery to the backfill progress lock.
Corrections (added later; the released 1.8.0 text above is unchanged)
Section titled “Corrections (added later; the released 1.8.0 text above is unchanged)”Two statements above were true when 1.8.0 shipped and are no longer true. They are corrected here rather than rewritten, so the released history stays byte-frozen:
- The
python -m mostlyright.weather.satellite backfillCLI no longer exists. The fleet backfill was relocated out of the published wheel to the non-published operator tree in 2.0.0; the equivalent entry point ispython -m services.weather.backfill backfill, which ships only in the ingest image and never in a PyPI dist.satellite()and the cache tier described above are unaffected. schema.satellite.v1station identity is no longer^[A-Z]{4}$. It was widened in 2.2.0 to a site-id alphabet so arbitrary coordinates carry a legible derived identity (40.7789, -73.9692→N4078W07397). ICAO remains a strict subset, so every partition, progress key, and serving route written under the old regex stays byte-valid.
[1.7.0] — 2026-06-12 — GEFS/CFS ensemble member selection
Section titled “[1.7.0] — 2026-06-12 — GEFS/CFS ensemble member selection”Minor release: forecast_nwp() gains a member= ensemble-member selector for GEFS and CFS.
member=ensemble selector onforecast_nwp()for GEFS + CFS (#75, closes #74).docs/forecasts.mddocumentedmember=for GEFS but the public function never exposed it — the path builders (ge{member}/ CFS6hrly_grib_{member}) supported member selection all along.forecast_nwp(station, "gefs", member="p05")now fetches a specific ensemble member; valid members are validated against the closed enums (GEFS:c00control +p01..p30perturbations +avg/sprstatistical products; CFS:01..04) with a loudValueErrorfor any other model or out-of-enum value, raised before the[nwp]extra imports.member=None(default) stays byte-identical to v1.6.0 behavior. Works on both the single-cycle path andcycle_range_start/cycle_range_endbackfills. The output schema is unchanged (nomembercolumn — selector only; tracked as future work). TS twin:ForecastNwpOptionsgainsreadonly member?: string(signature-forward; TS NWP execution remains v2.0+).
Changed
Section titled “Changed”mostlyrightmd[research]extra now pinsmostlyrightmd-weather>=1.7.0(was>=1.6.0): the coreforecast_nwp()wrapper threadsmember=to the weather impl, a kwarg introduced in weather 1.7.0. Default (member=None) calls remain call-compatible with older weather installs — the wrapper only passes the kwarg when explicitly set.
- Dual version bump: PyPI
1.7.0(mostlyrightmd,mostlyrightmd-weather,mostlyrightmd-markets) and npmvts-1.7.0(@mostlyrightmd/core,@mostlyrightmd/weather,@mostlyrightmd/markets,mostlyright).
[1.6.0] — 2026-06-06 — Open-Meteo forecast-join correctness, NWP fields, OM rate-limiting + research() docs
Section titled “[1.6.0] — 2026-06-06 — Open-Meteo forecast-join correctness, NWP fields, OM rate-limiting + research() docs”Minor release bundling two correctness fixes and two feature PRs.
- NWP forecast fields
cloud_cover_pct,visibility_m,cloud_ceiling_mfor HRRR/GFS (#68, closes #63). Adds three nullablefloat64columns toschema.forecast_nwp.v1(additive, backward-compatible) with per-model GRIB maps for HRRR + GFS and QC range predicates. The change also resolves a pre-existing latent GFS bug:(variable, level)keys that resolve to two.idxrecords (GFSTCDC:entire atmosphereinstantaneous-vs-averaged, and theAPCP:surfacetwin) tripped theGribIntegrityErrorambiguity guard on the defaultforecast_nwp(station, "gfs")path — deterministic record disambiguation now picks the instantaneous record (lowestrecord_notiebreak) while keeping the loud-fail guard for genuinely unexpected duplicates. TS twin:schema.forecast_nwp.v1is now wired into the TS codegen so@mostlyrightmd/coreships aForecastNwpV1type + ajv validator with the new columns. - Open-Meteo forecast cache wiring + weight-aware throttle + variable trim (#66, closes #64). The Phase 20 forecast cache is now wired into
research(..., forecast_source="open_meteo")— previous-runs data is immutable, so repeated backtests serve from disk instead of re-hitting the free-tier rate limit. The research path also requests only the 3 variables the pairs join consumes (down from 18, cutting weighted call cost ~1.8×) and chunks long windows. Cache partitions are written on full-month boundaries so a subrange request can’t leave a partition incomplete (a later same-month window is served complete from disk, not silently truncated).
build_pairs_row()misclassified Open-Meteo forecasts as IEM MOS when a derivedissued_atwas present (#69, fixes #67). Phase 20+ Open-Meteo rows carry a derivedissued_at, so the oldissued_at-presence discriminator routed them into the IEM MOS aggregation path — silently nulling forecast temperatures and polluting IEM run-selection when both sources were combined. Records are now split by the authoritativesourcefield (open_meteo*→ Open-Meteo, else IEM; legacy source-less/issued_at-less rows stay Open-Meteo for backward compatibility). The fix also preserves Open-Meteopop_6hr_pct/qpf_6hr_inandfcst_issued_atprovenance through the source-routed path, and — critically — excludes Open-Meteo rows from runs issued after market close from the temp/POP/QPF aggregation (not just the timestamp), closing a lookahead-leakage path. The IEM-MOS byte-equivalent parity gate is unaffected.
Documentation
Section titled “Documentation”- Clarified that
research()returns daily rows, not hourly observations (#70, addresses #52). TheReturnsdocstring now states thatresearch()yields one daily settlement-summary row per date (obs_*are settlement-window aggregates), points toweather.obs()for an observation-only daily frame, and notes that sub-daily /raw_metaraccess is a Sprint 0.5+ item.
- Dual version bump: PyPI
1.6.0(mostlyrightmd,mostlyrightmd-weather,mostlyrightmd-markets) and npmvts-1.6.0(@mostlyrightmd/core,@mostlyrightmd/weather,@mostlyrightmd/markets,mostlyright). The TS twin gains theForecastNwpV1type + ajv validator for the new NWP columns ([#63] parity); the Open-Meteo rate-limiting ([#64]) and forecast-join ([#67]) changes are Python-internal and tracked for TS via CROSS-SDK-SYNC.md.
[1.5.2] — 2026-06-01 — Fetcher correctness fixes + configurable HTTP retries/timeout
Section titled “[1.5.2] — 2026-06-01 — Fetcher correctness fixes + configurable HTTP retries/timeout”Patch release: four bug fixes across the Python and TypeScript SDKs — fractional integer-field handling in Open-Meteo, an exact-window observation fetch that no longer over-fetches a whole year, bounded-parallel IEM MOS forecast fetches, and env-var overrides for HTTP retry/timeout.
- Open-Meteo integer-schema fields dropped rows on fractional values (#59 Python + TypeScript parity mirror). When the upstream API returned a fractional value in a nominally-integer column (e.g.
cloud_coverof12.5), pandas’ safeInt64cast raised and rows could be silently dropped. The fetcher now rounds before the cast; the TypeScript fetcher mirrors this with banker’s rounding (round half to even) so integer-schema fields (wind_dir_deg,cloud_cover_pct,freezing_level_m,visibility_m,weather_code) stay byte-equivalent to the Python output and valid againstschema.forecast.station.v1. obs(strategy="exact_window")over-fetched a whole calendar year (TypeScript) (#60). The TS IEM ASOS fetcher always widened the start to Jan 1 (the cache-key shape needed by archive callers), so a 1-day exact-window lookup pulled ~734 KB instead of ~9.8 KB. A new internalexactStartpath issues a single date-bounded request for[from, to+1 day), matching the Python_exact_fetchbehavior. The default year-padded path is unchanged forwarm_cache/research()callers.- IEM MOS forecast fetches ran serially (TypeScript) (#61). Independent runtime-cycle requests now fan out with a bounded concurrency pool (cap 8) instead of one-at-a-time awaits — most of the speedup on typical short windows while staying polite on large historical ranges. Output is byte-identical to the serial path (input ordering preserved); the pool fails fast on a non-404 HTTP error.
- Configurable HTTP retries and timeout via environment variables (Python) (#62).
MOSTLYRIGHT_HTTP_MAX_RETRIESandMOSTLYRIGHT_HTTP_TIMEOUTlet batch callers tune IEM 429 backoff without monkey-patching site-packages. Defaults are unchanged (3 retries, 60 s). Invalid values fall back to the defaults with a warning:MAX_RETRIESrequires a minimum of 1 (0 would issue no request at all), and the timeout rejects non-positive and non-finite (nan/inf) values.
- Dual publish: PyPI
1.5.2(mostlyrightmd,mostlyrightmd-weather,mostlyrightmd-markets) carries the Python Open-Meteo rounding fix [#59] + HTTP env overrides [#62]; npmvts-1.5.2(@mostlyrightmd/core,@mostlyrightmd/weather,@mostlyrightmd/markets,mostlyright) carries the TypeScript Open-Meteo rounding mirror [#59], the exact-window fetch fix [#60], and the bounded-parallel MOS fetch [#61].
[1.5.1] — 2026-05-31 — Open-Meteo Single-Runs HTTP 400 fix + ingest parallelization
Section titled “[1.5.1] — 2026-05-31 — Open-Meteo Single-Runs HTTP 400 fix + ingest parallelization”- Open-Meteo Single-Runs API rejected with HTTP 400 (#50 Python, #53 TypeScript — cross-SDK parity). The Single-Runs endpoint (
single-runs-api.open-meteo.com/v1/forecast) returns HTTP 400 when sentstart_date/end_date, so every Single-Runs request (training mode +issued_at) was failing against the live API. Both fetchers were sending the date params unconditionally. Fix mirrorsfetch_open_meteoin both SDKs: for the Single-Runs endpoint sendrun=only, then clip the full run-horizon response to the requested[from_date, to_date]window (end-inclusive day) after parsing. All other endpoints (Previous-Runs, Live, Seamless) keep their date params unchanged. TDD regression tests in both SDKs assert the URL omitsstart_date/end_date+ carriesrun=, and that a multi-day horizon is trimmed to the requested day.
Changed
Section titled “Changed”- Phase 24 — ingest performance: parallelized forecast extraction (#49). Forecast extraction now runs per-station work in parallel during ingest. Python-internal performance change only — byte-identical output, no public-API or behavior change. No TypeScript counterpart.
- Dual publish: PyPI
1.5.1(mostlyrightmd,mostlyrightmd-weather,mostlyrightmd-markets) carries the Python Single-Runs fix [#50] + the ingest parallelization [#49]; npmvts-1.5.1(@mostlyrightmd/core,@mostlyrightmd/weather,@mostlyrightmd/markets,mostlyright) carries the TypeScript Single-Runs fix [#53].
[1.5.0] — 2026-05-30 — Phase 23: Polymarket settlement catalog refresh
Section titled “[1.5.0] — 2026-05-30 — Phase 23: Polymarket settlement catalog refresh”Changed — Phase 23: Polymarket settlement catalog refresh (BREAKING, pre-1.0)
Section titled “Changed — Phase 23: Polymarket settlement catalog refresh (BREAKING, pre-1.0)”- Registry expanded to 94 stations (29 US + 65 international). Added 28 records to reconcile the catalog to Polymarket’s authoritative 51-city settlement roster: 4 US (
KLGANYC,KORDChicago,KDALDallas,KBKFDenver) + 24 international (21 net-new cities — Ankara, Busan, Cape Town, Chengdu, Chongqing, Guangzhou, Istanbul, Jeddah, Jinan, Karachi, Kuala Lumpur, Lucknow, Manila, Mexico City, Panama City, Qingdao, Shenzhen, Tel Aviv, Toronto, Wuhan, Zhengzhou — plus move targetsEGLC,UUWW,RCSS). mostlyright.stations._POLYMARKET_ICAOSis now an explicit enumerated set (50 stations), not country-derived. Polymarket no longer settles every international station, so “non-US ⇒ polymarket” was retired.filter_by_venue("polymarket")equals the registry-resident stations of the 51-city map (the 51st, Hong Kong, settles againstHKO— the Observatory, no airport ICAO, v0.2-deferred).- 7 station moves (Polymarket only; old ICAOs kept as bare records): Dallas
KDFW→KDAL, DenverKDEN→KBKF, LondonEGLL→EGLC, MoscowUUEE→UUWW, TaipeiRCTP→RCSS, Paris defaultLFPG→LFPB, Hong KongVHHH→HKO. - Houston cross-venue conflict. Polymarket moved to
KHOU(Hobby) while Kalshi staysKIAH(Intercontinental) — a new cross-issuer divergence alongside NYC (KNYCvsKLGA) and Chicago (KMDWvsKORD). Guarded bytests/test_cross_issuer_station_identity.py. - 27 stale Polymarket cities untagged (station records retained as bare weather stations): 6 US (Boston, DC, Minneapolis, Philadelphia, Phoenix, Detroit — Detroit stays
kalshi) + 21 international. - Deferred sources retargeted.
DEFERRED_STATIONS{VHHH, RCTP}→{HKO, RCSS}; Hong Kong now fully defers every measure (previously HK-high routed via VHHH METAR). Taipei defers viaRCSS(CWA). v0.2 lands theweather.gov.hk+ CWA clients. - Kalshi roster frozen —
_KALSHI_ICAOS(21) unchanged; nokalshitag removed. v0.14.1 parity gate stays green. - TS ships lockstep via codegen (
schemas/stations.json,polymarket-city-stations.json→@mostlyrightmd/core+@mostlyrightmd/markets). - Note: new non-K station metadata uses authoritative public airport data; live IEM/AWC coverage per ICAO should be confirmed (
@pytest.mark.live) before advertising these as live markets.
[1.4.0] — 2026-05-28 — Phase 22: Station-model refactor + Las Vegas (Kalshi TLV)
Section titled “[1.4.0] — 2026-05-28 — Phase 22: Station-model refactor + Las Vegas (Kalshi TLV)”Changed — Phase 22: Station-model refactor (BREAKING, pre-1.0)
Section titled “Changed — Phase 22: Station-model refactor (BREAKING, pre-1.0)”- Unified, venue-agnostic station catalog in core. New
mostlyright.stationsmodule exposesStationCatalog(CATALOG,Station) withget(),filter_by_venue(), andfilter_by_country(). A station is a physical fact; prediction-market participation is avenuestag (frozenset[str]) onStationInfo, not a separate module or a US/international split. - Registry expanded to 66 stations (25 US + 41 international). Added the five Kalshi settlement stations the v0.14.1 set had wrong —
KIAH(Houston Intercontinental, notKHOU),KDTW,KCVG,KBNA,KSLC. The four non-settlement US weather stations (KHOU/KMSY/KOKC/KSAT) remain queryable with an emptyvenuestag. KLAS(Las Vegas, tickerTLV) tagged as a Kalshi settlement station (issue #39). Kalshi tradesKXHIGHTLV/KXLOWTLV, settling against the NWS CLILAS report; the station was already in the registry but carried novenuestag.- Venue tags follow each issuer’s actual settlement map, not country. Kalshi and Polymarket settle several shared cities against different stations (NYC:
KNYCvsKLGA; Chicago:KMDWvsKORD), sofilter_by_venue("kalshi")≠ “every US station”.kalshi=21,polymarket=56. Markets derives its settlement universe by filtering the catalog; contract tests pin the tags against the citation + city-station maps. StationInfo.kalshi_tradedis now derived from venue tags instead of hard-codedTrue.TradewindsError→MostlyRightErrorandTradewindsResult→MostlyRightResultacross Python and TypeScript — no deprecation alias (pre-1.0). The base error code string changedTRADEWINDS_ERROR→MOSTLYRIGHT_ERROR. TheTRADEWINDS_CACHE_DIRenv-var migration shim is unaffected (separate v0.3-removal track).schemas/stations.jsonnow exports thevenuestag for TS codegen parity.
[1.3.1] — 2026-05-28 — GHCNh integer-°F precision fix
Section titled “[1.3.1] — 2026-05-28 — GHCNh integer-°F precision fix”- GHCNh
temp_ffalse precision for U.S. ASOS stations (#36, closes #16). GHCNh-sourced observations emitted back-convertedtemp_f(e.g.51.08°Ffortemp_c=10.6°C) viacelsius_to_fahrenheit. The raw METAR preserved in the GHCNh PSVREMcolumn carries the T-group remark (e.g.T01060067), marking the row as integer-°F native;temp_f(anddewpoint_f) is now recovered asround(temp_c * 9/5 + 32)— matching the AWC (PREC-01) and IEM (PREC-02) paths. Non-T-group rows (synoptic / international) keep the legacycelsius_to_fahrenheitpath.temp_cis unchanged. Closes the Phase 18 GHCNh deferral. - Observation cache invalidation for the GHCNh change. Bumped
_CACHE_SCHEMA_VERSION(v2-phase18-integer-f→v3-ghcnh-integer-f) so existing observation caches re-parse instead of serving stale GHCNhtemp_fto upgraded users.
- Affects
mostlyrightmd-weatheronly. No TypeScript changes (the TS GHCNh adapter does not yet exist), so npm packages are not part of this release.
[1.3.0] — 2026-05-28 — Phase 20: Open-Meteo Forecast Source Integration
Section titled “[1.3.0] — 2026-05-28 — Phase 20: Open-Meteo Forecast Source Integration”Added — Phase 20: Open-Meteo Forecast Source Integration (leakage-safe, 36-model)
Section titled “Added — Phase 20: Open-Meteo Forecast Source Integration (leakage-safe, 36-model)”- Open-Meteo as third forecast source (Phase 20). 36 models across NCEP / ECMWF / DWD / Météo-France / JMA-KMA-CMA-BoM / UKMO-MetNo / GEM Canada providers. New
mostlyright.weather.fetch_open_meteo()Python API and@mostlyrightmd/weatheropenMeteoForecasts()TS API ship in lockstep. schema.forecast.station.v1unified per-station forecast schema covering IEM MOS shared core + Open-Meteo extras (apparent_temp_c,shortwave_radiation_wm2,cape_jkg,cloud_cover_pct,surface_pressure_hpa,freezing_level_m, etc.).schema.forecast.iem_mos.v1retained as back-compat alias.research(forecast_source="open_meteo")+ list formforecast_source=["iem_mos", "open_meteo"]— cross-source training data without silent merge; every row carries explicit source identity (open_meteo.previous_runs,open_meteo.single_run,open_meteo.live).IssuedAtMissingError+OpenMeteoSeamlessLeakageErrortyped exceptions in both Python (mostlyright.core.exceptions) and TypeScript (@mostlyrightmd/core/exceptions). Payload keys snake_case for cross-SDK MCP parity; both carryorigin_issue="Tarabcak/mostlyright#70".~/.mostlyright/cache/v1/forecasts/{source}/{model}/{station}/{YYYY}/{MM}.parquetcache tier withfilelock+ per-source partitioning. Live + seamless sources never cached.- 5-fixture OM-08 leakage regression suite (Python + TS) reproducing the exact Tarabcak/mostlyright#70 case. Headline assertion: NYC 2024-06-01 23:00Z
_previous_day1→issued_at = 2024-05-31T18:00Z(provably ≤ the 17:00Z snapshot). docs/forecast-sources.mdcomparing IEM MOS / NWP gridded / Open-Meteo across models, latency, units, leakage-safety. Includes Python + TS quickstart examples for the newforecast_source="open_meteo"surface.
Notes — Phase 20 origin + design floor
Section titled “Notes — Phase 20 origin + design floor”- The Open-Meteo Historical Forecast (seamless) endpoint is BANNED for training data.
fetch_open_meteo(mode="seamless")raisesOpenMeteoSeamlessLeakageErrorunlessallow_leakage=Trueis passed. Even then,LeakageDetectorrejects the rows whenas_ofis asserted. Origin: Tarabcak/mostlyright#70 — legacy/forecast_seriesproxied the seamless feed without preservingissued_at, silently using post-snapshot 18Z runs in h13 EDT training data. Apparent +6pp lift on the MCAGE-OM benchmark was partly attributable to this leakage and not reproducible live. - Live mode (
mode="live") derivesissued_atvia conservative cycle-math fallbackfloor_to_cycle(now − publish_lag(model), cycles)per Phase 20 D-06. Open-Meteo’s Metadata API integration (for authoritativelast_run_initialisation_time) is a follow-up — the public HTTPS URL was not documented as of 2026-05-28. - Distribution: lockstep PyPI (
mostlyrightmd,mostlyrightmd-weather) + npm (@mostlyrightmd/weather,@mostlyrightmd/core).
Added — earlier Phase 21 follow-ups
Section titled “Added — earlier Phase 21 follow-ups”NwpNotAvailableErrortyped subclass in@mostlyrightmd/core. Raised byforecastNwp()instead of the genericDataAvailabilityError. Carries typed.stationand.modelproperties so consumers getinstanceof-based dispatch + IDE autocomplete instead of parsing the hint string. Back-compat preserved —NwpNotAvailableErrorextendsDataAvailabilityErrorwithreason="model_unavailable", so existingcatch (e instanceof DataAvailabilityError)paths continue to work unchanged. 6 new tests cover the subclass + back-compat catch paths.docs/nwp-forecasts.md— dedicated TypeScript-focused page documenting the v1.xforecastNwp()deferral. Explains why GRIB2 decode is browser-impractical today (eccodes / cfgrib are native-only; WASM port has prohibitive bundle size), lists the recommended catch pattern, points atiemMosForecasts()for the 7 MOS-covered stations and the Python SDK for everything else, lists all 24 signature-supported NwpModel literals, and tracks the v2.0+ roadmap.- TS package READMEs now visibly call out the stub surfaces (
forecastNwp,climateGaps) instead of burying them in the docs site.@mostlyrightmd/weatherREADME includes a “What’s NOT in the TypeScript SDK (yet)” section;mostlyrightmeta-package README ships a decision-matrix table showing which functions are wired today vs deferred to v2.0+.
Changed
Section titled “Changed”forecastNwp()hint string now points atdocs/nwp-forecasts.mdinstead of the inlinedocs/forecasts.md#typescript-laneanchor. The dedicated page surfaces better in search + IDE link previews than an in-page anchor.
[1.2.0] — 2026-05-27 — Phase 21: TypeScript SDK Parity Completion
Section titled “[1.2.0] — 2026-05-27 — Phase 21: TypeScript SDK Parity Completion”Closes the 11 audit-identified deltas between the Python and TypeScript SDKs from the 2026-05-27 cross-SDK audit. The TS SDK is now surface-equivalent to Python across research() kwargs, the typed-exception hierarchy, the cache versioning invariant, the obs() ingest-planner port, the dailyExtremes() wrapper, the markets trades shape, the forecastNwp() stub messaging, the codegen description propagation, the preprocessing namespace, and the climateGaps() structured error.
DataAvailabilityErrortyped exception in BOTH SDKs (closes Issue #26). Shared 6-value reason enum (model_unavailable | out_of_window | cache_miss | source_404 | source_5xx | rate_limited) lets consumers branch one.reasoninstead of string-matching the error message. SubclassesTradewindsError— existingexcept TradewindsErrorhandlers continue to work. Three Python and four TS raise sites migrated;SourceUnavailableErrorremains in place for back-compat at other call sites.research()composable kwargs surface — both SDKs (TSPARITY-01). The TSresearch()accepts the same 9 kwargs as Python:include_forecast,forecast_model,forecast_models,qc,tz_override,sources,source,backend,return_type. Per D-03,backend="polars"raisesDataAvailabilityError(reason="model_unavailable")in TS (no Polars in browser/Node);return_type="wrapper"is accepted as no-op. Both SDKs now reject mutually-exclusive misuse (sources+source,forecast_model+forecast_models, forecast model withoutinclude_forecast=True) asTypeErrorbefore any network fetch. The TS validator treatsnullandundefineduniformly as absent so JSONNone ↔ null ↔ undefinedround-trip matches Pythonis not Nonesemantics.- TS
obs(station, from, to, opts?)(TSPARITY-04) — port of Pythontw.weather.obs()Phase 7 ingest-planner. Strategy dispatch:auto(window-size heuristic),exact_window(date-bounded fetch — small calls pull small payloads),warm_cache(year-padded fetch for overlapping callers),hosted(raisesDataAvailabilityError).source="ghcnh"raisesDataAvailabilityError(TS GHCNh fetcher port deferred) rather than silently returning[]. - TS
dailyExtremes(station, from, to, opts?)(TSPARITY-05) — fetch+rollup wrapper matching Pythonmostlyright.international.daily_extremes(). Day-bucketing uses the station’s IANA local tz from the STATIONS registry; pre-rollup fetch widens by ±1 UTC day so tz-edge observations are captured; post-rollup output clipped to caller’s[fromDate, toDate]by station-local date. US ASOS stations get integer-°F precision (Phase 18 invariant); other stations get 0.1-precision values. - TS
versionedCacheStoreadapter (TSPARITY-03) — wraps anyCacheStoreso reads with mismatched/missing_cache_schema_versionreturnnull(cache miss → re-fetch). Matches Python’s Phase 18 18-08 parquet kv_metadata version stamp ("v2-phase18-integer-f"). Wired into BOTHdefaultCacheStore()entries (Node + browser). Pre-bump cached values silently re-fetch on next call. - TS
mostlyright.preprocessingnamespace (TSPARITY-10) — lowercase namespace alias matching Python’smostlyright.preprocessing. The PascalCasePreprocessingalias is deprecated but kept for back-compat; removal target v2. - TS validator schema descriptions via codegen (TSPARITY-08) —
ColumnSpec.notes→ JSON Schemadescription→ TS interface TSDoc/** ... */comments. Behavior was already shipped viajson-schema-to-typescript; Phase 21 added a regression test (withts.transpileModule()parse check) to guard against future codegen-pipeline regressions. - TS parity gate scaffold (TSPARITY-02) —
packages-ts/meta/tests/parity/README.mddocuments the 5-fixture row-equivalence assertion shape. Operator-triggered msw recording capture is the remaining gate-activation step. - Markets trades shape parity tests (TSPARITY-06) — three potential divergences from the 21-06 plan locked in via
packages-ts/markets/tests/parity/trades-shape.parity.test.ts: Kalshi candle bucket label, Polymarket pagination default, and equal-timestamp trade-ID tiebreaker (assertion: both SDKs preserve HTTP order — no in-SDK sort). - TS
forecastNwp()improved stub messaging (TSPARITY-07) — raisesDataAvailabilityError(reason="model_unavailable")with a hint pointing operators atiemMosForecasts()workaround for stations with MOS coverage. New banner indocs/forecasts.md. - TS
climateGaps()structured error (TSPARITY-11) — raisesClimateGapsNotImplementedError(subclass ofDataAvailabilityError) with a hint explaining the server-only architectural constraint. Newdocs/climate-gaps.mdpage.
Changed
Section titled “Changed”research()validation now fires BEFORE backend dispatch + network fetch on both SDKs. A typo in kwargs no longer hits live APIs / mutates the cache before raising.SourceUnavailableErrormigration sites — three Python raise sites moved toDataAvailabilityError(reason="model_unavailable"):obs(strategy="hosted")(wasNotImplementedError),catalog.get_adapter(unknown)(wasSourceUnavailableError),forecasts.forecast_nwp()ImportError fallback (wasSourceUnavailableError).obs(strategy="exact_window")now issues a date-bounded IEM request instead of pulling whole calendar years. A 1-day call pulls ~1 day of bytes (matching Python’s Phase 7 ingest-planner semantics).- README + package metadata: SEO copy sweep across top-level README, every per-package README, every
pyproject.toml/package.jsondescription, and (newly)keywordsarrays on the npm packages. Repositions the SDK as a universal public-data interface — weather + prediction-markets are today’s adapters; SEC filings, Federal Reserve series, court filings, FDA approvals, and equities structured data are named in the roadmap. - README package tables: added
equities,courts, andfdato the planned-adapters list on both the Python and TypeScript sides so the roadmap is legible at a glance. - GitHub repo: description repositioned as a public-data SDK with weather + prediction-markets as launch adapters and a named adapter roadmap (EDGAR, FRED, court filings, FDA, equities). Discovery topics expanded to include
public-data,financial-data,sec-edgar, andfred.
- TS cache returned raw concrete store —
defaultCacheStore()in both Node (default.ts) and browser (index.browser.ts) entries now wraps the inner store inversionedCacheStore(CACHE_SCHEMA_VERSION)so pre-Phase-18 ASOS0.06°F-precision cache rows silently miss instead of being served stale. Iter-1/iter-2 review fix. - TS
dailyExtremes()tz-edge silent row loss — pre-fix the filter compared UTC date prefix against station-local date strings asymmetrically, dropping local-day observations that fell on adjacent UTC days. Iter-2 fix widens pre-rollup UTC fetch by ±1 day; clip post-rollup by station-locallocalDatematching Python parity. - TS
obs(source="ghcnh"|"cli")silent empty rows — pre-fix the type allowed those values butfetchByStrategyhad no branch, so the call silently returned[]. Iter-1 fix:"cli"removed (not a valid Python source filter);"ghcnh"raisesDataAvailabilityErrorupfront. - README: switch PyPI download badges from Pepy/Shields to Badgen with PyPIStats detail links because Pepy has not indexed the
mostlyrightmd*projects and Shields can surface upstream rate limits for fresh packages.
[1.1.3] — 2026-05-27
Section titled “[1.1.3] — 2026-05-27”Metadata-only republish. No runtime API or source-code behavior changes.
Changed
Section titled “Changed”- PyPI + npm: republish the already-merged registry metadata so latest package artifacts include website, docs, repository, issue-tracker, and changelog links.
- PyPI + npm: keep the broadened core descriptions that position the SDK as prediction-market research over public data, not only weather research.
[1.1.2] — 2026-05-27
Section titled “[1.1.2] — 2026-05-27”Patch release. Closes plan 18-09 (parity fixture re-capture), plan 18-11c Task 2 (TS parity fixture re-export), and refreshes registry-visible metadata. Runtime SDK APIs and source behavior are unchanged.
Changed
Section titled “Changed”- README: add PyPI/npm monthly download badges, keep the public-data positioning beyond weather-only, and surface package-level download badges for stable Python and TypeScript packages.
- PyPI metadata: add Homepage, Documentation, Repository, Issues, and Changelog URLs to all three Python distributions; broaden the core
mostlyrightmdpackage description beyond weather-only. - npm metadata: add homepage, repository, and issue-tracker fields to all four published TypeScript packages; broaden the
@mostlyrightmd/coredescription beyond weather-only. - Docs: refresh the Sphinx landing description to position the SDK as public-data prediction-market research, not only weather settlement research.
(The v1.1.1 number was burned by a concurrent registry-metadata refresh published from a separate branch. This v1.1.2 release ships the parity work originally targeted for v1.1.1.)
- Parity fixture re-capture (plan 18-09): All 5 cases re-captured against post-Phase-18
research()via live network. Outcome: all 5 parquet bytes IDENTICAL to the pre-Phase-18 v0.14.1 baseline. The chosen cases (Jan 2025 → Nov 2025) fall outside AWC’s 168h archive window, so they source from IEM/GHCNh — whosetemp_fpaths Phase 18 does NOT change. Future parity cases that exercise the AWC live window WILL surface the integer-°F shift; this release confirms the existing 5 cases remain valid baselines. - TS parity fixture re-export (plan 18-11c Task 2): Re-ran
tests/fixtures/parity/export_for_ts.pyagainst the new parquets; produced byte-identical TS JSONs since the source parquets didn’t shift. - Only artifact change:
tests/fixtures/parity/expected_dtypes.jsoncolumn ordering — schema-ordered now (date, station, cli_*, obs_*, fcst_*, market_close_utc) instead of alphabetical. Test reads as dict — equality assertion preserved. - Parity HARD GATE remains GREEN.
uv run pytest tests/test_parity.py -m live -v→ 5 PASSED in ~58s. - No behavior change. Drop-in patch for any 1.1.0 caller.
Audit trail
Section titled “Audit trail”- See
.planning/phases/18-precision-fix-asos-integer-fahrenheit/18-09-PARITY-DELTA.mdfor per-case shasums + why-no-shift explanation.
[1.1.0] — 2026-05-27
Section titled “[1.1.0] — 2026-05-27”Phase 18 release. Recovers native integer-°F precision for U.S. ASOS stations and fixes the temp_f false-precision bug surfaced in issue #16. ASOS sensors observe in integer °F; the Tgroup in METAR remarks (T########) is a downstream encoding of the whole-°F internal value, not an independent precision tier. Pre-Phase-18 code back-converted Tgroup tenths-°C → °F via celsius_to_fahrenheit(), producing artifacts like 80.06°F where the native reading was 80°F. After this release, temp_f is integer-valued for ASOS rows recovered from Tgroup; non-Tgroup (international) stations keep the legacy float path.
- PyPI + npm: shared
parse_tgroup/parseTgrouphelper at_internal/tgroup.py/_internal/tgroup.ts— single source of truth for ASOS Tgroup parsing across AWC + IEM parsers. - PyPI + npm: AWC integer-°F recovery — when raw METAR contains a Tgroup,
temp_f/dewpoint_fare emitted as the recovered integer °F (e.g.T02670122→temp_f=80.0, NOT80.06). - PyPI + npm: IEM Tgroup override of
temp_c/dewpoint_c— when raw METAR contains a Tgroup, override the back-derived tenths with the source-truth Tgroup tenths. Critical:temp_fstays asraw_tmpf(NOT derived fromtemp_c); the two fields are different coded views of the same integer-°F sensor reading. - PyPI: parquet observation cache embeds
_cache_schema_version = "v2-phase18-integer-f"in file metadata + auto-invalidate on read mismatch. Existing pre-Phase-18 user caches silently re-fetch on next call. (packages/weather/src/mostlyright/weather/cache.py) - PyPI: new property test (hypothesis) covering every integer °F in [-50, 140] round-trip through Tgroup tenths-°C; new 12-station AWC↔IEM cross-source consistency test.
- PyPI: new live anti-regression test at
packages/weather/tests/live/test_12_station_asos_integer_f.pycovering KLGA / KJFK / KEWR / KBOS / KORD / KDFW / KLAX / KMIA / KDEN / KSEA / KATL / KPHX (gated behind-m live; run pre-publish). - npm: 5 new TypeScript test files (25 new tests + 12 skipped live) covering the same invariants as the Python suite — Tgroup helper unit tests, AWC integer-°F recovery, IEM Tgroup-override + consistency, exhaustive 191-value round-trip, 12-station live anti-regression.
- npm:
internationalDailyExtremesPhase 18 lattice-rationale comment documenting how callers should pickprecision=1for US-ASOS-lattice data vsprecision=0for international.
Changed
Section titled “Changed”- PyPI: schema description language updated in
observation.json/daily_extreme.json/synoptic_extremes.json— “0.1°C precision for US stations” was overstated; the tenths-°C is a Tgroup-encoded display precision, not independent source resolution. - PyPI:
_ghcnh.pycarries a Phase 18 boundary-marker comment above thecelsius_to_fahrenheit(temp_c)path documenting the deferral pending NCEI native-units documentation. - PyPI:
mostlyright.international.daily_extremesprecision-rationale comment documents how US-ASOS-lattice inputs interact with theprecision = 1 if is_us else 0branch. - PyPI:
test_international.pysynthetic non-lattice temperatures replaced with realistic integer-°F-lattice values ([10.0, 11.1, 12.2, 9.4]from[50, 52, 54, 49]°F); a separate dedicated HALF_UP-mechanics test preserves the rounding-semantics coverage. - Docs:
docs/live-streaming.mdalert example caststemp_fviaint()(Python) /Math.round()(TS) so the rendered example never surfaces back-conversion artifacts. New “Note ontemp_fprecision (Phase 18)” section.
- Parity fixtures NOT re-captured in this release. Plan 18-09 requires live network calls + operator-approval checkpoint and is deferred. The
tests/test_parity.pyHARD GATE still compares against the pre-Phase-18 baseline; users who rely on byte-equivalent parity vsmostlyright==0.14.1should pin to 1.0.x until 18-09 ships. - TS cache schema-version field deferred. The TS cache is a generic key/value abstraction (Memory/Fs/IndexedDB) used for many caller value shapes; adding a version field across all
CacheEntryconsumers requires a separate refactor. TS callers that hit a pre-Phase-18 IndexedDB / FsStore cache may see staletempF=80.06values until they invalidate manually. - Existing user Python caches auto-invalidate on next
research()call via the new_cache_schema_versionmechanism — one round of slow first-read, no manual user action required.
Review
Section titled “Review”- Two-iteration two-reviewer panel per
REVIEW-DISCIPLINE.md. Iter-1: Python Architect PASS; codexhigh+ TS Architect REVISE (both independently surfaced the same CRITICAL — IEM Tgroup-override missing the tmpf-validity gate from Python_iem.py:184-188). Iter-2: all three PASS after the gate was restored and 3 regression tests added.
[1.0.1] — 2026-05-27
Section titled “[1.0.1] — 2026-05-27”Hotfix release. Every live IEM MOS forecast fetch (NBE / GFS / LAV / MET / ECM) via either SDK has been broken end-to-end since the fetcher shipped — the IEM /api/1/mos.json endpoint validates the model query param against an uppercase-only regex and rejects lowercase values with HTTP 422. Both Python and TypeScript SDKs sent lowercase. This release fixes both sides in lockstep.
- PyPI:
mostlyright.weather._fetchers._iem_mos.fetch_iem_mosnow sends themodelquery param uppercased (e.g."NBE", not"nbe") to match IEM’s regex^(AVN|GFS|ETA|NAM|NBS|NBE|ECM|LAV|MEX)$. Closes #17. - npm:
@mostlyrightmd/weatheriemMosForecasts(...)applies the same uppercase fix to the TS fetcher. Same root cause; same one-character fix on both sides.
- No API changes.
pip install mostlyrightmd==1.0.1andnpm install @mostlyrightmd/[email protected]are drop-in patches for any caller already on 1.0.0. - The regression was invisible to the unit suites because both used opaque HTTP mocks (
MagicMock/vi.fn) that never inspected the actual query string. The new regression tests usehttpx.MockTransport(Python) and a recordingfetchFn(TS) to assert against the wire-level URL.
[1.0.0] — 2026-05-26
Section titled “[1.0.0] — 2026-05-26”First stable release. Promotes the 0.1.x line to SemVer-stable: the public API is committed to backward compatibility within the 1.x major.
- Code of conduct (Contributor Covenant 2.1) and SECURITY.md disclosure policy
- Status badges in the root README (PyPI version, npm version, license, docs)
Changed
Section titled “Changed”- Root README rewritten as user-facing copy. Positions the SDK as the public-data SDK for quants and AI agents, with weather + prediction-markets data shipping today and SEC filings (EDGAR) + Federal Reserve economic data (FRED) on the roadmap.
- Cross-package pin bound from
>=0.1.0,<0.2to>=1.0.0,<2.0on all 3 PyPI distros. Lockstep contract is now anchored to the 1.x major line. - No source-code or API changes vs. 0.1.4.
pip install mostlyrightmd==1.0.0andpip install mostlyrightmd==0.1.4produce identical runtime behavior.
[0.1.4] — 2026-05-26
Section titled “[0.1.4] — 2026-05-26”Coordinated release on PyPI + npm: refreshes registry-visible copy and graduates the TypeScript SDK from 0.1.0-rc.* to its 0.1.0 final.
Changed
Section titled “Changed”- PyPI:
mostlyrightmd,mostlyrightmd-weather,mostlyrightmd-marketsbumped0.1.3 → 0.1.4. No source-code or API changes. Per-package descriptions and READMEs cleaned for public consumption. - npm:
@mostlyrightmd/core,@mostlyrightmd/weather,@mostlyrightmd/markets, and the unscopedmostlyrightmeta-package shipped0.1.0(first non-rc release). All four moved from the@nextdist-tag tolatest. - CHANGELOG header corrected.
[0.1.0] — [0.1.3] — 2026-05-26
Section titled “[0.1.0] — [0.1.3] — 2026-05-26”Initial production releases on PyPI. Versions 0.1.0 through 0.1.3 shipped on the same day during a staggered first-publish sequence: each release iterated on trusted-publisher registration for one PyPI package at a time, with the prior versions re-publishing alongside.
Live on PyPI after [0.1.3]
Section titled “Live on PyPI after [0.1.3]”mostlyrightmd— versions[0.1.0, 0.1.1, 0.1.2, 0.1.3]mostlyrightmd-weather— versions[0.1.2, 0.1.3](versions 0.1.0 and 0.1.1 were not published due to a publisher-registration typo at the time; the gap is intentional and harmless because the package-extra resolves transitively to the newest available version)mostlyrightmd-markets— version[0.1.3](first available version on PyPI; the 0.1.0–0.1.2 gap mirrors the weather story above)
mostlyright.research(station, from_date, to_date)— the canonical observation × climate join, byte-equivalent tomostlyright==0.14.1’sclient.pairs()on the captured parity fixturesmostlyright.weather— direct fetchers for AWC, IEM ASOS, IEM CLI, GHCNh, NWS CLImostlyright.markets.catalog.kalshi_nhigh/kalshi_nlow— Kalshi NHIGH/NLOW contract resolversmostlyright.markets.polymarket— Polymarket discovery + settlement helpersmostlyright.core— schemas, validators, temporal-safety primitives (KnowledgeView,LeakageDetector), source-identity invariants (SourceMismatchError)- Local parquet cache at
~/.mostlyright/cache/(Python) — deterministic, file-locked, year-aligned - Three-package PyPI layout:
mostlyrightmd(core) +mostlyrightmd-weather+mostlyrightmd-markets
Renamed (from earlier scaffold)
Section titled “Renamed (from earlier scaffold)”- PyPI distribution names migrated from
mostlyright*tomostlyrightmd*. The Python import path remainsimport mostlyright. The PyPI install name move avoids version-shadowing the legacymostlyright==0.14.1package. - npm scope migrated from
@mostlyright/*to@mostlyrightmd/*. The unscopedmostlyrightmeta-package is unchanged. - Cache directory renamed from
~/.tradewinds/cache/to~/.mostlyright/cache/. Environment variable renamed fromTRADEWINDS_CACHE_DIRtoMOSTLYRIGHT_CACHE_DIR. Back-compat shim accepts both names during the 0.1.x line; the legacy name will be removed at v0.3.
[0.0.1] — Initial scaffold
Section titled “[0.0.1] — Initial scaffold”Workspace + three-package layout. SDK foundations ported from the predecessor codebase (tradewinds*).