Reference
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]”[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.
[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.
[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*).