mostlyright 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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/bounds-KSTXL77E.mjs +46 -0
- package/dist/bounds-KSTXL77E.mjs.map +1 -0
- package/dist/chunk-6ERO2BIY.mjs +436 -0
- package/dist/chunk-6ERO2BIY.mjs.map +1 -0
- package/dist/chunk-J5LGTIGS.mjs +10 -0
- package/dist/chunk-J5LGTIGS.mjs.map +1 -0
- package/dist/chunk-UKIFUUDX.mjs +108 -0
- package/dist/chunk-UKIFUUDX.mjs.map +1 -0
- package/dist/chunk-VESWR46G.mjs +82 -0
- package/dist/chunk-VESWR46G.mjs.map +1 -0
- package/dist/chunk-WYZFDCNR.mjs +1609 -0
- package/dist/chunk-WYZFDCNR.mjs.map +1 -0
- package/dist/iem-5RVPI3TY.mjs +11 -0
- package/dist/iem-5RVPI3TY.mjs.map +1 -0
- package/dist/iem-asos-O4CQWBXK.mjs +15 -0
- package/dist/iem-asos-O4CQWBXK.mjs.map +1 -0
- package/dist/index.bundle.mjs +4033 -0
- package/dist/index.bundle.mjs.map +1 -0
- package/dist/index.cjs +967 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +295 -0
- package/dist/index.d.ts +295 -0
- package/dist/index.global.js +6350 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.mjs +953 -0
- package/dist/index.mjs.map +1 -0
- package/dist/src-5L5C2EE7.mjs +88 -0
- package/dist/src-5L5C2EE7.mjs.map +1 -0
- package/package.json +46 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { helloCore } from "@mostlyrightmd/core";
|
|
3
|
+
import { helloWeather } from "@mostlyrightmd/weather";
|
|
4
|
+
import { helloMarkets } from "@mostlyrightmd/markets";
|
|
5
|
+
import * as core from "@mostlyrightmd/core";
|
|
6
|
+
import * as markets from "@mostlyrightmd/markets";
|
|
7
|
+
import * as weather from "@mostlyrightmd/weather";
|
|
8
|
+
|
|
9
|
+
// src/research.ts
|
|
10
|
+
import {
|
|
11
|
+
NotFoundError,
|
|
12
|
+
STATION_BY_CODE,
|
|
13
|
+
STATION_BY_ICAO,
|
|
14
|
+
settlementDateFor
|
|
15
|
+
} from "@mostlyrightmd/core";
|
|
16
|
+
import {
|
|
17
|
+
cacheKeyForClimate,
|
|
18
|
+
cacheKeyForObservations,
|
|
19
|
+
defaultCacheStore,
|
|
20
|
+
isLiveSource,
|
|
21
|
+
isWithinVolatileWindow,
|
|
22
|
+
isWritableMonth,
|
|
23
|
+
isWritableYear,
|
|
24
|
+
shouldSkipCacheForCurrentLstMonth,
|
|
25
|
+
shouldSkipCacheForCurrentLstYear
|
|
26
|
+
} from "@mostlyrightmd/core/internal/cache";
|
|
27
|
+
import { mergeClimate, mergeObservations } from "@mostlyrightmd/core/internal/merge";
|
|
28
|
+
import {
|
|
29
|
+
buildPairs
|
|
30
|
+
} from "@mostlyrightmd/core/internal/pairs";
|
|
31
|
+
import {
|
|
32
|
+
awcToObservation,
|
|
33
|
+
downloadCliRange,
|
|
34
|
+
downloadGhcnh,
|
|
35
|
+
downloadIemAsos,
|
|
36
|
+
fetchAwcMetars,
|
|
37
|
+
parseCliResponse,
|
|
38
|
+
parseGhcnhPsv,
|
|
39
|
+
parseIemCsv
|
|
40
|
+
} from "@mostlyrightmd/weather";
|
|
41
|
+
var AWC_MAX_HOURS = 168;
|
|
42
|
+
async function resolveCache(opts) {
|
|
43
|
+
if (opts.cache === null) return null;
|
|
44
|
+
if (opts.cache !== void 0) return opts.cache;
|
|
45
|
+
return await defaultCacheStore();
|
|
46
|
+
}
|
|
47
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
48
|
+
function normalizeStation(input) {
|
|
49
|
+
const raw = input.trim().toUpperCase();
|
|
50
|
+
if (raw.length === 0) {
|
|
51
|
+
throw new Error("station must be a non-empty string");
|
|
52
|
+
}
|
|
53
|
+
const byIcao = STATION_BY_ICAO.get(raw);
|
|
54
|
+
if (byIcao !== void 0) {
|
|
55
|
+
if (byIcao.code === null) {
|
|
56
|
+
throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
code: byIcao.code,
|
|
60
|
+
icao: byIcao.icao,
|
|
61
|
+
tz: byIcao.tz,
|
|
62
|
+
country: byIcao.country,
|
|
63
|
+
ghcnhId: byIcao.ghcnh_id
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const byCode = STATION_BY_CODE.get(raw);
|
|
67
|
+
if (byCode !== void 0) {
|
|
68
|
+
if (byCode.code === null) {
|
|
69
|
+
throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
code: byCode.code,
|
|
73
|
+
icao: byCode.icao,
|
|
74
|
+
tz: byCode.tz,
|
|
75
|
+
country: byCode.country,
|
|
76
|
+
ghcnhId: byCode.ghcnh_id
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (raw.startsWith("K") && raw.length === 4) {
|
|
80
|
+
const stripped = raw.slice(1);
|
|
81
|
+
const retry = STATION_BY_CODE.get(stripped);
|
|
82
|
+
if (retry !== void 0 && retry.code !== null) {
|
|
83
|
+
return {
|
|
84
|
+
code: retry.code,
|
|
85
|
+
icao: retry.icao,
|
|
86
|
+
tz: retry.tz,
|
|
87
|
+
country: retry.country,
|
|
88
|
+
ghcnhId: retry.ghcnh_id
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error(
|
|
93
|
+
`unknown station ${JSON.stringify(input)} \u2014 not found in STATION_BY_CODE or STATION_BY_ICAO`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function parseIsoDate(s) {
|
|
97
|
+
if (!DATE_RE.test(s)) {
|
|
98
|
+
throw new Error(`expected YYYY-MM-DD, got ${JSON.stringify(s)}`);
|
|
99
|
+
}
|
|
100
|
+
const [yStr, mStr, dStr] = s.split("-");
|
|
101
|
+
const year = Number(yStr);
|
|
102
|
+
const month = Number(mStr);
|
|
103
|
+
const day = Number(dStr);
|
|
104
|
+
const ms = Date.UTC(year, month - 1, day);
|
|
105
|
+
const d = new Date(ms);
|
|
106
|
+
if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) {
|
|
107
|
+
throw new Error(`invalid calendar date ${JSON.stringify(s)}`);
|
|
108
|
+
}
|
|
109
|
+
return d;
|
|
110
|
+
}
|
|
111
|
+
function formatDate(d) {
|
|
112
|
+
const y = d.getUTCFullYear();
|
|
113
|
+
const m = d.getUTCMonth() + 1;
|
|
114
|
+
const day = d.getUTCDate();
|
|
115
|
+
const mm = m < 10 ? `0${m}` : `${m}`;
|
|
116
|
+
const dd = day < 10 ? `0${day}` : `${day}`;
|
|
117
|
+
return `${y}-${mm}-${dd}`;
|
|
118
|
+
}
|
|
119
|
+
function buildDateList(fromDate, toDate) {
|
|
120
|
+
const from = parseIsoDate(fromDate);
|
|
121
|
+
const to = parseIsoDate(toDate);
|
|
122
|
+
if (from.getTime() > to.getTime()) {
|
|
123
|
+
throw new Error(`fromDate (${fromDate}) must be <= toDate (${toDate})`);
|
|
124
|
+
}
|
|
125
|
+
const dates = [];
|
|
126
|
+
for (let cursor = from.getTime(); cursor <= to.getTime(); cursor += 24 * 36e5) {
|
|
127
|
+
dates.push(formatDate(new Date(cursor)));
|
|
128
|
+
}
|
|
129
|
+
return dates;
|
|
130
|
+
}
|
|
131
|
+
function plusOneDay(isoDate) {
|
|
132
|
+
const d = parseIsoDate(isoDate);
|
|
133
|
+
return formatDate(new Date(d.getTime() + 24 * 36e5));
|
|
134
|
+
}
|
|
135
|
+
function isUsStation(station) {
|
|
136
|
+
return station.country === "US";
|
|
137
|
+
}
|
|
138
|
+
function anyDateOverlapsAwc(toDate, hours, now) {
|
|
139
|
+
const to = parseIsoDate(toDate);
|
|
140
|
+
const toEndMs = to.getTime() + 24 * 36e5;
|
|
141
|
+
const nowMs = now.getTime();
|
|
142
|
+
const cutoffMs = nowMs - hours * 36e5;
|
|
143
|
+
return toEndMs >= cutoffMs;
|
|
144
|
+
}
|
|
145
|
+
function observedSettlementDate(observedAt, station) {
|
|
146
|
+
const ms = Date.parse(observedAt);
|
|
147
|
+
if (!Number.isFinite(ms)) return null;
|
|
148
|
+
try {
|
|
149
|
+
return settlementDateFor(new Date(ms), station);
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function sortByObservedAtThenSource(rows) {
|
|
155
|
+
return [...rows].sort((a, b) => {
|
|
156
|
+
if (a.observed_at < b.observed_at) return -1;
|
|
157
|
+
if (a.observed_at > b.observed_at) return 1;
|
|
158
|
+
if (a.source < b.source) return -1;
|
|
159
|
+
if (a.source > b.source) return 1;
|
|
160
|
+
return 0;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function isYearVolatile(year, now) {
|
|
164
|
+
const yearEnd = `${String(year).padStart(4, "0")}-12-31`;
|
|
165
|
+
return isWithinVolatileWindow(yearEnd, formatDate(now), 30);
|
|
166
|
+
}
|
|
167
|
+
function lastDayOfMonth(year, month) {
|
|
168
|
+
const d = new Date(Date.UTC(year, month, 0));
|
|
169
|
+
return formatDate(d);
|
|
170
|
+
}
|
|
171
|
+
function isMonthVolatile(year, month, now) {
|
|
172
|
+
return isWithinVolatileWindow(lastDayOfMonth(year, month), formatDate(now), 30);
|
|
173
|
+
}
|
|
174
|
+
function monthsInRange(fromIsoDate, toIsoDate) {
|
|
175
|
+
const from = parseIsoDate(fromIsoDate);
|
|
176
|
+
const to = parseIsoDate(toIsoDate);
|
|
177
|
+
if (from.getTime() > to.getTime()) {
|
|
178
|
+
throw new Error(`fromDate (${fromIsoDate}) must be <= toDate (${toIsoDate})`);
|
|
179
|
+
}
|
|
180
|
+
const pairs = [];
|
|
181
|
+
let y = from.getUTCFullYear();
|
|
182
|
+
let m = from.getUTCMonth() + 1;
|
|
183
|
+
const endY = to.getUTCFullYear();
|
|
184
|
+
const endM = to.getUTCMonth() + 1;
|
|
185
|
+
while (y < endY || y === endY && m <= endM) {
|
|
186
|
+
pairs.push([y, m]);
|
|
187
|
+
m += 1;
|
|
188
|
+
if (m > 12) {
|
|
189
|
+
m = 1;
|
|
190
|
+
y += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return pairs;
|
|
194
|
+
}
|
|
195
|
+
async function fetchCliWithCache(fetchIcao, cacheCode, fromYear, toYear, opts, cache, now) {
|
|
196
|
+
const acc = [];
|
|
197
|
+
for (let year = fromYear; year <= toYear; year++) {
|
|
198
|
+
const writable = isWritableYear(year, now);
|
|
199
|
+
const skipCurrentYear = shouldSkipCacheForCurrentLstYear(cacheCode, year, now);
|
|
200
|
+
const skipVolatile = isYearVolatile(year, now);
|
|
201
|
+
const skip = !writable || skipCurrentYear || skipVolatile;
|
|
202
|
+
if (cache !== null && !skip) {
|
|
203
|
+
let cached = null;
|
|
204
|
+
try {
|
|
205
|
+
cached = await cache.get(cacheKeyForClimate(cacheCode, year));
|
|
206
|
+
} catch (cacheErr) {
|
|
207
|
+
console.warn(
|
|
208
|
+
`[mostlyright] CLI cache.get failed for code=${cacheCode} year=${year}; falling back to live fetch:`,
|
|
209
|
+
cacheErr
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (cached !== null) {
|
|
213
|
+
acc.push(...cached);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const cliOpts = {};
|
|
218
|
+
if (opts.signal !== void 0) cliOpts.signal = opts.signal;
|
|
219
|
+
if (opts.cliPolitenessMs !== void 0) cliOpts.politenessMs = opts.cliPolitenessMs;
|
|
220
|
+
const cliRaw = await downloadCliRange(fetchIcao, year, year, cliOpts);
|
|
221
|
+
const parsed = parseCliResponse(cliRaw, cacheCode);
|
|
222
|
+
acc.push(...parsed);
|
|
223
|
+
const sample = parsed[0]?.source;
|
|
224
|
+
if (cache !== null && !skip && !isLiveSource(sample)) {
|
|
225
|
+
try {
|
|
226
|
+
await cache.set(cacheKeyForClimate(cacheCode, year), parsed);
|
|
227
|
+
} catch (cacheErr) {
|
|
228
|
+
console.warn(
|
|
229
|
+
`[mostlyright] CLI cache.set failed for code=${cacheCode} year=${year}; in-memory rows preserved:`,
|
|
230
|
+
cacheErr
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return acc;
|
|
236
|
+
}
|
|
237
|
+
async function fetchIemAsosWithCache(stationCode, _fromYear, _extendedToYear, fromDate, extendedTo, opts, cache, now) {
|
|
238
|
+
const acc = [];
|
|
239
|
+
const yearByReportType = /* @__PURE__ */ new Map();
|
|
240
|
+
async function fetchYearOnce(year, reportType) {
|
|
241
|
+
const memoKey = `${year}:${reportType}`;
|
|
242
|
+
const cached = yearByReportType.get(memoKey);
|
|
243
|
+
if (cached !== void 0) return cached;
|
|
244
|
+
const iemOpts = {
|
|
245
|
+
reportType,
|
|
246
|
+
politenessMs: opts.iemPolitenessMs ?? 1e3
|
|
247
|
+
};
|
|
248
|
+
if (opts.signal !== void 0) iemOpts.signal = opts.signal;
|
|
249
|
+
const chunks = await downloadIemAsos(stationCode, `${year}-01-01`, `${year}-12-31`, iemOpts);
|
|
250
|
+
const fetched = [];
|
|
251
|
+
for (const chunk of chunks) {
|
|
252
|
+
const parsed = parseIemCsv(chunk.csv, {
|
|
253
|
+
observationTypeOverride: reportType === 3 ? "METAR" : "SPECI"
|
|
254
|
+
});
|
|
255
|
+
fetched.push(...parsed);
|
|
256
|
+
}
|
|
257
|
+
yearByReportType.set(memoKey, fetched);
|
|
258
|
+
return fetched;
|
|
259
|
+
}
|
|
260
|
+
function filterMonth(rows, year, month) {
|
|
261
|
+
const yyyy = String(year).padStart(4, "0");
|
|
262
|
+
const mm = String(month).padStart(2, "0");
|
|
263
|
+
const prefix = `${yyyy}-${mm}-`;
|
|
264
|
+
const out = [];
|
|
265
|
+
for (const r of rows) {
|
|
266
|
+
if (r.observed_at.startsWith(prefix)) out.push(r);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
const pairs = monthsInRange(fromDate, extendedTo);
|
|
271
|
+
for (const [year, month] of pairs) {
|
|
272
|
+
const cacheKey = cacheKeyForObservations(stationCode, year, month, "iem");
|
|
273
|
+
const writable = isWritableMonth(year, month, now);
|
|
274
|
+
const skipCurrentMonth = shouldSkipCacheForCurrentLstMonth(stationCode, year, month, now);
|
|
275
|
+
const skipVolatile = isMonthVolatile(year, month, now);
|
|
276
|
+
const skipCache = !writable || skipCurrentMonth || skipVolatile;
|
|
277
|
+
let monthRows = null;
|
|
278
|
+
if (cache !== null && !skipCache) {
|
|
279
|
+
try {
|
|
280
|
+
const cached = await cache.get(cacheKey);
|
|
281
|
+
if (cached !== null) monthRows = cached;
|
|
282
|
+
} catch (cacheErr) {
|
|
283
|
+
console.warn(
|
|
284
|
+
`[mostlyright] IEM ASOS cache.get failed for key=${cacheKey}; falling back to live fetch:`,
|
|
285
|
+
cacheErr
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (monthRows === null) {
|
|
290
|
+
const metar = await fetchYearOnce(year, 3);
|
|
291
|
+
const speci = await fetchYearOnce(year, 4);
|
|
292
|
+
const monthMetar = filterMonth(metar, year, month);
|
|
293
|
+
const monthSpeci = filterMonth(speci, year, month);
|
|
294
|
+
monthRows = [...monthMetar, ...monthSpeci];
|
|
295
|
+
const sample = monthRows[0]?.source;
|
|
296
|
+
if (cache !== null && !skipCache && !isLiveSource(sample)) {
|
|
297
|
+
try {
|
|
298
|
+
await cache.set(cacheKey, monthRows);
|
|
299
|
+
} catch (cacheErr) {
|
|
300
|
+
console.warn(
|
|
301
|
+
`[mostlyright] IEM ASOS cache.set failed for key=${cacheKey}; in-memory rows preserved:`,
|
|
302
|
+
cacheErr
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const obs of monthRows) {
|
|
308
|
+
const obsDate = obs.observed_at.slice(0, 10);
|
|
309
|
+
if (obsDate >= fromDate && obsDate <= extendedTo) acc.push(obs);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return acc;
|
|
313
|
+
}
|
|
314
|
+
async function fetchGhcnhWithCache(stationCode, ghcnhId, fromDate, extendedTo, opts, cache, now) {
|
|
315
|
+
const acc = [];
|
|
316
|
+
const yearCache = /* @__PURE__ */ new Map();
|
|
317
|
+
async function fetchYearOnce(year) {
|
|
318
|
+
const cached = yearCache.get(year);
|
|
319
|
+
if (cached !== void 0) return cached;
|
|
320
|
+
const ghcnhOpts = {};
|
|
321
|
+
if (opts.signal !== void 0) ghcnhOpts.signal = opts.signal;
|
|
322
|
+
let parsed;
|
|
323
|
+
try {
|
|
324
|
+
const yr = await downloadGhcnh(ghcnhId, year, ghcnhOpts);
|
|
325
|
+
parsed = parseGhcnhPsv(yr.psv);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
if (err instanceof NotFoundError) {
|
|
328
|
+
parsed = [];
|
|
329
|
+
} else {
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
yearCache.set(year, parsed);
|
|
334
|
+
return parsed;
|
|
335
|
+
}
|
|
336
|
+
function filterMonth(rows, year, month) {
|
|
337
|
+
const yyyy = String(year).padStart(4, "0");
|
|
338
|
+
const mm = String(month).padStart(2, "0");
|
|
339
|
+
const prefix = `${yyyy}-${mm}-`;
|
|
340
|
+
const out = [];
|
|
341
|
+
for (const r of rows) {
|
|
342
|
+
if (r.observed_at.startsWith(prefix) && r.station_code === stationCode) out.push(r);
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
const pairs = monthsInRange(fromDate, extendedTo);
|
|
347
|
+
for (const [year, month] of pairs) {
|
|
348
|
+
const cacheKey = cacheKeyForObservations(stationCode, year, month, "ghcnh");
|
|
349
|
+
const writable = isWritableMonth(year, month, now);
|
|
350
|
+
const skipCurrentMonth = shouldSkipCacheForCurrentLstMonth(stationCode, year, month, now);
|
|
351
|
+
const skipVolatile = isMonthVolatile(year, month, now);
|
|
352
|
+
const skipCache = !writable || skipCurrentMonth || skipVolatile;
|
|
353
|
+
let monthRows = null;
|
|
354
|
+
if (cache !== null && !skipCache) {
|
|
355
|
+
try {
|
|
356
|
+
const cached = await cache.get(cacheKey);
|
|
357
|
+
if (cached !== null) monthRows = cached;
|
|
358
|
+
} catch (cacheErr) {
|
|
359
|
+
console.warn(
|
|
360
|
+
`[mostlyright] GHCNh cache.get failed for key=${cacheKey}; falling back to live fetch:`,
|
|
361
|
+
cacheErr
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (monthRows === null) {
|
|
366
|
+
const yearRows = await fetchYearOnce(year);
|
|
367
|
+
monthRows = filterMonth(yearRows, year, month);
|
|
368
|
+
const sample = monthRows[0]?.source;
|
|
369
|
+
if (cache !== null && !skipCache && !isLiveSource(sample)) {
|
|
370
|
+
try {
|
|
371
|
+
await cache.set(cacheKey, monthRows);
|
|
372
|
+
} catch (cacheErr) {
|
|
373
|
+
console.warn(
|
|
374
|
+
`[mostlyright] GHCNh cache.set failed for key=${cacheKey}; in-memory rows preserved:`,
|
|
375
|
+
cacheErr
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const obs of monthRows) {
|
|
381
|
+
const obsDate = obs.observed_at.slice(0, 10);
|
|
382
|
+
if (obsDate >= fromDate && obsDate <= extendedTo) acc.push(obs);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return acc;
|
|
386
|
+
}
|
|
387
|
+
async function research(station, fromDate, toDate, opts = {}) {
|
|
388
|
+
const hasCity = typeof opts.city === "string" && opts.city.length > 0;
|
|
389
|
+
const hasContract = typeof opts.contract === "string" && opts.contract.length > 0;
|
|
390
|
+
const hasContracts = Array.isArray(opts.contracts) && opts.contracts.length > 0;
|
|
391
|
+
const hasStation = typeof station === "string" && station.length > 0;
|
|
392
|
+
const selectorCount = Number(hasStation) + Number(hasCity) + Number(hasContract) + Number(hasContracts);
|
|
393
|
+
if (selectorCount === 0) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"research(): exactly one of station, opts.city, opts.contract, opts.contracts must be provided"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (selectorCount > 1) {
|
|
399
|
+
const names = [];
|
|
400
|
+
if (hasStation) names.push("station");
|
|
401
|
+
if (hasCity) names.push("city");
|
|
402
|
+
if (hasContract) names.push("contract");
|
|
403
|
+
if (hasContracts) names.push("contracts");
|
|
404
|
+
throw new Error(`research(): selectors are mutually exclusive; got ${JSON.stringify(names)}`);
|
|
405
|
+
}
|
|
406
|
+
if (opts.sources !== void 0 && opts.source !== void 0) {
|
|
407
|
+
throw new Error("research(): sources and source are mutually exclusive");
|
|
408
|
+
}
|
|
409
|
+
if (opts.sources !== void 0 || opts.source !== void 0) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
"research(): sources / source validation surface is shipped in Phase 10 v0.2 but the data-selection wiring lands in v0.3. For Mode 2 single-source pinning today, use `researchBySource(station, source, ...)` from @mostlyrightmd/meta."
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (opts.stationOverride !== void 0 && !hasContract) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
"research(): stationOverride requires contract (not standalone station/city/contracts)"
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
if (opts.includeTrades === true && !(hasContract || hasContracts)) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
"research(): includeTrades requires contract or contracts (station/city selectors have no trade timeseries)"
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (hasCity || hasContract || hasContracts) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
"research(): city/contract/contracts selectors are validated in Phase 10 v0.2 but the multi-station/multi-issuer JOIN + trade attachment lands in v0.3. For now, use `discover({city})` to find the station then call `research(station, fromDate, toDate)` directly."
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const resolved = normalizeStation(station);
|
|
430
|
+
const dates = buildDateList(fromDate, toDate);
|
|
431
|
+
const extendedTo = plusOneDay(toDate);
|
|
432
|
+
const fromYear = Number(fromDate.slice(0, 4));
|
|
433
|
+
const toYear = Number(toDate.slice(0, 4));
|
|
434
|
+
const extendedToYear = Number(extendedTo.slice(0, 4));
|
|
435
|
+
const baseOpts = {};
|
|
436
|
+
if (opts.signal !== void 0) baseOpts.signal = opts.signal;
|
|
437
|
+
const cache = await resolveCache(opts);
|
|
438
|
+
const cacheNow = opts.now ?? /* @__PURE__ */ new Date();
|
|
439
|
+
let mergedClimate = [];
|
|
440
|
+
try {
|
|
441
|
+
const cliRows = await fetchCliWithCache(
|
|
442
|
+
resolved.icao,
|
|
443
|
+
resolved.code,
|
|
444
|
+
fromYear,
|
|
445
|
+
toYear,
|
|
446
|
+
opts,
|
|
447
|
+
cache,
|
|
448
|
+
cacheNow
|
|
449
|
+
);
|
|
450
|
+
mergedClimate = mergeClimate(cliRows);
|
|
451
|
+
} catch (err) {
|
|
452
|
+
if (err instanceof DOMException && (err.name === "AbortError" || err.name === "TimeoutError")) {
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const awcHours = opts.awcHours ?? AWC_MAX_HOURS;
|
|
457
|
+
const awcRows = [];
|
|
458
|
+
if (anyDateOverlapsAwc(toDate, awcHours, opts.now ?? /* @__PURE__ */ new Date())) {
|
|
459
|
+
const awcOpts = { hours: awcHours };
|
|
460
|
+
if (opts.signal !== void 0) awcOpts.signal = opts.signal;
|
|
461
|
+
const awcRaw = await fetchAwcMetars([resolved.icao], awcOpts);
|
|
462
|
+
for (const m of awcRaw) {
|
|
463
|
+
const obs = awcToObservation(m);
|
|
464
|
+
if (obs !== null) awcRows.push(obs);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const iemRows = await fetchIemAsosWithCache(
|
|
468
|
+
resolved.code,
|
|
469
|
+
fromYear,
|
|
470
|
+
extendedToYear,
|
|
471
|
+
fromDate,
|
|
472
|
+
extendedTo,
|
|
473
|
+
opts,
|
|
474
|
+
cache,
|
|
475
|
+
cacheNow
|
|
476
|
+
);
|
|
477
|
+
let ghcnhRows = [];
|
|
478
|
+
if (isUsStation(resolved) && resolved.ghcnhId !== null && resolved.ghcnhId.length > 0) {
|
|
479
|
+
ghcnhRows = await fetchGhcnhWithCache(
|
|
480
|
+
resolved.code,
|
|
481
|
+
resolved.ghcnhId,
|
|
482
|
+
fromDate,
|
|
483
|
+
extendedTo,
|
|
484
|
+
opts,
|
|
485
|
+
cache,
|
|
486
|
+
cacheNow
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const combinedRaw = [...awcRows, ...iemRows, ...ghcnhRows];
|
|
490
|
+
const sorted = sortByObservedAtThenSource(combinedRaw);
|
|
491
|
+
const merged = mergeObservations(sorted);
|
|
492
|
+
const observationsByDate = {};
|
|
493
|
+
const dateLo = dates[0] ?? "";
|
|
494
|
+
const dateHi = dates[dates.length - 1] ?? "";
|
|
495
|
+
for (const obs of merged) {
|
|
496
|
+
const settleDate = observedSettlementDate(obs.observed_at, resolved.code);
|
|
497
|
+
if (settleDate === null) continue;
|
|
498
|
+
if (settleDate < dateLo || settleDate > dateHi) continue;
|
|
499
|
+
let bucket = observationsByDate[settleDate];
|
|
500
|
+
if (bucket === void 0) {
|
|
501
|
+
bucket = [];
|
|
502
|
+
observationsByDate[settleDate] = bucket;
|
|
503
|
+
}
|
|
504
|
+
bucket.push(obs);
|
|
505
|
+
}
|
|
506
|
+
const climateByDate = {};
|
|
507
|
+
for (const cli of mergedClimate) {
|
|
508
|
+
climateByDate[cli.observation_date] = cli;
|
|
509
|
+
}
|
|
510
|
+
return buildPairs(resolved.code, dates, observationsByDate, climateByDate);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/mode2.ts
|
|
514
|
+
import {
|
|
515
|
+
NotFoundError as NotFoundError2,
|
|
516
|
+
STATION_BY_CODE as STATION_BY_CODE2,
|
|
517
|
+
STATION_BY_ICAO as STATION_BY_ICAO2,
|
|
518
|
+
SourceMismatchError
|
|
519
|
+
} from "@mostlyrightmd/core";
|
|
520
|
+
import {
|
|
521
|
+
awcToObservation as awcToObservation2,
|
|
522
|
+
downloadGhcnh as downloadGhcnh2,
|
|
523
|
+
downloadIemAsos as downloadIemAsos2,
|
|
524
|
+
fetchAwcMetars as fetchAwcMetars2,
|
|
525
|
+
parseGhcnhPsv as parseGhcnhPsv2,
|
|
526
|
+
parseIemCsv as parseIemCsv2
|
|
527
|
+
} from "@mostlyrightmd/weather";
|
|
528
|
+
var MODE2_SOURCES = ["iem.archive", "iem.live", "awc.live", "ghcnh.archive"];
|
|
529
|
+
var SOURCE_ALIASES = /* @__PURE__ */ new Map([
|
|
530
|
+
["iem.archive", /* @__PURE__ */ new Set(["iem", "iem.archive"])],
|
|
531
|
+
["iem.live", /* @__PURE__ */ new Set(["iem", "iem.live"])],
|
|
532
|
+
["awc.live", /* @__PURE__ */ new Set(["awc", "awc.live"])],
|
|
533
|
+
["ghcnh.archive", /* @__PURE__ */ new Set(["ghcnh", "ghcnh.archive"])]
|
|
534
|
+
]);
|
|
535
|
+
function isMode2Source(value) {
|
|
536
|
+
return typeof value === "string" && MODE2_SOURCES.includes(value);
|
|
537
|
+
}
|
|
538
|
+
function assertSourceIdentity(rows, expected, role = "observations") {
|
|
539
|
+
const accept = typeof expected === "string" ? /* @__PURE__ */ new Set([expected]) : expected;
|
|
540
|
+
const expectedLabel = typeof expected === "string" ? expected : [...accept].sort().join("|");
|
|
541
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
542
|
+
let bad = 0;
|
|
543
|
+
for (const r of rows) {
|
|
544
|
+
const src = r?.source;
|
|
545
|
+
if (typeof src !== "string") continue;
|
|
546
|
+
if (!accept.has(src)) {
|
|
547
|
+
distinct.add(src);
|
|
548
|
+
bad += 1;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (bad === 0) return;
|
|
552
|
+
const others = [...distinct].sort();
|
|
553
|
+
const first = others[0] ?? "<unknown>";
|
|
554
|
+
throw new SourceMismatchError(
|
|
555
|
+
`Mode 2 dispatch requested '${expectedLabel}' but received ${bad} row(s) with other sources: [${others.map((s) => `'${s}'`).join(", ")}]`,
|
|
556
|
+
{
|
|
557
|
+
schemaSource: expectedLabel,
|
|
558
|
+
dataSource: first,
|
|
559
|
+
role,
|
|
560
|
+
catalogWarning: null
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
var AWC_MAX_HOURS2 = 168;
|
|
565
|
+
var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
566
|
+
function resolveStation(input) {
|
|
567
|
+
const raw = input.trim().toUpperCase();
|
|
568
|
+
if (raw.length === 0) {
|
|
569
|
+
throw new Error("station must be a non-empty string");
|
|
570
|
+
}
|
|
571
|
+
const byIcao = STATION_BY_ICAO2.get(raw);
|
|
572
|
+
if (byIcao !== void 0) {
|
|
573
|
+
if (byIcao.code === null) {
|
|
574
|
+
throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
code: byIcao.code,
|
|
578
|
+
icao: byIcao.icao,
|
|
579
|
+
country: byIcao.country,
|
|
580
|
+
ghcnhId: byIcao.ghcnh_id
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const byCode = STATION_BY_CODE2.get(raw);
|
|
584
|
+
if (byCode !== void 0) {
|
|
585
|
+
if (byCode.code === null) {
|
|
586
|
+
throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
code: byCode.code,
|
|
590
|
+
icao: byCode.icao,
|
|
591
|
+
country: byCode.country,
|
|
592
|
+
ghcnhId: byCode.ghcnh_id
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (raw.startsWith("K") && raw.length === 4) {
|
|
596
|
+
const stripped = raw.slice(1);
|
|
597
|
+
const retry = STATION_BY_CODE2.get(stripped);
|
|
598
|
+
if (retry !== void 0 && retry.code !== null) {
|
|
599
|
+
return {
|
|
600
|
+
code: retry.code,
|
|
601
|
+
icao: retry.icao,
|
|
602
|
+
country: retry.country,
|
|
603
|
+
ghcnhId: retry.ghcnh_id
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
throw new Error(
|
|
608
|
+
`unknown station ${JSON.stringify(input)} \u2014 not found in STATION_BY_CODE or STATION_BY_ICAO`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
function validateDateFormat(label, value) {
|
|
612
|
+
if (!DATE_RE2.test(value)) {
|
|
613
|
+
throw new Error(`${label} must be YYYY-MM-DD, got ${JSON.stringify(value)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function yearOf(isoDate) {
|
|
617
|
+
return Number(isoDate.slice(0, 4));
|
|
618
|
+
}
|
|
619
|
+
async function researchBySource(station, source, fromDate, toDate, opts = {}) {
|
|
620
|
+
if (!isMode2Source(source)) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Mode 2 source must be one of ${JSON.stringify(
|
|
623
|
+
MODE2_SOURCES
|
|
624
|
+
)}; got ${JSON.stringify(source)}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (source === "iem.live") {
|
|
628
|
+
throw new Error(
|
|
629
|
+
"Mode 2 source 'iem.live' not yet implemented in v0.1.0 (Parity-Ticket: requires per-month live IEM endpoint not yet ported). Use 'iem.archive' for historical IEM rows."
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
validateDateFormat("fromDate", fromDate);
|
|
633
|
+
validateDateFormat("toDate", toDate);
|
|
634
|
+
if (fromDate > toDate) {
|
|
635
|
+
throw new Error(`fromDate (${fromDate}) must be <= toDate (${toDate})`);
|
|
636
|
+
}
|
|
637
|
+
const resolved = resolveStation(station);
|
|
638
|
+
const accept = SOURCE_ALIASES.get(source);
|
|
639
|
+
if (accept === void 0) {
|
|
640
|
+
throw new Error(`internal: no SOURCE_ALIASES entry for '${source}'`);
|
|
641
|
+
}
|
|
642
|
+
let rows;
|
|
643
|
+
switch (source) {
|
|
644
|
+
case "awc.live": {
|
|
645
|
+
const awcOpts = {
|
|
646
|
+
hours: opts.awcHours ?? AWC_MAX_HOURS2
|
|
647
|
+
};
|
|
648
|
+
if (opts.signal !== void 0) awcOpts.signal = opts.signal;
|
|
649
|
+
const raw = await fetchAwcMetars2([resolved.icao], awcOpts);
|
|
650
|
+
const parsed = [];
|
|
651
|
+
for (const m of raw) {
|
|
652
|
+
const obs = awcToObservation2(m);
|
|
653
|
+
if (obs !== null) parsed.push(obs);
|
|
654
|
+
}
|
|
655
|
+
rows = parsed.filter((r) => {
|
|
656
|
+
const d = r.observed_at.slice(0, 10);
|
|
657
|
+
return d >= fromDate && d <= toDate;
|
|
658
|
+
});
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case "iem.archive": {
|
|
662
|
+
const fromYear = yearOf(fromDate);
|
|
663
|
+
const toYear = yearOf(toDate);
|
|
664
|
+
const collected = [];
|
|
665
|
+
for (let year = fromYear; year <= toYear; year++) {
|
|
666
|
+
for (const reportType of [3, 4]) {
|
|
667
|
+
const iemOpts = {
|
|
668
|
+
reportType,
|
|
669
|
+
politenessMs: opts.iemPolitenessMs ?? 1e3
|
|
670
|
+
};
|
|
671
|
+
if (opts.signal !== void 0) iemOpts.signal = opts.signal;
|
|
672
|
+
const chunks = await downloadIemAsos2(
|
|
673
|
+
resolved.code,
|
|
674
|
+
`${year}-01-01`,
|
|
675
|
+
`${year}-12-31`,
|
|
676
|
+
iemOpts
|
|
677
|
+
);
|
|
678
|
+
for (const chunk of chunks) {
|
|
679
|
+
const parsed = parseIemCsv2(chunk.csv, {
|
|
680
|
+
observationTypeOverride: reportType === 3 ? "METAR" : "SPECI"
|
|
681
|
+
});
|
|
682
|
+
collected.push(...parsed);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
rows = collected.filter((r) => {
|
|
687
|
+
const d = r.observed_at.slice(0, 10);
|
|
688
|
+
return d >= fromDate && d <= toDate;
|
|
689
|
+
});
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
case "ghcnh.archive": {
|
|
693
|
+
if (resolved.country !== "US" || resolved.ghcnhId === null || resolved.ghcnhId.length === 0) {
|
|
694
|
+
throw new NotFoundError2(
|
|
695
|
+
`GHCNh archive is US-only; station ${JSON.stringify(station)} (country=${resolved.country ?? "null"}, ghcnh_id=${resolved.ghcnhId === null ? "null" : JSON.stringify(resolved.ghcnhId)}) has no GHCNh coverage`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
const fromYear = yearOf(fromDate);
|
|
699
|
+
const toYear = yearOf(toDate);
|
|
700
|
+
const collected = [];
|
|
701
|
+
for (let year = fromYear; year <= toYear; year++) {
|
|
702
|
+
const ghcnhOpts = {};
|
|
703
|
+
if (opts.signal !== void 0) ghcnhOpts.signal = opts.signal;
|
|
704
|
+
try {
|
|
705
|
+
const yr = await downloadGhcnh2(resolved.ghcnhId, year, ghcnhOpts);
|
|
706
|
+
const parsed = parseGhcnhPsv2(yr.psv);
|
|
707
|
+
for (const r of parsed) {
|
|
708
|
+
if (r.station_code === resolved.code) collected.push(r);
|
|
709
|
+
}
|
|
710
|
+
} catch (err) {
|
|
711
|
+
if (err instanceof NotFoundError2) continue;
|
|
712
|
+
throw err;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
rows = collected.filter((r) => {
|
|
716
|
+
const d = r.observed_at.slice(0, 10);
|
|
717
|
+
return d >= fromDate && d <= toDate;
|
|
718
|
+
});
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const filtered = rows.filter((r) => accept.has(r.source));
|
|
723
|
+
assertSourceIdentity(filtered, accept, "observations");
|
|
724
|
+
return filtered;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/compose.ts
|
|
728
|
+
import {
|
|
729
|
+
KALSHI_SETTLEMENT_STATIONS,
|
|
730
|
+
POLYMARKET_CITY_STATIONS
|
|
731
|
+
} from "@mostlyrightmd/markets";
|
|
732
|
+
import { POLYMARKET_KNOWN_WRONG_STATIONS } from "@mostlyrightmd/markets/polymarket";
|
|
733
|
+
var SELECTOR_NAMES = ["station", "city", "contract", "contracts"];
|
|
734
|
+
var KALSHI_TICKER_ALIASES = {
|
|
735
|
+
NY: "NYC"
|
|
736
|
+
};
|
|
737
|
+
var CITY_SLUG_ALIASES = {
|
|
738
|
+
// short_kalshi (lower) → [polymarket_long, kalshi_upper]
|
|
739
|
+
nyc: ["nyc", "NYC"],
|
|
740
|
+
chi: ["chicago", "CHI"],
|
|
741
|
+
lax: ["los_angeles", "LAX"],
|
|
742
|
+
mia: ["miami", "MIA"],
|
|
743
|
+
den: ["denver", "DEN"],
|
|
744
|
+
bos: ["boston", "BOS"],
|
|
745
|
+
aus: ["austin", "AUS"],
|
|
746
|
+
dca: ["washington_dc", "DCA"],
|
|
747
|
+
phl: ["philadelphia", "PHL"],
|
|
748
|
+
sfo: ["san_francisco", "SFO"],
|
|
749
|
+
sea: ["seattle", "SEA"],
|
|
750
|
+
atl: ["atlanta", "ATL"],
|
|
751
|
+
hou: ["houston", "HOU"],
|
|
752
|
+
dal: ["dallas", "DAL"],
|
|
753
|
+
phx: ["phoenix", "PHX"],
|
|
754
|
+
msp: ["minneapolis", "MSP"],
|
|
755
|
+
dtw: ["detroit", "DTW"]
|
|
756
|
+
};
|
|
757
|
+
var CITY_SLUG_ALIASES_REVERSE = (() => {
|
|
758
|
+
const out = {};
|
|
759
|
+
for (const [shortLower, [longPoly, kalshiUpper]] of Object.entries(CITY_SLUG_ALIASES)) {
|
|
760
|
+
out[longPoly] = [shortLower, kalshiUpper];
|
|
761
|
+
}
|
|
762
|
+
return out;
|
|
763
|
+
})();
|
|
764
|
+
function normalizeCitySlugs(city) {
|
|
765
|
+
const lower = city.toLowerCase();
|
|
766
|
+
const upper = city.toUpperCase();
|
|
767
|
+
const direct = CITY_SLUG_ALIASES[lower];
|
|
768
|
+
if (direct !== void 0) return direct;
|
|
769
|
+
const reverse = CITY_SLUG_ALIASES_REVERSE[lower];
|
|
770
|
+
if (reverse !== void 0) return [lower, reverse[1]];
|
|
771
|
+
return [lower, upper];
|
|
772
|
+
}
|
|
773
|
+
function validateSelectors(args) {
|
|
774
|
+
const provided = [];
|
|
775
|
+
if (typeof args.station === "string" && args.station.length > 0) provided.push("station");
|
|
776
|
+
if (typeof args.city === "string" && args.city.length > 0) provided.push("city");
|
|
777
|
+
if (typeof args.contract === "string" && args.contract.length > 0) provided.push("contract");
|
|
778
|
+
if (Array.isArray(args.contracts) && args.contracts.length > 0) provided.push("contracts");
|
|
779
|
+
if (provided.length === 0) {
|
|
780
|
+
throw new Error(
|
|
781
|
+
"research(): exactly one of station, city, contract, contracts must be provided"
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (provided.length > 1) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`research(): selectors are mutually exclusive; got ${JSON.stringify(provided)}`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return provided[0];
|
|
790
|
+
}
|
|
791
|
+
function resolveContract(contractId) {
|
|
792
|
+
if (typeof contractId !== "string" || !contractId.includes(":")) {
|
|
793
|
+
throw new TypeError(`contract id must be \`<issuer>:<id>\`; got ${JSON.stringify(contractId)}`);
|
|
794
|
+
}
|
|
795
|
+
const colonIdx = contractId.indexOf(":");
|
|
796
|
+
const issuer = contractId.slice(0, colonIdx).toLowerCase();
|
|
797
|
+
const raw = contractId.slice(colonIdx + 1);
|
|
798
|
+
const rawUpper = raw.toUpperCase();
|
|
799
|
+
if (issuer === "kalshi") {
|
|
800
|
+
let normalized = rawUpper;
|
|
801
|
+
if (normalized.startsWith("KX")) {
|
|
802
|
+
normalized = `K${normalized.slice(2)}`;
|
|
803
|
+
}
|
|
804
|
+
const cityOnly = normalized.split("-", 1)[0] ?? "";
|
|
805
|
+
let cityTickerRaw = null;
|
|
806
|
+
if (cityOnly.startsWith("KHIGH") && cityOnly.length > 5) {
|
|
807
|
+
cityTickerRaw = cityOnly.slice(5);
|
|
808
|
+
} else if (cityOnly.startsWith("KLOW") && cityOnly.length > 4) {
|
|
809
|
+
cityTickerRaw = cityOnly.slice(4);
|
|
810
|
+
} else {
|
|
811
|
+
throw new Error(
|
|
812
|
+
`unsupported kalshi contract format: ${JSON.stringify(raw)}; expected KHIGH<CITY>* / KXHIGH<CITY>* / KLOW<CITY>* / KXLOW<CITY>* prefix`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const cityTicker = KALSHI_TICKER_ALIASES[cityTickerRaw] ?? cityTickerRaw;
|
|
816
|
+
const entry = KALSHI_SETTLEMENT_STATIONS[cityTicker];
|
|
817
|
+
if (entry === void 0) {
|
|
818
|
+
throw new Error(`unknown Kalshi city ticker: ${JSON.stringify(cityTicker)}`);
|
|
819
|
+
}
|
|
820
|
+
return [entry.station, "kalshi"];
|
|
821
|
+
}
|
|
822
|
+
if (issuer === "polymarket") {
|
|
823
|
+
throw new Error(
|
|
824
|
+
"polymarket contract resolution requires event_id \u2192 station lookup via polymarketDiscover()/polymarketSettle(); Phase 10 v0.2 defers this to v0.3. Use `city: 'nyc'` or pass `stationOverride` until then."
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
throw new Error(
|
|
828
|
+
`unknown issuer prefix: ${JSON.stringify(issuer)}; expected kalshi or polymarket`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
function resolveCity(city) {
|
|
832
|
+
if (typeof city !== "string" || !city) {
|
|
833
|
+
throw new Error(`city must be a non-empty string; got ${JSON.stringify(city)}`);
|
|
834
|
+
}
|
|
835
|
+
const [polySlug, kalshiSlug] = normalizeCitySlugs(city);
|
|
836
|
+
const out = [];
|
|
837
|
+
const kalshi = KALSHI_SETTLEMENT_STATIONS[kalshiSlug];
|
|
838
|
+
if (kalshi !== void 0 && !out.includes(kalshi.station)) {
|
|
839
|
+
out.push(kalshi.station);
|
|
840
|
+
}
|
|
841
|
+
const poly = POLYMARKET_CITY_STATIONS[polySlug];
|
|
842
|
+
if (poly !== void 0) {
|
|
843
|
+
for (const measure of ["default", "high", "low"]) {
|
|
844
|
+
const st = poly[measure];
|
|
845
|
+
if (typeof st === "string" && !out.includes(st)) out.push(st);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const wrong = POLYMARKET_KNOWN_WRONG_STATIONS[polySlug];
|
|
849
|
+
if (wrong !== void 0) {
|
|
850
|
+
const sortedWrong = [...wrong].sort();
|
|
851
|
+
for (const st of sortedWrong) {
|
|
852
|
+
if (!out.includes(st)) out.push(st);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (out.length === 0) {
|
|
856
|
+
throw new Error(`unknown city ${JSON.stringify(city)}; not in kalshi or polymarket catalogs`);
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
function annotateSettlesFor(station, city) {
|
|
861
|
+
if (city === null) return [];
|
|
862
|
+
const [polySlug, kalshiSlug] = normalizeCitySlugs(city);
|
|
863
|
+
const out = [];
|
|
864
|
+
const kalshi = KALSHI_SETTLEMENT_STATIONS[kalshiSlug];
|
|
865
|
+
if (kalshi !== void 0 && kalshi.station === station) {
|
|
866
|
+
out.push(`kalshi:${kalshiSlug}`);
|
|
867
|
+
}
|
|
868
|
+
const poly = POLYMARKET_CITY_STATIONS[polySlug];
|
|
869
|
+
if (poly !== void 0) {
|
|
870
|
+
for (const measure of ["default", "high", "low"]) {
|
|
871
|
+
if (poly[measure] === station) {
|
|
872
|
+
out.push(`polymarket:${polySlug}`);
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return out.sort();
|
|
878
|
+
}
|
|
879
|
+
function buildOverrideWarning(contractStation, overrideStation) {
|
|
880
|
+
return {
|
|
881
|
+
kind: "StationOverrideWarning",
|
|
882
|
+
contractStation,
|
|
883
|
+
overrideStation,
|
|
884
|
+
message: `stationOverride=${JSON.stringify(overrideStation)} differs from contract's canonical settlement station ${JSON.stringify(contractStation)}; output row will carry settlementMismatch=true`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/discover.ts
|
|
889
|
+
function discover(args) {
|
|
890
|
+
if (typeof args !== "object" || args === null) {
|
|
891
|
+
throw new TypeError(`discover(): args must be an object; got ${typeof args}`);
|
|
892
|
+
}
|
|
893
|
+
const stations = resolveCity(args.city);
|
|
894
|
+
const rows = stations.map((station) => ({
|
|
895
|
+
city: args.city,
|
|
896
|
+
station,
|
|
897
|
+
settlesFor: annotateSettlesFor(station, args.city)
|
|
898
|
+
}));
|
|
899
|
+
return Object.freeze({
|
|
900
|
+
rows: Object.freeze(rows),
|
|
901
|
+
city: args.city,
|
|
902
|
+
source: "discover"
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/index.ts
|
|
907
|
+
import {
|
|
908
|
+
POLITE_FLOORS_S,
|
|
909
|
+
SOURCE_IDENTITY_TAGS,
|
|
910
|
+
SUPPORTED_SOURCES,
|
|
911
|
+
isLiveSource as isLiveSource2,
|
|
912
|
+
latest,
|
|
913
|
+
sourceTag,
|
|
914
|
+
stream,
|
|
915
|
+
validatePollSeconds,
|
|
916
|
+
validateSource
|
|
917
|
+
} from "@mostlyrightmd/weather";
|
|
918
|
+
import { LiveStreamError, NoLiveDataError } from "@mostlyrightmd/core";
|
|
919
|
+
var version = "0.0.0";
|
|
920
|
+
export {
|
|
921
|
+
LiveStreamError,
|
|
922
|
+
MODE2_SOURCES,
|
|
923
|
+
NoLiveDataError,
|
|
924
|
+
POLITE_FLOORS_S,
|
|
925
|
+
SELECTOR_NAMES,
|
|
926
|
+
SOURCE_ALIASES,
|
|
927
|
+
SOURCE_IDENTITY_TAGS,
|
|
928
|
+
SUPPORTED_SOURCES,
|
|
929
|
+
annotateSettlesFor,
|
|
930
|
+
assertSourceIdentity,
|
|
931
|
+
buildOverrideWarning,
|
|
932
|
+
core,
|
|
933
|
+
discover,
|
|
934
|
+
helloCore,
|
|
935
|
+
helloMarkets,
|
|
936
|
+
helloWeather,
|
|
937
|
+
isLiveSource2 as isLiveSource,
|
|
938
|
+
isMode2Source,
|
|
939
|
+
latest,
|
|
940
|
+
markets,
|
|
941
|
+
research,
|
|
942
|
+
researchBySource,
|
|
943
|
+
resolveCity,
|
|
944
|
+
resolveContract,
|
|
945
|
+
sourceTag,
|
|
946
|
+
stream,
|
|
947
|
+
validatePollSeconds,
|
|
948
|
+
validateSelectors,
|
|
949
|
+
validateSource,
|
|
950
|
+
version,
|
|
951
|
+
weather
|
|
952
|
+
};
|
|
953
|
+
//# sourceMappingURL=index.mjs.map
|