@mostlyrightmd/core 0.1.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/discovery/index.cjs +1646 -0
  4. package/dist/discovery/index.cjs.map +1 -0
  5. package/dist/discovery/index.d.cts +313 -0
  6. package/dist/discovery/index.d.ts +313 -0
  7. package/dist/discovery/index.mjs +1609 -0
  8. package/dist/discovery/index.mjs.map +1 -0
  9. package/dist/formats/index.cjs +498 -0
  10. package/dist/formats/index.cjs.map +1 -0
  11. package/dist/formats/index.d.cts +97 -0
  12. package/dist/formats/index.d.ts +97 -0
  13. package/dist/formats/index.mjs +465 -0
  14. package/dist/formats/index.mjs.map +1 -0
  15. package/dist/index.cjs +1624 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +559 -0
  18. package/dist/index.d.ts +559 -0
  19. package/dist/index.global.js +1582 -0
  20. package/dist/index.global.js.map +1 -0
  21. package/dist/index.mjs +1557 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/internal/bounds.cjs +125 -0
  24. package/dist/internal/bounds.cjs.map +1 -0
  25. package/dist/internal/bounds.d.cts +36 -0
  26. package/dist/internal/bounds.d.ts +36 -0
  27. package/dist/internal/bounds.mjs +81 -0
  28. package/dist/internal/bounds.mjs.map +1 -0
  29. package/dist/internal/cache/fs.cjs +217 -0
  30. package/dist/internal/cache/fs.cjs.map +1 -0
  31. package/dist/internal/cache/fs.d.cts +57 -0
  32. package/dist/internal/cache/fs.d.ts +57 -0
  33. package/dist/internal/cache/fs.mjs +179 -0
  34. package/dist/internal/cache/fs.mjs.map +1 -0
  35. package/dist/internal/cache/index.browser.cjs +1184 -0
  36. package/dist/internal/cache/index.browser.cjs.map +1 -0
  37. package/dist/internal/cache/index.browser.d.cts +20 -0
  38. package/dist/internal/cache/index.browser.d.ts +20 -0
  39. package/dist/internal/cache/index.browser.mjs +36 -0
  40. package/dist/internal/cache/index.browser.mjs.map +1 -0
  41. package/dist/internal/cache/index.cjs +1389 -0
  42. package/dist/internal/cache/index.cjs.map +1 -0
  43. package/dist/internal/cache/index.d.cts +16 -0
  44. package/dist/internal/cache/index.d.ts +16 -0
  45. package/dist/internal/cache/index.mjs +40 -0
  46. package/dist/internal/cache/index.mjs.map +1 -0
  47. package/dist/internal/chunk-PKJXHY27.mjs +1137 -0
  48. package/dist/internal/chunk-PKJXHY27.mjs.map +1 -0
  49. package/dist/internal/convert.cjs +161 -0
  50. package/dist/internal/convert.cjs.map +1 -0
  51. package/dist/internal/convert.d.cts +44 -0
  52. package/dist/internal/convert.d.ts +44 -0
  53. package/dist/internal/convert.mjs +117 -0
  54. package/dist/internal/convert.mjs.map +1 -0
  55. package/dist/internal/fs-O6XR4WWW.mjs +183 -0
  56. package/dist/internal/fs-O6XR4WWW.mjs.map +1 -0
  57. package/dist/internal/keys-B7C8C88N.d.cts +191 -0
  58. package/dist/internal/keys-B7C8C88N.d.ts +191 -0
  59. package/dist/internal/merge/index.cjs +75 -0
  60. package/dist/internal/merge/index.cjs.map +1 -0
  61. package/dist/internal/merge/index.d.cts +74 -0
  62. package/dist/internal/merge/index.d.ts +74 -0
  63. package/dist/internal/merge/index.mjs +46 -0
  64. package/dist/internal/merge/index.mjs.map +1 -0
  65. package/dist/internal/pairs.cjs +328 -0
  66. package/dist/internal/pairs.cjs.map +1 -0
  67. package/dist/internal/pairs.d.cts +105 -0
  68. package/dist/internal/pairs.d.ts +105 -0
  69. package/dist/internal/pairs.mjs +298 -0
  70. package/dist/internal/pairs.mjs.map +1 -0
  71. package/dist/qc/index.cjs +247 -0
  72. package/dist/qc/index.cjs.map +1 -0
  73. package/dist/qc/index.d.cts +140 -0
  74. package/dist/qc/index.d.ts +140 -0
  75. package/dist/qc/index.mjs +212 -0
  76. package/dist/qc/index.mjs.map +1 -0
  77. package/dist/temporal/index.cjs +504 -0
  78. package/dist/temporal/index.cjs.map +1 -0
  79. package/dist/temporal/index.d.cts +121 -0
  80. package/dist/temporal/index.d.ts +121 -0
  81. package/dist/temporal/index.mjs +474 -0
  82. package/dist/temporal/index.mjs.map +1 -0
  83. package/dist/transforms/index.cjs +399 -0
  84. package/dist/transforms/index.cjs.map +1 -0
  85. package/dist/transforms/index.d.cts +193 -0
  86. package/dist/transforms/index.d.ts +193 -0
  87. package/dist/transforms/index.mjs +362 -0
  88. package/dist/transforms/index.mjs.map +1 -0
  89. package/dist/validator.cjs +1870 -0
  90. package/dist/validator.cjs.map +1 -0
  91. package/dist/validator.d.cts +30 -0
  92. package/dist/validator.d.ts +30 -0
  93. package/dist/validator.mjs +1843 -0
  94. package/dist/validator.mjs.map +1 -0
  95. package/package.json +115 -0
@@ -0,0 +1,105 @@
1
+ /** Subset of fields `_obsAggregates` reads from each observation row. */
2
+ interface PairsObservationLike {
3
+ readonly temp_f?: number | null;
4
+ readonly dewpoint_f?: number | null;
5
+ readonly wind_speed_kt?: number | null;
6
+ readonly wind_gust_kt?: number | null;
7
+ readonly precip_1hr_inches?: number | null;
8
+ }
9
+ /** Subset of `ClimateObservation` fields buildPairs reads from each CLI row. */
10
+ interface PairsClimateLike {
11
+ readonly high_temp_f: number | null;
12
+ readonly low_temp_f: number | null;
13
+ readonly report_type: string;
14
+ }
15
+ /** Aggregated observation summary for one settlement day. */
16
+ interface ObsAggregates {
17
+ readonly obs_high_f: number | null;
18
+ readonly obs_low_f: number | null;
19
+ readonly obs_mean_f: number | null;
20
+ readonly obs_mean_dewpoint_f: number | null;
21
+ readonly obs_max_wind_kt: number | null;
22
+ readonly obs_max_gust_kt: number | null;
23
+ readonly obs_total_precip_in: number | null;
24
+ readonly obs_count: number;
25
+ }
26
+ /**
27
+ * One settlement-date row — 20 columns, byte-shape-equivalent to Python
28
+ * `build_pairs_row` output. The `fcst_*` columns are unconditionally
29
+ * `null` in TS-W2 (Mode 1 only — forecast wiring is TS-W5+).
30
+ *
31
+ * Object-key order is preserved verbatim so `JSON.stringify` produces
32
+ * column ordering byte-stable across SDKs.
33
+ */
34
+ interface PairsRow {
35
+ readonly date: string;
36
+ readonly station: string;
37
+ readonly cli_high_f: number | null;
38
+ readonly cli_low_f: number | null;
39
+ readonly cli_report_type: string | null;
40
+ readonly obs_high_f: number | null;
41
+ readonly obs_low_f: number | null;
42
+ readonly obs_mean_f: number | null;
43
+ readonly obs_mean_dewpoint_f: number | null;
44
+ readonly obs_max_wind_kt: number | null;
45
+ readonly obs_max_gust_kt: number | null;
46
+ readonly obs_total_precip_in: number | null;
47
+ readonly obs_count: number;
48
+ readonly fcst_high_f: null;
49
+ readonly fcst_low_f: null;
50
+ readonly fcst_model: null;
51
+ readonly fcst_issued_at: null;
52
+ readonly fcst_pop_6hr_pct: null;
53
+ readonly fcst_qpf_6hr_in: null;
54
+ readonly market_close_utc: string;
55
+ }
56
+ interface BuildPairsOptions {
57
+ /** Forwarded to `marketCloseUtc` (rare — used for synthetic test stations). */
58
+ readonly tzOverride?: string;
59
+ }
60
+ /**
61
+ * Aggregate one day's observation rows into the 8-field `obs_*` summary.
62
+ *
63
+ * Rules (byte-faithful with Python `_obs_aggregates` at `_pairs.py:97-150`):
64
+ * - obs_high_f / obs_low_f / obs_mean_f: max / min / arithmetic mean over
65
+ * non-null `temp_f`. Mean-of-null-only → null.
66
+ * - obs_mean_dewpoint_f: mean over non-null `dewpoint_f`.
67
+ * - obs_max_wind_kt / obs_max_gust_kt: max over non-null wind/gust.
68
+ * - obs_total_precip_in: sum over non-null precip; `null` if NO non-null
69
+ * precip rows (mirrors Python `sum(precips) if precips else None`).
70
+ * - obs_count: total row count, INCLUDING rows where every measure is null.
71
+ *
72
+ * Numeric-stability note: mean is non-associative for floats. Callers MUST
73
+ * pass observations in a deterministic order to preserve byte-equivalent
74
+ * float aggregation. Plan 06's research orchestrator sorts by
75
+ * `(observed_at, source)` before calling this.
76
+ *
77
+ * Returns a `Object.freeze`-d aggregate with key order matching Python.
78
+ */
79
+ declare function _obsAggregates(observations: ReadonlyArray<PairsObservationLike>): ObsAggregates;
80
+ /**
81
+ * Build one PairsRow for a given (station, date) from its observation +
82
+ * climate inputs. Mode 1 only — fcst_* are unconditionally null.
83
+ *
84
+ * `market_close_utc` is formatted `YYYY-MM-DDTHH:MM:SSZ` (no milliseconds)
85
+ * via `Date.toISOString().slice(0, 19) + "Z"` — mirrors Python strftime.
86
+ */
87
+ declare function buildPairsRow(dateStr: string, station: string, observations: ReadonlyArray<PairsObservationLike>, climate: PairsClimateLike | null, opts?: BuildPairsOptions): PairsRow;
88
+ /**
89
+ * Build PairsRows for every date in `dates` (input-order preserved).
90
+ *
91
+ * `observationsByDate[date]` and `climateByDate[date]` are looked up
92
+ * defensively — missing keys are treated as empty obs / null climate.
93
+ *
94
+ * Returns a `Object.freeze`-d array.
95
+ */
96
+ declare function buildPairs(station: string, dates: ReadonlyArray<string>, observationsByDate: Readonly<Record<string, ReadonlyArray<PairsObservationLike>>>, climateByDate: Readonly<Record<string, PairsClimateLike | null>>, opts?: BuildPairsOptions): ReadonlyArray<PairsRow>;
97
+ /**
98
+ * Surface-parity alias of `buildPairs` output. Python's `pairs_to_dataframe`
99
+ * converts the list[dict] into a pandas DataFrame indexed by date; TS has
100
+ * no DataFrame, so this is identity. Exists for cross-SDK signature parity
101
+ * per CROSS-SDK-SYNC.md.
102
+ */
103
+ declare function pairsToRows(rows: ReadonlyArray<PairsRow>): ReadonlyArray<PairsRow>;
104
+
105
+ export { type BuildPairsOptions, type ObsAggregates, type PairsClimateLike, type PairsObservationLike, type PairsRow, _obsAggregates, buildPairs, buildPairsRow, pairsToRows };
@@ -0,0 +1,105 @@
1
+ /** Subset of fields `_obsAggregates` reads from each observation row. */
2
+ interface PairsObservationLike {
3
+ readonly temp_f?: number | null;
4
+ readonly dewpoint_f?: number | null;
5
+ readonly wind_speed_kt?: number | null;
6
+ readonly wind_gust_kt?: number | null;
7
+ readonly precip_1hr_inches?: number | null;
8
+ }
9
+ /** Subset of `ClimateObservation` fields buildPairs reads from each CLI row. */
10
+ interface PairsClimateLike {
11
+ readonly high_temp_f: number | null;
12
+ readonly low_temp_f: number | null;
13
+ readonly report_type: string;
14
+ }
15
+ /** Aggregated observation summary for one settlement day. */
16
+ interface ObsAggregates {
17
+ readonly obs_high_f: number | null;
18
+ readonly obs_low_f: number | null;
19
+ readonly obs_mean_f: number | null;
20
+ readonly obs_mean_dewpoint_f: number | null;
21
+ readonly obs_max_wind_kt: number | null;
22
+ readonly obs_max_gust_kt: number | null;
23
+ readonly obs_total_precip_in: number | null;
24
+ readonly obs_count: number;
25
+ }
26
+ /**
27
+ * One settlement-date row — 20 columns, byte-shape-equivalent to Python
28
+ * `build_pairs_row` output. The `fcst_*` columns are unconditionally
29
+ * `null` in TS-W2 (Mode 1 only — forecast wiring is TS-W5+).
30
+ *
31
+ * Object-key order is preserved verbatim so `JSON.stringify` produces
32
+ * column ordering byte-stable across SDKs.
33
+ */
34
+ interface PairsRow {
35
+ readonly date: string;
36
+ readonly station: string;
37
+ readonly cli_high_f: number | null;
38
+ readonly cli_low_f: number | null;
39
+ readonly cli_report_type: string | null;
40
+ readonly obs_high_f: number | null;
41
+ readonly obs_low_f: number | null;
42
+ readonly obs_mean_f: number | null;
43
+ readonly obs_mean_dewpoint_f: number | null;
44
+ readonly obs_max_wind_kt: number | null;
45
+ readonly obs_max_gust_kt: number | null;
46
+ readonly obs_total_precip_in: number | null;
47
+ readonly obs_count: number;
48
+ readonly fcst_high_f: null;
49
+ readonly fcst_low_f: null;
50
+ readonly fcst_model: null;
51
+ readonly fcst_issued_at: null;
52
+ readonly fcst_pop_6hr_pct: null;
53
+ readonly fcst_qpf_6hr_in: null;
54
+ readonly market_close_utc: string;
55
+ }
56
+ interface BuildPairsOptions {
57
+ /** Forwarded to `marketCloseUtc` (rare — used for synthetic test stations). */
58
+ readonly tzOverride?: string;
59
+ }
60
+ /**
61
+ * Aggregate one day's observation rows into the 8-field `obs_*` summary.
62
+ *
63
+ * Rules (byte-faithful with Python `_obs_aggregates` at `_pairs.py:97-150`):
64
+ * - obs_high_f / obs_low_f / obs_mean_f: max / min / arithmetic mean over
65
+ * non-null `temp_f`. Mean-of-null-only → null.
66
+ * - obs_mean_dewpoint_f: mean over non-null `dewpoint_f`.
67
+ * - obs_max_wind_kt / obs_max_gust_kt: max over non-null wind/gust.
68
+ * - obs_total_precip_in: sum over non-null precip; `null` if NO non-null
69
+ * precip rows (mirrors Python `sum(precips) if precips else None`).
70
+ * - obs_count: total row count, INCLUDING rows where every measure is null.
71
+ *
72
+ * Numeric-stability note: mean is non-associative for floats. Callers MUST
73
+ * pass observations in a deterministic order to preserve byte-equivalent
74
+ * float aggregation. Plan 06's research orchestrator sorts by
75
+ * `(observed_at, source)` before calling this.
76
+ *
77
+ * Returns a `Object.freeze`-d aggregate with key order matching Python.
78
+ */
79
+ declare function _obsAggregates(observations: ReadonlyArray<PairsObservationLike>): ObsAggregates;
80
+ /**
81
+ * Build one PairsRow for a given (station, date) from its observation +
82
+ * climate inputs. Mode 1 only — fcst_* are unconditionally null.
83
+ *
84
+ * `market_close_utc` is formatted `YYYY-MM-DDTHH:MM:SSZ` (no milliseconds)
85
+ * via `Date.toISOString().slice(0, 19) + "Z"` — mirrors Python strftime.
86
+ */
87
+ declare function buildPairsRow(dateStr: string, station: string, observations: ReadonlyArray<PairsObservationLike>, climate: PairsClimateLike | null, opts?: BuildPairsOptions): PairsRow;
88
+ /**
89
+ * Build PairsRows for every date in `dates` (input-order preserved).
90
+ *
91
+ * `observationsByDate[date]` and `climateByDate[date]` are looked up
92
+ * defensively — missing keys are treated as empty obs / null climate.
93
+ *
94
+ * Returns a `Object.freeze`-d array.
95
+ */
96
+ declare function buildPairs(station: string, dates: ReadonlyArray<string>, observationsByDate: Readonly<Record<string, ReadonlyArray<PairsObservationLike>>>, climateByDate: Readonly<Record<string, PairsClimateLike | null>>, opts?: BuildPairsOptions): ReadonlyArray<PairsRow>;
97
+ /**
98
+ * Surface-parity alias of `buildPairs` output. Python's `pairs_to_dataframe`
99
+ * converts the list[dict] into a pandas DataFrame indexed by date; TS has
100
+ * no DataFrame, so this is identity. Exists for cross-SDK signature parity
101
+ * per CROSS-SDK-SYNC.md.
102
+ */
103
+ declare function pairsToRows(rows: ReadonlyArray<PairsRow>): ReadonlyArray<PairsRow>;
104
+
105
+ export { type BuildPairsOptions, type ObsAggregates, type PairsClimateLike, type PairsObservationLike, type PairsRow, _obsAggregates, buildPairs, buildPairsRow, pairsToRows };
@@ -0,0 +1,298 @@
1
+ // src/snapshot.ts
2
+ var _STATION_TZ = Object.freeze({
3
+ // Eastern (UTC-5 standard / UTC-4 DST)
4
+ NYC: "America/New_York",
5
+ JFK: "America/New_York",
6
+ LGA: "America/New_York",
7
+ EWR: "America/New_York",
8
+ ATL: "America/New_York",
9
+ BOS: "America/New_York",
10
+ PHL: "America/New_York",
11
+ DCA: "America/New_York",
12
+ IAD: "America/New_York",
13
+ BWI: "America/New_York",
14
+ MIA: "America/New_York",
15
+ MCO: "America/New_York",
16
+ TPA: "America/New_York",
17
+ CLT: "America/New_York",
18
+ RDU: "America/New_York",
19
+ CLE: "America/New_York",
20
+ PIT: "America/New_York",
21
+ BUF: "America/New_York",
22
+ DTW: "America/Detroit",
23
+ IND: "America/Indiana/Indianapolis",
24
+ CVG: "America/New_York",
25
+ CMH: "America/New_York",
26
+ SYR: "America/New_York",
27
+ ALB: "America/New_York",
28
+ BTV: "America/New_York",
29
+ ORF: "America/New_York",
30
+ RIC: "America/New_York",
31
+ GSO: "America/New_York",
32
+ CHS: "America/New_York",
33
+ SAV: "America/New_York",
34
+ JAX: "America/New_York",
35
+ RSW: "America/New_York",
36
+ PBI: "America/New_York",
37
+ FLL: "America/New_York",
38
+ // Central (UTC-6 standard / UTC-5 DST)
39
+ ORD: "America/Chicago",
40
+ MDW: "America/Chicago",
41
+ DFW: "America/Chicago",
42
+ DAL: "America/Chicago",
43
+ IAH: "America/Chicago",
44
+ HOU: "America/Chicago",
45
+ MSP: "America/Chicago",
46
+ STL: "America/Chicago",
47
+ MCI: "America/Chicago",
48
+ OMA: "America/Chicago",
49
+ MKE: "America/Chicago",
50
+ MSY: "America/Chicago",
51
+ MEM: "America/Chicago",
52
+ BNA: "America/Chicago",
53
+ OKC: "America/Chicago",
54
+ SAT: "America/Chicago",
55
+ AUS: "America/Chicago",
56
+ DSM: "America/Chicago",
57
+ TUL: "America/Chicago",
58
+ LIT: "America/Chicago",
59
+ BIR: "America/Chicago",
60
+ SDF: "America/Chicago",
61
+ HSV: "America/Chicago",
62
+ BHM: "America/Chicago",
63
+ MOB: "America/Chicago",
64
+ BTR: "America/Chicago",
65
+ SHV: "America/Chicago",
66
+ // Mountain (UTC-7 standard / UTC-6 DST)
67
+ DEN: "America/Denver",
68
+ SLC: "America/Denver",
69
+ ABQ: "America/Denver",
70
+ BOI: "America/Boise",
71
+ BZN: "America/Denver",
72
+ GJT: "America/Denver",
73
+ // Arizona: no DST (UTC-7 always)
74
+ PHX: "America/Phoenix",
75
+ TUS: "America/Phoenix",
76
+ // Pacific (UTC-8 standard / UTC-7 DST)
77
+ LAX: "America/Los_Angeles",
78
+ SFO: "America/Los_Angeles",
79
+ SEA: "America/Los_Angeles",
80
+ PDX: "America/Los_Angeles",
81
+ LAS: "America/Los_Angeles",
82
+ SAN: "America/Los_Angeles",
83
+ OAK: "America/Los_Angeles",
84
+ SJC: "America/Los_Angeles",
85
+ SMF: "America/Los_Angeles",
86
+ RNO: "America/Los_Angeles",
87
+ FAT: "America/Los_Angeles",
88
+ SNA: "America/Los_Angeles",
89
+ ONT: "America/Los_Angeles",
90
+ BUR: "America/Los_Angeles",
91
+ // Alaska (UTC-9 standard / UTC-8 DST)
92
+ ANC: "America/Anchorage",
93
+ FAI: "America/Anchorage",
94
+ JNU: "America/Juneau",
95
+ // Hawaii (UTC-10, no DST)
96
+ HNL: "Pacific/Honolulu",
97
+ OGG: "Pacific/Honolulu",
98
+ KOA: "Pacific/Honolulu",
99
+ // International (iter-6 H12): minimal set required to un-skip the
100
+ // case-5 RJTT year-wrap cache behavior test. Python's
101
+ // `mostlyright.snapshot._resolve_tz` falls back to the broader STATIONS
102
+ // registry for intl ICAOs; the TS port hasn't ported that fallback
103
+ // yet (tracked as TS-W6 — exhaustive intl-station tz coverage). This
104
+ // entry closes H12 cleanly without pulling the whole STATIONS map in.
105
+ // ICAO key (RJTT) — international stations have no 3-letter NWS code.
106
+ // Tokyo Haneda — UTC+9 LST, no DST.
107
+ RJTT: "Asia/Tokyo"
108
+ });
109
+ var _JAN_REF = new Date(Date.UTC(2024, 0, 15, 12, 0, 0));
110
+ var _MARKET_CLOSE_HOUR_LST = 16;
111
+ var _MARKET_CLOSE_MINUTE_LST = 30;
112
+ var _OFFSET_CACHE = /* @__PURE__ */ new Map();
113
+ function _lstOffsetHours(stationTz) {
114
+ const cached = _OFFSET_CACHE.get(stationTz);
115
+ if (cached !== void 0) return cached;
116
+ const fmt = new Intl.DateTimeFormat("en-US", {
117
+ timeZone: stationTz,
118
+ hour12: false,
119
+ year: "numeric",
120
+ month: "2-digit",
121
+ day: "2-digit",
122
+ hour: "2-digit",
123
+ minute: "2-digit",
124
+ second: "2-digit"
125
+ });
126
+ const parts = fmt.formatToParts(_JAN_REF);
127
+ const get = (type) => {
128
+ const part = parts.find((p) => p.type === type);
129
+ if (!part) {
130
+ throw new Error(`Intl.DateTimeFormat missing ${type} for tz=${stationTz}`);
131
+ }
132
+ return Number(part.value);
133
+ };
134
+ const year = get("year");
135
+ const month = get("month");
136
+ const day = get("day");
137
+ let hour = get("hour");
138
+ const minute = get("minute");
139
+ const second = get("second");
140
+ if (hour === 24) hour = 0;
141
+ const localAsUtc = Date.UTC(year, month - 1, day, hour, minute, second);
142
+ const offsetMs = localAsUtc - _JAN_REF.getTime();
143
+ const offsetHours = offsetMs / 36e5;
144
+ _OFFSET_CACHE.set(stationTz, offsetHours);
145
+ return offsetHours;
146
+ }
147
+ function _stationCodeNormalized(station) {
148
+ const s = station.trim().toUpperCase();
149
+ if (s.length === 4 && s.startsWith("K")) {
150
+ return s.substring(1);
151
+ }
152
+ return s;
153
+ }
154
+ function _resolveStationTz(station, tzOverride) {
155
+ if (tzOverride) return tzOverride;
156
+ const code = _stationCodeNormalized(station);
157
+ const tz = _STATION_TZ[code];
158
+ if (tz) return tz;
159
+ throw new Error(
160
+ `Unknown station timezone: ${JSON.stringify(code)}. Add it to _STATION_TZ or pass tzOverride="America/...".`
161
+ );
162
+ }
163
+ function marketCloseUtc(dateStr, station, tzOverride) {
164
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
165
+ if (!match) {
166
+ throw new Error(`Invalid ISO date for market close: ${JSON.stringify(dateStr)}`);
167
+ }
168
+ const [, yStr, mStr, dStr] = match;
169
+ const year = Number(yStr);
170
+ const month = Number(mStr);
171
+ const day = Number(dStr);
172
+ const tz = _resolveStationTz(station, tzOverride);
173
+ const offsetHours = _lstOffsetHours(tz);
174
+ const marketCloseAsUtcMs = Date.UTC(
175
+ year,
176
+ month - 1,
177
+ day,
178
+ _MARKET_CLOSE_HOUR_LST,
179
+ _MARKET_CLOSE_MINUTE_LST,
180
+ 0
181
+ );
182
+ return new Date(marketCloseAsUtcMs - offsetHours * 36e5);
183
+ }
184
+
185
+ // src/internal/pairs.ts
186
+ function collectNonNull(obs, key) {
187
+ const out = [];
188
+ for (const o of obs) {
189
+ const v = o[key];
190
+ if (typeof v === "number" && Number.isFinite(v)) out.push(v);
191
+ }
192
+ return out;
193
+ }
194
+ function meanOrNull(vs) {
195
+ if (vs.length === 0) return null;
196
+ let s = 0;
197
+ for (const v of vs) s += v;
198
+ return s / vs.length;
199
+ }
200
+ function maxOrNull(vs) {
201
+ if (vs.length === 0) return null;
202
+ let best = vs[0];
203
+ for (let i = 1; i < vs.length; i++) {
204
+ const v = vs[i];
205
+ if (v > best) best = v;
206
+ }
207
+ return best;
208
+ }
209
+ function minOrNull(vs) {
210
+ if (vs.length === 0) return null;
211
+ let best = vs[0];
212
+ for (let i = 1; i < vs.length; i++) {
213
+ const v = vs[i];
214
+ if (v < best) best = v;
215
+ }
216
+ return best;
217
+ }
218
+ function sumOrNull(vs) {
219
+ if (vs.length === 0) return null;
220
+ let s = 0;
221
+ for (const v of vs) s += v;
222
+ return s;
223
+ }
224
+ function _obsAggregates(observations) {
225
+ if (observations.length === 0) {
226
+ return Object.freeze({
227
+ obs_high_f: null,
228
+ obs_low_f: null,
229
+ obs_mean_f: null,
230
+ obs_mean_dewpoint_f: null,
231
+ obs_max_wind_kt: null,
232
+ obs_max_gust_kt: null,
233
+ obs_total_precip_in: null,
234
+ obs_count: 0
235
+ });
236
+ }
237
+ const temps = collectNonNull(observations, "temp_f");
238
+ const dewps = collectNonNull(observations, "dewpoint_f");
239
+ const winds = collectNonNull(observations, "wind_speed_kt");
240
+ const gusts = collectNonNull(observations, "wind_gust_kt");
241
+ const precips = collectNonNull(observations, "precip_1hr_inches");
242
+ return Object.freeze({
243
+ obs_high_f: maxOrNull(temps),
244
+ obs_low_f: minOrNull(temps),
245
+ obs_mean_f: meanOrNull(temps),
246
+ obs_mean_dewpoint_f: meanOrNull(dewps),
247
+ obs_max_wind_kt: maxOrNull(winds),
248
+ obs_max_gust_kt: maxOrNull(gusts),
249
+ obs_total_precip_in: sumOrNull(precips),
250
+ obs_count: observations.length
251
+ });
252
+ }
253
+ function buildPairsRow(dateStr, station, observations, climate, opts = {}) {
254
+ const obsAgg = _obsAggregates(observations);
255
+ const closeUtc = marketCloseUtc(dateStr, station, opts.tzOverride);
256
+ const closeIso = `${closeUtc.toISOString().slice(0, 19)}Z`;
257
+ return Object.freeze({
258
+ date: dateStr,
259
+ station,
260
+ cli_high_f: climate ? climate.high_temp_f : null,
261
+ cli_low_f: climate ? climate.low_temp_f : null,
262
+ cli_report_type: climate ? climate.report_type : null,
263
+ obs_high_f: obsAgg.obs_high_f,
264
+ obs_low_f: obsAgg.obs_low_f,
265
+ obs_mean_f: obsAgg.obs_mean_f,
266
+ obs_mean_dewpoint_f: obsAgg.obs_mean_dewpoint_f,
267
+ obs_max_wind_kt: obsAgg.obs_max_wind_kt,
268
+ obs_max_gust_kt: obsAgg.obs_max_gust_kt,
269
+ obs_total_precip_in: obsAgg.obs_total_precip_in,
270
+ obs_count: obsAgg.obs_count,
271
+ fcst_high_f: null,
272
+ fcst_low_f: null,
273
+ fcst_model: null,
274
+ fcst_issued_at: null,
275
+ fcst_pop_6hr_pct: null,
276
+ fcst_qpf_6hr_in: null,
277
+ market_close_utc: closeIso
278
+ });
279
+ }
280
+ function buildPairs(station, dates, observationsByDate, climateByDate, opts = {}) {
281
+ const out = [];
282
+ for (const date of dates) {
283
+ const obs = observationsByDate[date] ?? [];
284
+ const climate = climateByDate[date] ?? null;
285
+ out.push(buildPairsRow(date, station, obs, climate, opts));
286
+ }
287
+ return Object.freeze(out);
288
+ }
289
+ function pairsToRows(rows) {
290
+ return rows;
291
+ }
292
+ export {
293
+ _obsAggregates,
294
+ buildPairs,
295
+ buildPairsRow,
296
+ pairsToRows
297
+ };
298
+ //# sourceMappingURL=pairs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/snapshot.ts","../../src/internal/pairs.ts"],"sourcesContent":["// Snapshot math — settlement-window and market-close arithmetic.\n//\n// Ported from `packages/core/src/mostlyright/snapshot.py` and\n// `packages/core/src/mostlyright/_internal/_pairs.py:market_close_utc`.\n//\n// Key concepts:\n// - LOCAL STANDARD TIME (LST): station's standard UTC offset, DST ignored.\n// Kalshi NHIGH/NLOW contracts define the settlement window in LST.\n// - Settlement window: midnight-midnight LST for a given date.\n// During US daylight saving the clock window is 1:00 AM–1:00 AM next day\n// (EDT), but the UTC bounds are the same year-round.\n// - CLI publication delay: NWS issues the overnight final CLI ~04:00–10:00\n// UTC the day after observation. Default: 10 h after midnight LST.\n\n// ---------------------------------------------------------------------------\n// Station → IANA timezone database\n// ---------------------------------------------------------------------------\n//\n// Used to extract the LOCAL STANDARD TIME UTC offset via a January reference\n// moment. Ported from `mostlyright.snapshot._STATION_TZ`.\n\nexport const _STATION_TZ: Readonly<Record<string, string>> = Object.freeze({\n // Eastern (UTC-5 standard / UTC-4 DST)\n NYC: \"America/New_York\",\n JFK: \"America/New_York\",\n LGA: \"America/New_York\",\n EWR: \"America/New_York\",\n ATL: \"America/New_York\",\n BOS: \"America/New_York\",\n PHL: \"America/New_York\",\n DCA: \"America/New_York\",\n IAD: \"America/New_York\",\n BWI: \"America/New_York\",\n MIA: \"America/New_York\",\n MCO: \"America/New_York\",\n TPA: \"America/New_York\",\n CLT: \"America/New_York\",\n RDU: \"America/New_York\",\n CLE: \"America/New_York\",\n PIT: \"America/New_York\",\n BUF: \"America/New_York\",\n DTW: \"America/Detroit\",\n IND: \"America/Indiana/Indianapolis\",\n CVG: \"America/New_York\",\n CMH: \"America/New_York\",\n SYR: \"America/New_York\",\n ALB: \"America/New_York\",\n BTV: \"America/New_York\",\n ORF: \"America/New_York\",\n RIC: \"America/New_York\",\n GSO: \"America/New_York\",\n CHS: \"America/New_York\",\n SAV: \"America/New_York\",\n JAX: \"America/New_York\",\n RSW: \"America/New_York\",\n PBI: \"America/New_York\",\n FLL: \"America/New_York\",\n // Central (UTC-6 standard / UTC-5 DST)\n ORD: \"America/Chicago\",\n MDW: \"America/Chicago\",\n DFW: \"America/Chicago\",\n DAL: \"America/Chicago\",\n IAH: \"America/Chicago\",\n HOU: \"America/Chicago\",\n MSP: \"America/Chicago\",\n STL: \"America/Chicago\",\n MCI: \"America/Chicago\",\n OMA: \"America/Chicago\",\n MKE: \"America/Chicago\",\n MSY: \"America/Chicago\",\n MEM: \"America/Chicago\",\n BNA: \"America/Chicago\",\n OKC: \"America/Chicago\",\n SAT: \"America/Chicago\",\n AUS: \"America/Chicago\",\n DSM: \"America/Chicago\",\n TUL: \"America/Chicago\",\n LIT: \"America/Chicago\",\n BIR: \"America/Chicago\",\n SDF: \"America/Chicago\",\n HSV: \"America/Chicago\",\n BHM: \"America/Chicago\",\n MOB: \"America/Chicago\",\n BTR: \"America/Chicago\",\n SHV: \"America/Chicago\",\n // Mountain (UTC-7 standard / UTC-6 DST)\n DEN: \"America/Denver\",\n SLC: \"America/Denver\",\n ABQ: \"America/Denver\",\n BOI: \"America/Boise\",\n BZN: \"America/Denver\",\n GJT: \"America/Denver\",\n // Arizona: no DST (UTC-7 always)\n PHX: \"America/Phoenix\",\n TUS: \"America/Phoenix\",\n // Pacific (UTC-8 standard / UTC-7 DST)\n LAX: \"America/Los_Angeles\",\n SFO: \"America/Los_Angeles\",\n SEA: \"America/Los_Angeles\",\n PDX: \"America/Los_Angeles\",\n LAS: \"America/Los_Angeles\",\n SAN: \"America/Los_Angeles\",\n OAK: \"America/Los_Angeles\",\n SJC: \"America/Los_Angeles\",\n SMF: \"America/Los_Angeles\",\n RNO: \"America/Los_Angeles\",\n FAT: \"America/Los_Angeles\",\n SNA: \"America/Los_Angeles\",\n ONT: \"America/Los_Angeles\",\n BUR: \"America/Los_Angeles\",\n // Alaska (UTC-9 standard / UTC-8 DST)\n ANC: \"America/Anchorage\",\n FAI: \"America/Anchorage\",\n JNU: \"America/Juneau\",\n // Hawaii (UTC-10, no DST)\n HNL: \"Pacific/Honolulu\",\n OGG: \"Pacific/Honolulu\",\n KOA: \"Pacific/Honolulu\",\n // International (iter-6 H12): minimal set required to un-skip the\n // case-5 RJTT year-wrap cache behavior test. Python's\n // `mostlyright.snapshot._resolve_tz` falls back to the broader STATIONS\n // registry for intl ICAOs; the TS port hasn't ported that fallback\n // yet (tracked as TS-W6 — exhaustive intl-station tz coverage). This\n // entry closes H12 cleanly without pulling the whole STATIONS map in.\n // ICAO key (RJTT) — international stations have no 3-letter NWS code.\n // Tokyo Haneda — UTC+9 LST, no DST.\n RJTT: \"Asia/Tokyo\",\n});\n\n/** Reference UTC moment in January (no DST in Northern Hemisphere US). */\nexport const _JAN_REF = new Date(Date.UTC(2024, 0, 15, 12, 0, 0));\n\n/** NWS CLI typical publication delay: 10 h after midnight LST. */\nexport const _CLI_PUBLICATION_DELAY_HOURS = 10.0;\n\n/** Kalshi market typical close time (LST). */\nexport const _MARKET_CLOSE_HOUR_LST = 16;\nexport const _MARKET_CLOSE_MINUTE_LST = 30;\n\n// ---------------------------------------------------------------------------\n// LST offset extraction\n// ---------------------------------------------------------------------------\n\nconst _OFFSET_CACHE = new Map<string, number>();\n\n/**\n * Return the LOCAL STANDARD TIME UTC offset (in hours) for an IANA tz,\n * sampled from January 15 2024 12:00 UTC so the result is never affected\n * by DST in the Northern Hemisphere.\n *\n * Implementation: format `_JAN_REF` in the target tz via Intl.DateTimeFormat\n * and diff against the UTC formatted view to recover the offset.\n */\nexport function _lstOffsetHours(stationTz: string): number {\n const cached = _OFFSET_CACHE.get(stationTz);\n if (cached !== undefined) return cached;\n\n // We compute: localComponents(stationTz, _JAN_REF) − utcComponents(_JAN_REF).\n // The difference gives the tz offset in (hours). Negative for west of UTC.\n const fmt = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: stationTz,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n const parts = fmt.formatToParts(_JAN_REF);\n const get = (type: string): number => {\n const part = parts.find((p) => p.type === type);\n if (!part) {\n throw new Error(`Intl.DateTimeFormat missing ${type} for tz=${stationTz}`);\n }\n return Number(part.value);\n };\n\n const year = get(\"year\");\n const month = get(\"month\");\n const day = get(\"day\");\n let hour = get(\"hour\");\n const minute = get(\"minute\");\n const second = get(\"second\");\n // Some locales return hour \"24\" instead of \"00\" for midnight; normalize.\n if (hour === 24) hour = 0;\n\n // Compute the timezone's wall-clock for _JAN_REF treated as UTC.\n const localAsUtc = Date.UTC(year, month - 1, day, hour, minute, second);\n const offsetMs = localAsUtc - _JAN_REF.getTime();\n const offsetHours = offsetMs / 3_600_000;\n _OFFSET_CACHE.set(stationTz, offsetHours);\n return offsetHours;\n}\n\n// ---------------------------------------------------------------------------\n// Station code normalization + tz lookup\n// ---------------------------------------------------------------------------\n\nfunction _stationCodeNormalized(station: string): string {\n const s = station.trim().toUpperCase();\n if (s.length === 4 && s.startsWith(\"K\")) {\n return s.substring(1);\n }\n return s;\n}\n\n/**\n * Resolve a station code (NWS 3-letter, ICAO 4-letter) to an IANA tz string.\n * Honors `tzOverride` first, then the built-in `_STATION_TZ` map.\n * Throws if no tz can be resolved.\n */\nexport function _resolveStationTz(station: string, tzOverride?: string): string {\n if (tzOverride) return tzOverride;\n const code = _stationCodeNormalized(station);\n const tz = _STATION_TZ[code];\n if (tz) return tz;\n throw new Error(\n `Unknown station timezone: ${JSON.stringify(code)}. Add it to _STATION_TZ or pass tzOverride=\"America/...\".`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// as_of parsing\n// ---------------------------------------------------------------------------\n\nfunction _parseAsOf(asOf: Date | string): Date {\n if (asOf instanceof Date) {\n if (Number.isNaN(asOf.getTime())) {\n throw new Error(\"Invalid Date passed as asOf\");\n }\n return asOf;\n }\n let s = asOf.trim();\n // Python: bare ISO without tz → assume UTC.\n if (s.endsWith(\"Z\")) {\n // Date.parse handles \"Z\" natively.\n } else if (!/[+-]\\d{2}:?\\d{2}$/.test(s)) {\n // No timezone suffix — treat as UTC.\n s = `${s}Z`;\n }\n const ms = Date.parse(s);\n if (!Number.isFinite(ms)) {\n throw new Error(`Invalid as_of string: ${JSON.stringify(asOf)}`);\n }\n return new Date(ms);\n}\n\n// ---------------------------------------------------------------------------\n// Public surface\n// ---------------------------------------------------------------------------\n\nfunction _pad2(n: number): string {\n return n < 10 ? `0${n}` : `${n}`;\n}\n\nfunction _isoDate(year: number, month: number, day: number): string {\n return `${year}-${_pad2(month)}-${_pad2(day)}`;\n}\n\n/**\n * Return the Kalshi settlement date (YYYY-MM-DD LST) for a UTC moment.\n *\n * Kalshi NHIGH/NLOW contracts cover midnight–midnight LOCAL STANDARD TIME.\n * DST is ignored: the window is always fixed to the standard UTC offset.\n */\nexport function settlementDateFor(\n asOf: Date | string,\n station: string,\n tzOverride?: string,\n): string {\n const utcDt = _parseAsOf(asOf);\n const tz = _resolveStationTz(station, tzOverride);\n const offsetHours = _lstOffsetHours(tz);\n // offsetHours is negative for US stations → lstMs < utcMs.\n const lstMs = utcDt.getTime() + offsetHours * 3_600_000;\n const lst = new Date(lstMs);\n // Use getUTC* because we already shifted the epoch by the LST offset.\n return _isoDate(lst.getUTCFullYear(), lst.getUTCMonth() + 1, lst.getUTCDate());\n}\n\n/**\n * Return UTC start/end of the Kalshi settlement window for a date.\n * The window is midnight-midnight LST, expressed in UTC.\n */\nexport function settlementWindowUtc(\n dateStr: string,\n station: string,\n tzOverride?: string,\n): [Date, Date] {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(dateStr);\n if (!match) {\n throw new Error(`Invalid ISO date for settlement window: ${JSON.stringify(dateStr)}`);\n }\n const [, yStr, mStr, dStr] = match;\n const year = Number(yStr);\n const month = Number(mStr);\n const day = Number(dStr);\n const tz = _resolveStationTz(station, tzOverride);\n const offsetHours = _lstOffsetHours(tz);\n\n // midnight LST = 00:00 LST = (00:00 UTC) − offset (offset is negative)\n // Example: UTC-5 → midnight LST = 05:00 UTC.\n const midnightLstAsUtcMs = Date.UTC(year, month - 1, day, 0, 0, 0);\n const startMs = midnightLstAsUtcMs - offsetHours * 3_600_000;\n const start = new Date(startMs);\n const end = new Date(startMs + 24 * 3_600_000);\n return [start, end];\n}\n\n/**\n * Return the UTC time at which the NWS CLI for a date is expected to be\n * available. Default delay is 10 h after midnight LST on the next day.\n */\nexport function cliAvailableAt(\n dateStr: string,\n station: string,\n delayHours: number = _CLI_PUBLICATION_DELAY_HOURS,\n tzOverride?: string,\n): Date {\n const [, windowEnd] = settlementWindowUtc(dateStr, station, tzOverride);\n return new Date(windowEnd.getTime() + delayHours * 3_600_000);\n}\n\n/**\n * Return the UTC time of the Kalshi market close for a settlement date.\n * Kalshi NHIGH/NLOW markets close at 4:30 PM LST on the day of settlement.\n */\nexport function marketCloseUtc(dateStr: string, station: string, tzOverride?: string): Date {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(dateStr);\n if (!match) {\n throw new Error(`Invalid ISO date for market close: ${JSON.stringify(dateStr)}`);\n }\n const [, yStr, mStr, dStr] = match;\n const year = Number(yStr);\n const month = Number(mStr);\n const day = Number(dStr);\n const tz = _resolveStationTz(station, tzOverride);\n const offsetHours = _lstOffsetHours(tz);\n\n const marketCloseAsUtcMs = Date.UTC(\n year,\n month - 1,\n day,\n _MARKET_CLOSE_HOUR_LST,\n _MARKET_CLOSE_MINUTE_LST,\n 0,\n );\n return new Date(marketCloseAsUtcMs - offsetHours * 3_600_000);\n}\n","// buildPairs + _obsAggregates + pairsToRows — settlement-day row builder.\n//\n// Byte-faithful TS port of Python\n// `packages/core/src/mostlyright/_internal/_pairs.py::build_pairs` (Mode 1\n// subset — no forecast wiring; all fcst_* columns unconditionally null).\n//\n// The full Python `_select_best_run` / `_aggregate_fcst_temps_*` paths\n// (IEM MOS + Open-Meteo) are intentionally NOT ported here; forecast\n// support lands in TS-W5+. Same scope cut TS-W1 made for `research()`.\n//\n// Type strategy: structural `PairsObservationLike` + `PairsClimateLike`\n// interfaces. The full `Observation` (from weather/_parsers/awc.ts) and\n// `ClimateObservation` (from weather/_parsers/cli.ts) structurally\n// satisfy them — avoids a circular import + matches the Plan 04\n// `ObservationKey` discipline.\n\nimport { marketCloseUtc } from \"../snapshot.js\";\n\n/** Subset of fields `_obsAggregates` reads from each observation row. */\nexport interface PairsObservationLike {\n readonly temp_f?: number | null;\n readonly dewpoint_f?: number | null;\n readonly wind_speed_kt?: number | null;\n readonly wind_gust_kt?: number | null;\n readonly precip_1hr_inches?: number | null;\n}\n\n/** Subset of `ClimateObservation` fields buildPairs reads from each CLI row. */\nexport interface PairsClimateLike {\n readonly high_temp_f: number | null;\n readonly low_temp_f: number | null;\n readonly report_type: string;\n}\n\n/** Aggregated observation summary for one settlement day. */\nexport interface ObsAggregates {\n readonly obs_high_f: number | null;\n readonly obs_low_f: number | null;\n readonly obs_mean_f: number | null;\n readonly obs_mean_dewpoint_f: number | null;\n readonly obs_max_wind_kt: number | null;\n readonly obs_max_gust_kt: number | null;\n readonly obs_total_precip_in: number | null;\n readonly obs_count: number;\n}\n\n/**\n * One settlement-date row — 20 columns, byte-shape-equivalent to Python\n * `build_pairs_row` output. The `fcst_*` columns are unconditionally\n * `null` in TS-W2 (Mode 1 only — forecast wiring is TS-W5+).\n *\n * Object-key order is preserved verbatim so `JSON.stringify` produces\n * column ordering byte-stable across SDKs.\n */\nexport interface PairsRow {\n readonly date: string;\n readonly station: string;\n readonly cli_high_f: number | null;\n readonly cli_low_f: number | null;\n readonly cli_report_type: string | null;\n readonly obs_high_f: number | null;\n readonly obs_low_f: number | null;\n readonly obs_mean_f: number | null;\n readonly obs_mean_dewpoint_f: number | null;\n readonly obs_max_wind_kt: number | null;\n readonly obs_max_gust_kt: number | null;\n readonly obs_total_precip_in: number | null;\n readonly obs_count: number;\n readonly fcst_high_f: null;\n readonly fcst_low_f: null;\n readonly fcst_model: null;\n readonly fcst_issued_at: null;\n readonly fcst_pop_6hr_pct: null;\n readonly fcst_qpf_6hr_in: null;\n readonly market_close_utc: string;\n}\n\nexport interface BuildPairsOptions {\n /** Forwarded to `marketCloseUtc` (rare — used for synthetic test stations). */\n readonly tzOverride?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Aggregation helpers\n// ---------------------------------------------------------------------------\n\nfunction collectNonNull(\n obs: ReadonlyArray<PairsObservationLike>,\n key: keyof PairsObservationLike,\n): number[] {\n const out: number[] = [];\n for (const o of obs) {\n const v = o[key];\n if (typeof v === \"number\" && Number.isFinite(v)) out.push(v);\n }\n return out;\n}\n\nfunction meanOrNull(vs: number[]): number | null {\n if (vs.length === 0) return null;\n let s = 0;\n for (const v of vs) s += v;\n return s / vs.length;\n}\n\nfunction maxOrNull(vs: number[]): number | null {\n if (vs.length === 0) return null;\n let best = vs[0] as number;\n for (let i = 1; i < vs.length; i++) {\n const v = vs[i] as number;\n if (v > best) best = v;\n }\n return best;\n}\n\nfunction minOrNull(vs: number[]): number | null {\n if (vs.length === 0) return null;\n let best = vs[0] as number;\n for (let i = 1; i < vs.length; i++) {\n const v = vs[i] as number;\n if (v < best) best = v;\n }\n return best;\n}\n\nfunction sumOrNull(vs: number[]): number | null {\n if (vs.length === 0) return null;\n let s = 0;\n for (const v of vs) s += v;\n return s;\n}\n\n// ---------------------------------------------------------------------------\n// Aggregator\n// ---------------------------------------------------------------------------\n\n/**\n * Aggregate one day's observation rows into the 8-field `obs_*` summary.\n *\n * Rules (byte-faithful with Python `_obs_aggregates` at `_pairs.py:97-150`):\n * - obs_high_f / obs_low_f / obs_mean_f: max / min / arithmetic mean over\n * non-null `temp_f`. Mean-of-null-only → null.\n * - obs_mean_dewpoint_f: mean over non-null `dewpoint_f`.\n * - obs_max_wind_kt / obs_max_gust_kt: max over non-null wind/gust.\n * - obs_total_precip_in: sum over non-null precip; `null` if NO non-null\n * precip rows (mirrors Python `sum(precips) if precips else None`).\n * - obs_count: total row count, INCLUDING rows where every measure is null.\n *\n * Numeric-stability note: mean is non-associative for floats. Callers MUST\n * pass observations in a deterministic order to preserve byte-equivalent\n * float aggregation. Plan 06's research orchestrator sorts by\n * `(observed_at, source)` before calling this.\n *\n * Returns a `Object.freeze`-d aggregate with key order matching Python.\n */\nexport function _obsAggregates(observations: ReadonlyArray<PairsObservationLike>): ObsAggregates {\n if (observations.length === 0) {\n return Object.freeze({\n obs_high_f: null,\n obs_low_f: null,\n obs_mean_f: null,\n obs_mean_dewpoint_f: null,\n obs_max_wind_kt: null,\n obs_max_gust_kt: null,\n obs_total_precip_in: null,\n obs_count: 0,\n });\n }\n const temps = collectNonNull(observations, \"temp_f\");\n const dewps = collectNonNull(observations, \"dewpoint_f\");\n const winds = collectNonNull(observations, \"wind_speed_kt\");\n const gusts = collectNonNull(observations, \"wind_gust_kt\");\n const precips = collectNonNull(observations, \"precip_1hr_inches\");\n return Object.freeze({\n obs_high_f: maxOrNull(temps),\n obs_low_f: minOrNull(temps),\n obs_mean_f: meanOrNull(temps),\n obs_mean_dewpoint_f: meanOrNull(dewps),\n obs_max_wind_kt: maxOrNull(winds),\n obs_max_gust_kt: maxOrNull(gusts),\n obs_total_precip_in: sumOrNull(precips),\n obs_count: observations.length,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Row + batch builders\n// ---------------------------------------------------------------------------\n\n/**\n * Build one PairsRow for a given (station, date) from its observation +\n * climate inputs. Mode 1 only — fcst_* are unconditionally null.\n *\n * `market_close_utc` is formatted `YYYY-MM-DDTHH:MM:SSZ` (no milliseconds)\n * via `Date.toISOString().slice(0, 19) + \"Z\"` — mirrors Python strftime.\n */\nexport function buildPairsRow(\n dateStr: string,\n station: string,\n observations: ReadonlyArray<PairsObservationLike>,\n climate: PairsClimateLike | null,\n opts: BuildPairsOptions = {},\n): PairsRow {\n const obsAgg = _obsAggregates(observations);\n const closeUtc = marketCloseUtc(dateStr, station, opts.tzOverride);\n const closeIso = `${closeUtc.toISOString().slice(0, 19)}Z`;\n return Object.freeze({\n date: dateStr,\n station,\n cli_high_f: climate ? climate.high_temp_f : null,\n cli_low_f: climate ? climate.low_temp_f : null,\n cli_report_type: climate ? climate.report_type : null,\n obs_high_f: obsAgg.obs_high_f,\n obs_low_f: obsAgg.obs_low_f,\n obs_mean_f: obsAgg.obs_mean_f,\n obs_mean_dewpoint_f: obsAgg.obs_mean_dewpoint_f,\n obs_max_wind_kt: obsAgg.obs_max_wind_kt,\n obs_max_gust_kt: obsAgg.obs_max_gust_kt,\n obs_total_precip_in: obsAgg.obs_total_precip_in,\n obs_count: obsAgg.obs_count,\n fcst_high_f: null,\n fcst_low_f: null,\n fcst_model: null,\n fcst_issued_at: null,\n fcst_pop_6hr_pct: null,\n fcst_qpf_6hr_in: null,\n market_close_utc: closeIso,\n });\n}\n\n/**\n * Build PairsRows for every date in `dates` (input-order preserved).\n *\n * `observationsByDate[date]` and `climateByDate[date]` are looked up\n * defensively — missing keys are treated as empty obs / null climate.\n *\n * Returns a `Object.freeze`-d array.\n */\nexport function buildPairs(\n station: string,\n dates: ReadonlyArray<string>,\n observationsByDate: Readonly<Record<string, ReadonlyArray<PairsObservationLike>>>,\n climateByDate: Readonly<Record<string, PairsClimateLike | null>>,\n opts: BuildPairsOptions = {},\n): ReadonlyArray<PairsRow> {\n const out: PairsRow[] = [];\n for (const date of dates) {\n const obs = observationsByDate[date] ?? [];\n const climate = climateByDate[date] ?? null;\n out.push(buildPairsRow(date, station, obs, climate, opts));\n }\n return Object.freeze(out);\n}\n\n/**\n * Surface-parity alias of `buildPairs` output. Python's `pairs_to_dataframe`\n * converts the list[dict] into a pandas DataFrame indexed by date; TS has\n * no DataFrame, so this is identity. Exists for cross-SDK signature parity\n * per CROSS-SDK-SYNC.md.\n */\nexport function pairsToRows(rows: ReadonlyArray<PairsRow>): ReadonlyArray<PairsRow> {\n return rows;\n}\n"],"mappings":";AAqBO,IAAM,cAAgD,OAAO,OAAO;AAAA;AAAA,EAEzE,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,E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","names":[]}