Skip to content

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.

  • The flood and CAL FIRE timestamp grammars refuse the midnight-24 form on every interpreter. Newer CPython’s fromisoformat accepts T24:00:00 and 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 tropical raw_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/weather gains two verbs on its root and on the lean @mostlyrightmd/weather/hazards subpath: wildfires and hotspots, writing two new row contracts — schema.hazard.wildfire.v1 (one row per incident x capture) and schema.hazard.hotspot.v1 (one row per detection). Both row types are codegen output from the same committed JSON schemas the Python columns come from.

    wildfires spans both providers on one source= axis: the national interagency feed through scope (current / season / history) and the California state feed through year / activeOnly. There is no separate California verb — both write the same row and the source column says which answered. A past window against scope: "current" throws HazardRetentionError quoting 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.

    hotspots takes 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 in MOSTLYRIGHT_FIRMS_MAP_KEY. Every detection row carries the NASA acknowledgment in its attribution column, and FIRMS_ATTRIBUTION is exported for a result with no rows.

    The runtime splits per leg, and each leg declares it. wildfires() runs in a browser — services3.arcgis.com answers a cross-origin request with Access-Control-Allow-Origin: *. wildfires({ source: "calfire" }) and hotspots() are Node-only: incidents.fire.ca.gov and firms.modaps.eosdis.nasa.gov send 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. See docs/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.hazards gains four verbs — gauges, stage_and_flow, rating_curve, and water_observations — and TypeScript ships the four camelCase twins gauges, stageAndFlow, ratingCurve, and waterObservations on the @mostlyrightmd/weather root and on the lean @mostlyrightmd/weather/hazards subpath. There are four verbs and not five: gauges covers both the bounding-box scope and the single-gauge scope, and the record_scope column 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 by series), schema.hazard.rating_curve.v1 (one row per curve point, ascending by stage_ft), and schema.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 routes
    • nwps.stage_and_flow — the stage-and-flow and rating routes
    • usgs.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_kcfs keeps the upstream number and only flow_cfs is comparable to a *_flow_cfs threshold. 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’s persist= is for. USGS marks recent values Provisional and flips them to Approved 60 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 newer last_modified_at_utc rather 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. persist is 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.gov host 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.

  • The private core module mostlyright._international is now mostlyright._daily_extremes. It computes station-local daily extremes for Polymarket settlement — the same computation TypeScript already spells dailyExtremes — 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.

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.hazards ships nine names: alerts, text_products, post_storm_reports, issuing_offices, tropical_cyclones, best_track, best_track_revisions, plus capture_history and persist_rows for the local vintage ledger. TypeScript ships the seven camelCase twins — alerts, textProducts, postStormReports, issuingOffices, tropicalCyclones, bestTrack, bestTrackRevisions — on the @mostlyrightmd/weather root and on a lean @mostlyrightmd/weather/hazards subpath.

    Four row contracts: schema.hazard.alert.v1 (one row per issuance x UGC zone), schema.hazard.text_product.v1 (one row per bulletin, raw_afos verbatim), schema.hazard.tropical_cyclone.v1 (one row per active storm), and schema.hazard.best_track.v1 (one row per synoptic fix, with HURDAT2’s L landfall flag as a named column). Every row carries captured_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.gov keeps exactly 7 days on both /alerts and /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 -AAA post-storm amendments. alerts refuses an out-of-retention window with HazardRetentionError naming source="iem" rather than returning the empty result the live feed would give, which reads as “no hazards occurred”.

    capture_history and persist_rows are Python-only: both read and write a local parquet vintage ledger, and pyarrow and filelock have no browser analogue. tropicalCyclones, bestTrack, and bestTrackRevisions are Node-only in TypeScript: www.nhc.noaa.gov sends no Access-Control-Allow-Origin header, so each refuses a browser page with a typed ContractError naming the host and the remedy instead of surfacing an opaque CORS TypeError. An extension service worker with the host in host_permissions can still call them. Both sets are reasoned omissions with their reason stated in the docs, not parity gaps.

    The settlement-cited host alerts.weather.gov is 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. Returns schema.economy.releases.v1 rows (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/dates leg. as_of= reconstructs the schedule as it was known at a past instant: a reschedule is a new row at a new schedule_knowledge_time, never an overwrite. A release that vanishes from the feed while still in the future becomes an explicit schedule_state='cancelled' row labelled state_evidence='sdk_inferred_absence'. An uncovered scope raises ReleaseScheduleUnavailableError — never an empty frame. No delivery= keyword and no hosted call. Coverage is stated rather than faked: gdp at exact time from BEA, fed_funds/fed_decision at exact time from the curated table, and the nine BLS-family indicators only through the date-only FRED leg or releases(), because www.bls.gov returns 403 to programmatic fetch and api.bls.gov has no calendar endpoint.

  • ReleaseScheduleUnavailableError in both SDKs (a SourceUnavailableError subclass), raised on an empty parse or an uncovered scope.

  • BEHAVIOR CHANGE — a cached release calendar moves settlement anchors EARLIER. series, snapshot and training_table write a settlement-grade first print’s vintage_time, knowledge_time and release_datetime from one resolved anchor, and that anchor is now resolved in three tiers: the cached agency calendar, then the curated releases() table, then the end-of-day fallback. With no calendar cached the output is byte-identical to before — proven by comparing a history() 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 ALFRED realtime_start day 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, a date_only row 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/).
  • 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_KEY configured, resolution raises Python ApiKeyRequiredError / throws TypeScript ApiKeyRequiredError at 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 ManifestUnavailableError even 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_PATH points resolution at a local manifest file — the seam the repo’s test suite uses via tests_support/manifest_fixture.json, which ships in neither the wheel nor the sdist.
  • 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 / sourceAuth accept an optional { signal, fetchImpl } (ResolveOptions), and every call-time resolution site — the four weather fetchers and the markets clients — threads its caller’s own AbortSignal / 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 injected fetchImpl carries the manifest request instead of being bypassed. FetchWithRetryOptions gains a fetchImpl injection seam, which the weather fetchers inherit as a caller-facing option. A keyless caller raises ApiKeyRequiredError before any request (see Changed (BREAKING)). The Python resolver keeps its plain (key, fallback) signature by design — documented in _manifest_resolve.py and in the resolver’s divergence list.

  • TypeScript ./market-data clients 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_BASE stay exported as the fallbacks, and an explicit baseUrl / gammaBaseUrl / clobBaseUrl / dataBaseUrl option wins over the manifest. Previously a manifest catalog entry for a markets source redirected Python requests and silently did nothing in TypeScript.

  • The TypeScript ./trades and ./polymarket clients 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, and fetchEventById. Each module keeps its own host constant as the fallback for a catalog with no usable entry for that source, and fetchTrades and fetchEvents resolve 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 fetch fetchFn, mapped onto the resolver’s fetchImpl alongside signal — 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.

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.kalshiseries(), events(), markets(), market(url_or_id), candles(), trades(), orderbook() — and six on mostlyright.markets.polymarket (events(), markets(), market(url_or_slug), candles(), trades(), orderbook()) plus a typed polymarket.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 called volume. 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 (carries interval / supported / venue) and VenueCapabilityError (carries venue / capability / remedy), both extending ContractError in Python and its TypeScript twin, so an existing coarse catch keeps working.

  • discover(query, live=True) and kind="market": an explicit opt-in to a cross-venue live market search, returning the unchanged 17-column discover envelope. The default discover() stays byte-identically network-free, and kind="market" without live=True returns an empty, correctly shaped frame naming the opt-in rather than raising or reaching the network.

  • The @mostlyrightmd/markets/market-data TypeScript subpath: the same verbs in camelCase with snake_case row keys, no node: import, and no client-identity header, so every Polymarket verb runs from a page.

  • The markets.polymarket_data manifest 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. Pass lat= and lon= 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.9692N4078W07397) at 0.01° precision, which is what selects the cache partition, the object-store key, and the row’s station value. site_id= and tz_override= override the derived identity and timezone. Passing both a station and coordinates, only one of lat/lon, or neither raises before any network call, and an explicit site_id cannot 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 hosted satellite() gains the same coordinate options, deriving the identical site id in the browser.

  • --sites on 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-level eccodes reader — cfgrib has no space_view entry 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. eccodes and ECMWF’s eccodeslib binary 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 own scale_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 for satellite="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/** and packages-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.

  • _kalshi_client.fetch_market resolves 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.com rejects any non-kalshi.com Origin server-side with 403 (so host_permissions does not help); every Polymarket host answers Access-Control-Allow-Origin: *.

  • _polymarket_trades.history and the TypeScript PolymarketHistoryArgs harmonized to from_time / to_time and fromTime / toTime, matching the window grammar every other verb speaks.

  • economy_trades.candles now routes through the Kalshi /historical tier 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; the source column and df.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_SOURCES declares whether its bytes may be stored, and a row carrying an unregistered or missing source raises UnregisteredEconomySourceError instead 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 its MOSTLYRIGHT_PERSIST_FRED=1 opt-in.

  • 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 passing satellite=, you previously received noaa_viirs rows; you now receive eumetsat_meteosat rows, and without an EUMETSAT key you get a MeteosatCredentialsRequiredError instead of data. To keep the old behaviour, pass satellite="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-summaries subpath (TS, formerly /climate). The verb now speaks the same vocabulary as the daily_summary_* columns it emits; “climate” misread as climatological normals. No alias — the old names raise a teaching AttributeError (Python) or fail at the exports map (TS). Public TS types renamed Climate*DailySummary* (incl. the DailySummaryObservation barrel alias).

  • weather.label.cli()weather.label.daily_summary(), and training_table(label="cli")label="daily_summary" (Python + TS, same recipe; label="cli" raises a teaching error). The registry id weather.label.cli, the recipe key "cli", and RECIPE_LABEL_COLUMNS["cli"] are wire identities and unchanged.

  • weather.climate_gaps()weather.daily_summary_gaps() (Python); the TS climateGaps teaching stub is now dailySummaryGaps (DailySummaryGapsNotImplementedError). availability() output keys renamed: climate_years / first_climate_year / last_climate_yeardaily_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.v1 schema id, source= values ("iem" / "cli" / "cli.archive"), the cli.archive frame tag, cache tier paths (.../v1/climate/...), _internal/merge/climate.py + TS internal/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, economy derives 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 costs knowledge_time precision 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_economy and economy.snapshot told the reader to run pip install mostlyrightmd-economy[pandas]. That extra was removed in 2.0 — pandas ships as a base dependency — so the command failed. The hint now names pip install pandas and 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’s SatelliteError handler could not catch, so a single odd name ended the whole task. Name-parse failures now raise SatelliteFilenameParseError and skip that one object.
  • economy_trades.PUBLIC_DEPTH_FLOOR and economy_trades.MAX_PUBLIC_DEPTH_NOTE, and the public_depth_limited / depth_note frame 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=KXHIGHNY paged 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.

  • A default manifest endpoint. A caller with MOSTLYRIGHT_API_KEY set but no MOSTLYRIGHT_MANIFEST_URL now resolves source addresses against https://api.mostlyright.md/api/manifest instead 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 explicit MOSTLYRIGHT_MANIFEST_URL still takes precedence, so a self-hosted or staging catalog is unaffected.

    Python and TypeScript resolve against the same address — DEFAULT_MANIFEST_URL is 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_KEY check runs before the default is read, in both SDKs, pinned by a test in each.

  • The keyless-decay warning points at https://app.mostlyright.md, replacing https://mostlyright.md/signup, a path that was never deployed. Both SDKs.
  • The hosted-call gate (scripts/check_no_hosted_calls.py) scanned only Python. It now scans packages-ts/*/src as well, including the checked-in generated .js validators 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.json did 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 exported version constant read 2.0.0 while the package shipped 2.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 against package.json by a test rather than kept in lockstep by comment.

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) / CITIES on @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 of NoLiveDataError, error code LIVE_THROTTLED, carries retry_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) and language_probability are always surfaced. The default engine call is unchanged.
  • observations() accepts a list/tuple of stations (concatenated in input order), and return_type="list" now honors quality_control and carries knowledge_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 same event_time_col name 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, with mr.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.py still called the removed research()); it now drives weather.training_table() like the parity gate, and compare.py speaks 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 typed ContractError.
  • 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); TS dailyExtremes() 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_at never exceeds knowledge_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; /stream segments carry an absolute published_at.
  • TS parity: southern-hemisphere LST offsets (Sydney/Melbourne/Auckland were 1 h off), the registered-source union + forecast.nwp.v1 validator entry, and canonical-ICAO station keys from buildPairsRow.
  • TS packaging: the weather root bundle no longer re-exports the hosted shim (@mostlyrightmd/weather/hosted only — 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.

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 — datelocal_standard_date, cli_*daily_summary_*, obs_*observed_*. The full column rename table is in docs/reference/migration-2.0.md.

  • The composition verbs research(), dataset(), and pairs() are gone in favor of a single training_table() (TypeScript: trainingTable()); hand-composition moves to mostlyright.experimental.
  • Feature engineering leaves the SDK: the transforms and preprocessing helpers (lag, diff, rolling, calendar_features, wind_chill, heat_index, clip_outliers, spread, …) and feature_catalog are 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, and include_cwop, plus days() and the granularity argument, are removed.
  • The redundant DataFrame extras mostlyrightmd[parquet] and mostlyrightmd-economy[pandas] are removed: pandas is a base dependency now, so a plain pip install mostlyrightmd returns 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_DIR cache-dir env alias (use MOSTLYRIGHT_CACHE_DIR), and the retired hosted-seam placeholders.
  • obs() is renamed to observations(), and it always returns the merged per-report grain (the rows 1.x granularity="observation" returned). The 1.x daily default (one aggregated row per LST settlement day) lives in training_table().
  • The economic-data domain is renamed from econ to economy at every layer, with no alias: import path from mostlyright import economy, PyPI distribution mostlyrightmd-economy, npm package @mostlyrightmd/economy.
  • Keyword arguments line up across verbs: dates are from_date / to_date, times are from_time / to_time, and the station argument is station everywhere; as_dataframe= becomes return_type=. Type renames: MostlyRightResultProvenancedFrame, LazySourceSpecDeferredSource.
  • Hosted-seam environment variables are namespaced under MOSTLYRIGHT_ (EARNINGS_HOSTED_URLMOSTLYRIGHT_FINANCE_HOSTED_URL, WEATHER_HOSTED_URLMOSTLYRIGHT_WEATHER_HOSTED_URL); MOSTLYRIGHT_API_KEY is unchanged.
  • The SDK ships as six PyPI wheels + six npm packages, installed together via the mostlyrightmd meta distribution (pip install mostlyrightmd) and the mostlyright npm 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 explicit source="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 raw httpx exception out of observations() / 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-shot ExperimentalFeatureWarning when they register their own contracts at import time. The warning now fires only when you call the experimental registry.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 raised LeakageError names 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 bitemporal SourceContract (event_time + knowledge_time, revision_order tie-break, schema id + version, per-column units, license flag, pit_fidelity, valid_from coverage, native frequency, availability py|ts) EXPRESSES the legacy climate dedup (report_type_priority STRICT >, first-seen-wins) with no special case inside align() (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; loud ContractError on 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 factories weather.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 own weather.days()weather.label.cli()align() desugaring, and a visible graduation error naming spine+align (never a bare TypeError) when you pass an unsupported kwarg. markets.kalshi.pairs / markets.polymarket.pairs are 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} usage per row, and an availability runtime filter. Keyword-only city= 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) under tests/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/catalog lazy static-JSON subpath (byte-deterministic under export_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 against docs/error-copy-style-guide.md (four required fields: source id, row/entity, both timestamps where temporal, concrete fix) and built through the single _errmsg construction path — including the unknown-entity did you mean suggestion (EntityNotFoundError).
  • 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). MostlyRightErrorLeakageError / ContractError / NoDataError; LiveStreamError re-parented under NoDataError (additive MRO) so issubclass(NoCWOPDataError, NoDataError) is True. The three are importable one deep (except mr.LeakageError:).
  • Relocated root symbols keep byte-stable importable shims (removal at 2.0). from mostlyright import research/dataset/Station/CATALOG/StationCatalog and mostlyright.live now resolve via a once-per-session DeprecationWarning shim naming the mostlyright.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 FutureWarning is 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.econ vertical now mirrors the cross-vertical docs/source-identity.md surface it was unified under: series() is the canonical read function (history() is a byte-identical, docstring-@deprecated silent alias — no runtime warning this release, warning next minor, removal at 2.0); series()/research_econ() gain source= (contract §1 provenance: accepted &#123;None,"fred","bls","bea","dol", "fed"}, an unknown source OR a valid authority that cannot serve the indicator raises ValueError before any network call — never a silent fallback) and delivery= (contract §2: "live" default | "hosted" raises SourceUnavailableError naming ECON_HOSTED_URL + MOSTLYRIGHT_API_KEY, the reserved paid-tier seam); and a new snapshot(indicator, *, as_of=None) returns the settlement-target state — the latest settlement_grade=True vintage with vintage_date <= as_of, raising IndicatorNotYetReleasedError when nothing is knowable. TS mirrors the whole surface (series/snapshot, @deprecated history re-export, source/delivery options 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’s dataset()/research() are not modified (an include_econ covariate 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); the test-ts CI 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 changesets fixed group) is wired for @mostlyrightmd/econ so 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 a FutureWarning. Calling dataset() without an explicit label= still returns the cli_* 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 to label=None (features-only) at 2.0, after a ≥2-minor bridge window. Say what you mean today: label="cli" keeps settlement labels forever, label=None opts into the future default now. research() is unchanged and never warns — the legacy alias keeps its implicit cli label until it retires at 2.0; its output is byte-stable. Note for -W error users: our own CI/suites do not run warnings-as-errors — promoting warnings to errors is a user opt-in; if you do, a bare dataset() raises and the fix is the same explicit label=. Parity byte-stability is guaranteed by explicit label="cli" pins in the parity gate, not by a warnings filter. See docs/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 native daily_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 reserved obs_/cli_/wu_/noaa_wrh_ prefixes, publication discipline via labels_as_known="settled" | "publication_lag"). Misalignment errors loudly with the typed LabelAlignmentError (reason enum), never silently. research() and dataset() are now distinct thin wrappers over one shared body — the alias split that lets dataset() warn while research() stays silent.
  • Thin markets delegators (both SDKs). markets.kalshi.dataset(ticker, from_date, to_date, outcome=...) routes to core dataset(label="cli", ...) (Kalshi NHIGH/NLOW settle on the CLI product); markets.polymarket.dataset(event_id, ..., outcome=...) routes to label="daily_extremes" (Polymarket resolves on WU/NOAA-WRH extremes, NOT CLI — today’s cli_* frame was the wrong ground truth for Polymarket models). outcome=True appends a binary label_outcome column 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). Polymarket above/below/between mirrors the same inclusive rule. Malformed tickers raise typed KalshiTickerError / 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 in attrs["stations"].
  • Experimental contributor registry (D-24, both SDKs). The documented mostlyright.experimental.contributor decorator (TS: @mostlyrightmd/meta/experimental registerContributor) registers third-party post-join feature contributors — public but experimental/semver-exempt until 2.0 (first registration warns with ExperimentalFeatureWarning). The labels-only firewall is unconditional: contributors can never emit label-reserved columns (obs_/cli_/wu_/noaa_wrh_ prefixes and label/date/station exacts). 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 accreting include_* 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 with mostlyright.provenance.history(station, window=None). Zero read-path impact; a ledger hiccup can never fail a fetch. This is the audit trail Phase 33’s reconcile() 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.
  • Bare dataset() without label=FutureWarning (the D-22 bridge above). Bytes unchanged this release; default flips to label=None at 2.0. research() never warns.
  • include_forecast/include_satellite/include_cwop/include_trades= — deprecated aliases of features=[...] (DeprecationWarning, byte-identical, removal no earlier than 2 minors out).
  • contract= / contracts= / include_trades= on core dataset() — venue knowledge is leaving the core (DeprecationWarning, byte-identical below the warning, ≥2 minors); use markets.kalshi.dataset / markets.polymarket.dataset.

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 the obs(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_* when include_forecast, and sat_*/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. granularity values 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 carry settlement_date / settlementDate (both SDKs, D-19). Every obs(granularity="observation") report row now stamps the ISO LST settlement day (via snapshot.settlement_date_for / settlementDateFor), never a naive observed_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 / tzOverride is threaded through the settlement machinery on this path.
  • Pseudo-replication guardrail docs (D-20). docs/source-identity.md gains 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 is GroupKFold (or equivalent) keyed on settlement_date. docs/migration-1.14.md carries 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 / a ValueError in 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 / tzOverride is 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.

  • Ships in the same phase (D-21). dataset({ granularity }) + settlementDate mirror the Python semantics: "daily" default returns the unchanged PairsRow[]; "observation" returns the per-report rows joined to every daily PairsRow column by settlementDate. camelCase where the TS surface already camelCases (settlementDate, tzOverride); daily columns keep their wire-parity snake_case PairsRow names. 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: Python obs() defaults to granularity="daily", TS obs() defaults to "observation"; the dataset() 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 on research() 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 and observation_type/raw_metar surfaced. Unpinned frames carry the new frame-identity tag merged.live_v1, accepted only by the new schema.observation.merged.v1 — the frozen schema.observation.v1 rejects it, so fused frames can never masquerade as single-source. The default granularity="daily" output is byte-identical to 1.12.1. TS obs() 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 stamp retrieved_at provenance.
  • climate() — standalone NWS CLI settlement-label table (both SDKs). The daily cli_high_f/cli_low_f labels previously visible only inside the dataset() join are now a first-class, source-identified domain table in the JOIN column vocabulary, window-trimmed (no whole-year leak), frame tag cli.archive with truthful per-row endpoint tag iem. Python: mostlyright.weather.climate(...); TS: import { climate } from "@mostlyrightmd/weather/climate" (lean subpath — keeps the root barrel inside its size budget). TS climate() resolves NWS↔ICAO station codes through the station registry so the station column matches Python cross-SDK.
  • dataset(include_satellite=, include_cwop=) — opt-in covariate composition (Python). LEFT-joins per-LST-settlement-day sat_* / cwop_* covariate columns onto the composed training frame behind flags defaulting False. 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.
  • Output-knob unification (D-06): backend= + return_type= is the one output convention. obs() gains backend/return_type; return_type="list" added across the dispatch layer. TS return_type accepts "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 names SOURCE_PINNED_SOURCES/SourcePinnedSource/ isSourcePinnedSource are 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.

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() → use obs(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 than research_by_source’s post-merge filter. Retrain/re-backtest before switching production strategies (docs/migration-1.13.md).
  • mostlyright.mode2 import pathobs(...) from mostlyright.weather and assert_source_identity from mostlyright.core (its new canonical home, re-exported).
  • as_dataframe= on dataset()/research(), research_by_source(), and obs()return_type= (True"dataframe", False"list"; an explicit return_type= wins).
  • TS MODE2_SOURCES/Mode2Source/isMode2Source → the SOURCE_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 via importlib.metadata (was hardcoded and stale: core/weather reported 0.1.0rc1, markets 0.0.1, while dists shipped 1.12.x). Falls back to 0.0.0+unknown in metadata-less source trees. DataVersion tokens computed via for_research() without an explicit sdk_version now reflect the true SDK version (opt-in surface; not wired into research(), no cache/settlement impact).
  • Cross-SDK unknown-source error text unified on the research() surface: both SDKs now emit research(): source must be one of [...], exact-message-pinned in both test suites. researchBySource’s Mode 2 source must be one of text is unchanged.
  • TS sources: null honors the Phase 21 null-as-absent wire contract (was throwing the v0.3 deferred error for a JSON-round-tripped Python sources=None); removed a dead TS mutual-exclusion check whose only reachable firing was that same wire-contract bug, and the mirrored unreachable Python ValueError block.
  • Pinned-source provenance threads explicitly through the wrapper path (wrap_result(source=...), mirroring mode2.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=) under as_dataframe=False, polars wrapper (skip-guarded), empty-after-pin attrs; TS sources: null regression; 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 singular source= kwarg is now functional (was NotImplementedError since Phase 10): observation rows are filtered to the requested source’s alias-accept set after the multi-source merge and before daily obs_* 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 carries df.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 claims source= is unwired. The source=None default path is byte-identical to 1.11.0 (parity gate green). TS source: null follows the Phase 21 wire contract (null-as-absent). Follow-up nits: #107.
  • mode2.research_by_source returned whole calendar months instead of the requested window. The month-granular parquet cache flows through _fetch_observations_range unfiltered, so a KNYC Jan 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 own train = 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 extend to_date by one day.
  • Earnings venue-rule compound-word under-count (D-30). Hyphenated compounds (e.g. supply-chain for chain) 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-occurrence compound_type axis to schema.earnings_fact.v1 (standalone/open/hyphenated/closed/affix_derivation — additive, nullable): closed compounds (wildfire for fire) are Kalshi-No + Polymarket human-review candidates, affix derivations (joyful for joy) count for neither venue, and an unknown value fails loud in every venue tally, at the FactLedger durable-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 as offset_seconds (never as the wallclock spoken_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 &lt;company> say &lt;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 &lt;company> say &lt;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 firewall assert_not_backtest_source / assert_settlement_source).
    • EarningsAdapter (mostlyright.weather.catalog.earnings) — thin consumer with earnings.live / earnings.hosted sources (byte-identical rows) and a live stream() 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/MV3 EventSource live consumer.
    • New [earnings] optional extra on mostlyrightmd-weather shipping 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.live is BYO-audio and earnings.hosted fills 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-weather now floors its core dependency at mostlyrightmd>=1.10.0,<2.0 so mostlyright.weather.cwop’s NoCWOPDataError resolves on a partial upgrade. Previously a stale-core install could ImportError at cwop import time.
  • APRS parser (_aprs.py): numeric fields are only coerced when raw.isascii() and raw.isdigit() (Unicode-digit strings no longer slip through int()); positionless _ weather packets retain their weather payload when the timestamp fails to parse; _closest_to rejects future timestamps and applies a 10-minute skew tolerance.
  • QC (qc/_cwop.py): qc_reliability_score is floored at max(0.05, mean) so a fully-penalized station never reports an exactly-zero (divide-unsafe) reliability.
  • Cache (_cache.py): write_cwop_cache pins an explicit pyarrow schema for byte-stable schema.cwop.v1 round-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 the scan, nearby, snapshot, and stream entry 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.cwop public 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 to scan())
    • 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 once min_reports stations are seen)
    • stream(station, *, qc=True) — async generator yielding QC-scored CWOPObservations (deduped by (station_id, observed_at))
    • snapshot(station, *, duration_seconds=60, qc=True, persist=False) — collect a fixed window into a schema.cwop.v1 DataFrame; persist=True also writes the backtest parquet cache
    • latest(station) — most-recent observation for a single station
    • history(station, from_date, to_date, *, qc_status) — backtest replay from persisted cache, returns schema.cwop.v1 DataFrame
    • persist_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 (honors MOSTLYRIGHT_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 GPLv2 aprslib dependency; pure stdlib (socket + re).
  • NoCWOPDataError — subclass of NoLiveDataError; raised instead of returning []/None.
  • schema.cwop.v1 — new schema, accepts source tags "cwop.live" and "cwop.cache" via _registered_sources. schema.observation.v1 observation_type enum is untouched (parity firewall).
  • [cwop] extra — pulls pandas for snapshot()/history() DataFrames.
  • Parity firewall holds. CWOP is not registered in research(), merge/observations.py (SOURCE_PRIORITY), or live/_sources.py. Models access CWOP only through cwop.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.v1 source SET {noaa_goes, jma_himawari, noaa_viirs, eumetsat_meteosat}, per-source product mini-registries, an extended satellite= 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-25 noaa_goes behavior is byte-unchanged.
    • noaa_goes GOES-West (goes18 operational PACUS, goes17 archive) — config-swap on the existing GOES path, same noaa_goes source 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 anonymous noaa-himawari8/9 buckets. 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 anonymous noaa-nesdis-{snpp,n20,n21}-pds buckets. Global incl. poles, ~2 overpasses/day. Rows carry sat_lon_used=null (no single sub-satellite point) — the row contract was generalized to make sat_lon_used nullable for swath sources.
    • Row-contract generalizationsat_lon_used is now nullable (geostationary sources populate it; the VIIRS polar swath leaves it null), pixel_row/pixel_col documented as native grid OR swath line/sample, and source-generic notes — 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 (integer CFAC/LFAC/COFF/LOFF decode; “Meteosat sweeps y” — latitude drives the line; sub-sat /45.5°E), distinct from GOES scan-angle and Himawari 2D geolocation; sat_lon_used populated. 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 (env EUMETSAT_CONSUMER_KEY/_SECRETeumdac stored creds), one injectable Data-Store client (bearer token via HTTP Basic client_credentials at api.eumetsat.int/token); missing creds raise SourceUnavailableError only when the live path is invoked, with the eumdac set-credentials / env-var hint. Live file-reading is a forward seam: the real Data-Store MSG-CLM product 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_nav reads 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 explicit satellite="meteosat-0deg" still dispatches to the handler; the band auto-routes to the VIIRS fill. _open_dataset opens with mask_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 defaults satellite=/product= to None and 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. Explicit satellite= bypasses routing entirely (Phase-25 parity). Unknown station under auto-routing raises a clear ValueError. The Europe/Africa/Indian-Ocean band was reserved as a clean Meteosat extension point — Wave 6 landed the eumetsat_meteosat handler + MSG-CLM product + 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 explicit satellite="meteosat-0deg" still dispatches to the Meteosat handler. The routing decision is key-free; flip auto_route_target=True when the live reader lands and the band auto-routes to Meteosat with no router change.
  • Packaging. The [satellite] extra now pins h5py>=3.0 (real HDF5 reads) and adds eumdac>=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 — no api.mostlyright.md anywhere), and the EUMETSAT key-setup section (EUMETSAT_CONSUMER_KEY/_SECRET env 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.live test.
  • 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 (both noaa_goes but a different platform — e.g. an East station plus a mid-Pacific one), the collapse warning previously compared only source and 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 eccodes API because cfgrib has no space_view entry in its grid map. auto_route_target is now True, so the Europe/Africa/Indian-Ocean band auto-routes to Meteosat instead of falling back to the VIIRS fill. latlon_to_seviri_pixel is 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 via delivery="hosted" plus MOSTLYRIGHT_WEATHER_HOSTED_URL and MOSTLYRIGHT_API_KEY; hosted rows are byte-identical to the local live path. 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 are MOSTLYRIGHT_WEATHER_HOSTED_URL, MOSTLYRIGHT_FINANCE_HOSTED_URL, and MOSTLYRIGHT_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 (AWS noaa-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 profiles LVMPC / LVTPC.
    • schema.satellite.v1 with source identity "noaa_goes" (shared by live self-extraction and the future paid adapter so a cross-delivery model reconciles), a per-row delivery lineage 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 through KnowledgeView so satellite is leakage-safe in research()/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 --mirror selector, 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 raises SourceUnavailableError with a pip install mostlyrightmd-weather[satellite] hint when it is absent.
  • Dual version bump: PyPI 1.8.0 (mostlyrightmd, mostlyrightmd-weather, mostlyrightmd-markets) and npm vts-1.8.0 (@mostlyrightmd/core, @mostlyrightmd/weather, @mostlyrightmd/markets, mostlyright). This release is Python-only — the TypeScript satellite reader is a deferred parity ticket (future h5wasm impl) per CROSS-SDK-SYNC.md; the npm 1.8.0 carries 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 backfill CLI 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 is python -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.v1 station 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.9692N4078W07397). 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 on forecast_nwp() for GEFS + CFS (#75, closes #74). docs/forecasts.md documented member= for GEFS but the public function never exposed it — the path builders (ge{member} / CFS 6hrly_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: c00 control + p01..p30 perturbations + avg/spr statistical products; CFS: 01..04) with a loud ValueError for 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 and cycle_range_start/cycle_range_end backfills. The output schema is unchanged (no member column — selector only; tracked as future work). TS twin: ForecastNwpOptions gains readonly member?: string (signature-forward; TS NWP execution remains v2.0+).
  • mostlyrightmd[research] extra now pins mostlyrightmd-weather>=1.7.0 (was >=1.6.0): the core forecast_nwp() wrapper threads member= 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 npm vts-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_m for HRRR/GFS (#68, closes #63). Adds three nullable float64 columns to schema.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 .idx records (GFS TCDC:entire atmosphere instantaneous-vs-averaged, and the APCP:surface twin) tripped the GribIntegrityError ambiguity guard on the default forecast_nwp(station, "gfs") path — deterministic record disambiguation now picks the instantaneous record (lowest record_no tiebreak) while keeping the loud-fail guard for genuinely unexpected duplicates. TS twin: schema.forecast_nwp.v1 is now wired into the TS codegen so @mostlyrightmd/core ships a ForecastNwpV1 type + 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 derived issued_at was present (#69, fixes #67). Phase 20+ Open-Meteo rows carry a derived issued_at, so the old issued_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 authoritative source field (open_meteo* → Open-Meteo, else IEM; legacy source-less/issued_at-less rows stay Open-Meteo for backward compatibility). The fix also preserves Open-Meteo pop_6hr_pct / qpf_6hr_in and fcst_issued_at provenance 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.
  • Clarified that research() returns daily rows, not hourly observations (#70, addresses #52). The Returns docstring now states that research() yields one daily settlement-summary row per date (obs_* are settlement-window aggregates), points to weather.obs() for an observation-only daily frame, and notes that sub-daily / raw_metar access is a Sprint 0.5+ item.
  • Dual version bump: PyPI 1.6.0 (mostlyrightmd, mostlyrightmd-weather, mostlyrightmd-markets) and npm vts-1.6.0 (@mostlyrightmd/core, @mostlyrightmd/weather, @mostlyrightmd/markets, mostlyright). The TS twin gains the ForecastNwpV1 type + 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_cover of 12.5), pandas’ safe Int64 cast 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 against schema.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 internal exactStart path issues a single date-bounded request for [from, to+1 day), matching the Python _exact_fetch behavior. The default year-padded path is unchanged for warm_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_RETRIES and MOSTLYRIGHT_HTTP_TIMEOUT let 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_RETRIES requires 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]; npm vts-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 sent start_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 mirrors fetch_open_meteo in both SDKs: for the Single-Runs endpoint send run= 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 omits start_date/end_date + carries run=, and that a multi-day horizon is trimmed to the requested day.
  • 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]; npm vts-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 (KLGA NYC, KORD Chicago, KDAL Dallas, KBKF Denver) + 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 targets EGLC, UUWW, RCSS).
  • mostlyright.stations._POLYMARKET_ICAOS is 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 against HKO — the Observatory, no airport ICAO, v0.2-deferred).
  • 7 station moves (Polymarket only; old ICAOs kept as bare records): Dallas KDFWKDAL, Denver KDENKBKF, London EGLLEGLC, Moscow UUEEUUWW, Taipei RCTPRCSS, Paris default LFPGLFPB, Hong Kong VHHHHKO.
  • Houston cross-venue conflict. Polymarket moved to KHOU (Hobby) while Kalshi stays KIAH (Intercontinental) — a new cross-issuer divergence alongside NYC (KNYC vs KLGA) and Chicago (KMDW vs KORD). Guarded by tests/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 via RCSS (CWA). v0.2 lands the weather.gov.hk + CWA clients.
  • Kalshi roster frozen_KALSHI_ICAOS (21) unchanged; no kalshi tag 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.stations module exposes StationCatalog (CATALOG, Station) with get(), filter_by_venue(), and filter_by_country(). A station is a physical fact; prediction-market participation is a venues tag (frozenset[str]) on StationInfo, 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, not KHOU), KDTW, KCVG, KBNA, KSLC. The four non-settlement US weather stations (KHOU/KMSY/KOKC/KSAT) remain queryable with an empty venues tag.
  • KLAS (Las Vegas, ticker TLV) tagged as a Kalshi settlement station (issue #39). Kalshi trades KXHIGHTLV/KXLOWTLV, settling against the NWS CLILAS report; the station was already in the registry but carried no venues tag.
  • Venue tags follow each issuer’s actual settlement map, not country. Kalshi and Polymarket settle several shared cities against different stations (NYC: KNYC vs KLGA; Chicago: KMDW vs KORD), so filter_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_traded is now derived from venue tags instead of hard-coded True.
  • TradewindsErrorMostlyRightError and TradewindsResultMostlyRightResult across Python and TypeScript — no deprecation alias (pre-1.0). The base error code string changed TRADEWINDS_ERRORMOSTLYRIGHT_ERROR. The TRADEWINDS_CACHE_DIR env-var migration shim is unaffected (separate v0.3-removal track).
  • schemas/stations.json now exports the venues tag 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_f false precision for U.S. ASOS stations (#36, closes #16). GHCNh-sourced observations emitted back-converted temp_f (e.g. 51.08°F for temp_c=10.6°C) via celsius_to_fahrenheit. The raw METAR preserved in the GHCNh PSV REM column carries the T-group remark (e.g. T01060067), marking the row as integer-°F native; temp_f (and dewpoint_f) is now recovered as round(temp_c * 9/5 + 32) — matching the AWC (PREC-01) and IEM (PREC-02) paths. Non-T-group rows (synoptic / international) keep the legacy celsius_to_fahrenheit path. temp_c is unchanged. Closes the Phase 18 GHCNh deferral.
  • Observation cache invalidation for the GHCNh change. Bumped _CACHE_SCHEMA_VERSION (v2-phase18-integer-fv3-ghcnh-integer-f) so existing observation caches re-parse instead of serving stale GHCNh temp_f to upgraded users.
  • Affects mostlyrightmd-weather only. 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/weather openMeteoForecasts() TS API ship in lockstep.
  • schema.forecast.station.v1 unified 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.v1 retained as back-compat alias.
  • research(forecast_source="open_meteo") + list form forecast_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 + OpenMeteoSeamlessLeakageError typed exceptions in both Python (mostlyright.core.exceptions) and TypeScript (@mostlyrightmd/core/exceptions). Payload keys snake_case for cross-SDK MCP parity; both carry origin_issue="Tarabcak/mostlyright#70".
  • ~/.mostlyright/cache/v1/forecasts/{source}/{model}/{station}/{YYYY}/{MM}.parquet cache tier with filelock + 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_day1issued_at = 2024-05-31T18:00Z (provably ≤ the 17:00Z snapshot).
  • docs/forecast-sources.md comparing IEM MOS / NWP gridded / Open-Meteo across models, latency, units, leakage-safety. Includes Python + TS quickstart examples for the new forecast_source="open_meteo" surface.
  • The Open-Meteo Historical Forecast (seamless) endpoint is BANNED for training data. fetch_open_meteo(mode="seamless") raises OpenMeteoSeamlessLeakageError unless allow_leakage=True is passed. Even then, LeakageDetector rejects the rows when as_of is asserted. Origin: Tarabcak/mostlyright#70 — legacy /forecast_series proxied the seamless feed without preserving issued_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") derives issued_at via conservative cycle-math fallback floor_to_cycle(now − publish_lag(model), cycles) per Phase 20 D-06. Open-Meteo’s Metadata API integration (for authoritative last_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).
  • NwpNotAvailableError typed subclass in @mostlyrightmd/core. Raised by forecastNwp() instead of the generic DataAvailabilityError. Carries typed .station and .model properties so consumers get instanceof-based dispatch + IDE autocomplete instead of parsing the hint string. Back-compat preserved — NwpNotAvailableError extends DataAvailabilityError with reason="model_unavailable", so existing catch (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.x forecastNwp() 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 at iemMosForecasts() 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/weather README includes a “What’s NOT in the TypeScript SDK (yet)” section; mostlyright meta-package README ships a decision-matrix table showing which functions are wired today vs deferred to v2.0+.
  • forecastNwp() hint string now points at docs/nwp-forecasts.md instead of the inline docs/forecasts.md#typescript-lane anchor. 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.

  • DataAvailabilityError typed 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 on e.reason instead of string-matching the error message. Subclasses TradewindsError — existing except TradewindsError handlers continue to work. Three Python and four TS raise sites migrated; SourceUnavailableError remains in place for back-compat at other call sites.
  • research() composable kwargs surface — both SDKs (TSPARITY-01). The TS research() 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" raises DataAvailabilityError(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 without include_forecast=True) as TypeError before any network fetch. The TS validator treats null and undefined uniformly as absent so JSON None ↔ null ↔ undefined round-trip matches Python is not None semantics.
  • TS obs(station, from, to, opts?) (TSPARITY-04) — port of Python tw.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 (raises DataAvailabilityError). source="ghcnh" raises DataAvailabilityError (TS GHCNh fetcher port deferred) rather than silently returning [].
  • TS dailyExtremes(station, from, to, opts?) (TSPARITY-05) — fetch+rollup wrapper matching Python mostlyright.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 versionedCacheStore adapter (TSPARITY-03) — wraps any CacheStore so reads with mismatched/missing _cache_schema_version return null (cache miss → re-fetch). Matches Python’s Phase 18 18-08 parquet kv_metadata version stamp ("v2-phase18-integer-f"). Wired into BOTH defaultCacheStore() entries (Node + browser). Pre-bump cached values silently re-fetch on next call.
  • TS mostlyright.preprocessing namespace (TSPARITY-10) — lowercase namespace alias matching Python’s mostlyright.preprocessing. The PascalCase Preprocessing alias is deprecated but kept for back-compat; removal target v2.
  • TS validator schema descriptions via codegen (TSPARITY-08) — ColumnSpec.notes → JSON Schema description → TS interface TSDoc /** ... */ comments. Behavior was already shipped via json-schema-to-typescript; Phase 21 added a regression test (with ts.transpileModule() parse check) to guard against future codegen-pipeline regressions.
  • TS parity gate scaffold (TSPARITY-02) — packages-ts/meta/tests/parity/README.md documents 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) — raises DataAvailabilityError(reason="model_unavailable") with a hint pointing operators at iemMosForecasts() workaround for stations with MOS coverage. New banner in docs/forecasts.md.
  • TS climateGaps() structured error (TSPARITY-11) — raises ClimateGapsNotImplementedError (subclass of DataAvailabilityError) with a hint explaining the server-only architectural constraint. New docs/climate-gaps.md page.
  • 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.
  • SourceUnavailableError migration sites — three Python raise sites moved to DataAvailabilityError(reason="model_unavailable"): obs(strategy="hosted") (was NotImplementedError), catalog.get_adapter(unknown) (was SourceUnavailableError), forecasts.forecast_nwp() ImportError fallback (was SourceUnavailableError).
  • 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.json description, and (newly) keywords arrays 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, and fda to 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, and fred.
  • TS cache returned raw concrete storedefaultCacheStore() in both Node (default.ts) and browser (index.browser.ts) entries now wraps the inner store in versionedCacheStore(CACHE_SCHEMA_VERSION) so pre-Phase-18 ASOS 0.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-local localDate matching Python parity.
  • TS obs(source="ghcnh"|"cli") silent empty rows — pre-fix the type allowed those values but fetchByStrategy had no branch, so the call silently returned []. Iter-1 fix: "cli" removed (not a valid Python source filter); "ghcnh" raises DataAvailabilityError upfront.
  • 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.

Metadata-only republish. No runtime API or source-code behavior changes.

  • 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.

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.

  • 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 mostlyrightmd package description beyond weather-only.
  • npm metadata: add homepage, repository, and issue-tracker fields to all four published TypeScript packages; broaden the @mostlyrightmd/core description 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 — whose temp_f paths 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.py against the new parquets; produced byte-identical TS JSONs since the source parquets didn’t shift.
  • Only artifact change: tests/fixtures/parity/expected_dtypes.json column 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.
  • See .planning/phases/18-precision-fix-asos-integer-fahrenheit/18-09-PARITY-DELTA.md for per-case shasums + why-no-shift explanation.

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 / parseTgroup helper 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_f are emitted as the recovered integer °F (e.g. T02670122temp_f=80.0, NOT 80.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_f stays as raw_tmpf (NOT derived from temp_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.py covering 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: internationalDailyExtremes Phase 18 lattice-rationale comment documenting how callers should pick precision=1 for US-ASOS-lattice data vs precision=0 for international.
  • 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.py carries a Phase 18 boundary-marker comment above the celsius_to_fahrenheit(temp_c) path documenting the deferral pending NCEI native-units documentation.
  • PyPI: mostlyright.international.daily_extremes precision-rationale comment documents how US-ASOS-lattice inputs interact with the precision = 1 if is_us else 0 branch.
  • PyPI: test_international.py synthetic 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.md alert example casts temp_f via int() (Python) / Math.round() (TS) so the rendered example never surfaces back-conversion artifacts. New “Note on temp_f precision (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.py HARD GATE still compares against the pre-Phase-18 baseline; users who rely on byte-equivalent parity vs mostlyright==0.14.1 should 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 CacheEntry consumers requires a separate refactor. TS callers that hit a pre-Phase-18 IndexedDB / FsStore cache may see stale tempF=80.06 values until they invalidate manually.
  • Existing user Python caches auto-invalidate on next research() call via the new _cache_schema_version mechanism — one round of slow first-read, no manual user action required.
  • Two-iteration two-reviewer panel per REVIEW-DISCIPLINE.md. Iter-1: Python Architect PASS; codex high + 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.

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_mos now sends the model query 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/weather iemMosForecasts(...) 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.1 and npm 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 use httpx.MockTransport (Python) and a recording fetchFn (TS) to assert against the wire-level URL.

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)
  • 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.2 to >=1.0.0,<2.0 on 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.0 and pip install mostlyrightmd==0.1.4 produce identical runtime behavior.

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.

  • PyPI: mostlyrightmd, mostlyrightmd-weather, mostlyrightmd-markets bumped 0.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 unscoped mostlyright meta-package shipped 0.1.0 (first non-rc release). All four moved from the @next dist-tag to latest.
  • CHANGELOG header corrected.

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.

  • 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 to mostlyright==0.14.1’s client.pairs() on the captured parity fixtures
  • mostlyright.weather — direct fetchers for AWC, IEM ASOS, IEM CLI, GHCNh, NWS CLI
  • mostlyright.markets.catalog.kalshi_nhigh / kalshi_nlow — Kalshi NHIGH/NLOW contract resolvers
  • mostlyright.markets.polymarket — Polymarket discovery + settlement helpers
  • mostlyright.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
  • PyPI distribution names migrated from mostlyright* to mostlyrightmd*. The Python import path remains import mostlyright. The PyPI install name move avoids version-shadowing the legacy mostlyright==0.14.1 package.
  • npm scope migrated from @mostlyright/* to @mostlyrightmd/*. The unscoped mostlyright meta-package is unchanged.
  • Cache directory renamed from ~/.tradewinds/cache/ to ~/.mostlyright/cache/. Environment variable renamed from TRADEWINDS_CACHE_DIR to MOSTLYRIGHT_CACHE_DIR. Back-compat shim accepts both names during the 0.1.x line; the legacy name will be removed at v0.3.

Workspace + three-package layout. SDK foundations ported from the predecessor codebase (tradewinds*).