Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ repos:
- id: check-toml
- id: check-added-large-files
args: [--maxkb=5000] # parity fixture parquets can be larger than default 500KB
exclude: ^tests/fixtures/parity/pinned_cache\.tar\.xz$ # 5.1MB pinned raw-source bundle (deliberate, documented in fixtures/parity/README.md)

- repo: local
hooks:
Expand Down
96 changes: 96 additions & 0 deletions tests/fixtures/parity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,102 @@
> Do **NOT** regenerate these files in CI. They are settlement-grade
> reference data; the bytes ARE the Day 3 hard-gate ground truth.

## Pinned-input redesign (2026-07-15, operator-directed — release 1.17.0 gate)

The parity gate now runs in TWO variants (see `tests/test_parity.py`):

- **`test_parity_case_pinned` — the HARD gate.** Runs `research()` fully
OFFLINE (socket-level network block) and **CACHE-COLD** over
`pinned_cache.tar.xz` — a committed bundle of the RAW `v1/sources/`
fetch text (IEM CSV / GHCNh PSV / CLI JSON; 82 MB raw → 5.1 MB xz),
captured live on **2026-07-15** by `capture_pinned_cache.py` from main
@ `02580c3`. Cold replay forces the production parsers,
`merge_observations`/`merge_climate` SOURCE_PRIORITY logic, aggregation,
and the join to execute on every gate run (codex PR-124 CRITICAL:
pinning the derived parquet tiers instead would let the warm-cache path
skip all of them). Deterministic forever; lives in the default fast
suite (every CI push), not behind `-m live`. All-5-case replay ≈ 7 s.
- **`test_parity_case_live` — the upstream-drift monitor** (`-m live`,
pre-publish). A failure here while the pinned gate is green means the
mutable IEM/AWC archive drifted since the pin — audit that the diff is
confined to count-consistent `obs_*` sum-aggregates (`cli_*` +
`obs_high_f`/`obs_low_f` MUST stay clean), then deliberately re-pin via
`capture_pinned_cache.py` and extend the audit table below.

**Validation of the approach:** replaying the pinned inputs through current
code reproduces the original v0.14.1 fixtures for cases 1/2/5 EXACTLY
(within the standing ULP `atol`) — the code is byte-faithful to 0.14.1
wherever the archive did not move.

### Drifted-cell re-base audit (2026-07-15)

Between the May-2026 capture and the 2026-07-15 pin, the upstream archive
restated 1 KLAX day + 17 KMIA days (late/corrected METARs — every diff is
count-consistent). The 55 cells below were re-based onto the pinned-archive
values; **every other cell in every fixture remains original 0.14.1
bytes.** Settlement-critical columns (`cli_high_f`/`cli_low_f`/
`cli_report_type`/`obs_high_f`/`obs_low_f`) were untouched on all cases.
The pre-patch parquets are preserved in git history (last at the
`v1.17.0`-candidate merge `02580c3`).

| case | date | column | v0.14.1 fixture value | pinned-archive value |
|------|------|--------|-----------------------|----------------------|
| case-3 | 2025-03-12 | obs_mean_f | 56.739999999999995 | 56.782641509433965 |
| case-3 | 2025-03-12 | obs_mean_dewpoint_f | 52.61076923076923 | 52.61924528301887 |
| case-3 | 2025-03-12 | obs_count | 52 | 53 |
| case-4 | 2025-01-31 | obs_mean_f | 73.7235294117647 | 73.81714285714285 |
| case-4 | 2025-02-07 | obs_mean_f | 74.67870967741935 | 74.846875 |
| case-4 | 2025-02-09 | obs_mean_f | 75.6174193548387 | 75.784375 |
| case-4 | 2025-02-10 | obs_mean_f | 75.41085714285714 | 75.54 |
| case-4 | 2025-02-11 | obs_mean_f | 75.39696969696969 | 75.50235294117647 |
| case-4 | 2025-02-20 | obs_mean_f | 71.244 | 71.3488888888889 |
| case-4 | 2025-03-12 | obs_mean_f | 69.67548387096774 | 69.938125 |
| case-4 | 2025-05-12 | obs_mean_f | 75.60444444444444 | 75.72086956521738 |
| case-4 | 2025-06-02 | obs_mean_f | 77.77771428571428 | 77.64611111111111 |
| case-4 | 2025-06-11 | obs_mean_f | 83.93935483870968 | 84.00424242424242 |
| case-4 | 2025-06-16 | obs_mean_f | 84.48451612903226 | 84.565625 |
| case-4 | 2025-06-21 | obs_mean_f | 84.06484848484848 | 84.15352941176471 |
| case-4 | 2025-09-11 | obs_mean_f | 80.73441860465117 | 80.78454545454545 |
| case-4 | 2025-10-19 | obs_mean_f | 80.77483870967743 | 80.8425 |
| case-4 | 2025-10-31 | obs_mean_f | 69.70967741935483 | 69.841875 |
| case-4 | 2025-11-07 | obs_mean_f | 79.54709677419355 | 79.653125 |
| case-4 | 2025-11-08 | obs_mean_f | 80.47625 | 80.58363636363637 |
| case-4 | 2025-01-31 | obs_mean_dewpoint_f | 66.08764705882353 | 66.08571428571429 |
| case-4 | 2025-02-07 | obs_mean_dewpoint_f | 65.6116129032258 | 65.5625 |
| case-4 | 2025-02-09 | obs_mean_dewpoint_f | 68.03225806451613 | 67.969375 |
| case-4 | 2025-02-10 | obs_mean_dewpoint_f | 67.57771428571428 | 67.55944444444444 |
| case-4 | 2025-02-11 | obs_mean_dewpoint_f | 66.38545454545454 | 66.28470588235294 |
| case-4 | 2025-02-20 | obs_mean_dewpoint_f | 63.15657142857143 | 63.07111111111111 |
| case-4 | 2025-03-12 | obs_mean_dewpoint_f | 53.189677419354844 | 53.2475 |
| case-4 | 2025-05-12 | obs_mean_dewpoint_f | 72.41555555555556 | 72.40565217391304 |
| case-4 | 2025-06-02 | obs_mean_dewpoint_f | 73.2782857142857 | 73.21666666666667 |
| case-4 | 2025-06-11 | obs_mean_dewpoint_f | 73.61419354838709 | 73.60666666666667 |
| case-4 | 2025-06-16 | obs_mean_dewpoint_f | 71.54645161290323 | 71.593125 |
| case-4 | 2025-06-21 | obs_mean_dewpoint_f | 72.7248484848485 | 72.67588235294117 |
| case-4 | 2025-09-11 | obs_mean_dewpoint_f | 75.52139534883722 | 75.465 |
| case-4 | 2025-10-19 | obs_mean_dewpoint_f | 72.29290322580646 | 72.31625 |
| case-4 | 2025-10-31 | obs_mean_dewpoint_f | 57.097419354838706 | 57.095 |
| case-4 | 2025-11-07 | obs_mean_dewpoint_f | 68.94193548387096 | 68.9125 |
| case-4 | 2025-11-08 | obs_mean_dewpoint_f | 72.12125 | 72.05636363636364 |
| case-4 | 2025-09-11 | obs_total_precip_in | 2.08 | 2.1784251968503936 |
| case-4 | 2025-01-31 | obs_count | 34 | 35 |
| case-4 | 2025-02-07 | obs_count | 31 | 32 |
| case-4 | 2025-02-09 | obs_count | 31 | 32 |
| case-4 | 2025-02-10 | obs_count | 35 | 36 |
| case-4 | 2025-02-11 | obs_count | 33 | 34 |
| case-4 | 2025-02-20 | obs_count | 35 | 36 |
| case-4 | 2025-03-12 | obs_count | 31 | 32 |
| case-4 | 2025-05-12 | obs_count | 45 | 46 |
| case-4 | 2025-06-02 | obs_count | 35 | 36 |
| case-4 | 2025-06-11 | obs_count | 31 | 33 |
| case-4 | 2025-06-16 | obs_count | 31 | 32 |
| case-4 | 2025-06-21 | obs_count | 33 | 34 |
| case-4 | 2025-09-11 | obs_count | 43 | 44 |
| case-4 | 2025-10-19 | obs_count | 31 | 32 |
| case-4 | 2025-10-31 | obs_count | 31 | 32 |
| case-4 | 2025-11-07 | obs_count | 31 | 32 |
| case-4 | 2025-11-08 | obs_count | 32 | 33 |

## Case-4 re-baseline (2026-07-09, operator-approved — release 1.13.0 gate)

`case_4_KMIA_2024-12-01_2025-11-30.parquet` (and its derived
Expand Down
87 changes: 87 additions & 0 deletions tests/fixtures/parity/capture_pinned_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Capture the PINNED-INPUT cache bundle for the parity gate.

Fetches the 5 parity windows live via ``mostlyright.research()`` into a
staging ``MOSTLYRIGHT_CACHE_DIR``, strips the cache to the RAW source tier
(``v1/sources/`` — the fetched IEM CSV / GHCNh PSV / CLI JSON text), and
writes it compressed to ``tests/fixtures/parity/pinned_cache.tar.xz``.

RAW sources, deliberately NOT the derived parquet tiers (codex PR-124
CRITICAL): the offline gate replays CACHE-COLD from the raw text, so the
production parsers (``_fetch_iem_month``/GHCNh/CLI), ``merge_observations``
/ ``merge_climate`` SOURCE_PRIORITY logic, aggregation, and the join all
execute under the network block on every run — a parser or merge
regression fails the gate. Pinning derived parquets instead would let the
warm-cache path skip all of that. (82 MB raw → 5.1 MB xz; cold replay of
all 5 cases ≈ 5 s.)

Why this exists (2026-07-15): the live parity gate compared "today's code +
today's IEM/AWC archive" against fixtures frozen from ``mostlyright==0.14.1``
in May 2026. The upstream archive is MUTABLE — late/corrected METARs shift
the sum-then-divide ``obs_*`` aggregates (KLAX 2025-03, KMIA 2024-12..2025-11
drifted) — and the 0.14.1 oracle can never be re-captured
(api.mostlyright.md was re-pointed in Phase 28). Pinning the INPUTS converts
the gate into a deterministic test of OUR aggregation/join code against the
0.14.1 outputs, which is what it was always meant to prove.

Run (live network, ~50 min — KMIA is a year window)::

uv run python tests/fixtures/parity/capture_pinned_cache.py

Re-running OVERWRITES the pinned bundle from today's archive — only do this
deliberately (it re-bases the input ground truth) and re-run the pinned gate
plus the drifted-cell audit in README.md afterwards.
"""

from __future__ import annotations

import os
import shutil
import sys
import tarfile
import tempfile
import time
from pathlib import Path

FIXTURES = Path(__file__).resolve().parent
PINNED_TARBALL = FIXTURES / "pinned_cache.tar.xz"

CASES: list[tuple[int, str, str, str]] = [
(1, "KNYC", "2025-01-06", "2025-01-12"),
(2, "KMDW", "2025-04-01", "2025-04-30"),
(3, "KLAX", "2025-03-01", "2025-03-31"),
(4, "KMIA", "2024-12-01", "2025-11-30"),
(5, "KMSY", "2024-09-08", "2024-09-22"),
]

#: The RAW source tier the cache-cold offline replay parses. Derived tiers
#: (``v1/observations``/``v1/climate``) and ``provenance/`` are deliberately
#: excluded — they are rebuilt from raw text at test time so the production
#: parse+merge+aggregate code is exercised on every gate run.
_KEEP_TIER = "v1/sources"


def main() -> int:
staging = Path(tempfile.mkdtemp(prefix="mr-parity-pin-"))
os.environ["MOSTLYRIGHT_CACHE_DIR"] = str(staging)
import mostlyright

for num, station, frm, to in CASES:
t0 = time.time()
df = mostlyright.research(station, frm, to, label="cli")
print(f"case-{num} {station} {frm}..{to}: {len(df)} rows in {time.time() - t0:.0f}s")

src = staging / _KEEP_TIER
if not src.is_dir():
raise RuntimeError(f"expected raw-source cache tier missing: {src}")
files = [f for f in sorted(src.rglob("*")) if f.is_file()]
with tarfile.open(PINNED_TARBALL, "w:xz") as tar:
for f in files:
tar.add(f, arcname=str(f.relative_to(staging)))
size_mb = PINNED_TARBALL.stat().st_size / 1e6
print(f"pinned {len(files)} raw-source files -> {PINNED_TARBALL} ({size_mb:.1f} MB)")
shutil.rmtree(staging)
return 0


if __name__ == "__main__":
sys.exit(main())
Binary file modified tests/fixtures/parity/case_3_KLAX_2025-03-01_2025-03-31.parquet
Binary file not shown.
Binary file modified tests/fixtures/parity/case_4_KMIA_2024-12-01_2025-11-30.parquet
Binary file not shown.
Binary file added tests/fixtures/parity/pinned_cache.tar.xz
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,13 @@
"fcst_pop_6hr_pct": null,
"fcst_qpf_6hr_in": null,
"market_close_utc": "2025-03-13T00:30:00Z",
"obs_count": 52,
"obs_count": 53,
"obs_high_f": 60.08,
"obs_low_f": 53.0,
"obs_max_gust_kt": 31.0,
"obs_max_wind_kt": 17,
"obs_mean_dewpoint_f": 52.61076923076923,
"obs_mean_f": 56.739999999999995,
"obs_mean_dewpoint_f": 52.61924528301887,
"obs_mean_f": 56.782641509433965,
"obs_total_precip_in": 0.15000000000000002,
"station": "LAX"
},
Expand Down
Loading
Loading