Skip to content

Phase 30: observation-API unification — dataset(), climate(), observation grain, D-16 covariates, 1.13.0#112

Merged
helloiamvu merged 54 commits into
mainfrom
phase30/api-unification-one-mode
Jul 9, 2026
Merged

Phase 30: observation-API unification — dataset(), climate(), observation grain, D-16 covariates, 1.13.0#112
helloiamvu merged 54 commits into
mainfrom
phase30/api-unification-one-mode

Conversation

@helloiamvu

Copy link
Copy Markdown
Member

What this ships (dual-SDK, releases as 1.13.0)

  • research()dataset() rename, alias-first (no warning this release; identical callable, parity-locked dataset()==research() with check_dtype=True).
  • obs(granularity="observation") — merged per-report rows (METAR+SPECI), window-trimmed, new schema.observation.merged.v1 with merged.live_v1 frame identity that pinned schemas loudly reject; retrieved_at stamped; strategy= demoted to escape hatch.
  • NEW climate() standalone NWS-CLI settlement-label table (Python + TS @mostlyrightmd/weather/climate subpath), join-vocabulary columns, NWS↔ICAO registry resolution matched cross-SDK.
  • D-16 covariate composition: dataset(include_satellite=, include_cwop=) LEFT-joins per-LST-settlement-day sat_*/cwop_* columns. Labels byte-identical flags on/off (test-enforced); four parity-firewall files zero-diff (grep-gated); extras fail loud; CWOP QC-clean only with ledger left-edge.
  • Deprecation train: mode2/research_by_source/as_dataframe warn (behavior unchanged, ≥2-minor windows); output kwargs unified on backend+return_type.
  • Docs: NEW docs/source-identity.md (cross-vertical kwarg contract), Mode-1/2 vocabulary purged, docs/migration-1.13.md (headline: migrating pinned features = retrain event).
  • TS parity same-phase: dataset alias, obs granularity, MODE2_* alias train, size gate (weather budget 26→27 KB, its own commit).

Review loop (all P1/P2 clean)

4-reviewer panel (deep reviewer + Codex gpt-5.5 high ×3 rounds + Python Architect + TS Architect). 9 findings fixed & Codex-verified: TS climate NWS↔ICAO parity, STATION_CODE_RE guard ported to ALL TS network surfaces (incl. satelliteHosted), pinned-awc genuinely fail-loud (fetcher-level failLoud), retrieved_at stamping, sat_scan_count contributing-scans-only, firewall grep coverage. Architects: Python + TS slices CLEAN. Full report: .planning/phases/30-.../30-REVIEW.md.

Release verification (30-06)

Live parity 5/5 byte-green after operator-approved case-4 re-baseline (v0.14.1 data plane decommissioned by Phase 28 — fresh captures impossible; 3/365 days upstream mean restatement, obs_count identical, settlement labels + extremes byte-identical across all 365 days; prior fixture preserved at e6a0ca1). Fast suite, hosted grep-gate, firewall zero-diff, TS build/typecheck/suites/size all green pre- and post-bump. uv build ×3 → wheel-install __version__ == 1.13.0 verified. Includes #111's earnings test-isolation fix via main merge.

Live-trading guarantee: default paths byte-identical to 1.12.1; only additive kwargs + DeprecationWarnings (see migration guide's -W error callout).

After merge: tag v1.13.0 + vts-1.13.0 on the merge SHA (operator-gated) → PyPI ×3 + npm ×4.

🤖 Generated with Claude Code

helloiamvu and others added 30 commits July 8, 2026 22:54
…nal._window

- add mostlyright._internal._window.in_query_window (byte-identical semantics)
- mode2._in_query_window becomes a re-export alias (call sites unchanged)
- add 'list' to _SUPPORTED_RETURN_TYPES + ReturnTypeT
- wrap_result emits list[dict] for return_type='list'
- keep polars+{dataframe,list} incoherent-pair ValueError guard
- new frame identity tag merged.live_v1 accepted only by this schema
- plural _registered_sources only (no singular -> no source_drift audit trap)
- new ClassVar _registered_row_sources={awc,iem,ghcnh} convention
- COLUMNS declared over actual observation-grain parser row keys
- observation.py untouched (CWOP parity firewall / D-15)
…d_row_sources

- step-1b reads schema._registered_row_sources; when set, validates per-row
  source as MEMBERSHIP in that set (no nulls) instead of equality-to-attrs
- None (every legacy schema) keeps the equality check byte-unchanged
- merged.live_v1 frame with mixed {awc,iem,ghcnh} rows passes merged schema,
  still rejected by observation.v1 (CWOP firewall intact)
- add test_merged_observation_schema.py
- add granularity: Literal['daily','observation']='daily' (kw-only, validated)
- observation grain: trim merged raw_rows at list level via in_query_window,
  preserve bare per-row source, surface observation_type + raw_metar, NO bucketing
- coerce numeric cols to float64 so short-window all-None cols pass validator
- stamp df.attrs['source']=merged.live_v1 (unpinned) or bare source (pinned)
- daily branch + _aggregate_daily_rows byte-unchanged (D-15)
- add tests/weather/test_obs_observation_grain.py (path follows repo convention)
…/2 vocab

- module + obs() docstrings: strategy= is an advanced escape hatch (auto
  self-selects exact_window for pinned queries); delete stale NotImplementedError
  / PLAN-07-04 claims; document new granularity kwarg
- reword 'Mode 1'/'Mode-1' comments to 'default fused-source aggregates'
- routing logic UNCHANGED; warm_cache+source ValueError guard intact (D-07)
- wraps research._fetch_climate_range (annual cache + frozen merge_climate)
- window-trims year-granular fetch via shared in_query_window (no whole-year leak)
- public JOIN vocab: date, station, cli_high_f, cli_low_f, cli_report_type + source, issued_at
- frame identity df.attrs source=cli.archive; per-row source=iem (endpoint)
- source accept-set {None,iem,cli,cli.archive}; acis raises ValueError
- pandas lazy-imported inside the return path
- add climate import + "climate" to __all__ alongside obs
… one body

- dataset() is now the canonical def in research.py (single ~450-line body)
- research = dataset plain forwarding binding, NO DeprecationWarning this release (D-02)
- export both dataset and research from mostlyright.__init__ __all__
- positional signature + default-path aggregation unchanged (parity, D-15)
- mostlyright.core eagerly loads the pandas-backed validator, so importing
  _backend_dispatch at module top transitively pulled pandas
- move validate_backend_kwargs to a lazy in-function import; keep BackendT/
  ReturnTypeT under TYPE_CHECKING (annotations are strings via __future__)
- satisfies acceptance: 'from mostlyright.weather import climate' needs no
  pandas at module scope (pandas only imported when called)
…d re-export

- new core/source_identity.py is the canonical home (D-04)
- re-exported from mostlyright.core.__init__ __all__
- mode2 back-imports it (core defines, mode2 imports — no cycle)
- scrub 'Mode 2 dispatch requested' -> source-pinned neutral wording (D-11)
- add role= kwarg (default 'observations', backward-compatible)
…ence

- pytest.importorskip('pandas') guard for no-extras collection
- window-trim drops out-of-window year rows (whole-year-leak guard, 40ef469)
- frame/row source split: attrs cli.archive, per-row iem
- source=None == source=iem; acis raises ValueError
- return_type=list -> list[dict]; polars+dataframe raises ValueError
- byte-equivalence vs the build_pairs_row cli_* projection oracle
…t path

- call-time DeprecationWarning on research_by_source (every call, stacklevel=2)
- module __getattr__ once-per-session import-path warning (set guard)
- both warnings name obs(source=..., granularity="observation") + retrain note (D-04/D-12)
- KEEP post-merge body byte-unchanged (zero silent data change)
- purge Mode 1/Mode 2 vocab from mode2 docstrings/comments (D-11)
- runtime "Mode 2 source must be one of" string deferred to 30-03-5
…_dataframe

- new shared resolve_return_type + AS_DATAFRAME_DEPRECATION in _backend_dispatch (D-06)
- add backend/return_type to obs(); route via validate_backend_kwargs
- dataset()/research(), research_by_source(), obs(): warn on explicit as_dataframe
- map as_dataframe=False->return_type=list, True->dataframe; explicit return_type wins
- as_dataframe default -> None sentinel; no-arg call byte-stable per function
- daily_extremes docstring documents backend/return_type (already on the pair)
- update test_obs_surface signature assertion for sentinel + new kwargs (lockstep)
… runtime strings

- research.py Axis-B docstrings: Mode 1->IEM MOS, Mode 2->Per-NWP-model (NWP)
- research.py comments/source docstring -> source-blind/source-pinned language
- source= docstring now points at obs(source=..., granularity="observation")
- mode2.py runtime "Mode 2 source must be one of" -> "source must be one of"
- update test_mode2 match string in lockstep
- NOTE: stale plan line refs (1718-1720 NotImplementedError w/ Mode vocab) did not
  exist in live 1.12.1 code; TS mirror at mode2.test.ts:151 is 30-04 scope
- test_dataset_alias.py: dataset is research (one body), both exported, research
  warning-free this release
- test_mode2_deprecation.py: research_by_source call-time + mode2 import-path each
  warn exactly once naming obs(source=..., granularity="observation"); output
  unchanged; as_dataframe=False maps to list
- test_parity.py: dataset()==research() assertion in the live parity case +
  network-free identity lock in the fast suite
- ruff format normalization of line-wrapping in mode2/research/obs + new test
- dataset is the primary documented name (D-02); research retained as a
  working alias (same callable, export { research as dataset })
- no DeprecationWarning on research this release, mirroring Python 30-03
- built barrel exports both dataset and research; typecheck green
… identity

- ObsOptions.granularity?: 'daily'|'observation', default 'observation'
  (D-03; preserves byte-stable per-report TS behavior — D-12)
- granularity='daily' throws documented DataAvailabilityError (not yet
  ported in TS; CROSS-SDK-SYNC ticket) — never silently returns per-report
- obs() return stamps non-enumerable frameSource: 'merged.live_v1' (unpinned)
  / bare source (pinned), mirroring Python df.attrs['source']
- resolveAutoStrategy(source) routes pinned queries to exact_window (D-07)
- weather bundle 25.99 KB (within 26 KB budget)
- new climate(station, start, end, opts?) mirrors Python climate() (30-02):
  composes downloadCliRange/parseCliResponse/mergeClimate, window-trims to
  [start,end], projects JOIN vocab (cli_high_f/cli_low_f/cli_report_type +
  date/station/source/issued_at)
- frame identity 'cli.archive' (product) via non-enumerable frameSource;
  per-row source 'iem' (endpoint) — mirrors 30-02 frame-vs-row split
- source accept-set {null,'iem','cli','cli.archive'}; 'acis' throws
- surfaced via @mostlyrightmd/weather/climate subpath (NOT root barrel) to
  stay within the 26 KB TS-BUNDLE-01 budget (barrel would exceed by 274 B);
  mirrors live/forecasts/hosted lean-subpath pattern
- browser-safe fetch+JSON, no Node APIs; weather barrel 25.96 KB
… purge

- rename-with-alias train (D-11/D-02/D-04): SOURCE_PINNED_SOURCES /
  SourcePinnedSource / isSourcePinnedSource are the neutral names;
  MODE2_SOURCES / Mode2Source / isMode2Source kept as deprecated aliases
  (same values) — no consumer break, removal at 2.0
- return_type accepts 'dataframe' (canonical, D-06) + keeps 'frame' alias
  + 'wrapper' no-op; both type-check and validate
- runtime string purge (D-11): 'Mode 2 source must be one of' ->
  'source must be one of'; 'Mode 2 dispatch requested' ->
  'source-pinned dispatch requested' (lockstep with Python 30-03-2/30-03-5)
- 'Mode 1/Mode 2' vocab scrubbed from mode2.ts docstrings/comments
- mode2.test.ts:150 assertion updated in lockstep; barrel exports new names
- obs.observation.test.ts (8 tests): no-option default == observation grain
  byte-stable (D-12); window-trim; frameSource merged.live_v1/pinned +
  non-enumerable; granularity='daily' throws documented not-yet-ported error
- climate.test.ts (11 tests): JOIN-vocab column contract; window-trim; frame
  'cli.archive' vs per-row 'iem'; source accept-set + 'acis' throw; input guards
- full TS suite green (core 806, weather 421, markets 248, meta 163);
  pnpm -r typecheck green; pnpm run size within budgets (weather 25.9/26,
  meta 38.86/40)
- index.ts:32 comment 'Mode 2 source-explicit dispatch' -> 'source-pinned
  dispatch' + Mode2Source -> SourcePinnedSource (D-11 vocab purge continuation
  in a plan-touched file)
- add include_satellite/include_cwop keyword-only flags (default False)
- new _covariates.py: post-aggregation LEFT-join seam (lives OUTSIDE _internal/merge/)
- label columns byte-identical on == off; missing days NaN, never dropped/added
- lazy per-vertical imports inside enabled branches (flags-off pays zero import cost)
- prefix + collision guards; additive attrs["covariates"] provenance
- guard: covariates require the DataFrame return path (fail loud on return_type="list")

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…task 2)

- [satellite]-gated helper: reuses the existing extractor + cache (no new transport)
- buckets pixels by settlement_date_for (LST settlement day), NEVER UTC-day
- sat_<variable>_mean + sat_scan_count columns; missing days absent -> join NaN
- imported lazily from the core seam; extractor raises SourceUnavailableError
  naming [satellite] when the heavy deps are absent (fail-loud contract)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- [cwop]-gated, READ-ONLY, socket-free: registry.load_stations() radius filter
  for ICAO->PWS mapping (never scan()/nearby() which open an APRS-IS socket)
- QC-clean rows only (qc_status="clean", combined > 0.7) via cwop.history()
- buckets by settlement_date_for (LST settlement day), never UTC-day
- ledger left-edge (D-10): pre-capture-start days absent -> join NaN, never thinned
- cwop_temp_high_qc/low_qc + clean station/obs counts; fail-loud [cwop] extra guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k 4)

- test_dataset_covariates.py (skip-guards extras, no network): label byte-equiv
  flags on/off, missing-extra typed error, NaN-not-dropped, LST-day bucketing
  (non-UTC KLAX), QC-clean-only cwop, read-only-registry assertion, firewall
  grep gate (merge/observations, live/_sources, observation enum carry no
  cwop/satellite), flags-off import-cost exclusion
- CLAUDE.md CWOP section: labels-only firewall wording (four-file list verbatim),
  D-16 include_cwop covariate-columns rule replacing the blanket no-include claim
- docs/satellite.md + docs/cwop-adapter.md: D-16 cross-ref stubs (full docs 30-05)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NEW single contract page (no modes explainer, D-11)
- source= provenance / delivery= local|hosted / vertical-defined grain (D-08)
- real-time single-provider rule (D-09)
- two archive classes per FEED LEG, not per vertical (D-10 refined)
- frame-vs-row identity + merged.live_v1 / cli.archive contract
- two-layer model + labels-only firewall + D-16 covariate composition
- backend+return_type output convention (as_dataframe deprecated, D-06)
- train/trade symmetry example
- earnings coordination-only note naming the BACKLOG follow-up (D-14 half a)
helloiamvu and others added 22 commits July 9, 2026 01:41
- swap mode2.research_by_source for obs(source=, granularity="observation")
- delete Mode 1/Mode 2 wording; note pinned features = retrain event
- prefer dataset() (research() still works); link docs/source-identity.md
…tart

- ingest-strategies: 'research() Mode-1 obs aggregates' -> 'dataset()/research()
  default-path (fused-source) obs aggregates'; add strategy= demotion +
  granularity="observation" callout
- ts-quickstart: 'Mode 1 parity' heading -> 'default-path parity'
- forecasts.md: 'Mode 1: IEM MOS' -> 'IEM MOS'; 'Mode 2: ...NWP' -> 'per-model
  NWP'; prose 'Mode 1/Mode 2' -> 'IEM MOS surface / per-NWP-model surface';
  H3 'Settlement-day envelope (Mode 2)' -> '(NWP)'
- nwp-forecasts.md cross-ref: 'Mode 1/Mode 2' -> 'IEM MOS / per-NWP-model'
- semantic content preserved (rename, not delete) per D-11
- cwop-adapter: refine 'never feeds research()' firewall box to labels-only
  (never a source / label input; composes as covariate columns or hand-join);
  upgrade D-16 stub to full docs; add domain-table list incl. climate();
  cross-ref source-identity.md; refine two stale 'no research() integration' lines
- satellite: upgrade D-16 stub to full covariate docs; add domain-table list
  incl. climate(); cross-ref source-identity.md in See also
…scans

Move the counts[settle_day] increment AFTER the None/NaN/non-numeric
guards so sat_scan_count tallies only scans that contribute a usable
value. A settlement day whose scans are all fill-value/NaN now produces
NO row (dropped to NaN after the covariate LEFT join) instead of a
misleading sat_scan_count-only row that a coverage-gating trading model
would read as 'covered' while it carries zero real signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parity-firewall regression guard parametrized only observations.py
and live/_sources.py; _internal/merge/climate.py (the frozen climate
dedup, a named firewall file) was never grep-asserted free of
cwop/satellite registration. Add it to the parametrized rel_path list so
a future covariate-source edit to the climate merge is caught.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The granularity='observation' producer stamped only df.attrs['source'],
but schema.observation.merged.v1's validator contract requires
df.attrs['retrieved_at'] (core/validator.py:491-509). Stamp
datetime.now(UTC) alongside the source stamp under the same best-effort
contextlib.suppress pattern so the frame validates AS PRODUCED. Remove
the manual attr injection from the test and assert the attr is present +
tz-aware on the frame straight from obs(). Daily/dataset path untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Docstring-only, no behavior change: (a) document that compose_covariates
takes ownership of and mutates 'result' in place, so callers must pass a
frame they own; (b) document that attrs['covariates'] provenance survives
only the pandas return_type='dataframe' path (dropped by the
backend/wrapper conversion).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…parity

TS climate() uppercased the input and passed it verbatim to
downloadCliRange, projecting the parser's station_code. Python resolves
the input through the station registry, fetches by info.icao, and projects
info.code (the 3-letter NWS code) into every row's station column.

Add a shared weather-internal station module (src/_internal/stations.ts)
with resolveStation (mirrors the mode2.ts helper + Python _resolve_station):
accept ICAO or NWS code, fetch by ICAO, output station = NWS code. Unknown
station → typed ValidationError before any fetch. climate() now resolves
via it. The module also exposes assertStationFormat (STATION_CODE_RE
guard) reused by FIX-2. Tests cover NWS-code input, ICAO input, the
station-column value, and unknown/malformed station (no fetch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…patch

Python treats STATION_CODE_RE (\A[A-Z]{3,4}\Z, after trim+uppercase) as a
URL-security boundary; TS had no equivalent, so obs('../../etc/passwd')
would build a fetch task with the raw id. Apply assertStationFormat (the
shared guard from src/_internal/stations.ts) EAGERLY at the top of obs()
— before any fetch task is created — so a malformed station throws a typed
ValidationError with zero network dispatch. climate() gets the guard
implicitly via FIX-1's resolveStation. Tests assert no fetch occurs for
path-traversal and empty/whitespace station input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fetchByStrategy pushed fetchAwcForWindow(...).catch(() => []) even when
the caller pinned source='awc', so a network failure silently returned
[] — the opposite of the fail-loud contract obs() applies to unwired
ghcnh. Keep .catch(() => []) ONLY for the unpinned merge path; for a
pinned awc query, wrap the fetch error in a typed DataAvailabilityError
(reason='source_5xx') so the pinned caller learns the fetch failed. Tests:
pinned awc + rejection → typed error; unpinned + AWC rejection → still
returns IEM-only rows silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inned-awc fail-loud)

The weather barrel had 40 B of headroom (25.96/26 KB). CR-01's registry
resolution + Codex-P1's STATION_CODE_RE guard (shared src/_internal/
stations.ts) plus WR-06's pinned-awc fail-loud branch push the gzipped
ESM to 26.02 KB. After aggressive trimming (compact error strings, map
lookup, dropped param defaults) it remains ~17 B over 26 KB. Both are
P1/P2 correctness+security fixes, so bump the budget rather than drop
them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etwork surfaces

Round 1 added assertStationFormat to obs() + climate() only. Codex verified
three more exported surfaces reached the network without a format guard:

- live latest() default AWC path (fetchAwcLatest -> normalizeStation only
  trim/uppercase/K-prefix; the IEM path already guarded its stripped code)
- dailyExtremes() (AWC leg fetchAwcMetars([station]) uses encodeURIComponent
  only; the IEM leg validated but AWC did not)
- iemMosForecasts() (encodeURIComponent only into the MOS URL)

Each now calls assertStationFormat(station) at the public entry, before any
fetch task is created, so a malformed id throws a typed ValidationError and
never reaches the network. Grep sweep for other station-string URL
interpolation surfaced no additional weather cases (earnings hosted URLs
interpolate ticker/callId/token, not stations). Tests per surface assert
malformed input throws before fetch (spy not called).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The real AWC fetcher (awc.ts) swallows every non-OK/network failure into []
(graceful-degradation contract), so round-1's task-level .catch wrap on the
pinned source='awc' path NEVER fired in the real failure mode — a pinned-awc
fetch failure silently returned an empty result instead of failing loud.

Fetcher: add an opt-in { failLoud: true } flag. In fail-loud mode a genuine
FAILURE (non-OK HTTP after retries, or a network error) THROWS a typed
AwcFetchError (carrying the underlying error as cause) instead of returning [].
An empty-but-SUCCESSFUL response (HTTP 200 zero rows / non-array body) stays []
in BOTH modes — that is data truth, not a failure. All tolerant callers
(unpinned merge, latest(), dailyExtremes()) keep the swallow-into-[] behavior.

obs.ts: the pinned source='awc' path now calls the fetcher with failLoud=true
and wraps the real AwcFetchError as DataAvailabilityError(reason=source_5xx);
the unpinned path stays tolerant.

Tests: rewritten to exercise the REAL mode via a global-fetch mock (round-1
mocked fetchAwcMetars to reject, which never happened in production). Fetcher
unit tests cover 5xx/400/network → throw, and 200-empty/non-array → []. obs()
tests cover pinned+400 → DataAvailabilityError, unpinned+400 → IEM-only rows,
pinned+200-empty → empty result (no throw). A beforeEach mock-restore was added
to the obs fail-loud block because earlier describe blocks leak fetchAwcMetars
spies (vitest config sets no restoreMocks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
index.ts:207 still said climate() is omitted from the root barrel because it
exceeds the '26 KB' budget ('+274 B over'). The weather budget is now 27 KB, so
climate() would fit. Update the comment: the subpath decision stands, but for
budget HEADROOM + tree-shaking (root-barrel consumers should not pay for
climate()'s CLI parse/merge tree unless they import it), not because it no
longer fits. Comment-only change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…plane decommissioned; labels/extremes byte-identical, 3-day upstream mean restatement documented

- case_4 parquet re-baselined from fresh branch capture 2026-07-09
  (dataset() default path, isolated cache, pandas 2.3.3): the v0.14.1
  hosted data plane at api.mostlyright.md is decommissioned (routes 404;
  0.14.1 client raises NotFoundError), so the original fresh-vs-fresh
  re-capture is permanently impossible. Prior v0.14.1 fixture preserved
  at git e6a0ca1.
- Drift vs old fixture: 3/365 days restated upstream (obs_mean_f,
  obs_mean_dewpoint_f only; obs_count identical; cli_*/obs_high/low
  byte-identical) + sub-1e-12 ULP accumulation-order repr in the three
  sum-aggregate columns.
- ts/case_4 json + manifest regenerated via export_for_ts.py
  (cases 1/2/3/5 byte-unchanged); provenance section added to README.
- Non-live gates green post-swap: test_parity + test_parity_ts_export
  (8 passed, incl. manifest sha256 + determinism).
- 3 pyproject.toml + 4 package.json: 1.12.1 -> 1.13.0 (same SHA lockstep)
- 4 TS 'export const version' literals fixed: 0.0.0 -> 1.13.0 (were stale;
  now on the release checklist per 30-06 plan)
- hello.test.ts version assertions updated in lockstep (codegen stays 0.0.0,
  non-published)
- root workspace pyproject + root/codegen package.json left at 0.0.0
- inter-package >= pins untouched (minor bump per RELEASE-RUNBOOK)
- Python __version__ verified metadata-derived: core/weather/markets all
  report 1.13.0 from dist metadata, zero literal edits
- dry version-guards green: release-ts-preflight (vts-1.13.0, temp copy)
  + release.yml PEP440 guard logic (v1.13.0)
… notes

- dataset() rename + research alias; obs(granularity=observation) +
  merged.live_v1; NEW climate() (Python top-level, TS /climate subpath);
  D-16 covariate composition with labels-only firewall + retrain-event
  pointer; output-knob unification; mode-vocab retirement
- Deprecated section: research_by_source (with the NOT-drop-in coverage
  caveat), mode2 import path, as_dataframe, TS MODE2_* aliases; -W error
  callout
- Fixed section: TS station-format guard (incl. satelliteHosted),
  pinned-awc fail-loud, case-4 fixture re-baseline + v0.14.1-plane
  decommission fact
- [Unreleased] kept empty per pre-release gate
- Headline: migrating pinned features = retrain event, not find-replace
  (pre-merge full-coverage obs(source=, granularity=observation) vs
  research_by_source post-merge composition difference, D-12)
- Compatibility table: byte-stable / warns-identical / new-optional
- -W error production callout with targeted allowlist example
- TS notes: dataset alias, granularity default divergence, /climate
  subpath, MODE2_* aliases, return_type dataframe/frame
- Deprecation timeline (>=2 minors, removals at 2.0); links
  docs/source-identity.md
pnpm-lock.yaml requires no change: inter-package deps use the workspace
protocol and no external dependency changed (verified via
pnpm install --lockfile-only — zero diff).
@helloiamvu helloiamvu requested a review from Tarabcak July 9, 2026 07:30
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Docs-required check: PASS

API-surface change includes docs updates — no reminder needed.

API-surface files changed:

packages-ts/core/src/index.ts
packages-ts/markets/src/index.ts
packages-ts/meta/src/index.ts
packages-ts/meta/src/mode2.ts
packages-ts/meta/src/research.types.ts
packages-ts/weather/src/_fetchers/awc.ts
packages-ts/weather/src/climate.ts
packages-ts/weather/src/climate.types.ts
packages-ts/weather/src/dailyExtremes.ts
packages-ts/weather/src/forecasts/iem-mos.ts
packages-ts/weather/src/hosted/satellite.ts
packages-ts/weather/src/index.ts
packages-ts/weather/src/live/latest.ts
packages-ts/weather/src/obs.ts
packages-ts/weather/src/obs.types.ts
packages/core/src/mostlyright/__init__.py
packages/core/src/mostlyright/_covariates.py
packages/core/src/mostlyright/core/__init__.py
packages/core/src/mostlyright/core/_backend_dispatch.py
packages/core/src/mostlyright/core/schemas/__init__.py
packages/core/src/mostlyright/core/schemas/observation_merged.py
packages/core/src/mostlyright/core/source_identity.py
packages/core/src/mostlyright/core/validator.py
packages/core/src/mostlyright/international.py
packages/core/src/mostlyright/mode2.py
packages/core/src/mostlyright/research.py
packages/weather/src/mostlyright/weather/__init__.py
packages/weather/src/mostlyright/weather/climate.py
packages/weather/src/mostlyright/weather/cwop/_covariates.py
packages/weather/src/mostlyright/weather/obs.py
packages/weather/src/mostlyright/weather/satellite/_covariates.py

Docs files changed:

CHANGELOG.md
CLAUDE.md
README.md
docs/cwop-adapter.md
docs/forecasts.md
docs/ingest-strategies.md
docs/migration-1.13.md
docs/nwp-forecasts.md
docs/satellite.md
docs/source-identity.md
docs/ts-quickstart.md
tests/fixtures/parity/README.md

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Parity ticket gate: PASSED

parity-ticket-check: PR touches BOTH Python and TypeScript trigger surfaces — paired-language change satisfies the gate.

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

…e turns None into pd.NA, bypassing 'is None' (satellite variable leak, cwop observed_at/sid hazard)
@helloiamvu helloiamvu merged commit e3a17ba into main Jul 9, 2026
21 checks passed
@helloiamvu helloiamvu deleted the phase30/api-unification-one-mode branch July 9, 2026 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant