mostlyright 0.1.0-rc.7 → 0.1.0
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/README.md +15 -2
- package/dist/{chunk-UKIFUUDX.mjs → chunk-465FJZTS.mjs} +2 -2
- package/dist/{chunk-WYZFDCNR.mjs → chunk-IN2UOZYO.mjs} +1 -1
- package/dist/chunk-IN2UOZYO.mjs.map +1 -0
- package/dist/{iem-asos-O4CQWBXK.mjs → iem-asos-ZPUMH3KM.mjs} +3 -3
- package/dist/index.bundle.mjs +4 -4
- package/dist/index.bundle.mjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.global.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{src-5L5C2EE7.mjs → src-NP2MZ322.mjs} +2 -2
- package/package.json +5 -5
- package/dist/chunk-WYZFDCNR.mjs.map +0 -1
- /package/dist/{chunk-UKIFUUDX.mjs.map → chunk-465FJZTS.mjs.map} +0 -0
- /package/dist/{iem-asos-O4CQWBXK.mjs.map → iem-asos-ZPUMH3KM.mjs.map} +0 -0
- /package/dist/{src-5L5C2EE7.mjs.map → src-NP2MZ322.mjs.map} +0 -0
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/research.ts","../src/mode2.ts","../src/compose.ts","../src/discover.ts"],"sourcesContent":["// mostlyright (meta) — convenience re-export of the three scoped packages.\n// Use this if you want a single `import { research } from \"mostlyright\"` entry point;\n// otherwise import the scoped packages directly.\n//\n// Note: each underlying package exports its own `version` constant; to avoid\n// ambiguous re-exports we expose them under namespaced module objects in\n// addition to a top-level `version` for the meta package itself.\n\nexport { helloCore } from \"@mostlyrightmd/core\";\nexport { helloWeather } from \"@mostlyrightmd/weather\";\nexport { helloMarkets } from \"@mostlyrightmd/markets\";\n\nimport * as core from \"@mostlyrightmd/core\";\nimport * as markets from \"@mostlyrightmd/markets\";\nimport * as weather from \"@mostlyrightmd/weather\";\n\nexport { core, markets, weather };\n\n// TS-W2 Wave 4: full multi-source `research()` orchestrator (AWC + IEM\n// ASOS + GHCNh + CLI). Lives here (NOT in @mostlyrightmd/core) so the core\n// package stays dep-free; the orchestrator pulls in both core + weather.\n// `PairsRow` is the canonical row shape from @mostlyrightmd/core/internal/pairs.\nexport { research, type ResearchOptions, type PairsRow } from \"./research.js\";\n\n// TS-W4 Wave 1: Mode 2 source-explicit dispatch (researchBySource +\n// assertSourceIdentity + Mode2Source const-union). Lives in the meta\n// package alongside research() — the dispatch needs the @mostlyrightmd/weather\n// Observation type and assertSourceIdentity consumes it structurally;\n// @mostlyrightmd/core must NOT depend on weather (cycle).\nexport {\n MODE2_SOURCES,\n SOURCE_ALIASES,\n assertSourceIdentity,\n isMode2Source,\n researchBySource,\n type Mode2Source,\n type ResearchBySourceOptions,\n type SourceMismatchRole,\n} from \"./mode2.js\";\n\n// Phase 10: composable research() dispatcher + discover() ergonomic\n// surface. Lives in @mostlyrightmd/meta because compose.ts pulls in both\n// @mostlyrightmd/core (cache, station registry) and @mostlyrightmd/markets\n// (Kalshi catalog + Polymarket catalog + denylist) — keeping it in\n// core would create a cycle (markets depends on core).\nexport {\n SELECTOR_NAMES,\n annotateSettlesFor,\n buildOverrideWarning,\n resolveCity,\n resolveContract,\n validateSelectors,\n type SelectorArgs,\n type SelectorName,\n type StationOverrideWarning,\n} from \"./compose.js\";\n\nexport { discover, type DiscoverResult, type DiscoverRow } from \"./discover.js\";\n\n// Phase 11 — `mostlyright.live` ticker surface re-exported through the\n// meta package so all three import shapes resolve to the same surface:\n// import { stream, latest } from \"mostlyright\" // meta\n// import { stream } from \"@mostlyrightmd/weather\" // main barrel\n// import { stream } from \"@mostlyrightmd/weather/live\" // subpath\nexport {\n POLITE_FLOORS_S,\n SOURCE_IDENTITY_TAGS,\n SUPPORTED_SOURCES,\n isLiveSource,\n latest,\n sourceTag,\n stream,\n validatePollSeconds,\n validateSource,\n type LatestOptions,\n type LiveObservation,\n type LiveSource,\n type LiveSourceTag,\n type StreamOptions,\n} from \"@mostlyrightmd/weather\";\nexport { LiveStreamError, NoLiveDataError } from \"@mostlyrightmd/core\";\n\nexport const version = \"0.0.0\";\n","// `research()` orchestrator — TS-W2 multi-source Mode 1 join.\n//\n// Wires all four observation sources (AWC live, IEM ASOS archive, GHCNh\n// archive, IEM CLI climate) into the canonical `PairsRow` shape via\n// mergeObservations + mergeClimate + buildPairs. Mode 1 only — all\n// `fcst_*` columns are unconditionally null in this phase.\n//\n// Lives in `packages-ts/meta/` so `@mostlyrightmd/core` stays dep-free; this\n// orchestrator imports from both core (snapshot math + station table +\n// merge + pairs) and weather (4 fetchers + 4 parsers).\n//\n// W2 scope: AWC + IEM ASOS + GHCNh + CLI; no cache (TS-W3), no Mode 2\n// (TS-W4), no forecast (TS-W5+), no parallel prefetch (TS-W3+). Fetches\n// are sequential — fine for the parity gate; performance work is later.\n\nimport {\n NotFoundError,\n STATION_BY_CODE,\n STATION_BY_ICAO,\n settlementDateFor,\n} from \"@mostlyrightmd/core\";\nimport {\n type CacheStore,\n cacheKeyForClimate,\n cacheKeyForObservations,\n defaultCacheStore,\n isLiveSource,\n isWithinVolatileWindow,\n isWritableMonth,\n isWritableYear,\n shouldSkipCacheForCurrentLstMonth,\n shouldSkipCacheForCurrentLstYear,\n} from \"@mostlyrightmd/core/internal/cache\";\nimport { mergeClimate, mergeObservations } from \"@mostlyrightmd/core/internal/merge\";\nimport {\n type PairsClimateLike,\n type PairsObservationLike,\n type PairsRow,\n buildPairs,\n} from \"@mostlyrightmd/core/internal/pairs\";\nimport {\n type ClimateObservation,\n type Observation,\n awcToObservation,\n downloadCliRange,\n downloadGhcnh,\n downloadIemAsos,\n fetchAwcMetars,\n parseCliResponse,\n parseGhcnhPsv,\n parseIemCsv,\n} from \"@mostlyrightmd/weather\";\n\n// Re-export PairsRow so callers can `import { research, type PairsRow } from \"mostlyright\"`.\nexport type { PairsRow } from \"@mostlyrightmd/core/internal/pairs\";\n\nconst AWC_MAX_HOURS = 168;\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface ResearchOptions {\n /** Forward to all underlying fetchers; aborts the whole pipeline. */\n signal?: AbortSignal;\n /** AWC lookback window in hours. Default 168 (AWC max). Clamped by the fetcher. */\n awcHours?: number;\n /** Polite-delay (ms) between successive IEM ASOS year chunks. Default 1000. */\n iemPolitenessMs?: number;\n /** Polite-delay (ms) between successive GHCNh year requests. Default 1000. */\n ghcnhPolitenessMs?: number;\n /** Polite-delay (ms) between successive CLI year requests. Default 1000. */\n cliPolitenessMs?: number;\n /**\n * Reference clock for the AWC-window overlap check (test-only seam).\n * Defaults to `new Date()`. Pass an override to force-include AWC for\n * historical date ranges in unit tests.\n */\n now?: Date;\n /**\n * Pluggable cache backend (TS-W3). When omitted, uses\n * `defaultCacheStore()` (auto-detects IndexedDB → FsStore → MemoryStore).\n * Pass `null` to opt out of caching entirely.\n */\n cache?: CacheStore | null;\n\n // ── Phase 10: composable selectors (mutually exclusive with station). ──\n //\n // Per the Phase 10 v0.2 scope, the validation surface is shipped on\n // both Python and TS; the multi-station / multi-issuer JOIN +\n // trade-attachment is deferred to v0.3. Passing any of the three\n // selectors below currently throws a clear NotImplementedError-like\n // error pointing callers at `discover()` + the station= path until\n // v0.3 lands.\n\n /** Cross-issuer city selector. Returns rows for every station that any\n * issuer settles against (Kalshi + Polymarket + denylist backstops). */\n city?: string;\n /** Single-contract selector. Format: `\"<issuer>:<id>\"` (e.g.\n * `\"kalshi:KXHIGHNYC-25MAY26-T79\"`). Auto-resolves to the contract's\n * canonical settlement station via the Phase 8 catalog. */\n contract?: string;\n /** Multi-contract selector for basis-trade research. */\n contracts?: ReadonlyArray<string>;\n /** Override the contract's canonical settlement station. Emits a\n * StationOverrideWarning via `onWarning?`; output row carries\n * `settlementMismatch: true`. Only valid with `contract` selector. */\n stationOverride?: string;\n /** Mode 1 source subset — dedupe within. Mutually exclusive with `source`. */\n sources?: ReadonlyArray<string>;\n /** Mode 2 single-source pin — error on mismatch. Mutually exclusive with `sources`. */\n source?: string;\n /** Attach per-issuer trade timeseries via @mostlyrightmd/markets/trades.\n * Requires `contract` or `contracts`. */\n includeTrades?: boolean;\n /** Callback receiving Phase 10 StationOverrideWarning (no `warnings.warn()`\n * analogue in JS). */\n onWarning?: (w: import(\"./compose.js\").StationOverrideWarning) => void;\n}\n\n/**\n * Resolve the cache from opts. `null` means opt-out (returns null).\n *\n * Iter-1 H3: `defaultCacheStore()` is now async (FsStore loaded via\n * dynamic import behind a Node feature-detect). Caller already runs\n * inside `research()`'s async path, so awaiting here is free.\n */\nasync function resolveCache(opts: ResearchOptions): Promise<CacheStore | null> {\n if (opts.cache === null) return null;\n if (opts.cache !== undefined) return opts.cache;\n return await defaultCacheStore();\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\ninterface ResolvedStation {\n readonly code: string;\n readonly icao: string;\n readonly tz: string;\n readonly country: string | null;\n readonly ghcnhId: string | null;\n}\n\nfunction normalizeStation(input: string): ResolvedStation {\n const raw = input.trim().toUpperCase();\n if (raw.length === 0) {\n throw new Error(\"station must be a non-empty string\");\n }\n const byIcao = STATION_BY_ICAO.get(raw);\n if (byIcao !== undefined) {\n if (byIcao.code === null) {\n throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);\n }\n return {\n code: byIcao.code,\n icao: byIcao.icao,\n tz: byIcao.tz,\n country: byIcao.country,\n ghcnhId: byIcao.ghcnh_id,\n };\n }\n const byCode = STATION_BY_CODE.get(raw);\n if (byCode !== undefined) {\n if (byCode.code === null) {\n throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);\n }\n return {\n code: byCode.code,\n icao: byCode.icao,\n tz: byCode.tz,\n country: byCode.country,\n ghcnhId: byCode.ghcnh_id,\n };\n }\n if (raw.startsWith(\"K\") && raw.length === 4) {\n const stripped = raw.slice(1);\n const retry = STATION_BY_CODE.get(stripped);\n if (retry !== undefined && retry.code !== null) {\n return {\n code: retry.code,\n icao: retry.icao,\n tz: retry.tz,\n country: retry.country,\n ghcnhId: retry.ghcnh_id,\n };\n }\n }\n throw new Error(\n `unknown station ${JSON.stringify(input)} — not found in STATION_BY_CODE or STATION_BY_ICAO`,\n );\n}\n\nfunction parseIsoDate(s: string): Date {\n if (!DATE_RE.test(s)) {\n throw new Error(`expected YYYY-MM-DD, got ${JSON.stringify(s)}`);\n }\n const [yStr, mStr, dStr] = s.split(\"-\");\n const year = Number(yStr);\n const month = Number(mStr);\n const day = Number(dStr);\n const ms = Date.UTC(year, month - 1, day);\n const d = new Date(ms);\n if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) {\n throw new Error(`invalid calendar date ${JSON.stringify(s)}`);\n }\n return d;\n}\n\nfunction formatDate(d: Date): string {\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth() + 1;\n const day = d.getUTCDate();\n const mm = m < 10 ? `0${m}` : `${m}`;\n const dd = day < 10 ? `0${day}` : `${day}`;\n return `${y}-${mm}-${dd}`;\n}\n\nfunction buildDateList(fromDate: string, toDate: string): ReadonlyArray<string> {\n const from = parseIsoDate(fromDate);\n const to = parseIsoDate(toDate);\n if (from.getTime() > to.getTime()) {\n throw new Error(`fromDate (${fromDate}) must be <= toDate (${toDate})`);\n }\n const dates: string[] = [];\n for (let cursor = from.getTime(); cursor <= to.getTime(); cursor += 24 * 3_600_000) {\n dates.push(formatDate(new Date(cursor)));\n }\n return dates;\n}\n\n/** Plus-one-day in UTC. Used to extend the upper bound so the final LST\n * settlement window's pre-midnight UTC tail observations are captured. */\nfunction plusOneDay(isoDate: string): string {\n const d = parseIsoDate(isoDate);\n return formatDate(new Date(d.getTime() + 24 * 3_600_000));\n}\n\n/** US stations only — GHCNh PSV archive is US-only. International stations\n * have `ghcnh_id: null` AND `country !== \"US\"` in the TS codegen. */\nfunction isUsStation(station: ResolvedStation): boolean {\n return station.country === \"US\";\n}\n\n/** Returns true if any date in `[fromDate, toDate]` is within `hours` of `now`.\n * Mirrors Python `_month_overlaps_awc_window` semantics — defensive\n * short-circuit so we don't hit AWC for purely historical windows. */\nfunction anyDateOverlapsAwc(toDate: string, hours: number, now: Date): boolean {\n const to = parseIsoDate(toDate);\n // Window includes the END of toDate (LST close), so add 24h to the upper bound.\n const toEndMs = to.getTime() + 24 * 3_600_000;\n const nowMs = now.getTime();\n const cutoffMs = nowMs - hours * 3_600_000;\n return toEndMs >= cutoffMs;\n}\n\nfunction observedSettlementDate(observedAt: string, station: string): string | null {\n const ms = Date.parse(observedAt);\n if (!Number.isFinite(ms)) return null;\n try {\n return settlementDateFor(new Date(ms), station);\n } catch {\n return null;\n }\n}\n\n/** Lexicographic-on-`observed_at` sort, stable in `source`. Ensures\n * byte-equivalent float aggregation in `_obsAggregates` (mean is\n * non-associative for floats). */\nfunction sortByObservedAtThenSource(rows: ReadonlyArray<Observation>): Observation[] {\n return [...rows].sort((a, b) => {\n if (a.observed_at < b.observed_at) return -1;\n if (a.observed_at > b.observed_at) return 1;\n if (a.source < b.source) return -1;\n if (a.source > b.source) return 1;\n return 0;\n });\n}\n\n/**\n * True iff the end-of-year ISO date for `year` falls inside the 30-day\n * volatile amendment window relative to `now`. Used to gate archive\n * cache reads/writes for IEM ASOS yearly chunks AND IEM CLI yearly\n * chunks (iter-5 H9). Rationale: rows from a year whose 12-31 boundary\n * is within 30 days of \"now\" may still be amended upstream; caching\n * them would persist soon-to-be-stale values.\n *\n * For `year` strictly less than the current calendar year of `now`, the\n * 12-31 boundary is well past 30 days back → returns false (cacheable).\n * For `year` equal to the current LST year, the year-end is in the\n * future relative to `now` → predicate returns false (the per-year\n * current-LST-year gate handles that case first and is still required).\n * The window only fires for the immediate-post-year window — exactly\n * the case where freshly-archived rows are most likely to be revised.\n */\nfunction isYearVolatile(year: number, now: Date): boolean {\n const yearEnd = `${String(year).padStart(4, \"0\")}-12-31`;\n return isWithinVolatileWindow(yearEnd, formatDate(now), 30);\n}\n\n/**\n * Last calendar day of `(year, month)`. Used as archive-as-of for the\n * per-month volatile-window gate (iter-7 H13). Returns YYYY-MM-DD.\n */\nfunction lastDayOfMonth(year: number, month: number): string {\n // UTC math: day 0 of (month+1) === last day of (month).\n const d = new Date(Date.UTC(year, month, 0));\n return formatDate(d);\n}\n\n/**\n * True iff the end-of-month ISO date for `(year, month)` falls inside the\n * 30-day volatile amendment window relative to `now`. Per-month analog of\n * `isYearVolatile`, used to gate the per-month observations cache\n * (iter-7 H13). Rationale: rows from a month whose final day is within\n * 30 days of \"now\" may still be amended upstream; caching them would\n * persist soon-to-be-stale values. The window only fires for the\n * immediate-post-month window — exactly the case where freshly-archived\n * rows are most likely to be revised.\n */\nfunction isMonthVolatile(year: number, month: number, now: Date): boolean {\n return isWithinVolatileWindow(lastDayOfMonth(year, month), formatDate(now), 30);\n}\n\n/**\n * Enumerate `[year, month]` pairs that overlap `[fromIsoDate, toIsoDate]`\n * (inclusive on both ends). Used by the per-month observations cache\n * (iter-7 H13). Returns pairs in chronological order. Validates the\n * range; throws on inverted input.\n */\nfunction monthsInRange(\n fromIsoDate: string,\n toIsoDate: string,\n): ReadonlyArray<readonly [number, number]> {\n const from = parseIsoDate(fromIsoDate);\n const to = parseIsoDate(toIsoDate);\n if (from.getTime() > to.getTime()) {\n throw new Error(`fromDate (${fromIsoDate}) must be <= toDate (${toIsoDate})`);\n }\n const pairs: Array<readonly [number, number]> = [];\n let y = from.getUTCFullYear();\n let m = from.getUTCMonth() + 1; // 1-12\n const endY = to.getUTCFullYear();\n const endM = to.getUTCMonth() + 1;\n while (y < endY || (y === endY && m <= endM)) {\n pairs.push([y, m]);\n m += 1;\n if (m > 12) {\n m = 1;\n y += 1;\n }\n }\n return pairs;\n}\n\n/**\n * Fetch CLI climate per-year with read-through cache. Yearly chunks are\n * cached at `cacheKeyForClimate(code, year)`. Skip rules:\n * - Current LST year — mutable, never cached.\n * - 30-day volatile amendment window (iter-5 H9) — chunks whose\n * year-end is within 30 days of `now` MUST be re-fetched. The\n * window only fires for the year immediately preceding \"now\"\n * once the calendar rolls over.\n * - Live source (`.live`) — never cached (CLI is archive `iem.cli` →\n * this never fires today; defensive for future).\n *\n * iter-6 C12: cache failures must NEVER discard the in-memory rows.\n * `cache.get` failures degrade to a live fetch (the intent — read-through\n * is a perf optimization, not a correctness requirement). `cache.set`\n * failures AFTER a successful fetch+parse MUST log and continue —\n * persisting to the cache is a best-effort side effect, never a reason\n * to drop already-fetched climate data. The previous broad try/catch in\n * the caller swallowed cache.set throws as \"no CLI data,\" silently\n * corrupting research rows with null cli_* fields.\n */\nasync function fetchCliWithCache(\n fetchIcao: string,\n cacheCode: string,\n fromYear: number,\n toYear: number,\n opts: ResearchOptions,\n cache: CacheStore | null,\n now: Date,\n): Promise<ClimateObservation[]> {\n const acc: ClimateObservation[] = [];\n for (let year = fromYear; year <= toYear; year++) {\n // iter-12 C15: `isWritableYear` is the strictest temporal gate.\n // Any year that isn't STRICTLY in the past UTC-wise (future years\n // or the current UTC year, including the UTC Jan-1 boundary window\n // where a negative-offset station's LST is still in the prior year)\n // is never cacheable — regardless of LST or volatile-window logic.\n // Force a live fetch and skip both reads AND writes for non-writable\n // years.\n const writable = isWritableYear(year, now);\n const skipCurrentYear = shouldSkipCacheForCurrentLstYear(cacheCode, year, now);\n // iter-5 H9: the 30-day volatile amendment window MUST also block\n // cache reads — a hit served from inside the window would re-serve\n // soon-to-be-amended rows. Always prefer a fresh fetch when the\n // window is active.\n const skipVolatile = isYearVolatile(year, now);\n const skip = !writable || skipCurrentYear || skipVolatile;\n\n // --- Cache read (best-effort) -------------------------------------\n // iter-6 C12: a `cache.get` failure must not abort the per-year\n // chunk — fall through to the live fetch. A transient backend hiccup\n // is no reason to refuse climate data we can still fetch fresh.\n if (cache !== null && !skip) {\n let cached: ClimateObservation[] | null = null;\n try {\n cached = await cache.get<ClimateObservation[]>(cacheKeyForClimate(cacheCode, year));\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] CLI cache.get failed for code=${cacheCode} year=${year}; falling back to live fetch:`,\n cacheErr,\n );\n }\n if (cached !== null) {\n acc.push(...cached);\n continue;\n }\n }\n\n // --- Live fetch + parse (errors here ARE fatal to this chunk) -----\n // Abort propagates; other errors bubble to the caller's try/catch\n // which degrades to \"no CLI data\" for the affected years. This is\n // the existing behavior — DO NOT widen the catch to include cache\n // writes (see below).\n const cliOpts: { signal?: AbortSignal; politenessMs?: number } = {};\n if (opts.signal !== undefined) cliOpts.signal = opts.signal;\n if (opts.cliPolitenessMs !== undefined) cliOpts.politenessMs = opts.cliPolitenessMs;\n const cliRaw = await downloadCliRange(fetchIcao, year, year, cliOpts);\n const parsed = parseCliResponse(cliRaw, cacheCode);\n acc.push(...parsed);\n\n // --- Cache write (best-effort, AFTER rows are accumulated) --------\n // iter-6 C12: `cache.set` MUST be wrapped in its own try/catch so a\n // transient write failure cannot discard already-fetched rows. The\n // previous code put cache.set inside the caller's broad CLI try/catch,\n // which silently degraded write failures to \"no climate data\" —\n // returning research rows with null cli_* fields. That's silent data\n // corruption; this guard prevents it.\n const sample = parsed[0]?.source;\n if (cache !== null && !skip && !isLiveSource(sample)) {\n try {\n await cache.set(cacheKeyForClimate(cacheCode, year), parsed);\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] CLI cache.set failed for code=${cacheCode} year=${year}; in-memory rows preserved:`,\n cacheErr,\n );\n }\n }\n }\n return acc;\n}\n\n/**\n * Fetch IEM ASOS observations per-month with read-through cache.\n *\n * iter-7 H13: this previously cached at YEAR granularity using a sentinel\n * `:01:rt=N` key, violating the Python TS-CACHE-02 per-month contract.\n * The Python `read_cache(station, year, month)` / `write_cache(...)`\n * surface uses `(station, year, month)` triplets — one parquet per month\n * containing the merged METAR+SPECI slice. This helper now matches that\n * contract:\n *\n * 1. Enumerate `(year, month)` pairs overlapping the queried range.\n * 2. For each pair, attempt a per-month cache read using the\n * source-namespaced key `cacheKeyForObservations(station, year,\n * month, \"iem\")`.\n * 3. On cache miss, fetch the full year (single IEM HTTP request for\n * `report_type=3` + one for `=4`) — IEM ASOS is yearly-chunked at\n * the source — then partition parsed rows by `(year, month)` and\n * filter back to the requested month (mirrors Python research.py\n * L267-269 month-boundary filter).\n * 4. Apply per-MONTH skip rules:\n * - `shouldSkipCacheForCurrentLstMonth(station, year, month, now)` —\n * mutable current month; never written.\n * - `isMonthVolatile(year, month, now)` — 30-day amendment window\n * gate (iter-5 H9 / iter-7 H13). Within the window, both read\n * AND write are skipped (IEM may publish late-arriving METARs\n * or corrections).\n * 5. Write-through fires only when neither skip rule trips; otherwise\n * the month's rows are returned in-memory but never persisted.\n *\n * Per-year fetch results are cached in a local `yearCache` Map so multiple\n * months within the same year share one HTTP round-trip — this is the\n * critical perf invariant from the previous implementation, preserved\n * across the granularity change.\n *\n * iter-6 C12: mirrors `fetchCliWithCache`'s split-try pattern — cache\n * `get` / `set` failures are logged but never discard the in-memory\n * rows. A cache backend hiccup must not silently drop observations\n * that were successfully fetched + parsed.\n */\nasync function fetchIemAsosWithCache(\n stationCode: string,\n _fromYear: number,\n _extendedToYear: number,\n fromDate: string,\n extendedTo: string,\n opts: ResearchOptions,\n cache: CacheStore | null,\n now: Date,\n): Promise<Observation[]> {\n void _fromYear;\n void _extendedToYear;\n const acc: Observation[] = [];\n\n // Per-call memoization: avoid re-fetching the same (year, reportType)\n // when multiple months in the same year miss the cache.\n const yearByReportType = new Map<string, Observation[]>();\n\n async function fetchYearOnce(year: number, reportType: 3 | 4): Promise<Observation[]> {\n const memoKey = `${year}:${reportType}`;\n const cached = yearByReportType.get(memoKey);\n if (cached !== undefined) return cached;\n const iemOpts: { reportType: 3 | 4; politenessMs: number; signal?: AbortSignal } = {\n reportType,\n politenessMs: opts.iemPolitenessMs ?? 1000,\n };\n if (opts.signal !== undefined) iemOpts.signal = opts.signal;\n const chunks = await downloadIemAsos(stationCode, `${year}-01-01`, `${year}-12-31`, iemOpts);\n const fetched: Observation[] = [];\n for (const chunk of chunks) {\n const parsed = parseIemCsv(chunk.csv, {\n observationTypeOverride: reportType === 3 ? \"METAR\" : \"SPECI\",\n });\n fetched.push(...parsed);\n }\n yearByReportType.set(memoKey, fetched);\n return fetched;\n }\n\n function filterMonth(\n rows: ReadonlyArray<Observation>,\n year: number,\n month: number,\n ): Observation[] {\n const yyyy = String(year).padStart(4, \"0\");\n const mm = String(month).padStart(2, \"0\");\n const prefix = `${yyyy}-${mm}-`;\n const out: Observation[] = [];\n for (const r of rows) {\n if (r.observed_at.startsWith(prefix)) out.push(r);\n }\n return out;\n }\n\n const pairs = monthsInRange(fromDate, extendedTo);\n for (const [year, month] of pairs) {\n const cacheKey = cacheKeyForObservations(stationCode, year, month, \"iem\");\n // iter-12 C14: `isWritableMonth` is the strictest temporal gate.\n // Any month that isn't STRICTLY in the past UTC-wise (future months\n // or the current UTC month, including the UTC-rollover tail where\n // LST is still in the prior UTC month) is never cacheable —\n // regardless of LST or volatile-window logic. Force a live fetch\n // and skip both reads AND writes for non-writable months.\n const writable = isWritableMonth(year, month, now);\n const skipCurrentMonth = shouldSkipCacheForCurrentLstMonth(stationCode, year, month, now);\n const skipVolatile = isMonthVolatile(year, month, now);\n const skipCache = !writable || skipCurrentMonth || skipVolatile;\n\n // --- Cache read (best-effort) -------------------------------------\n // iter-6 C12: a `cache.get` failure must not abort the month — fall\n // through to the live fetch. The cached value combines METAR+SPECI\n // (single per-month entry), so a hit yields both report types.\n let monthRows: Observation[] | null = null;\n if (cache !== null && !skipCache) {\n try {\n const cached = await cache.get<Observation[]>(cacheKey);\n if (cached !== null) monthRows = cached;\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] IEM ASOS cache.get failed for key=${cacheKey}; falling back to live fetch:`,\n cacheErr,\n );\n }\n }\n\n if (monthRows === null) {\n // --- Live fetch + parse (errors here propagate to the caller) ---\n // Fetch both report types for the year (memoized) and partition\n // to this month. Combining METAR+SPECI matches the Python contract\n // (write_cache receives one merged list per month).\n const metar = await fetchYearOnce(year, 3);\n const speci = await fetchYearOnce(year, 4);\n const monthMetar = filterMonth(metar, year, month);\n const monthSpeci = filterMonth(speci, year, month);\n monthRows = [...monthMetar, ...monthSpeci];\n\n // --- Cache write (best-effort, AFTER rows are accumulated) ------\n // iter-6 C12: `cache.set` failures MUST NOT propagate — a\n // transient write failure cannot be allowed to discard rows that\n // were just successfully fetched + parsed. Log and continue; the\n // in-memory `monthRows` is appended to `acc` below regardless.\n const sample = monthRows[0]?.source;\n if (cache !== null && !skipCache && !isLiveSource(sample)) {\n try {\n await cache.set(cacheKey, monthRows);\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] IEM ASOS cache.set failed for key=${cacheKey}; in-memory rows preserved:`,\n cacheErr,\n );\n }\n }\n }\n\n for (const obs of monthRows) {\n const obsDate = obs.observed_at.slice(0, 10);\n if (obsDate >= fromDate && obsDate <= extendedTo) acc.push(obs);\n }\n }\n return acc;\n}\n\n/**\n * Fetch GHCNh archive observations per-month with read-through cache.\n *\n * iter-7 H14: previously the GHCNh path called `downloadGhcnhRange` on\n * every `research()` invocation and never touched the cache. TS-W3\n * requires GHCNh chunks to be cacheable just like IEM ASOS — this helper\n * applies the same per-month contract as `fetchIemAsosWithCache`:\n *\n * 1. Enumerate `(year, month)` pairs overlapping the queried range.\n * 2. For each pair, attempt a per-month cache read using the source-\n * namespaced key `cacheKeyForObservations(station, year, month,\n * \"ghcnh\")`. The `\"ghcnh\"` source segment prevents collision with\n * IEM ASOS writes for the same `(station, year, month)` triplet\n * (iter-7 H13 introduced `\"iem\"` namespacing).\n * 3. On cache miss, fetch the full year via `downloadGhcnh` (single\n * PSV per station-year — NCEI's archive is yearly-chunked at the\n * source) — memoized within the helper so multiple months in the\n * same year share one HTTP round-trip.\n * 4. Per-month skip rules: `shouldSkipCacheForCurrentLstMonth` +\n * `isMonthVolatile` (30-day amendment window). NCEI republishes\n * `GHCNh_<id>_<YEAR>.psv` as new months land, so the same skip\n * logic the IEM helper uses applies here.\n * 5. 404-as-no-data: a `NotFoundError` from `downloadGhcnh` means NCEI\n * has no archive for this station-year (typical for recent partial\n * years or pre-1973 stations). We memoize an empty year and treat\n * every month as cache-eligible-but-empty. The Python range fetcher\n * silently swallows 404 too (research.py L160-166 logs + continues).\n *\n * iter-6 C12: mirrors the split-try pattern — cache `get` / `set`\n * failures are logged but never discard the in-memory rows.\n */\nasync function fetchGhcnhWithCache(\n stationCode: string,\n ghcnhId: string,\n fromDate: string,\n extendedTo: string,\n opts: ResearchOptions,\n cache: CacheStore | null,\n now: Date,\n): Promise<Observation[]> {\n const acc: Observation[] = [];\n\n // Per-call memoization: avoid re-fetching the same year when multiple\n // months in the same year miss the cache. `null` sentinel records a 404\n // (no data) so subsequent months in that year skip the HTTP call too.\n const yearCache = new Map<number, ReadonlyArray<Observation>>();\n\n async function fetchYearOnce(year: number): Promise<ReadonlyArray<Observation>> {\n const cached = yearCache.get(year);\n if (cached !== undefined) return cached;\n const ghcnhOpts: { signal?: AbortSignal } = {};\n if (opts.signal !== undefined) ghcnhOpts.signal = opts.signal;\n let parsed: ReadonlyArray<Observation>;\n try {\n const yr = await downloadGhcnh(ghcnhId, year, ghcnhOpts);\n parsed = parseGhcnhPsv(yr.psv);\n } catch (err) {\n if (err instanceof NotFoundError) {\n // NCEI 404 → no data for this station-year. Mirrors the\n // `downloadGhcnhRange` swallow-404 behavior; memoize empty so\n // subsequent months in this year don't re-hit NCEI.\n parsed = [];\n } else {\n throw err;\n }\n }\n yearCache.set(year, parsed);\n return parsed;\n }\n\n function filterMonth(\n rows: ReadonlyArray<Observation>,\n year: number,\n month: number,\n ): Observation[] {\n const yyyy = String(year).padStart(4, \"0\");\n const mm = String(month).padStart(2, \"0\");\n const prefix = `${yyyy}-${mm}-`;\n const out: Observation[] = [];\n for (const r of rows) {\n if (r.observed_at.startsWith(prefix) && r.station_code === stationCode) out.push(r);\n }\n return out;\n }\n\n const pairs = monthsInRange(fromDate, extendedTo);\n for (const [year, month] of pairs) {\n const cacheKey = cacheKeyForObservations(stationCode, year, month, \"ghcnh\");\n // iter-12 C14: stricter additional temporal gate — see the matching\n // comment in `fetchIemAsosWithCache`. NCEI's archive can return\n // empty data for not-yet-published months; we must NEVER persist a\n // not-strictly-past UTC month as if it were complete.\n const writable = isWritableMonth(year, month, now);\n const skipCurrentMonth = shouldSkipCacheForCurrentLstMonth(stationCode, year, month, now);\n const skipVolatile = isMonthVolatile(year, month, now);\n const skipCache = !writable || skipCurrentMonth || skipVolatile;\n\n // --- Cache read (best-effort) -------------------------------------\n let monthRows: Observation[] | null = null;\n if (cache !== null && !skipCache) {\n try {\n const cached = await cache.get<Observation[]>(cacheKey);\n if (cached !== null) monthRows = cached;\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] GHCNh cache.get failed for key=${cacheKey}; falling back to live fetch:`,\n cacheErr,\n );\n }\n }\n\n if (monthRows === null) {\n // --- Live fetch + parse (errors here propagate to the caller) ---\n const yearRows = await fetchYearOnce(year);\n monthRows = filterMonth(yearRows, year, month);\n\n // --- Cache write (best-effort, AFTER rows are accumulated) ------\n // iter-6 C12: `cache.set` failures MUST NOT propagate. Even an\n // empty month list is written when the year was successfully\n // fetched — it pins the \"no observations for this month\" fact so\n // the next call doesn't re-fetch the year just to discover nothing.\n const sample = monthRows[0]?.source;\n if (cache !== null && !skipCache && !isLiveSource(sample)) {\n try {\n await cache.set(cacheKey, monthRows);\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] GHCNh cache.set failed for key=${cacheKey}; in-memory rows preserved:`,\n cacheErr,\n );\n }\n }\n }\n\n for (const obs of monthRows) {\n const obsDate = obs.observed_at.slice(0, 10);\n if (obsDate >= fromDate && obsDate <= extendedTo) acc.push(obs);\n }\n }\n return acc;\n}\n\n// ---------------------------------------------------------------------------\n// Public surface\n// ---------------------------------------------------------------------------\n\n/**\n * Build daily research rows for a station + date window.\n *\n * @param station NWS 3-letter code (e.g. \"NYC\") OR 4-letter ICAO (e.g. \"KNYC\").\n * @param fromDate Inclusive start date, ISO YYYY-MM-DD (LST).\n * @param toDate Inclusive end date, ISO YYYY-MM-DD (LST).\n * @param opts See {@link ResearchOptions}.\n *\n * Returns an immutable array of frozen {@link PairsRow}s — one per LST day\n * in `[fromDate, toDate]`. Each row carries:\n * - `cli_*` populated from IEM CLI (final preferred per `mergeClimate`).\n * - `obs_*` daily aggregates over the 3-source merged observations\n * (AWC > IEM > GHCNh per `mergeObservations`).\n * - `fcst_*` unconditionally null (Mode 1).\n * - `market_close_utc` formatted `YYYY-MM-DDTHH:MM:SSZ`.\n *\n * Throws on unknown station, malformed dates, or fromDate > toDate.\n * AbortSignal propagates from underlying fetchers.\n */\nexport async function research(\n station: string,\n fromDate: string,\n toDate: string,\n opts: ResearchOptions = {},\n): Promise<ReadonlyArray<PairsRow>> {\n // ── Phase 10 selector + cross-arg validation ─────────────────────────\n //\n // The TS signature pre-dates Phase 10's composable kwargs, so the\n // `station` positional is still always passed. The new selectors live\n // on `opts` (city / contract / contracts) and are validated here:\n // exactly one of station / city / contract / contracts is allowed.\n //\n // v0.2 ships only the validation surface; the multi-station JOIN +\n // trade-attachment lands in v0.3. Passing any non-station selector\n // surfaces a clear NotImplementedError-style error so callers can\n // route via discover() + the station-path until v0.3.\n const hasCity = typeof opts.city === \"string\" && opts.city.length > 0;\n const hasContract = typeof opts.contract === \"string\" && opts.contract.length > 0;\n const hasContracts = Array.isArray(opts.contracts) && opts.contracts.length > 0;\n const hasStation = typeof station === \"string\" && station.length > 0;\n const selectorCount =\n Number(hasStation) + Number(hasCity) + Number(hasContract) + Number(hasContracts);\n if (selectorCount === 0) {\n throw new Error(\n \"research(): exactly one of station, opts.city, opts.contract, opts.contracts must be provided\",\n );\n }\n if (selectorCount > 1) {\n const names: string[] = [];\n if (hasStation) names.push(\"station\");\n if (hasCity) names.push(\"city\");\n if (hasContract) names.push(\"contract\");\n if (hasContracts) names.push(\"contracts\");\n throw new Error(`research(): selectors are mutually exclusive; got ${JSON.stringify(names)}`);\n }\n if (opts.sources !== undefined && opts.source !== undefined) {\n throw new Error(\"research(): sources and source are mutually exclusive\");\n }\n // Iter-1 codex HIGH: sources / source validation is shipped in Phase 10\n // v0.2 but the data-selection wiring lands in v0.3. Without this guard\n // the station-path runs the full multi-source merge regardless — silent\n // data-selection corruption.\n if (opts.sources !== undefined || opts.source !== undefined) {\n throw new Error(\n \"research(): sources / source validation surface is shipped in Phase 10 v0.2 \" +\n \"but the data-selection wiring lands in v0.3. For Mode 2 single-source pinning \" +\n \"today, use `researchBySource(station, source, ...)` from @mostlyrightmd/meta.\",\n );\n }\n if (opts.stationOverride !== undefined && !hasContract) {\n throw new Error(\n \"research(): stationOverride requires contract (not standalone station/city/contracts)\",\n );\n }\n if (opts.includeTrades === true && !(hasContract || hasContracts)) {\n throw new Error(\n \"research(): includeTrades requires contract or contracts (station/city selectors have no trade timeseries)\",\n );\n }\n if (hasCity || hasContract || hasContracts) {\n throw new Error(\n \"research(): city/contract/contracts selectors are validated in Phase 10 v0.2 \" +\n \"but the multi-station/multi-issuer JOIN + trade attachment lands in v0.3. \" +\n \"For now, use `discover({city})` to find the station then call \" +\n \"`research(station, fromDate, toDate)` directly.\",\n );\n }\n // ── Backwards-compat station path (existing implementation) ─────────\n const resolved = normalizeStation(station);\n const dates = buildDateList(fromDate, toDate);\n const extendedTo = plusOneDay(toDate);\n\n const fromYear = Number(fromDate.slice(0, 4));\n const toYear = Number(toDate.slice(0, 4));\n const extendedToYear = Number(extendedTo.slice(0, 4));\n\n const baseOpts: { signal?: AbortSignal } = {};\n if (opts.signal !== undefined) baseOpts.signal = opts.signal;\n\n const cache = await resolveCache(opts);\n const cacheNow = opts.now ?? new Date();\n\n // --- IEM CLI climate (per-year) ---------------------------------------\n // Cache strategy: read-through per (station code, year). Skip the current\n // LST year (mutable) and never cache `.live` sources. `iem.cli` is\n // archive → cacheable for completed years. Fetcher takes the ICAO\n // (resolved.icao), cache key uses the 3-letter NWS code (resolved.code).\n let mergedClimate: ReadonlyArray<ClimateObservation> = [];\n try {\n const cliRows = await fetchCliWithCache(\n resolved.icao,\n resolved.code,\n fromYear,\n toYear,\n opts,\n cache,\n cacheNow,\n );\n mergedClimate = mergeClimate(cliRows);\n } catch (err) {\n if (err instanceof DOMException && (err.name === \"AbortError\" || err.name === \"TimeoutError\")) {\n throw err;\n }\n // Degrade to no CLI data — buildPairs emits null cli_* for affected dates.\n }\n\n // --- AWC live observations (short-circuit on stale windows) -----------\n const awcHours = opts.awcHours ?? AWC_MAX_HOURS;\n const awcRows: Observation[] = [];\n if (anyDateOverlapsAwc(toDate, awcHours, opts.now ?? new Date())) {\n const awcOpts: { hours: number; signal?: AbortSignal } = { hours: awcHours };\n if (opts.signal !== undefined) awcOpts.signal = opts.signal;\n const awcRaw = await fetchAwcMetars([resolved.icao], awcOpts);\n for (const m of awcRaw) {\n const obs = awcToObservation(m);\n if (obs !== null) awcRows.push(obs);\n }\n }\n\n // --- IEM ASOS archive observations (per-year × {METAR, SPECI}) --------\n // IEM ASOS expects the 3-letter NWS station code (`station=NYC`),\n // NOT the 4-letter ICAO. Python `_fetchers/iem_asos.py:119` uses\n // `station={station.code}`. Use resolved.code, NOT resolved.icao.\n const iemRows = await fetchIemAsosWithCache(\n resolved.code,\n fromYear,\n extendedToYear,\n fromDate,\n extendedTo,\n opts,\n cache,\n cacheNow,\n );\n\n // --- GHCNh archive observations (US stations only) --------------------\n // iter-7 H14: now wraps `downloadGhcnh` in `fetchGhcnhWithCache` so\n // GHCNh chunks are persisted at the same per-month granularity as IEM\n // ASOS. Repeat `research()` calls for the same range skip NCEI\n // entirely on cache hit. Non-US stations short-circuit before reaching\n // the helper — GHCNh PSVs are US-only.\n let ghcnhRows: Observation[] = [];\n if (isUsStation(resolved) && resolved.ghcnhId !== null && resolved.ghcnhId.length > 0) {\n ghcnhRows = await fetchGhcnhWithCache(\n resolved.code,\n resolved.ghcnhId,\n fromDate,\n extendedTo,\n opts,\n cache,\n cacheNow,\n );\n }\n\n // --- Merge observations + bucket by settlement date -------------------\n const combinedRaw = [...awcRows, ...iemRows, ...ghcnhRows];\n const sorted = sortByObservedAtThenSource(combinedRaw);\n const merged = mergeObservations(sorted);\n\n const observationsByDate: Record<string, PairsObservationLike[]> = {};\n // dates is guaranteed non-empty by buildDateList contract (throws on\n // fromDate > toDate; both validated above).\n const dateLo = dates[0] ?? \"\";\n const dateHi = dates[dates.length - 1] ?? \"\";\n for (const obs of merged) {\n const settleDate = observedSettlementDate(obs.observed_at, resolved.code);\n if (settleDate === null) continue;\n if (settleDate < dateLo || settleDate > dateHi) continue;\n let bucket = observationsByDate[settleDate];\n if (bucket === undefined) {\n bucket = [];\n observationsByDate[settleDate] = bucket;\n }\n bucket.push(obs);\n }\n\n // --- Bucket climate by date (mergeClimate already deduped) ------------\n const climateByDate: Record<string, PairsClimateLike | null> = {};\n for (const cli of mergedClimate) {\n climateByDate[cli.observation_date] = cli;\n }\n\n // --- buildPairs join + return -----------------------------------------\n return buildPairs(resolved.code, dates, observationsByDate, climateByDate);\n}\n","// Mode 2 — source-explicit research() variant.\n//\n// Mirrors packages/core/src/mostlyright/mode2.py. Mode 1 (the existing\n// `research()`) merges AWC > IEM > GHCNh; Mode 2 lets the caller pin\n// observations to a single named source for source-identified\n// training pairs (the workflow Vojtech wanted for backtests that\n// need source-identity invariants).\n//\n// Lives in @mostlyrightmd/meta (alongside `research()`), NOT in\n// @mostlyrightmd/core — `assertSourceIdentity` consumes the\n// @mostlyrightmd/weather `Observation` type, which @mostlyrightmd/core\n// must not depend on (would create a cycle).\n//\n// ── Vocabulary ───────────────────────────────────────────────────────\n// TS narrows what Python widens: at the input boundary, TS accepts\n// ONLY the four canonical dotted-form sources. Bare forms (`iem`,\n// `awc`, `ghcnh`) are NEVER accepted at the API; they only ever\n// appear as parser-emitted PER-ROW source tags. The alias table\n// (`SOURCE_ALIASES`) bridges the boundary: filter rows whose bare\n// tag is in the dotted source's alias set, but NEVER rewrite the\n// per-row source — that would silently corrupt downstream Validator\n// invariants. See Python mode2.py:161-166 for the canonical comment.\n\nimport {\n NotFoundError,\n STATION_BY_CODE,\n STATION_BY_ICAO,\n SourceMismatchError,\n type SourceMismatchRole,\n} from \"@mostlyrightmd/core\";\nimport {\n type Observation,\n awcToObservation,\n downloadGhcnh,\n downloadIemAsos,\n fetchAwcMetars,\n parseGhcnhPsv,\n parseIemCsv,\n} from \"@mostlyrightmd/weather\";\n\nexport type { SourceMismatchRole };\n\n/** Mode 2 canonical source vocabulary. Exactly four dotted values. */\nexport const MODE2_SOURCES = [\"iem.archive\", \"iem.live\", \"awc.live\", \"ghcnh.archive\"] as const;\n\n/**\n * Mode 2 source-identity type. Const-union derived from the\n * `MODE2_SOURCES` tuple-literal (NOT a TS `enum` — `enum` defeats\n * tree-shaking per TS Architect rubric §5).\n */\nexport type Mode2Source = (typeof MODE2_SOURCES)[number];\n\n/**\n * Map each canonical dotted source to the bare parser-emitted tags\n * that satisfy it. Parsers emit bare `iem`/`awc`/`ghcnh` per\n * packages-ts/weather; mostlyright' canonical vocab is dotted. The\n * alias table bridges both at the boundary without rewriting the\n * per-row source — downstream consumers see the truthful\n * parser-emitted tag.\n *\n * Mirrors packages/core/src/mostlyright/mode2.py:55-63.\n */\nexport const SOURCE_ALIASES: ReadonlyMap<Mode2Source, ReadonlySet<string>> = new Map<\n Mode2Source,\n ReadonlySet<string>\n>([\n [\"iem.archive\", new Set([\"iem\", \"iem.archive\"])],\n [\"iem.live\", new Set([\"iem\", \"iem.live\"])],\n [\"awc.live\", new Set([\"awc\", \"awc.live\"])],\n [\"ghcnh.archive\", new Set([\"ghcnh\", \"ghcnh.archive\"])],\n]);\n\n/**\n * Type-guard: narrow an unknown value to {@link Mode2Source}. Returns\n * true iff `value` is one of the four canonical dotted strings.\n * Bare-form inputs (`'iem'`, `'awc'`, `'ghcnh'`) return false — TS\n * narrows what Python widens.\n */\nexport function isMode2Source(value: unknown): value is Mode2Source {\n return typeof value === \"string\" && (MODE2_SOURCES as readonly string[]).includes(value);\n}\n\n/**\n * Throw {@link SourceMismatchError} if any row's `source` field\n * disagrees with the expected source vocabulary. Rows missing the\n * `source` field (undefined / null / non-string) are skipped\n * (matches Python mode2.py:181-182 — `if \"source\" not in df.columns:\n * return`). Empty `rows` passes silently.\n *\n * The `expected` parameter accepts EITHER:\n *\n * - a single string — the most common case; downstream callers\n * can pass `\"iem.archive\"` and the check is `src === \"iem.archive\"`.\n * - a `ReadonlySet<string>` — used by `researchBySource` to pass\n * the {@link SOURCE_ALIASES} entry so bare-form parser tags\n * (`'iem'`) are accepted alongside the dotted canonical form\n * (`'iem.archive'`). Without this, the per-row source-preserved\n * invariant (Python mode2.py:161-166) would force the assertion\n * to fire on every Mode 2 call.\n *\n * @param rows rows to check (any shape with `source?: string`)\n * @param expected the source string OR alias-set the caller asked for\n * @param role role-name vocabulary; defaults to 'observations'\n *\n * @throws SourceMismatchError with `schemaSource` = the expected label\n * (the input string, or `[...accept].sort().join(\"|\")`\n * when an alias-set was passed), `dataSource` =\n * first sorted distinct mismatched source,\n * `role` = the caller-provided role,\n * `catalogWarning` = null.\n */\nexport function assertSourceIdentity<Row extends { source?: string | null | undefined }>(\n rows: ReadonlyArray<Row>,\n expected: string | ReadonlySet<string>,\n role: SourceMismatchRole = \"observations\",\n): void {\n const accept: ReadonlySet<string> =\n typeof expected === \"string\" ? new Set<string>([expected]) : expected;\n const expectedLabel: string =\n typeof expected === \"string\" ? expected : [...accept].sort().join(\"|\");\n\n const distinct = new Set<string>();\n let bad = 0;\n for (const r of rows) {\n const src = r?.source;\n if (typeof src !== \"string\") continue;\n if (!accept.has(src)) {\n distinct.add(src);\n bad += 1;\n }\n }\n if (bad === 0) return;\n const others = [...distinct].sort();\n const first = others[0] ?? \"<unknown>\";\n throw new SourceMismatchError(\n `Mode 2 dispatch requested '${expectedLabel}' but received ${bad} row(s) with other sources: [${others\n .map((s) => `'${s}'`)\n .join(\", \")}]`,\n {\n schemaSource: expectedLabel,\n dataSource: first,\n role,\n catalogWarning: null,\n },\n );\n}\n\n// ---------------------------------------------------------------------------\n// researchBySource — Mode 2 dispatch entry point\n// ---------------------------------------------------------------------------\n\n/** Mode 2 caller-supplied options. Subset of `ResearchOptions` — Mode 2\n * returns observations only, so forecast + climate + cache opts are\n * intentionally excluded. */\nexport interface ResearchBySourceOptions {\n /** Forward to the underlying fetcher; aborts the dispatch. */\n signal?: AbortSignal;\n /** AWC lookback window in hours (clamped by the fetcher). Default 168. */\n awcHours?: number;\n /** Polite-delay (ms) between IEM ASOS yearly chunks. Default 1000. */\n iemPolitenessMs?: number;\n}\n\n/** AWC live serves at most ~168 hours (7 days). Mirrors research.ts. */\nconst AWC_MAX_HOURS = 168;\n\ninterface ResolvedStation {\n readonly code: string;\n readonly icao: string;\n readonly country: string | null;\n readonly ghcnhId: string | null;\n}\n\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * Resolve a station identifier (3-letter NWS code OR 4-letter ICAO)\n * to the full record. Inlined here instead of imported from `research.ts`\n * to keep mode2 self-contained — the ~30-line duplication is cheaper\n * than threading internal helpers through a new module boundary.\n */\nfunction resolveStation(input: string): ResolvedStation {\n const raw = input.trim().toUpperCase();\n if (raw.length === 0) {\n throw new Error(\"station must be a non-empty string\");\n }\n const byIcao = STATION_BY_ICAO.get(raw);\n if (byIcao !== undefined) {\n if (byIcao.code === null) {\n throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);\n }\n return {\n code: byIcao.code,\n icao: byIcao.icao,\n country: byIcao.country,\n ghcnhId: byIcao.ghcnh_id,\n };\n }\n const byCode = STATION_BY_CODE.get(raw);\n if (byCode !== undefined) {\n if (byCode.code === null) {\n throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);\n }\n return {\n code: byCode.code,\n icao: byCode.icao,\n country: byCode.country,\n ghcnhId: byCode.ghcnh_id,\n };\n }\n if (raw.startsWith(\"K\") && raw.length === 4) {\n const stripped = raw.slice(1);\n const retry = STATION_BY_CODE.get(stripped);\n if (retry !== undefined && retry.code !== null) {\n return {\n code: retry.code,\n icao: retry.icao,\n country: retry.country,\n ghcnhId: retry.ghcnh_id,\n };\n }\n }\n throw new Error(\n `unknown station ${JSON.stringify(input)} — not found in STATION_BY_CODE or STATION_BY_ICAO`,\n );\n}\n\nfunction validateDateFormat(label: string, value: string): void {\n if (!DATE_RE.test(value)) {\n throw new Error(`${label} must be YYYY-MM-DD, got ${JSON.stringify(value)}`);\n }\n}\n\n/**\n * Year extracted from a YYYY-MM-DD string. Caller must validate format\n * first via `validateDateFormat`.\n */\nfunction yearOf(isoDate: string): number {\n return Number(isoDate.slice(0, 4));\n}\n\n/**\n * Mode 2 source-explicit observation fetch.\n *\n * Dispatches to a single source's fetcher (no merge) and returns raw\n * {@link Observation}s tagged with that source. Mirrors Python\n * `mostlyright.mode2.research_by_source` (packages/core/src/mostlyright/mode2.py).\n *\n * The four supported sources:\n *\n * - `'iem.archive'` → IEM ASOS historical CSVs (METAR + SPECI).\n * - `'iem.live'` → v0.1.0 parity gap; throws. Use `'iem.archive'`.\n * - `'awc.live'` → AWC live METAR JSON (≤168h lookback).\n * - `'ghcnh.archive'` → NCEI GHCNh PSV (US stations only).\n *\n * The returned rows preserve the parser-emitted per-row `source` field\n * verbatim — NEVER rewritten to the dotted canonical form. Bare tags\n * (`'iem'`, `'awc'`, `'ghcnh'`) survive intact so downstream Validator\n * schemas see the truthful provenance. Mode 2 still calls\n * {@link assertSourceIdentity} internally (defense-in-depth) before\n * returning — using the {@link SOURCE_ALIASES} entry so the bare-form\n * tags pass.\n *\n * @param station NWS 3-letter code (e.g. `\"NYC\"`) OR 4-letter ICAO (e.g. `\"KNYC\"`).\n * @param source One of {@link MODE2_SOURCES}.\n * @param fromDate Inclusive start, ISO `YYYY-MM-DD`.\n * @param toDate Inclusive end, ISO `YYYY-MM-DD`.\n * @param opts See {@link ResearchBySourceOptions}.\n *\n * @returns Frozen array of {@link Observation}s whose `source` is in\n * `SOURCE_ALIASES.get(source)`. Empty array on no data\n * (NOT a throw).\n *\n * @throws Error if `source` is not one of {@link MODE2_SOURCES}.\n * Throws BEFORE any network call — no quota burn\n * on invalid input.\n * @throws Error if `source === 'iem.live'` (v0.1.0 parity gap;\n * v0.2 will add per-month live IEM).\n * @throws Error if `station` is unknown, or dates are malformed.\n * @throws NotFoundError if `source === 'ghcnh.archive'` and `station`\n * is non-US (GHCNh PSV files are US-only).\n * @throws SourceMismatchError if a row's `source` disagrees with the alias\n * set for `source` (defense-in-depth; should\n * never fire under correct fetcher behavior).\n */\nexport async function researchBySource(\n station: string,\n source: Mode2Source,\n fromDate: string,\n toDate: string,\n opts: ResearchBySourceOptions = {},\n): Promise<ReadonlyArray<Observation>> {\n // ── Synchronous-style guards (BEFORE any network call) ────────────\n // Architect rubric: unknown-source rejection MUST run before any\n // fetcher import/call (else invalid input burns API quota).\n if (!isMode2Source(source)) {\n throw new Error(\n `Mode 2 source must be one of ${JSON.stringify(\n MODE2_SOURCES,\n )}; got ${JSON.stringify(source)}`,\n );\n }\n if (source === \"iem.live\") {\n throw new Error(\n \"Mode 2 source 'iem.live' not yet implemented in v0.1.0 \" +\n \"(Parity-Ticket: requires per-month live IEM endpoint not yet ported). \" +\n \"Use 'iem.archive' for historical IEM rows.\",\n );\n }\n validateDateFormat(\"fromDate\", fromDate);\n validateDateFormat(\"toDate\", toDate);\n if (fromDate > toDate) {\n throw new Error(`fromDate (${fromDate}) must be <= toDate (${toDate})`);\n }\n const resolved = resolveStation(station);\n\n const accept = SOURCE_ALIASES.get(source);\n if (accept === undefined) {\n // Unreachable — isMode2Source guard above guarantees a hit.\n throw new Error(`internal: no SOURCE_ALIASES entry for '${source}'`);\n }\n\n // ── Per-source dispatch ──────────────────────────────────────────\n let rows: ReadonlyArray<Observation>;\n switch (source) {\n case \"awc.live\": {\n const awcOpts: { hours: number; signal?: AbortSignal } = {\n hours: opts.awcHours ?? AWC_MAX_HOURS,\n };\n if (opts.signal !== undefined) awcOpts.signal = opts.signal;\n const raw = await fetchAwcMetars([resolved.icao], awcOpts);\n const parsed: Observation[] = [];\n for (const m of raw) {\n const obs = awcToObservation(m);\n if (obs !== null) parsed.push(obs);\n }\n // Filter to the queried [fromDate, toDate] window (inclusive) — match\n // the IEM/GHCNh branches. AWC's lookback (~168h) can return METARs\n // outside the caller's window; per-source Mode 2 callers expect rows\n // strictly inside [fromDate, toDate]. Python mode2.py parity.\n rows = parsed.filter((r) => {\n const d = r.observed_at.slice(0, 10);\n return d >= fromDate && d <= toDate;\n });\n break;\n }\n case \"iem.archive\": {\n const fromYear = yearOf(fromDate);\n const toYear = yearOf(toDate);\n const collected: Observation[] = [];\n for (let year = fromYear; year <= toYear; year++) {\n for (const reportType of [3, 4] as const) {\n const iemOpts: {\n reportType: 3 | 4;\n politenessMs: number;\n signal?: AbortSignal;\n } = {\n reportType,\n politenessMs: opts.iemPolitenessMs ?? 1000,\n };\n if (opts.signal !== undefined) iemOpts.signal = opts.signal;\n const chunks = await downloadIemAsos(\n resolved.code,\n `${year}-01-01`,\n `${year}-12-31`,\n iemOpts,\n );\n for (const chunk of chunks) {\n const parsed = parseIemCsv(chunk.csv, {\n observationTypeOverride: reportType === 3 ? \"METAR\" : \"SPECI\",\n });\n collected.push(...parsed);\n }\n }\n }\n // Filter to the queried [fromDate, toDate] window (inclusive).\n rows = collected.filter((r) => {\n const d = r.observed_at.slice(0, 10);\n return d >= fromDate && d <= toDate;\n });\n break;\n }\n case \"ghcnh.archive\": {\n // GHCNh PSV files are US-only. Non-US stations are advertised by\n // null `ghcnh_id` and country !== \"US\" in the codegen.\n if (resolved.country !== \"US\" || resolved.ghcnhId === null || resolved.ghcnhId.length === 0) {\n throw new NotFoundError(\n `GHCNh archive is US-only; station ${JSON.stringify(station)} ` +\n `(country=${resolved.country ?? \"null\"}, ghcnh_id=${\n resolved.ghcnhId === null ? \"null\" : JSON.stringify(resolved.ghcnhId)\n }) has no GHCNh coverage`,\n );\n }\n const fromYear = yearOf(fromDate);\n const toYear = yearOf(toDate);\n const collected: Observation[] = [];\n for (let year = fromYear; year <= toYear; year++) {\n const ghcnhOpts: { signal?: AbortSignal } = {};\n if (opts.signal !== undefined) ghcnhOpts.signal = opts.signal;\n try {\n const yr = await downloadGhcnh(resolved.ghcnhId, year, ghcnhOpts);\n const parsed = parseGhcnhPsv(yr.psv);\n for (const r of parsed) {\n if (r.station_code === resolved.code) collected.push(r);\n }\n } catch (err) {\n // 404 = no data for this station-year (typical for partial /\n // pre-1973 years). Mirrors Python research.py 404-as-skip.\n if (err instanceof NotFoundError) continue;\n throw err;\n }\n }\n rows = collected.filter((r) => {\n const d = r.observed_at.slice(0, 10);\n return d >= fromDate && d <= toDate;\n });\n break;\n }\n // iem.live is rejected above; the type narrowing here is\n // exhaustive over Mode2Source minus iem.live (which is unreachable).\n }\n\n // ── Filter to the alias set (Python parity: row keep iff parser-tag in alias) ──\n // biome-ignore lint/style/noNonNullAssertion: rows is assigned in every reachable case\n const filtered = rows!.filter((r) => accept.has(r.source));\n\n // ── Defense-in-depth: assertSourceIdentity (Python mode2.py:173-193) ────\n // Empty result still passes (no rows → no mismatch).\n assertSourceIdentity(filtered, accept, \"observations\");\n\n return filtered;\n}\n","// Phase 10 — composable research() dispatcher (TS port of\n// packages/core/src/mostlyright/_compose.py).\n//\n// Translates the new selectors (`city`, `contract`, `contracts`) into\n// resolution metadata + station lists. Pure logic, no I/O.\n\nimport {\n KALSHI_SETTLEMENT_STATIONS,\n type KalshiStation,\n POLYMARKET_CITY_STATIONS,\n} from \"@mostlyrightmd/markets\";\nimport { POLYMARKET_KNOWN_WRONG_STATIONS } from \"@mostlyrightmd/markets/polymarket\";\n\n/** The four mutually-exclusive selector names. */\nexport const SELECTOR_NAMES = [\"station\", \"city\", \"contract\", \"contracts\"] as const;\nexport type SelectorName = (typeof SELECTOR_NAMES)[number];\n\n/**\n * Kalshi short-ticker → canonical city slug. Real Kalshi tickers use\n * variable-length city suffixes: `KXHIGHNY-...` (NY → NYC),\n * `KXHIGHCHI-...` (CHI → CHI). The `KALSHI_SETTLEMENT_STATIONS` catalog\n * is keyed by the canonical 3-letter slug; this alias normalizes the\n * variable-length Kalshi suffix to the catalog key before lookup.\n */\nconst KALSHI_TICKER_ALIASES: Record<string, string> = {\n NY: \"NYC\",\n};\n\n/**\n * Kalshi-short ↔ Polymarket-long city slug alias. Iter-1 python-architect\n * HIGH: without this, `resolveCity(\"LAX\")` would miss Polymarket's KLAX\n * (keyed as `los_angeles`); `resolveCity(\"chicago\")` would miss Kalshi's\n * KMDW (keyed as `CHI`). Bi-directional probe surfaces the full\n * cross-issuer settlement neighborhood regardless of which slug form\n * the caller passed.\n */\nconst CITY_SLUG_ALIASES: Record<string, readonly [string, string]> = {\n // short_kalshi (lower) → [polymarket_long, kalshi_upper]\n nyc: [\"nyc\", \"NYC\"],\n chi: [\"chicago\", \"CHI\"],\n lax: [\"los_angeles\", \"LAX\"],\n mia: [\"miami\", \"MIA\"],\n den: [\"denver\", \"DEN\"],\n bos: [\"boston\", \"BOS\"],\n aus: [\"austin\", \"AUS\"],\n dca: [\"washington_dc\", \"DCA\"],\n phl: [\"philadelphia\", \"PHL\"],\n sfo: [\"san_francisco\", \"SFO\"],\n sea: [\"seattle\", \"SEA\"],\n atl: [\"atlanta\", \"ATL\"],\n hou: [\"houston\", \"HOU\"],\n dal: [\"dallas\", \"DAL\"],\n phx: [\"phoenix\", \"PHX\"],\n msp: [\"minneapolis\", \"MSP\"],\n dtw: [\"detroit\", \"DTW\"],\n};\n\nconst CITY_SLUG_ALIASES_REVERSE: Record<string, readonly [string, string]> = (() => {\n const out: Record<string, readonly [string, string]> = {};\n for (const [shortLower, [longPoly, kalshiUpper]] of Object.entries(CITY_SLUG_ALIASES)) {\n out[longPoly] = [shortLower, kalshiUpper];\n }\n return out;\n})();\n\n/** Return `[polymarket_slug_lower, kalshi_slug_upper]` for `city`. */\nfunction normalizeCitySlugs(city: string): readonly [string, string] {\n const lower = city.toLowerCase();\n const upper = city.toUpperCase();\n const direct = CITY_SLUG_ALIASES[lower];\n if (direct !== undefined) return direct;\n const reverse = CITY_SLUG_ALIASES_REVERSE[lower];\n if (reverse !== undefined) return [lower, reverse[1]];\n return [lower, upper];\n}\n\n/**\n * Structured warning emitted when `stationOverride` deliberately\n * mismatches the contract's canonical settlement station. The output\n * row carries `settlementMismatch: true`.\n *\n * JS has no `warnings.warn()` analogue; callers receive these via the\n * `onWarning?` callback in ResearchOptions.\n */\nexport interface StationOverrideWarning {\n readonly kind: \"StationOverrideWarning\";\n readonly contractStation: string;\n readonly overrideStation: string;\n readonly message: string;\n}\n\n/** Selector kwargs accepted by research(). Exactly one MUST be provided. */\nexport interface SelectorArgs {\n readonly station?: string;\n readonly city?: string;\n readonly contract?: string;\n readonly contracts?: ReadonlyArray<string>;\n}\n\n/**\n * Validate selector arity. Returns the active selector name; throws when\n * zero or >1 selectors are provided.\n */\nexport function validateSelectors(args: SelectorArgs): SelectorName {\n const provided: SelectorName[] = [];\n if (typeof args.station === \"string\" && args.station.length > 0) provided.push(\"station\");\n if (typeof args.city === \"string\" && args.city.length > 0) provided.push(\"city\");\n if (typeof args.contract === \"string\" && args.contract.length > 0) provided.push(\"contract\");\n if (Array.isArray(args.contracts) && args.contracts.length > 0) provided.push(\"contracts\");\n\n if (provided.length === 0) {\n throw new Error(\n \"research(): exactly one of station, city, contract, contracts must be provided\",\n );\n }\n if (provided.length > 1) {\n throw new Error(\n `research(): selectors are mutually exclusive; got ${JSON.stringify(provided)}`,\n );\n }\n return provided[0] as SelectorName;\n}\n\n/**\n * Resolve a `\"<issuer>:<id>\"` contract id to `[station, issuer]`.\n *\n * Supported: `kalshi:KHIGH<CITY>` / `kalshi:KXHIGH<CITY>-<DATE>-<STRIKE>`\n * and `kalshi:KLOW<CITY>` / `kalshi:KXLOW<CITY>-<DATE>-<STRIKE>`.\n *\n * Polymarket contract resolution requires an event_id → station lookup\n * (via polymarket-discover); Phase 10 v0.2 defers to v0.3 and throws.\n */\nexport function resolveContract(contractId: string): readonly [string, string] {\n if (typeof contractId !== \"string\" || !contractId.includes(\":\")) {\n throw new TypeError(`contract id must be \\`<issuer>:<id>\\`; got ${JSON.stringify(contractId)}`);\n }\n const colonIdx = contractId.indexOf(\":\");\n const issuer = contractId.slice(0, colonIdx).toLowerCase();\n const raw = contractId.slice(colonIdx + 1);\n const rawUpper = raw.toUpperCase();\n\n if (issuer === \"kalshi\") {\n // Strip KX exchange prefix (KXHIGHNYC → KHIGHNYC) and trailing\n // -DATE-STRIKE suffix to recover the legacy KHIGH<CITY> / KLOW<CITY>\n // shape the KALSHI_SETTLEMENT_STATIONS map keys are derived from.\n let normalized = rawUpper;\n if (normalized.startsWith(\"KX\")) {\n normalized = `K${normalized.slice(2)}`;\n }\n const cityOnly = normalized.split(\"-\", 1)[0] ?? \"\";\n let cityTickerRaw: string | null = null;\n if (cityOnly.startsWith(\"KHIGH\") && cityOnly.length > 5) {\n cityTickerRaw = cityOnly.slice(5);\n } else if (cityOnly.startsWith(\"KLOW\") && cityOnly.length > 4) {\n cityTickerRaw = cityOnly.slice(4);\n } else {\n throw new Error(\n `unsupported kalshi contract format: ${JSON.stringify(raw)}; expected KHIGH<CITY>* / KXHIGH<CITY>* / KLOW<CITY>* / KXLOW<CITY>* prefix`,\n );\n }\n // Iter-1 codex HIGH: normalize variable-length Kalshi ticker suffix\n // (NY → NYC, etc.) via the alias table before the catalog lookup.\n const cityTicker = KALSHI_TICKER_ALIASES[cityTickerRaw] ?? cityTickerRaw;\n const entry: KalshiStation | undefined = KALSHI_SETTLEMENT_STATIONS[cityTicker];\n if (entry === undefined) {\n throw new Error(`unknown Kalshi city ticker: ${JSON.stringify(cityTicker)}`);\n }\n return [entry.station, \"kalshi\"] as const;\n }\n if (issuer === \"polymarket\") {\n throw new Error(\n \"polymarket contract resolution requires event_id → station lookup via \" +\n \"polymarketDiscover()/polymarketSettle(); Phase 10 v0.2 defers this to \" +\n \"v0.3. Use `city: 'nyc'` or pass `stationOverride` until then.\",\n );\n }\n throw new Error(\n `unknown issuer prefix: ${JSON.stringify(issuer)}; expected kalshi or polymarket`,\n );\n}\n\n/**\n * Resolve a city slug to all stations any issuer settles against.\n * Returns deduplicated array in stable order: Kalshi → Polymarket default/high/low\n * → Polymarket denylist backstops.\n */\nexport function resolveCity(city: string): readonly string[] {\n if (typeof city !== \"string\" || !city) {\n throw new Error(`city must be a non-empty string; got ${JSON.stringify(city)}`);\n }\n // Iter-1 python-architect HIGH: cross-issuer slug alias surfaces the\n // full settlement neighborhood for either input form.\n const [polySlug, kalshiSlug] = normalizeCitySlugs(city);\n const out: string[] = [];\n\n const kalshi = KALSHI_SETTLEMENT_STATIONS[kalshiSlug];\n if (kalshi !== undefined && !out.includes(kalshi.station)) {\n out.push(kalshi.station);\n }\n const poly = POLYMARKET_CITY_STATIONS[polySlug];\n if (poly !== undefined) {\n for (const measure of [\"default\", \"high\", \"low\"] as const) {\n const st = poly[measure];\n if (typeof st === \"string\" && !out.includes(st)) out.push(st);\n }\n }\n const wrong = POLYMARKET_KNOWN_WRONG_STATIONS[polySlug];\n if (wrong !== undefined) {\n const sortedWrong = [...wrong].sort();\n for (const st of sortedWrong) {\n if (!out.includes(st)) out.push(st);\n }\n }\n if (out.length === 0) {\n throw new Error(`unknown city ${JSON.stringify(city)}; not in kalshi or polymarket catalogs`);\n }\n return out;\n}\n\n/**\n * Return the list of `\"<issuer>:<ticker>\"` markers that settle against\n * `station` for `city`. Empty array when no issuer settles against this\n * station (typically a denylist backstop).\n */\nexport function annotateSettlesFor(station: string, city: string | null): readonly string[] {\n if (city === null) return [];\n // Iter-1 python-architect HIGH: cross-issuer slug alias annotates both\n // issuers regardless of slug form.\n const [polySlug, kalshiSlug] = normalizeCitySlugs(city);\n const out: string[] = [];\n const kalshi = KALSHI_SETTLEMENT_STATIONS[kalshiSlug];\n if (kalshi !== undefined && kalshi.station === station) {\n out.push(`kalshi:${kalshiSlug}`);\n }\n const poly = POLYMARKET_CITY_STATIONS[polySlug];\n if (poly !== undefined) {\n for (const measure of [\"default\", \"high\", \"low\"] as const) {\n if (poly[measure] === station) {\n out.push(`polymarket:${polySlug}`);\n break;\n }\n }\n }\n return out.sort();\n}\n\n/**\n * Build a structured `StationOverrideWarning` payload. Callers receive\n * these via the optional `onWarning?` callback on research options.\n */\nexport function buildOverrideWarning(\n contractStation: string,\n overrideStation: string,\n): StationOverrideWarning {\n return {\n kind: \"StationOverrideWarning\",\n contractStation,\n overrideStation,\n message: `stationOverride=${JSON.stringify(overrideStation)} differs from contract's canonical settlement station ${JSON.stringify(contractStation)}; output row will carry settlementMismatch=true`,\n };\n}\n","// Phase 10 — discover({city}) ergonomic surface (TS port of\n// packages/core/src/mostlyright/discover.py).\n//\n// Pre-research lookup. Shows quants which station settles which issuer's\n// market for a given city so they can pick the right selector before\n// invoking research(). Especially useful for cross-issuer cities like NYC\n// where Kalshi settles against KNYC and Polymarket against KLGA.\n\nimport { annotateSettlesFor, resolveCity } from \"./compose.js\";\n\n/** One row per station in the resolved city neighborhood. */\nexport interface DiscoverRow {\n /** Echo of the input city. */\n readonly city: string;\n /** 4-char K-prefix ICAO. */\n readonly station: string;\n /**\n * `\"<issuer>:<ticker>\"` markers that resolve against this station.\n * Empty array = denylist backstop surfaced for explicit awareness.\n */\n readonly settlesFor: ReadonlyArray<string>;\n}\n\n/** Envelope mirrors the Python `df.attrs` pattern. */\nexport interface DiscoverResult {\n readonly rows: ReadonlyArray<DiscoverRow>;\n readonly city: string;\n readonly source: \"discover\";\n}\n\n/**\n * Return per-station discovery table for `city`.\n *\n * Each row shows one settlement station + the issuer:ticker markers that\n * resolve against it. Stations in the per-city Polymarket denylist also\n * appear with empty `settlesFor` so quants see the full neighborhood\n * before deciding whether to use `stationOverride`.\n *\n * @example\n * const result = discover({ city: \"NYC\" });\n * // rows include {station: \"KNYC\", settlesFor: [\"kalshi:NYC\"]},\n * // {station: \"KLGA\", settlesFor: [\"polymarket:nyc\"]},\n * // {station: \"KJFK\", settlesFor: []}, // denylist\n * // {station: \"KEWR\", settlesFor: []}.\n */\nexport function discover(args: { readonly city: string }): DiscoverResult {\n if (typeof args !== \"object\" || args === null) {\n throw new TypeError(`discover(): args must be an object; got ${typeof args}`);\n }\n const stations = resolveCity(args.city);\n const rows: DiscoverRow[] = stations.map((station) => ({\n city: args.city,\n station,\n settlesFor: annotateSettlesFor(station, args.city),\n }));\n return Object.freeze({\n rows: Object.freeze(rows),\n city: args.city,\n source: \"discover\" as const,\n });\n}\n"],"mappings":"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","names":["import_core","import_weather","import_markets","import_core","import_weather","AWC_MAX_HOURS","DATE_RE","import_weather","import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/research.ts","../src/mode2.ts","../src/compose.ts","../src/discover.ts"],"sourcesContent":["// mostlyright (meta) — convenience re-export of the three scoped packages.\n// Use this if you want a single `import { research } from \"mostlyright\"` entry point;\n// otherwise import the scoped packages directly.\n//\n// Note: each underlying package exports its own `version` constant; to avoid\n// ambiguous re-exports we expose them under namespaced module objects in\n// addition to a top-level `version` for the meta package itself.\n\nexport { helloCore } from \"@mostlyrightmd/core\";\nexport { helloWeather } from \"@mostlyrightmd/weather\";\nexport { helloMarkets } from \"@mostlyrightmd/markets\";\n\nimport * as core from \"@mostlyrightmd/core\";\nimport * as markets from \"@mostlyrightmd/markets\";\nimport * as weather from \"@mostlyrightmd/weather\";\n\nexport { core, markets, weather };\n\n// TS-W2 Wave 4: full multi-source `research()` orchestrator (AWC + IEM\n// ASOS + GHCNh + CLI). Lives here (NOT in @mostlyrightmd/core) so the core\n// package stays dep-free; the orchestrator pulls in both core + weather.\n// `PairsRow` is the canonical row shape from @mostlyrightmd/core/internal/pairs.\nexport { research, type ResearchOptions, type PairsRow } from \"./research.js\";\n\n// TS-W4 Wave 1: Mode 2 source-explicit dispatch (researchBySource +\n// assertSourceIdentity + Mode2Source const-union). Lives in the meta\n// package alongside research() — the dispatch needs the @mostlyrightmd/weather\n// Observation type and assertSourceIdentity consumes it structurally;\n// @mostlyrightmd/core must NOT depend on weather (cycle).\nexport {\n MODE2_SOURCES,\n SOURCE_ALIASES,\n assertSourceIdentity,\n isMode2Source,\n researchBySource,\n type Mode2Source,\n type ResearchBySourceOptions,\n type SourceMismatchRole,\n} from \"./mode2.js\";\n\n// Phase 10: composable research() dispatcher + discover() ergonomic\n// surface. Lives in @mostlyrightmd/meta because compose.ts pulls in both\n// @mostlyrightmd/core (cache, station registry) and @mostlyrightmd/markets\n// (Kalshi catalog + Polymarket catalog + denylist) — keeping it in\n// core would create a cycle (markets depends on core).\nexport {\n SELECTOR_NAMES,\n annotateSettlesFor,\n buildOverrideWarning,\n resolveCity,\n resolveContract,\n validateSelectors,\n type SelectorArgs,\n type SelectorName,\n type StationOverrideWarning,\n} from \"./compose.js\";\n\nexport { discover, type DiscoverResult, type DiscoverRow } from \"./discover.js\";\n\n// Phase 11 — `mostlyright.live` ticker surface re-exported through the\n// meta package so all three import shapes resolve to the same surface:\n// import { stream, latest } from \"mostlyright\" // meta\n// import { stream } from \"@mostlyrightmd/weather\" // main barrel\n// import { stream } from \"@mostlyrightmd/weather/live\" // subpath\nexport {\n POLITE_FLOORS_S,\n SOURCE_IDENTITY_TAGS,\n SUPPORTED_SOURCES,\n isLiveSource,\n latest,\n sourceTag,\n stream,\n validatePollSeconds,\n validateSource,\n type LatestOptions,\n type LiveObservation,\n type LiveSource,\n type LiveSourceTag,\n type StreamOptions,\n} from \"@mostlyrightmd/weather\";\nexport { LiveStreamError, NoLiveDataError } from \"@mostlyrightmd/core\";\n\n/**\n * Placeholder version string for the meta package. The authoritative\n * package version lives in `package.json#version` (currently\n * `0.1.0-rc.7`); this constant has not been bumped. Sibling packages\n * (`@mostlyrightmd/core` / `weather` / `markets`) each export their own\n * `version` constant, exposed here via the namespaced module objects.\n */\nexport const version = \"0.0.0\";\n","// `research()` orchestrator — TS-W2 multi-source Mode 1 join.\n//\n// Wires all four observation sources (AWC live, IEM ASOS archive, GHCNh\n// archive, IEM CLI climate) into the canonical `PairsRow` shape via\n// mergeObservations + mergeClimate + buildPairs. Mode 1 only — all\n// `fcst_*` columns are unconditionally null in this phase.\n//\n// Lives in `packages-ts/meta/` so `@mostlyrightmd/core` stays dep-free; this\n// orchestrator imports from both core (snapshot math + station table +\n// merge + pairs) and weather (4 fetchers + 4 parsers).\n//\n// W2 scope: AWC + IEM ASOS + GHCNh + CLI; no cache (TS-W3), no Mode 2\n// (TS-W4), no forecast (TS-W5+), no parallel prefetch (TS-W3+). Fetches\n// are sequential — fine for the parity gate; performance work is later.\n\nimport {\n NotFoundError,\n STATION_BY_CODE,\n STATION_BY_ICAO,\n settlementDateFor,\n} from \"@mostlyrightmd/core\";\nimport {\n type CacheStore,\n cacheKeyForClimate,\n cacheKeyForObservations,\n defaultCacheStore,\n isLiveSource,\n isWithinVolatileWindow,\n isWritableMonth,\n isWritableYear,\n shouldSkipCacheForCurrentLstMonth,\n shouldSkipCacheForCurrentLstYear,\n} from \"@mostlyrightmd/core/internal/cache\";\nimport { mergeClimate, mergeObservations } from \"@mostlyrightmd/core/internal/merge\";\nimport {\n type PairsClimateLike,\n type PairsObservationLike,\n type PairsRow,\n buildPairs,\n} from \"@mostlyrightmd/core/internal/pairs\";\nimport {\n type ClimateObservation,\n type Observation,\n awcToObservation,\n downloadCliRange,\n downloadGhcnh,\n downloadIemAsos,\n fetchAwcMetars,\n parseCliResponse,\n parseGhcnhPsv,\n parseIemCsv,\n} from \"@mostlyrightmd/weather\";\n\n// Re-export PairsRow so callers can `import { research, type PairsRow } from \"mostlyright\"`.\nexport type { PairsRow } from \"@mostlyrightmd/core/internal/pairs\";\n\nconst AWC_MAX_HOURS = 168;\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface ResearchOptions {\n /** Forward to all underlying fetchers; aborts the whole pipeline. */\n signal?: AbortSignal;\n /** AWC lookback window in hours. Default 168 (AWC max). Clamped by the fetcher. */\n awcHours?: number;\n /** Polite-delay (ms) between successive IEM ASOS year chunks. Default 1000. */\n iemPolitenessMs?: number;\n /** Polite-delay (ms) between successive GHCNh year requests. Default 1000. */\n ghcnhPolitenessMs?: number;\n /** Polite-delay (ms) between successive CLI year requests. Default 1000. */\n cliPolitenessMs?: number;\n /**\n * Reference clock for the AWC-window overlap check (test-only seam).\n * Defaults to `new Date()`. Pass an override to force-include AWC for\n * historical date ranges in unit tests.\n */\n now?: Date;\n /**\n * Pluggable cache backend (TS-W3). When omitted, uses\n * `defaultCacheStore()` (auto-detects IndexedDB → FsStore → MemoryStore).\n * Pass `null` to opt out of caching entirely.\n */\n cache?: CacheStore | null;\n\n // ── Phase 10: composable selectors (mutually exclusive with station). ──\n //\n // Per the Phase 10 v0.2 scope, the validation surface is shipped on\n // both Python and TS; the multi-station / multi-issuer JOIN +\n // trade-attachment is deferred to v0.3. Passing any of the three\n // selectors below currently throws a clear NotImplementedError-like\n // error pointing callers at `discover()` + the station= path until\n // v0.3 lands.\n\n /** Cross-issuer city selector. Returns rows for every station that any\n * issuer settles against (Kalshi + Polymarket + denylist backstops). */\n city?: string;\n /** Single-contract selector. Format: `\"<issuer>:<id>\"` (e.g.\n * `\"kalshi:KXHIGHNYC-25MAY26-T79\"`). Auto-resolves to the contract's\n * canonical settlement station via the Phase 8 catalog. */\n contract?: string;\n /** Multi-contract selector for basis-trade research. */\n contracts?: ReadonlyArray<string>;\n /** Override the contract's canonical settlement station. Emits a\n * StationOverrideWarning via `onWarning?`; output row carries\n * `settlementMismatch: true`. Only valid with `contract` selector. */\n stationOverride?: string;\n /** Mode 1 source subset — dedupe within. Mutually exclusive with `source`. */\n sources?: ReadonlyArray<string>;\n /** Mode 2 single-source pin — error on mismatch. Mutually exclusive with `sources`. */\n source?: string;\n /** Attach per-issuer trade timeseries via @mostlyrightmd/markets/trades.\n * Requires `contract` or `contracts`. */\n includeTrades?: boolean;\n /** Callback receiving Phase 10 StationOverrideWarning (no `warnings.warn()`\n * analogue in JS). */\n onWarning?: (w: import(\"./compose.js\").StationOverrideWarning) => void;\n}\n\n/**\n * Resolve the cache from opts. `null` means opt-out (returns null).\n *\n * Iter-1 H3: `defaultCacheStore()` is now async (FsStore loaded via\n * dynamic import behind a Node feature-detect). Caller already runs\n * inside `research()`'s async path, so awaiting here is free.\n */\nasync function resolveCache(opts: ResearchOptions): Promise<CacheStore | null> {\n if (opts.cache === null) return null;\n if (opts.cache !== undefined) return opts.cache;\n return await defaultCacheStore();\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\ninterface ResolvedStation {\n readonly code: string;\n readonly icao: string;\n readonly tz: string;\n readonly country: string | null;\n readonly ghcnhId: string | null;\n}\n\nfunction normalizeStation(input: string): ResolvedStation {\n const raw = input.trim().toUpperCase();\n if (raw.length === 0) {\n throw new Error(\"station must be a non-empty string\");\n }\n const byIcao = STATION_BY_ICAO.get(raw);\n if (byIcao !== undefined) {\n if (byIcao.code === null) {\n throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);\n }\n return {\n code: byIcao.code,\n icao: byIcao.icao,\n tz: byIcao.tz,\n country: byIcao.country,\n ghcnhId: byIcao.ghcnh_id,\n };\n }\n const byCode = STATION_BY_CODE.get(raw);\n if (byCode !== undefined) {\n if (byCode.code === null) {\n throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);\n }\n return {\n code: byCode.code,\n icao: byCode.icao,\n tz: byCode.tz,\n country: byCode.country,\n ghcnhId: byCode.ghcnh_id,\n };\n }\n if (raw.startsWith(\"K\") && raw.length === 4) {\n const stripped = raw.slice(1);\n const retry = STATION_BY_CODE.get(stripped);\n if (retry !== undefined && retry.code !== null) {\n return {\n code: retry.code,\n icao: retry.icao,\n tz: retry.tz,\n country: retry.country,\n ghcnhId: retry.ghcnh_id,\n };\n }\n }\n throw new Error(\n `unknown station ${JSON.stringify(input)} — not found in STATION_BY_CODE or STATION_BY_ICAO`,\n );\n}\n\nfunction parseIsoDate(s: string): Date {\n if (!DATE_RE.test(s)) {\n throw new Error(`expected YYYY-MM-DD, got ${JSON.stringify(s)}`);\n }\n const [yStr, mStr, dStr] = s.split(\"-\");\n const year = Number(yStr);\n const month = Number(mStr);\n const day = Number(dStr);\n const ms = Date.UTC(year, month - 1, day);\n const d = new Date(ms);\n if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) {\n throw new Error(`invalid calendar date ${JSON.stringify(s)}`);\n }\n return d;\n}\n\nfunction formatDate(d: Date): string {\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth() + 1;\n const day = d.getUTCDate();\n const mm = m < 10 ? `0${m}` : `${m}`;\n const dd = day < 10 ? `0${day}` : `${day}`;\n return `${y}-${mm}-${dd}`;\n}\n\nfunction buildDateList(fromDate: string, toDate: string): ReadonlyArray<string> {\n const from = parseIsoDate(fromDate);\n const to = parseIsoDate(toDate);\n if (from.getTime() > to.getTime()) {\n throw new Error(`fromDate (${fromDate}) must be <= toDate (${toDate})`);\n }\n const dates: string[] = [];\n for (let cursor = from.getTime(); cursor <= to.getTime(); cursor += 24 * 3_600_000) {\n dates.push(formatDate(new Date(cursor)));\n }\n return dates;\n}\n\n/** Plus-one-day in UTC. Used to extend the upper bound so the final LST\n * settlement window's pre-midnight UTC tail observations are captured. */\nfunction plusOneDay(isoDate: string): string {\n const d = parseIsoDate(isoDate);\n return formatDate(new Date(d.getTime() + 24 * 3_600_000));\n}\n\n/** US stations only — GHCNh PSV archive is US-only. International stations\n * have `ghcnh_id: null` AND `country !== \"US\"` in the TS codegen. */\nfunction isUsStation(station: ResolvedStation): boolean {\n return station.country === \"US\";\n}\n\n/** Returns true if any date in `[fromDate, toDate]` is within `hours` of `now`.\n * Mirrors Python `_month_overlaps_awc_window` semantics — defensive\n * short-circuit so we don't hit AWC for purely historical windows. */\nfunction anyDateOverlapsAwc(toDate: string, hours: number, now: Date): boolean {\n const to = parseIsoDate(toDate);\n // Window includes the END of toDate (LST close), so add 24h to the upper bound.\n const toEndMs = to.getTime() + 24 * 3_600_000;\n const nowMs = now.getTime();\n const cutoffMs = nowMs - hours * 3_600_000;\n return toEndMs >= cutoffMs;\n}\n\nfunction observedSettlementDate(observedAt: string, station: string): string | null {\n const ms = Date.parse(observedAt);\n if (!Number.isFinite(ms)) return null;\n try {\n return settlementDateFor(new Date(ms), station);\n } catch {\n return null;\n }\n}\n\n/** Lexicographic-on-`observed_at` sort, stable in `source`. Ensures\n * byte-equivalent float aggregation in `_obsAggregates` (mean is\n * non-associative for floats). */\nfunction sortByObservedAtThenSource(rows: ReadonlyArray<Observation>): Observation[] {\n return [...rows].sort((a, b) => {\n if (a.observed_at < b.observed_at) return -1;\n if (a.observed_at > b.observed_at) return 1;\n if (a.source < b.source) return -1;\n if (a.source > b.source) return 1;\n return 0;\n });\n}\n\n/**\n * True iff the end-of-year ISO date for `year` falls inside the 30-day\n * volatile amendment window relative to `now`. Used to gate archive\n * cache reads/writes for IEM ASOS yearly chunks AND IEM CLI yearly\n * chunks (iter-5 H9). Rationale: rows from a year whose 12-31 boundary\n * is within 30 days of \"now\" may still be amended upstream; caching\n * them would persist soon-to-be-stale values.\n *\n * For `year` strictly less than the current calendar year of `now`, the\n * 12-31 boundary is well past 30 days back → returns false (cacheable).\n * For `year` equal to the current LST year, the year-end is in the\n * future relative to `now` → predicate returns false (the per-year\n * current-LST-year gate handles that case first and is still required).\n * The window only fires for the immediate-post-year window — exactly\n * the case where freshly-archived rows are most likely to be revised.\n */\nfunction isYearVolatile(year: number, now: Date): boolean {\n const yearEnd = `${String(year).padStart(4, \"0\")}-12-31`;\n return isWithinVolatileWindow(yearEnd, formatDate(now), 30);\n}\n\n/**\n * Last calendar day of `(year, month)`. Used as archive-as-of for the\n * per-month volatile-window gate (iter-7 H13). Returns YYYY-MM-DD.\n */\nfunction lastDayOfMonth(year: number, month: number): string {\n // UTC math: day 0 of (month+1) === last day of (month).\n const d = new Date(Date.UTC(year, month, 0));\n return formatDate(d);\n}\n\n/**\n * True iff the end-of-month ISO date for `(year, month)` falls inside the\n * 30-day volatile amendment window relative to `now`. Per-month analog of\n * `isYearVolatile`, used to gate the per-month observations cache\n * (iter-7 H13). Rationale: rows from a month whose final day is within\n * 30 days of \"now\" may still be amended upstream; caching them would\n * persist soon-to-be-stale values. The window only fires for the\n * immediate-post-month window — exactly the case where freshly-archived\n * rows are most likely to be revised.\n */\nfunction isMonthVolatile(year: number, month: number, now: Date): boolean {\n return isWithinVolatileWindow(lastDayOfMonth(year, month), formatDate(now), 30);\n}\n\n/**\n * Enumerate `[year, month]` pairs that overlap `[fromIsoDate, toIsoDate]`\n * (inclusive on both ends). Used by the per-month observations cache\n * (iter-7 H13). Returns pairs in chronological order. Validates the\n * range; throws on inverted input.\n */\nfunction monthsInRange(\n fromIsoDate: string,\n toIsoDate: string,\n): ReadonlyArray<readonly [number, number]> {\n const from = parseIsoDate(fromIsoDate);\n const to = parseIsoDate(toIsoDate);\n if (from.getTime() > to.getTime()) {\n throw new Error(`fromDate (${fromIsoDate}) must be <= toDate (${toIsoDate})`);\n }\n const pairs: Array<readonly [number, number]> = [];\n let y = from.getUTCFullYear();\n let m = from.getUTCMonth() + 1; // 1-12\n const endY = to.getUTCFullYear();\n const endM = to.getUTCMonth() + 1;\n while (y < endY || (y === endY && m <= endM)) {\n pairs.push([y, m]);\n m += 1;\n if (m > 12) {\n m = 1;\n y += 1;\n }\n }\n return pairs;\n}\n\n/**\n * Fetch CLI climate per-year with read-through cache. Yearly chunks are\n * cached at `cacheKeyForClimate(code, year)`. Skip rules:\n * - Current LST year — mutable, never cached.\n * - 30-day volatile amendment window (iter-5 H9) — chunks whose\n * year-end is within 30 days of `now` MUST be re-fetched. The\n * window only fires for the year immediately preceding \"now\"\n * once the calendar rolls over.\n * - Live source (`.live`) — never cached (CLI is archive `iem.cli` →\n * this never fires today; defensive for future).\n *\n * iter-6 C12: cache failures must NEVER discard the in-memory rows.\n * `cache.get` failures degrade to a live fetch (the intent — read-through\n * is a perf optimization, not a correctness requirement). `cache.set`\n * failures AFTER a successful fetch+parse MUST log and continue —\n * persisting to the cache is a best-effort side effect, never a reason\n * to drop already-fetched climate data. The previous broad try/catch in\n * the caller swallowed cache.set throws as \"no CLI data,\" silently\n * corrupting research rows with null cli_* fields.\n */\nasync function fetchCliWithCache(\n fetchIcao: string,\n cacheCode: string,\n fromYear: number,\n toYear: number,\n opts: ResearchOptions,\n cache: CacheStore | null,\n now: Date,\n): Promise<ClimateObservation[]> {\n const acc: ClimateObservation[] = [];\n for (let year = fromYear; year <= toYear; year++) {\n // iter-12 C15: `isWritableYear` is the strictest temporal gate.\n // Any year that isn't STRICTLY in the past UTC-wise (future years\n // or the current UTC year, including the UTC Jan-1 boundary window\n // where a negative-offset station's LST is still in the prior year)\n // is never cacheable — regardless of LST or volatile-window logic.\n // Force a live fetch and skip both reads AND writes for non-writable\n // years.\n const writable = isWritableYear(year, now);\n const skipCurrentYear = shouldSkipCacheForCurrentLstYear(cacheCode, year, now);\n // iter-5 H9: the 30-day volatile amendment window MUST also block\n // cache reads — a hit served from inside the window would re-serve\n // soon-to-be-amended rows. Always prefer a fresh fetch when the\n // window is active.\n const skipVolatile = isYearVolatile(year, now);\n const skip = !writable || skipCurrentYear || skipVolatile;\n\n // --- Cache read (best-effort) -------------------------------------\n // iter-6 C12: a `cache.get` failure must not abort the per-year\n // chunk — fall through to the live fetch. A transient backend hiccup\n // is no reason to refuse climate data we can still fetch fresh.\n if (cache !== null && !skip) {\n let cached: ClimateObservation[] | null = null;\n try {\n cached = await cache.get<ClimateObservation[]>(cacheKeyForClimate(cacheCode, year));\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] CLI cache.get failed for code=${cacheCode} year=${year}; falling back to live fetch:`,\n cacheErr,\n );\n }\n if (cached !== null) {\n acc.push(...cached);\n continue;\n }\n }\n\n // --- Live fetch + parse (errors here ARE fatal to this chunk) -----\n // Abort propagates; other errors bubble to the caller's try/catch\n // which degrades to \"no CLI data\" for the affected years. This is\n // the existing behavior — DO NOT widen the catch to include cache\n // writes (see below).\n const cliOpts: { signal?: AbortSignal; politenessMs?: number } = {};\n if (opts.signal !== undefined) cliOpts.signal = opts.signal;\n if (opts.cliPolitenessMs !== undefined) cliOpts.politenessMs = opts.cliPolitenessMs;\n const cliRaw = await downloadCliRange(fetchIcao, year, year, cliOpts);\n const parsed = parseCliResponse(cliRaw, cacheCode);\n acc.push(...parsed);\n\n // --- Cache write (best-effort, AFTER rows are accumulated) --------\n // iter-6 C12: `cache.set` MUST be wrapped in its own try/catch so a\n // transient write failure cannot discard already-fetched rows. The\n // previous code put cache.set inside the caller's broad CLI try/catch,\n // which silently degraded write failures to \"no climate data\" —\n // returning research rows with null cli_* fields. That's silent data\n // corruption; this guard prevents it.\n const sample = parsed[0]?.source;\n if (cache !== null && !skip && !isLiveSource(sample)) {\n try {\n await cache.set(cacheKeyForClimate(cacheCode, year), parsed);\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] CLI cache.set failed for code=${cacheCode} year=${year}; in-memory rows preserved:`,\n cacheErr,\n );\n }\n }\n }\n return acc;\n}\n\n/**\n * Fetch IEM ASOS observations per-month with read-through cache.\n *\n * iter-7 H13: this previously cached at YEAR granularity using a sentinel\n * `:01:rt=N` key, violating the Python TS-CACHE-02 per-month contract.\n * The Python `read_cache(station, year, month)` / `write_cache(...)`\n * surface uses `(station, year, month)` triplets — one parquet per month\n * containing the merged METAR+SPECI slice. This helper now matches that\n * contract:\n *\n * 1. Enumerate `(year, month)` pairs overlapping the queried range.\n * 2. For each pair, attempt a per-month cache read using the\n * source-namespaced key `cacheKeyForObservations(station, year,\n * month, \"iem\")`.\n * 3. On cache miss, fetch the full year (single IEM HTTP request for\n * `report_type=3` + one for `=4`) — IEM ASOS is yearly-chunked at\n * the source — then partition parsed rows by `(year, month)` and\n * filter back to the requested month (mirrors Python research.py\n * L267-269 month-boundary filter).\n * 4. Apply per-MONTH skip rules:\n * - `shouldSkipCacheForCurrentLstMonth(station, year, month, now)` —\n * mutable current month; never written.\n * - `isMonthVolatile(year, month, now)` — 30-day amendment window\n * gate (iter-5 H9 / iter-7 H13). Within the window, both read\n * AND write are skipped (IEM may publish late-arriving METARs\n * or corrections).\n * 5. Write-through fires only when neither skip rule trips; otherwise\n * the month's rows are returned in-memory but never persisted.\n *\n * Per-year fetch results are cached in a local `yearCache` Map so multiple\n * months within the same year share one HTTP round-trip — this is the\n * critical perf invariant from the previous implementation, preserved\n * across the granularity change.\n *\n * iter-6 C12: mirrors `fetchCliWithCache`'s split-try pattern — cache\n * `get` / `set` failures are logged but never discard the in-memory\n * rows. A cache backend hiccup must not silently drop observations\n * that were successfully fetched + parsed.\n */\nasync function fetchIemAsosWithCache(\n stationCode: string,\n _fromYear: number,\n _extendedToYear: number,\n fromDate: string,\n extendedTo: string,\n opts: ResearchOptions,\n cache: CacheStore | null,\n now: Date,\n): Promise<Observation[]> {\n void _fromYear;\n void _extendedToYear;\n const acc: Observation[] = [];\n\n // Per-call memoization: avoid re-fetching the same (year, reportType)\n // when multiple months in the same year miss the cache.\n const yearByReportType = new Map<string, Observation[]>();\n\n async function fetchYearOnce(year: number, reportType: 3 | 4): Promise<Observation[]> {\n const memoKey = `${year}:${reportType}`;\n const cached = yearByReportType.get(memoKey);\n if (cached !== undefined) return cached;\n const iemOpts: { reportType: 3 | 4; politenessMs: number; signal?: AbortSignal } = {\n reportType,\n politenessMs: opts.iemPolitenessMs ?? 1000,\n };\n if (opts.signal !== undefined) iemOpts.signal = opts.signal;\n const chunks = await downloadIemAsos(stationCode, `${year}-01-01`, `${year}-12-31`, iemOpts);\n const fetched: Observation[] = [];\n for (const chunk of chunks) {\n const parsed = parseIemCsv(chunk.csv, {\n observationTypeOverride: reportType === 3 ? \"METAR\" : \"SPECI\",\n });\n fetched.push(...parsed);\n }\n yearByReportType.set(memoKey, fetched);\n return fetched;\n }\n\n function filterMonth(\n rows: ReadonlyArray<Observation>,\n year: number,\n month: number,\n ): Observation[] {\n const yyyy = String(year).padStart(4, \"0\");\n const mm = String(month).padStart(2, \"0\");\n const prefix = `${yyyy}-${mm}-`;\n const out: Observation[] = [];\n for (const r of rows) {\n if (r.observed_at.startsWith(prefix)) out.push(r);\n }\n return out;\n }\n\n const pairs = monthsInRange(fromDate, extendedTo);\n for (const [year, month] of pairs) {\n const cacheKey = cacheKeyForObservations(stationCode, year, month, \"iem\");\n // iter-12 C14: `isWritableMonth` is the strictest temporal gate.\n // Any month that isn't STRICTLY in the past UTC-wise (future months\n // or the current UTC month, including the UTC-rollover tail where\n // LST is still in the prior UTC month) is never cacheable —\n // regardless of LST or volatile-window logic. Force a live fetch\n // and skip both reads AND writes for non-writable months.\n const writable = isWritableMonth(year, month, now);\n const skipCurrentMonth = shouldSkipCacheForCurrentLstMonth(stationCode, year, month, now);\n const skipVolatile = isMonthVolatile(year, month, now);\n const skipCache = !writable || skipCurrentMonth || skipVolatile;\n\n // --- Cache read (best-effort) -------------------------------------\n // iter-6 C12: a `cache.get` failure must not abort the month — fall\n // through to the live fetch. The cached value combines METAR+SPECI\n // (single per-month entry), so a hit yields both report types.\n let monthRows: Observation[] | null = null;\n if (cache !== null && !skipCache) {\n try {\n const cached = await cache.get<Observation[]>(cacheKey);\n if (cached !== null) monthRows = cached;\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] IEM ASOS cache.get failed for key=${cacheKey}; falling back to live fetch:`,\n cacheErr,\n );\n }\n }\n\n if (monthRows === null) {\n // --- Live fetch + parse (errors here propagate to the caller) ---\n // Fetch both report types for the year (memoized) and partition\n // to this month. Combining METAR+SPECI matches the Python contract\n // (write_cache receives one merged list per month).\n const metar = await fetchYearOnce(year, 3);\n const speci = await fetchYearOnce(year, 4);\n const monthMetar = filterMonth(metar, year, month);\n const monthSpeci = filterMonth(speci, year, month);\n monthRows = [...monthMetar, ...monthSpeci];\n\n // --- Cache write (best-effort, AFTER rows are accumulated) ------\n // iter-6 C12: `cache.set` failures MUST NOT propagate — a\n // transient write failure cannot be allowed to discard rows that\n // were just successfully fetched + parsed. Log and continue; the\n // in-memory `monthRows` is appended to `acc` below regardless.\n const sample = monthRows[0]?.source;\n if (cache !== null && !skipCache && !isLiveSource(sample)) {\n try {\n await cache.set(cacheKey, monthRows);\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] IEM ASOS cache.set failed for key=${cacheKey}; in-memory rows preserved:`,\n cacheErr,\n );\n }\n }\n }\n\n for (const obs of monthRows) {\n const obsDate = obs.observed_at.slice(0, 10);\n if (obsDate >= fromDate && obsDate <= extendedTo) acc.push(obs);\n }\n }\n return acc;\n}\n\n/**\n * Fetch GHCNh archive observations per-month with read-through cache.\n *\n * iter-7 H14: previously the GHCNh path called `downloadGhcnhRange` on\n * every `research()` invocation and never touched the cache. TS-W3\n * requires GHCNh chunks to be cacheable just like IEM ASOS — this helper\n * applies the same per-month contract as `fetchIemAsosWithCache`:\n *\n * 1. Enumerate `(year, month)` pairs overlapping the queried range.\n * 2. For each pair, attempt a per-month cache read using the source-\n * namespaced key `cacheKeyForObservations(station, year, month,\n * \"ghcnh\")`. The `\"ghcnh\"` source segment prevents collision with\n * IEM ASOS writes for the same `(station, year, month)` triplet\n * (iter-7 H13 introduced `\"iem\"` namespacing).\n * 3. On cache miss, fetch the full year via `downloadGhcnh` (single\n * PSV per station-year — NCEI's archive is yearly-chunked at the\n * source) — memoized within the helper so multiple months in the\n * same year share one HTTP round-trip.\n * 4. Per-month skip rules: `shouldSkipCacheForCurrentLstMonth` +\n * `isMonthVolatile` (30-day amendment window). NCEI republishes\n * `GHCNh_<id>_<YEAR>.psv` as new months land, so the same skip\n * logic the IEM helper uses applies here.\n * 5. 404-as-no-data: a `NotFoundError` from `downloadGhcnh` means NCEI\n * has no archive for this station-year (typical for recent partial\n * years or pre-1973 stations). We memoize an empty year and treat\n * every month as cache-eligible-but-empty. The Python range fetcher\n * silently swallows 404 too (research.py L160-166 logs + continues).\n *\n * iter-6 C12: mirrors the split-try pattern — cache `get` / `set`\n * failures are logged but never discard the in-memory rows.\n */\nasync function fetchGhcnhWithCache(\n stationCode: string,\n ghcnhId: string,\n fromDate: string,\n extendedTo: string,\n opts: ResearchOptions,\n cache: CacheStore | null,\n now: Date,\n): Promise<Observation[]> {\n const acc: Observation[] = [];\n\n // Per-call memoization: avoid re-fetching the same year when multiple\n // months in the same year miss the cache. `null` sentinel records a 404\n // (no data) so subsequent months in that year skip the HTTP call too.\n const yearCache = new Map<number, ReadonlyArray<Observation>>();\n\n async function fetchYearOnce(year: number): Promise<ReadonlyArray<Observation>> {\n const cached = yearCache.get(year);\n if (cached !== undefined) return cached;\n const ghcnhOpts: { signal?: AbortSignal } = {};\n if (opts.signal !== undefined) ghcnhOpts.signal = opts.signal;\n let parsed: ReadonlyArray<Observation>;\n try {\n const yr = await downloadGhcnh(ghcnhId, year, ghcnhOpts);\n parsed = parseGhcnhPsv(yr.psv);\n } catch (err) {\n if (err instanceof NotFoundError) {\n // NCEI 404 → no data for this station-year. Mirrors the\n // `downloadGhcnhRange` swallow-404 behavior; memoize empty so\n // subsequent months in this year don't re-hit NCEI.\n parsed = [];\n } else {\n throw err;\n }\n }\n yearCache.set(year, parsed);\n return parsed;\n }\n\n function filterMonth(\n rows: ReadonlyArray<Observation>,\n year: number,\n month: number,\n ): Observation[] {\n const yyyy = String(year).padStart(4, \"0\");\n const mm = String(month).padStart(2, \"0\");\n const prefix = `${yyyy}-${mm}-`;\n const out: Observation[] = [];\n for (const r of rows) {\n if (r.observed_at.startsWith(prefix) && r.station_code === stationCode) out.push(r);\n }\n return out;\n }\n\n const pairs = monthsInRange(fromDate, extendedTo);\n for (const [year, month] of pairs) {\n const cacheKey = cacheKeyForObservations(stationCode, year, month, \"ghcnh\");\n // iter-12 C14: stricter additional temporal gate — see the matching\n // comment in `fetchIemAsosWithCache`. NCEI's archive can return\n // empty data for not-yet-published months; we must NEVER persist a\n // not-strictly-past UTC month as if it were complete.\n const writable = isWritableMonth(year, month, now);\n const skipCurrentMonth = shouldSkipCacheForCurrentLstMonth(stationCode, year, month, now);\n const skipVolatile = isMonthVolatile(year, month, now);\n const skipCache = !writable || skipCurrentMonth || skipVolatile;\n\n // --- Cache read (best-effort) -------------------------------------\n let monthRows: Observation[] | null = null;\n if (cache !== null && !skipCache) {\n try {\n const cached = await cache.get<Observation[]>(cacheKey);\n if (cached !== null) monthRows = cached;\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] GHCNh cache.get failed for key=${cacheKey}; falling back to live fetch:`,\n cacheErr,\n );\n }\n }\n\n if (monthRows === null) {\n // --- Live fetch + parse (errors here propagate to the caller) ---\n const yearRows = await fetchYearOnce(year);\n monthRows = filterMonth(yearRows, year, month);\n\n // --- Cache write (best-effort, AFTER rows are accumulated) ------\n // iter-6 C12: `cache.set` failures MUST NOT propagate. Even an\n // empty month list is written when the year was successfully\n // fetched — it pins the \"no observations for this month\" fact so\n // the next call doesn't re-fetch the year just to discover nothing.\n const sample = monthRows[0]?.source;\n if (cache !== null && !skipCache && !isLiveSource(sample)) {\n try {\n await cache.set(cacheKey, monthRows);\n } catch (cacheErr) {\n // eslint-disable-next-line no-console\n console.warn(\n `[mostlyright] GHCNh cache.set failed for key=${cacheKey}; in-memory rows preserved:`,\n cacheErr,\n );\n }\n }\n }\n\n for (const obs of monthRows) {\n const obsDate = obs.observed_at.slice(0, 10);\n if (obsDate >= fromDate && obsDate <= extendedTo) acc.push(obs);\n }\n }\n return acc;\n}\n\n// ---------------------------------------------------------------------------\n// Public surface\n// ---------------------------------------------------------------------------\n\n/**\n * Build daily research rows for a station + date window.\n *\n * @param station NWS 3-letter code (e.g. \"NYC\") OR 4-letter ICAO (e.g. \"KNYC\").\n * @param fromDate Inclusive start date, ISO YYYY-MM-DD (LST).\n * @param toDate Inclusive end date, ISO YYYY-MM-DD (LST).\n * @param opts See {@link ResearchOptions}.\n *\n * Returns an immutable array of frozen {@link PairsRow}s — one per LST day\n * in `[fromDate, toDate]`. Each row carries:\n * - `cli_*` populated from IEM CLI (final preferred per `mergeClimate`).\n * - `obs_*` daily aggregates over the 3-source merged observations\n * (AWC > IEM > GHCNh per `mergeObservations`).\n * - `fcst_*` unconditionally null (Mode 1).\n * - `market_close_utc` formatted `YYYY-MM-DDTHH:MM:SSZ`.\n *\n * Throws on unknown station, malformed dates, or fromDate > toDate.\n * AbortSignal propagates from underlying fetchers.\n */\nexport async function research(\n station: string,\n fromDate: string,\n toDate: string,\n opts: ResearchOptions = {},\n): Promise<ReadonlyArray<PairsRow>> {\n // ── Phase 10 selector + cross-arg validation ─────────────────────────\n //\n // The TS signature pre-dates Phase 10's composable kwargs, so the\n // `station` positional is still always passed. The new selectors live\n // on `opts` (city / contract / contracts) and are validated here:\n // exactly one of station / city / contract / contracts is allowed.\n //\n // v0.2 ships only the validation surface; the multi-station JOIN +\n // trade-attachment lands in v0.3. Passing any non-station selector\n // surfaces a clear NotImplementedError-style error so callers can\n // route via discover() + the station-path until v0.3.\n const hasCity = typeof opts.city === \"string\" && opts.city.length > 0;\n const hasContract = typeof opts.contract === \"string\" && opts.contract.length > 0;\n const hasContracts = Array.isArray(opts.contracts) && opts.contracts.length > 0;\n const hasStation = typeof station === \"string\" && station.length > 0;\n const selectorCount =\n Number(hasStation) + Number(hasCity) + Number(hasContract) + Number(hasContracts);\n if (selectorCount === 0) {\n throw new Error(\n \"research(): exactly one of station, opts.city, opts.contract, opts.contracts must be provided\",\n );\n }\n if (selectorCount > 1) {\n const names: string[] = [];\n if (hasStation) names.push(\"station\");\n if (hasCity) names.push(\"city\");\n if (hasContract) names.push(\"contract\");\n if (hasContracts) names.push(\"contracts\");\n throw new Error(`research(): selectors are mutually exclusive; got ${JSON.stringify(names)}`);\n }\n if (opts.sources !== undefined && opts.source !== undefined) {\n throw new Error(\"research(): sources and source are mutually exclusive\");\n }\n // Iter-1 codex HIGH: sources / source validation is shipped in Phase 10\n // v0.2 but the data-selection wiring lands in v0.3. Without this guard\n // the station-path runs the full multi-source merge regardless — silent\n // data-selection corruption.\n if (opts.sources !== undefined || opts.source !== undefined) {\n throw new Error(\n \"research(): sources / source validation surface is shipped in Phase 10 v0.2 \" +\n \"but the data-selection wiring lands in v0.3. For Mode 2 single-source pinning \" +\n \"today, use `researchBySource(station, source, ...)` from @mostlyrightmd/meta.\",\n );\n }\n if (opts.stationOverride !== undefined && !hasContract) {\n throw new Error(\n \"research(): stationOverride requires contract (not standalone station/city/contracts)\",\n );\n }\n if (opts.includeTrades === true && !(hasContract || hasContracts)) {\n throw new Error(\n \"research(): includeTrades requires contract or contracts (station/city selectors have no trade timeseries)\",\n );\n }\n if (hasCity || hasContract || hasContracts) {\n throw new Error(\n \"research(): city/contract/contracts selectors are validated in Phase 10 v0.2 \" +\n \"but the multi-station/multi-issuer JOIN + trade attachment lands in v0.3. \" +\n \"For now, use `discover({city})` to find the station then call \" +\n \"`research(station, fromDate, toDate)` directly.\",\n );\n }\n // ── Backwards-compat station path (existing implementation) ─────────\n const resolved = normalizeStation(station);\n const dates = buildDateList(fromDate, toDate);\n const extendedTo = plusOneDay(toDate);\n\n const fromYear = Number(fromDate.slice(0, 4));\n const toYear = Number(toDate.slice(0, 4));\n const extendedToYear = Number(extendedTo.slice(0, 4));\n\n const baseOpts: { signal?: AbortSignal } = {};\n if (opts.signal !== undefined) baseOpts.signal = opts.signal;\n\n const cache = await resolveCache(opts);\n const cacheNow = opts.now ?? new Date();\n\n // --- IEM CLI climate (per-year) ---------------------------------------\n // Cache strategy: read-through per (station code, year). Skip the current\n // LST year (mutable) and never cache `.live` sources. `iem.cli` is\n // archive → cacheable for completed years. Fetcher takes the ICAO\n // (resolved.icao), cache key uses the 3-letter NWS code (resolved.code).\n let mergedClimate: ReadonlyArray<ClimateObservation> = [];\n try {\n const cliRows = await fetchCliWithCache(\n resolved.icao,\n resolved.code,\n fromYear,\n toYear,\n opts,\n cache,\n cacheNow,\n );\n mergedClimate = mergeClimate(cliRows);\n } catch (err) {\n if (err instanceof DOMException && (err.name === \"AbortError\" || err.name === \"TimeoutError\")) {\n throw err;\n }\n // Degrade to no CLI data — buildPairs emits null cli_* for affected dates.\n }\n\n // --- AWC live observations (short-circuit on stale windows) -----------\n const awcHours = opts.awcHours ?? AWC_MAX_HOURS;\n const awcRows: Observation[] = [];\n if (anyDateOverlapsAwc(toDate, awcHours, opts.now ?? new Date())) {\n const awcOpts: { hours: number; signal?: AbortSignal } = { hours: awcHours };\n if (opts.signal !== undefined) awcOpts.signal = opts.signal;\n const awcRaw = await fetchAwcMetars([resolved.icao], awcOpts);\n for (const m of awcRaw) {\n const obs = awcToObservation(m);\n if (obs !== null) awcRows.push(obs);\n }\n }\n\n // --- IEM ASOS archive observations (per-year × {METAR, SPECI}) --------\n // IEM ASOS expects the 3-letter NWS station code (`station=NYC`),\n // NOT the 4-letter ICAO. Python `_fetchers/iem_asos.py:119` uses\n // `station={station.code}`. Use resolved.code, NOT resolved.icao.\n const iemRows = await fetchIemAsosWithCache(\n resolved.code,\n fromYear,\n extendedToYear,\n fromDate,\n extendedTo,\n opts,\n cache,\n cacheNow,\n );\n\n // --- GHCNh archive observations (US stations only) --------------------\n // iter-7 H14: now wraps `downloadGhcnh` in `fetchGhcnhWithCache` so\n // GHCNh chunks are persisted at the same per-month granularity as IEM\n // ASOS. Repeat `research()` calls for the same range skip NCEI\n // entirely on cache hit. Non-US stations short-circuit before reaching\n // the helper — GHCNh PSVs are US-only.\n let ghcnhRows: Observation[] = [];\n if (isUsStation(resolved) && resolved.ghcnhId !== null && resolved.ghcnhId.length > 0) {\n ghcnhRows = await fetchGhcnhWithCache(\n resolved.code,\n resolved.ghcnhId,\n fromDate,\n extendedTo,\n opts,\n cache,\n cacheNow,\n );\n }\n\n // --- Merge observations + bucket by settlement date -------------------\n const combinedRaw = [...awcRows, ...iemRows, ...ghcnhRows];\n const sorted = sortByObservedAtThenSource(combinedRaw);\n const merged = mergeObservations(sorted);\n\n const observationsByDate: Record<string, PairsObservationLike[]> = {};\n // dates is guaranteed non-empty by buildDateList contract (throws on\n // fromDate > toDate; both validated above).\n const dateLo = dates[0] ?? \"\";\n const dateHi = dates[dates.length - 1] ?? \"\";\n for (const obs of merged) {\n const settleDate = observedSettlementDate(obs.observed_at, resolved.code);\n if (settleDate === null) continue;\n if (settleDate < dateLo || settleDate > dateHi) continue;\n let bucket = observationsByDate[settleDate];\n if (bucket === undefined) {\n bucket = [];\n observationsByDate[settleDate] = bucket;\n }\n bucket.push(obs);\n }\n\n // --- Bucket climate by date (mergeClimate already deduped) ------------\n const climateByDate: Record<string, PairsClimateLike | null> = {};\n for (const cli of mergedClimate) {\n climateByDate[cli.observation_date] = cli;\n }\n\n // --- buildPairs join + return -----------------------------------------\n return buildPairs(resolved.code, dates, observationsByDate, climateByDate);\n}\n","// Mode 2 — source-explicit research() variant.\n//\n// Mirrors packages/core/src/mostlyright/mode2.py. Mode 1 (the existing\n// `research()`) merges AWC > IEM > GHCNh; Mode 2 lets the caller pin\n// observations to a single named source for source-identified\n// training pairs (the workflow Vojtech wanted for backtests that\n// need source-identity invariants).\n//\n// Lives in @mostlyrightmd/meta (alongside `research()`), NOT in\n// @mostlyrightmd/core — `assertSourceIdentity` consumes the\n// @mostlyrightmd/weather `Observation` type, which @mostlyrightmd/core\n// must not depend on (would create a cycle).\n//\n// ── Vocabulary ───────────────────────────────────────────────────────\n// TS narrows what Python widens: at the input boundary, TS accepts\n// ONLY the four canonical dotted-form sources. Bare forms (`iem`,\n// `awc`, `ghcnh`) are NEVER accepted at the API; they only ever\n// appear as parser-emitted PER-ROW source tags. The alias table\n// (`SOURCE_ALIASES`) bridges the boundary: filter rows whose bare\n// tag is in the dotted source's alias set, but NEVER rewrite the\n// per-row source — that would silently corrupt downstream Validator\n// invariants. See Python mode2.py:161-166 for the canonical comment.\n\nimport {\n NotFoundError,\n STATION_BY_CODE,\n STATION_BY_ICAO,\n SourceMismatchError,\n type SourceMismatchRole,\n} from \"@mostlyrightmd/core\";\nimport {\n type Observation,\n awcToObservation,\n downloadGhcnh,\n downloadIemAsos,\n fetchAwcMetars,\n parseGhcnhPsv,\n parseIemCsv,\n} from \"@mostlyrightmd/weather\";\n\nexport type { SourceMismatchRole };\n\n/** Mode 2 canonical source vocabulary. Exactly four dotted values. */\nexport const MODE2_SOURCES = [\"iem.archive\", \"iem.live\", \"awc.live\", \"ghcnh.archive\"] as const;\n\n/**\n * Mode 2 source-identity type. Const-union derived from the\n * `MODE2_SOURCES` tuple-literal (NOT a TS `enum` — `enum` defeats\n * tree-shaking per TS Architect rubric §5).\n */\nexport type Mode2Source = (typeof MODE2_SOURCES)[number];\n\n/**\n * Map each canonical dotted source to the bare parser-emitted tags\n * that satisfy it. Parsers emit bare `iem`/`awc`/`ghcnh` per\n * packages-ts/weather; mostlyright' canonical vocab is dotted. The\n * alias table bridges both at the boundary without rewriting the\n * per-row source — downstream consumers see the truthful\n * parser-emitted tag.\n *\n * Mirrors packages/core/src/mostlyright/mode2.py:55-63.\n */\nexport const SOURCE_ALIASES: ReadonlyMap<Mode2Source, ReadonlySet<string>> = new Map<\n Mode2Source,\n ReadonlySet<string>\n>([\n [\"iem.archive\", new Set([\"iem\", \"iem.archive\"])],\n [\"iem.live\", new Set([\"iem\", \"iem.live\"])],\n [\"awc.live\", new Set([\"awc\", \"awc.live\"])],\n [\"ghcnh.archive\", new Set([\"ghcnh\", \"ghcnh.archive\"])],\n]);\n\n/**\n * Type-guard: narrow an unknown value to {@link Mode2Source}. Returns\n * true iff `value` is one of the four canonical dotted strings.\n * Bare-form inputs (`'iem'`, `'awc'`, `'ghcnh'`) return false — TS\n * narrows what Python widens.\n */\nexport function isMode2Source(value: unknown): value is Mode2Source {\n return typeof value === \"string\" && (MODE2_SOURCES as readonly string[]).includes(value);\n}\n\n/**\n * Throw {@link SourceMismatchError} if any row's `source` field\n * disagrees with the expected source vocabulary. Rows missing the\n * `source` field (undefined / null / non-string) are skipped\n * (matches Python mode2.py:181-182 — `if \"source\" not in df.columns:\n * return`). Empty `rows` passes silently.\n *\n * The `expected` parameter accepts EITHER:\n *\n * - a single string — the most common case; downstream callers\n * can pass `\"iem.archive\"` and the check is `src === \"iem.archive\"`.\n * - a `ReadonlySet<string>` — used by `researchBySource` to pass\n * the {@link SOURCE_ALIASES} entry so bare-form parser tags\n * (`'iem'`) are accepted alongside the dotted canonical form\n * (`'iem.archive'`). Without this, the per-row source-preserved\n * invariant (Python mode2.py:161-166) would force the assertion\n * to fire on every Mode 2 call.\n *\n * @param rows rows to check (any shape with `source?: string`)\n * @param expected the source string OR alias-set the caller asked for\n * @param role role-name vocabulary; defaults to 'observations'\n *\n * @throws SourceMismatchError with `schemaSource` = the expected label\n * (the input string, or `[...accept].sort().join(\"|\")`\n * when an alias-set was passed), `dataSource` =\n * first sorted distinct mismatched source,\n * `role` = the caller-provided role,\n * `catalogWarning` = null.\n */\nexport function assertSourceIdentity<Row extends { source?: string | null | undefined }>(\n rows: ReadonlyArray<Row>,\n expected: string | ReadonlySet<string>,\n role: SourceMismatchRole = \"observations\",\n): void {\n const accept: ReadonlySet<string> =\n typeof expected === \"string\" ? new Set<string>([expected]) : expected;\n const expectedLabel: string =\n typeof expected === \"string\" ? expected : [...accept].sort().join(\"|\");\n\n const distinct = new Set<string>();\n let bad = 0;\n for (const r of rows) {\n const src = r?.source;\n if (typeof src !== \"string\") continue;\n if (!accept.has(src)) {\n distinct.add(src);\n bad += 1;\n }\n }\n if (bad === 0) return;\n const others = [...distinct].sort();\n const first = others[0] ?? \"<unknown>\";\n throw new SourceMismatchError(\n `Mode 2 dispatch requested '${expectedLabel}' but received ${bad} row(s) with other sources: [${others\n .map((s) => `'${s}'`)\n .join(\", \")}]`,\n {\n schemaSource: expectedLabel,\n dataSource: first,\n role,\n catalogWarning: null,\n },\n );\n}\n\n// ---------------------------------------------------------------------------\n// researchBySource — Mode 2 dispatch entry point\n// ---------------------------------------------------------------------------\n\n/** Mode 2 caller-supplied options. Subset of `ResearchOptions` — Mode 2\n * returns observations only, so forecast + climate + cache opts are\n * intentionally excluded. */\nexport interface ResearchBySourceOptions {\n /** Forward to the underlying fetcher; aborts the dispatch. */\n signal?: AbortSignal;\n /** AWC lookback window in hours (clamped by the fetcher). Default 168. */\n awcHours?: number;\n /** Polite-delay (ms) between IEM ASOS yearly chunks. Default 1000. */\n iemPolitenessMs?: number;\n}\n\n/** AWC live serves at most ~168 hours (7 days). Mirrors research.ts. */\nconst AWC_MAX_HOURS = 168;\n\ninterface ResolvedStation {\n readonly code: string;\n readonly icao: string;\n readonly country: string | null;\n readonly ghcnhId: string | null;\n}\n\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * Resolve a station identifier (3-letter NWS code OR 4-letter ICAO)\n * to the full record. Inlined here instead of imported from `research.ts`\n * to keep mode2 self-contained — the ~30-line duplication is cheaper\n * than threading internal helpers through a new module boundary.\n */\nfunction resolveStation(input: string): ResolvedStation {\n const raw = input.trim().toUpperCase();\n if (raw.length === 0) {\n throw new Error(\"station must be a non-empty string\");\n }\n const byIcao = STATION_BY_ICAO.get(raw);\n if (byIcao !== undefined) {\n if (byIcao.code === null) {\n throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);\n }\n return {\n code: byIcao.code,\n icao: byIcao.icao,\n country: byIcao.country,\n ghcnhId: byIcao.ghcnh_id,\n };\n }\n const byCode = STATION_BY_CODE.get(raw);\n if (byCode !== undefined) {\n if (byCode.code === null) {\n throw new Error(`station ${JSON.stringify(raw)} has no 3-letter NWS code`);\n }\n return {\n code: byCode.code,\n icao: byCode.icao,\n country: byCode.country,\n ghcnhId: byCode.ghcnh_id,\n };\n }\n if (raw.startsWith(\"K\") && raw.length === 4) {\n const stripped = raw.slice(1);\n const retry = STATION_BY_CODE.get(stripped);\n if (retry !== undefined && retry.code !== null) {\n return {\n code: retry.code,\n icao: retry.icao,\n country: retry.country,\n ghcnhId: retry.ghcnh_id,\n };\n }\n }\n throw new Error(\n `unknown station ${JSON.stringify(input)} — not found in STATION_BY_CODE or STATION_BY_ICAO`,\n );\n}\n\nfunction validateDateFormat(label: string, value: string): void {\n if (!DATE_RE.test(value)) {\n throw new Error(`${label} must be YYYY-MM-DD, got ${JSON.stringify(value)}`);\n }\n}\n\n/**\n * Year extracted from a YYYY-MM-DD string. Caller must validate format\n * first via `validateDateFormat`.\n */\nfunction yearOf(isoDate: string): number {\n return Number(isoDate.slice(0, 4));\n}\n\n/**\n * Mode 2 source-explicit observation fetch.\n *\n * Dispatches to a single source's fetcher (no merge) and returns raw\n * {@link Observation}s tagged with that source. Mirrors Python\n * `mostlyright.mode2.research_by_source` (packages/core/src/mostlyright/mode2.py).\n *\n * The four supported sources:\n *\n * - `'iem.archive'` → IEM ASOS historical CSVs (METAR + SPECI).\n * - `'iem.live'` → v0.1.0 parity gap; throws. Use `'iem.archive'`.\n * - `'awc.live'` → AWC live METAR JSON (≤168h lookback).\n * - `'ghcnh.archive'` → NCEI GHCNh PSV (US stations only).\n *\n * The returned rows preserve the parser-emitted per-row `source` field\n * verbatim — NEVER rewritten to the dotted canonical form. Bare tags\n * (`'iem'`, `'awc'`, `'ghcnh'`) survive intact so downstream Validator\n * schemas see the truthful provenance. Mode 2 still calls\n * {@link assertSourceIdentity} internally (defense-in-depth) before\n * returning — using the {@link SOURCE_ALIASES} entry so the bare-form\n * tags pass.\n *\n * @param station NWS 3-letter code (e.g. `\"NYC\"`) OR 4-letter ICAO (e.g. `\"KNYC\"`).\n * @param source One of {@link MODE2_SOURCES}.\n * @param fromDate Inclusive start, ISO `YYYY-MM-DD`.\n * @param toDate Inclusive end, ISO `YYYY-MM-DD`.\n * @param opts See {@link ResearchBySourceOptions}.\n *\n * @returns Frozen array of {@link Observation}s whose `source` is in\n * `SOURCE_ALIASES.get(source)`. Empty array on no data\n * (NOT a throw).\n *\n * @throws Error if `source` is not one of {@link MODE2_SOURCES}.\n * Throws BEFORE any network call — no quota burn\n * on invalid input.\n * @throws Error if `source === 'iem.live'` (v0.1.0 parity gap;\n * v0.2 will add per-month live IEM).\n * @throws Error if `station` is unknown, or dates are malformed.\n * @throws NotFoundError if `source === 'ghcnh.archive'` and `station`\n * is non-US (GHCNh PSV files are US-only).\n * @throws SourceMismatchError if a row's `source` disagrees with the alias\n * set for `source` (defense-in-depth; should\n * never fire under correct fetcher behavior).\n */\nexport async function researchBySource(\n station: string,\n source: Mode2Source,\n fromDate: string,\n toDate: string,\n opts: ResearchBySourceOptions = {},\n): Promise<ReadonlyArray<Observation>> {\n // ── Synchronous-style guards (BEFORE any network call) ────────────\n // Architect rubric: unknown-source rejection MUST run before any\n // fetcher import/call (else invalid input burns API quota).\n if (!isMode2Source(source)) {\n throw new Error(\n `Mode 2 source must be one of ${JSON.stringify(\n MODE2_SOURCES,\n )}; got ${JSON.stringify(source)}`,\n );\n }\n if (source === \"iem.live\") {\n throw new Error(\n \"Mode 2 source 'iem.live' not yet implemented in v0.1.0 \" +\n \"(Parity-Ticket: requires per-month live IEM endpoint not yet ported). \" +\n \"Use 'iem.archive' for historical IEM rows.\",\n );\n }\n validateDateFormat(\"fromDate\", fromDate);\n validateDateFormat(\"toDate\", toDate);\n if (fromDate > toDate) {\n throw new Error(`fromDate (${fromDate}) must be <= toDate (${toDate})`);\n }\n const resolved = resolveStation(station);\n\n const accept = SOURCE_ALIASES.get(source);\n if (accept === undefined) {\n // Unreachable — isMode2Source guard above guarantees a hit.\n throw new Error(`internal: no SOURCE_ALIASES entry for '${source}'`);\n }\n\n // ── Per-source dispatch ──────────────────────────────────────────\n let rows: ReadonlyArray<Observation>;\n switch (source) {\n case \"awc.live\": {\n const awcOpts: { hours: number; signal?: AbortSignal } = {\n hours: opts.awcHours ?? AWC_MAX_HOURS,\n };\n if (opts.signal !== undefined) awcOpts.signal = opts.signal;\n const raw = await fetchAwcMetars([resolved.icao], awcOpts);\n const parsed: Observation[] = [];\n for (const m of raw) {\n const obs = awcToObservation(m);\n if (obs !== null) parsed.push(obs);\n }\n // Filter to the queried [fromDate, toDate] window (inclusive) — match\n // the IEM/GHCNh branches. AWC's lookback (~168h) can return METARs\n // outside the caller's window; per-source Mode 2 callers expect rows\n // strictly inside [fromDate, toDate]. Python mode2.py parity.\n rows = parsed.filter((r) => {\n const d = r.observed_at.slice(0, 10);\n return d >= fromDate && d <= toDate;\n });\n break;\n }\n case \"iem.archive\": {\n const fromYear = yearOf(fromDate);\n const toYear = yearOf(toDate);\n const collected: Observation[] = [];\n for (let year = fromYear; year <= toYear; year++) {\n for (const reportType of [3, 4] as const) {\n const iemOpts: {\n reportType: 3 | 4;\n politenessMs: number;\n signal?: AbortSignal;\n } = {\n reportType,\n politenessMs: opts.iemPolitenessMs ?? 1000,\n };\n if (opts.signal !== undefined) iemOpts.signal = opts.signal;\n const chunks = await downloadIemAsos(\n resolved.code,\n `${year}-01-01`,\n `${year}-12-31`,\n iemOpts,\n );\n for (const chunk of chunks) {\n const parsed = parseIemCsv(chunk.csv, {\n observationTypeOverride: reportType === 3 ? \"METAR\" : \"SPECI\",\n });\n collected.push(...parsed);\n }\n }\n }\n // Filter to the queried [fromDate, toDate] window (inclusive).\n rows = collected.filter((r) => {\n const d = r.observed_at.slice(0, 10);\n return d >= fromDate && d <= toDate;\n });\n break;\n }\n case \"ghcnh.archive\": {\n // GHCNh PSV files are US-only. Non-US stations are advertised by\n // null `ghcnh_id` and country !== \"US\" in the codegen.\n if (resolved.country !== \"US\" || resolved.ghcnhId === null || resolved.ghcnhId.length === 0) {\n throw new NotFoundError(\n `GHCNh archive is US-only; station ${JSON.stringify(station)} ` +\n `(country=${resolved.country ?? \"null\"}, ghcnh_id=${\n resolved.ghcnhId === null ? \"null\" : JSON.stringify(resolved.ghcnhId)\n }) has no GHCNh coverage`,\n );\n }\n const fromYear = yearOf(fromDate);\n const toYear = yearOf(toDate);\n const collected: Observation[] = [];\n for (let year = fromYear; year <= toYear; year++) {\n const ghcnhOpts: { signal?: AbortSignal } = {};\n if (opts.signal !== undefined) ghcnhOpts.signal = opts.signal;\n try {\n const yr = await downloadGhcnh(resolved.ghcnhId, year, ghcnhOpts);\n const parsed = parseGhcnhPsv(yr.psv);\n for (const r of parsed) {\n if (r.station_code === resolved.code) collected.push(r);\n }\n } catch (err) {\n // 404 = no data for this station-year (typical for partial /\n // pre-1973 years). Mirrors Python research.py 404-as-skip.\n if (err instanceof NotFoundError) continue;\n throw err;\n }\n }\n rows = collected.filter((r) => {\n const d = r.observed_at.slice(0, 10);\n return d >= fromDate && d <= toDate;\n });\n break;\n }\n // iem.live is rejected above; the type narrowing here is\n // exhaustive over Mode2Source minus iem.live (which is unreachable).\n }\n\n // ── Filter to the alias set (Python parity: row keep iff parser-tag in alias) ──\n // biome-ignore lint/style/noNonNullAssertion: rows is assigned in every reachable case\n const filtered = rows!.filter((r) => accept.has(r.source));\n\n // ── Defense-in-depth: assertSourceIdentity (Python mode2.py:173-193) ────\n // Empty result still passes (no rows → no mismatch).\n assertSourceIdentity(filtered, accept, \"observations\");\n\n return filtered;\n}\n","// Phase 10 — composable research() dispatcher (TS port of\n// packages/core/src/mostlyright/_compose.py).\n//\n// Translates the new selectors (`city`, `contract`, `contracts`) into\n// resolution metadata + station lists. Pure logic, no I/O.\n\nimport {\n KALSHI_SETTLEMENT_STATIONS,\n type KalshiStation,\n POLYMARKET_CITY_STATIONS,\n} from \"@mostlyrightmd/markets\";\nimport { POLYMARKET_KNOWN_WRONG_STATIONS } from \"@mostlyrightmd/markets/polymarket\";\n\n/** The four mutually-exclusive selector names. */\nexport const SELECTOR_NAMES = [\"station\", \"city\", \"contract\", \"contracts\"] as const;\nexport type SelectorName = (typeof SELECTOR_NAMES)[number];\n\n/**\n * Kalshi short-ticker → canonical city slug. Real Kalshi tickers use\n * variable-length city suffixes: `KXHIGHNY-...` (NY → NYC),\n * `KXHIGHCHI-...` (CHI → CHI). The `KALSHI_SETTLEMENT_STATIONS` catalog\n * is keyed by the canonical 3-letter slug; this alias normalizes the\n * variable-length Kalshi suffix to the catalog key before lookup.\n */\nconst KALSHI_TICKER_ALIASES: Record<string, string> = {\n NY: \"NYC\",\n};\n\n/**\n * Kalshi-short ↔ Polymarket-long city slug alias. Iter-1 python-architect\n * HIGH: without this, `resolveCity(\"LAX\")` would miss Polymarket's KLAX\n * (keyed as `los_angeles`); `resolveCity(\"chicago\")` would miss Kalshi's\n * KMDW (keyed as `CHI`). Bi-directional probe surfaces the full\n * cross-issuer settlement neighborhood regardless of which slug form\n * the caller passed.\n */\nconst CITY_SLUG_ALIASES: Record<string, readonly [string, string]> = {\n // short_kalshi (lower) → [polymarket_long, kalshi_upper]\n nyc: [\"nyc\", \"NYC\"],\n chi: [\"chicago\", \"CHI\"],\n lax: [\"los_angeles\", \"LAX\"],\n mia: [\"miami\", \"MIA\"],\n den: [\"denver\", \"DEN\"],\n bos: [\"boston\", \"BOS\"],\n aus: [\"austin\", \"AUS\"],\n dca: [\"washington_dc\", \"DCA\"],\n phl: [\"philadelphia\", \"PHL\"],\n sfo: [\"san_francisco\", \"SFO\"],\n sea: [\"seattle\", \"SEA\"],\n atl: [\"atlanta\", \"ATL\"],\n hou: [\"houston\", \"HOU\"],\n dal: [\"dallas\", \"DAL\"],\n phx: [\"phoenix\", \"PHX\"],\n msp: [\"minneapolis\", \"MSP\"],\n dtw: [\"detroit\", \"DTW\"],\n};\n\nconst CITY_SLUG_ALIASES_REVERSE: Record<string, readonly [string, string]> = (() => {\n const out: Record<string, readonly [string, string]> = {};\n for (const [shortLower, [longPoly, kalshiUpper]] of Object.entries(CITY_SLUG_ALIASES)) {\n out[longPoly] = [shortLower, kalshiUpper];\n }\n return out;\n})();\n\n/** Return `[polymarket_slug_lower, kalshi_slug_upper]` for `city`. */\nfunction normalizeCitySlugs(city: string): readonly [string, string] {\n const lower = city.toLowerCase();\n const upper = city.toUpperCase();\n const direct = CITY_SLUG_ALIASES[lower];\n if (direct !== undefined) return direct;\n const reverse = CITY_SLUG_ALIASES_REVERSE[lower];\n if (reverse !== undefined) return [lower, reverse[1]];\n return [lower, upper];\n}\n\n/**\n * Structured warning emitted when `stationOverride` deliberately\n * mismatches the contract's canonical settlement station. The output\n * row carries `settlementMismatch: true`.\n *\n * JS has no `warnings.warn()` analogue; callers receive these via the\n * `onWarning?` callback in ResearchOptions.\n */\nexport interface StationOverrideWarning {\n readonly kind: \"StationOverrideWarning\";\n readonly contractStation: string;\n readonly overrideStation: string;\n readonly message: string;\n}\n\n/** Selector kwargs accepted by research(). Exactly one MUST be provided. */\nexport interface SelectorArgs {\n readonly station?: string;\n readonly city?: string;\n readonly contract?: string;\n readonly contracts?: ReadonlyArray<string>;\n}\n\n/**\n * Validate selector arity. Returns the active selector name; throws when\n * zero or >1 selectors are provided.\n */\nexport function validateSelectors(args: SelectorArgs): SelectorName {\n const provided: SelectorName[] = [];\n if (typeof args.station === \"string\" && args.station.length > 0) provided.push(\"station\");\n if (typeof args.city === \"string\" && args.city.length > 0) provided.push(\"city\");\n if (typeof args.contract === \"string\" && args.contract.length > 0) provided.push(\"contract\");\n if (Array.isArray(args.contracts) && args.contracts.length > 0) provided.push(\"contracts\");\n\n if (provided.length === 0) {\n throw new Error(\n \"research(): exactly one of station, city, contract, contracts must be provided\",\n );\n }\n if (provided.length > 1) {\n throw new Error(\n `research(): selectors are mutually exclusive; got ${JSON.stringify(provided)}`,\n );\n }\n return provided[0] as SelectorName;\n}\n\n/**\n * Resolve a `\"<issuer>:<id>\"` contract id to `[station, issuer]`.\n *\n * Supported: `kalshi:KHIGH<CITY>` / `kalshi:KXHIGH<CITY>-<DATE>-<STRIKE>`\n * and `kalshi:KLOW<CITY>` / `kalshi:KXLOW<CITY>-<DATE>-<STRIKE>`.\n *\n * Polymarket contract resolution requires an event_id → station lookup\n * (via polymarket-discover); Phase 10 v0.2 defers to v0.3 and throws.\n */\nexport function resolveContract(contractId: string): readonly [string, string] {\n if (typeof contractId !== \"string\" || !contractId.includes(\":\")) {\n throw new TypeError(`contract id must be \\`<issuer>:<id>\\`; got ${JSON.stringify(contractId)}`);\n }\n const colonIdx = contractId.indexOf(\":\");\n const issuer = contractId.slice(0, colonIdx).toLowerCase();\n const raw = contractId.slice(colonIdx + 1);\n const rawUpper = raw.toUpperCase();\n\n if (issuer === \"kalshi\") {\n // Strip KX exchange prefix (KXHIGHNYC → KHIGHNYC) and trailing\n // -DATE-STRIKE suffix to recover the legacy KHIGH<CITY> / KLOW<CITY>\n // shape the KALSHI_SETTLEMENT_STATIONS map keys are derived from.\n let normalized = rawUpper;\n if (normalized.startsWith(\"KX\")) {\n normalized = `K${normalized.slice(2)}`;\n }\n const cityOnly = normalized.split(\"-\", 1)[0] ?? \"\";\n let cityTickerRaw: string | null = null;\n if (cityOnly.startsWith(\"KHIGH\") && cityOnly.length > 5) {\n cityTickerRaw = cityOnly.slice(5);\n } else if (cityOnly.startsWith(\"KLOW\") && cityOnly.length > 4) {\n cityTickerRaw = cityOnly.slice(4);\n } else {\n throw new Error(\n `unsupported kalshi contract format: ${JSON.stringify(raw)}; expected KHIGH<CITY>* / KXHIGH<CITY>* / KLOW<CITY>* / KXLOW<CITY>* prefix`,\n );\n }\n // Iter-1 codex HIGH: normalize variable-length Kalshi ticker suffix\n // (NY → NYC, etc.) via the alias table before the catalog lookup.\n const cityTicker = KALSHI_TICKER_ALIASES[cityTickerRaw] ?? cityTickerRaw;\n const entry: KalshiStation | undefined = KALSHI_SETTLEMENT_STATIONS[cityTicker];\n if (entry === undefined) {\n throw new Error(`unknown Kalshi city ticker: ${JSON.stringify(cityTicker)}`);\n }\n return [entry.station, \"kalshi\"] as const;\n }\n if (issuer === \"polymarket\") {\n throw new Error(\n \"polymarket contract resolution requires event_id → station lookup via \" +\n \"polymarketDiscover()/polymarketSettle(); Phase 10 v0.2 defers this to \" +\n \"v0.3. Use `city: 'nyc'` or pass `stationOverride` until then.\",\n );\n }\n throw new Error(\n `unknown issuer prefix: ${JSON.stringify(issuer)}; expected kalshi or polymarket`,\n );\n}\n\n/**\n * Resolve a city slug to all stations any issuer settles against.\n * Returns deduplicated array in stable order: Kalshi → Polymarket default/high/low\n * → Polymarket denylist backstops.\n */\nexport function resolveCity(city: string): readonly string[] {\n if (typeof city !== \"string\" || !city) {\n throw new Error(`city must be a non-empty string; got ${JSON.stringify(city)}`);\n }\n // Iter-1 python-architect HIGH: cross-issuer slug alias surfaces the\n // full settlement neighborhood for either input form.\n const [polySlug, kalshiSlug] = normalizeCitySlugs(city);\n const out: string[] = [];\n\n const kalshi = KALSHI_SETTLEMENT_STATIONS[kalshiSlug];\n if (kalshi !== undefined && !out.includes(kalshi.station)) {\n out.push(kalshi.station);\n }\n const poly = POLYMARKET_CITY_STATIONS[polySlug];\n if (poly !== undefined) {\n for (const measure of [\"default\", \"high\", \"low\"] as const) {\n const st = poly[measure];\n if (typeof st === \"string\" && !out.includes(st)) out.push(st);\n }\n }\n const wrong = POLYMARKET_KNOWN_WRONG_STATIONS[polySlug];\n if (wrong !== undefined) {\n const sortedWrong = [...wrong].sort();\n for (const st of sortedWrong) {\n if (!out.includes(st)) out.push(st);\n }\n }\n if (out.length === 0) {\n throw new Error(`unknown city ${JSON.stringify(city)}; not in kalshi or polymarket catalogs`);\n }\n return out;\n}\n\n/**\n * Return the list of `\"<issuer>:<ticker>\"` markers that settle against\n * `station` for `city`. Empty array when no issuer settles against this\n * station (typically a denylist backstop).\n */\nexport function annotateSettlesFor(station: string, city: string | null): readonly string[] {\n if (city === null) return [];\n // Iter-1 python-architect HIGH: cross-issuer slug alias annotates both\n // issuers regardless of slug form.\n const [polySlug, kalshiSlug] = normalizeCitySlugs(city);\n const out: string[] = [];\n const kalshi = KALSHI_SETTLEMENT_STATIONS[kalshiSlug];\n if (kalshi !== undefined && kalshi.station === station) {\n out.push(`kalshi:${kalshiSlug}`);\n }\n const poly = POLYMARKET_CITY_STATIONS[polySlug];\n if (poly !== undefined) {\n for (const measure of [\"default\", \"high\", \"low\"] as const) {\n if (poly[measure] === station) {\n out.push(`polymarket:${polySlug}`);\n break;\n }\n }\n }\n return out.sort();\n}\n\n/**\n * Build a structured `StationOverrideWarning` payload. Callers receive\n * these via the optional `onWarning?` callback on research options.\n */\nexport function buildOverrideWarning(\n contractStation: string,\n overrideStation: string,\n): StationOverrideWarning {\n return {\n kind: \"StationOverrideWarning\",\n contractStation,\n overrideStation,\n message: `stationOverride=${JSON.stringify(overrideStation)} differs from contract's canonical settlement station ${JSON.stringify(contractStation)}; output row will carry settlementMismatch=true`,\n };\n}\n","// Phase 10 — discover({city}) ergonomic surface (TS port of\n// packages/core/src/mostlyright/discover.py).\n//\n// Pre-research lookup. Shows quants which station settles which issuer's\n// market for a given city so they can pick the right selector before\n// invoking research(). Especially useful for cross-issuer cities like NYC\n// where Kalshi settles against KNYC and Polymarket against KLGA.\n\nimport { annotateSettlesFor, resolveCity } from \"./compose.js\";\n\n/** One row per station in the resolved city neighborhood. */\nexport interface DiscoverRow {\n /** Echo of the input city. */\n readonly city: string;\n /** 4-char K-prefix ICAO. */\n readonly station: string;\n /**\n * `\"<issuer>:<ticker>\"` markers that resolve against this station.\n * Empty array = denylist backstop surfaced for explicit awareness.\n */\n readonly settlesFor: ReadonlyArray<string>;\n}\n\n/** Envelope mirrors the Python `df.attrs` pattern. */\nexport interface DiscoverResult {\n readonly rows: ReadonlyArray<DiscoverRow>;\n readonly city: string;\n readonly source: \"discover\";\n}\n\n/**\n * Return per-station discovery table for `city`.\n *\n * Each row shows one settlement station + the issuer:ticker markers that\n * resolve against it. Stations in the per-city Polymarket denylist also\n * appear with empty `settlesFor` so quants see the full neighborhood\n * before deciding whether to use `stationOverride`.\n *\n * @example\n * const result = discover({ city: \"NYC\" });\n * // rows include {station: \"KNYC\", settlesFor: [\"kalshi:NYC\"]},\n * // {station: \"KLGA\", settlesFor: [\"polymarket:nyc\"]},\n * // {station: \"KJFK\", settlesFor: []}, // denylist\n * // {station: \"KEWR\", settlesFor: []}.\n */\nexport function discover(args: { readonly city: string }): DiscoverResult {\n if (typeof args !== \"object\" || args === null) {\n throw new TypeError(`discover(): args must be an object; got ${typeof args}`);\n }\n const stations = resolveCity(args.city);\n const rows: DiscoverRow[] = stations.map((station) => ({\n city: args.city,\n station,\n settlesFor: annotateSettlesFor(station, args.city),\n }));\n return Object.freeze({\n rows: Object.freeze(rows),\n city: args.city,\n source: \"discover\" as const,\n });\n}\n"],"mappings":"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","names":["import_core","import_weather","import_markets","import_core","import_weather","AWC_MAX_HOURS","DATE_RE","import_weather","import_core"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -290,6 +290,13 @@ declare function discover(args: {
|
|
|
290
290
|
readonly city: string;
|
|
291
291
|
}): DiscoverResult;
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Placeholder version string for the meta package. The authoritative
|
|
295
|
+
* package version lives in `package.json#version` (currently
|
|
296
|
+
* `0.1.0-rc.7`); this constant has not been bumped. Sibling packages
|
|
297
|
+
* (`@mostlyrightmd/core` / `weather` / `markets`) each export their own
|
|
298
|
+
* `version` constant, exposed here via the namespaced module objects.
|
|
299
|
+
*/
|
|
293
300
|
declare const version = "0.0.0";
|
|
294
301
|
|
|
295
302
|
export { type DiscoverResult, type DiscoverRow, MODE2_SOURCES, type Mode2Source, type ResearchBySourceOptions, type ResearchOptions, SELECTOR_NAMES, SOURCE_ALIASES, type SelectorArgs, type SelectorName, type StationOverrideWarning, annotateSettlesFor, assertSourceIdentity, buildOverrideWarning, discover, isMode2Source, research, researchBySource, resolveCity, resolveContract, validateSelectors, version };
|
package/dist/index.d.ts
CHANGED
|
@@ -290,6 +290,13 @@ declare function discover(args: {
|
|
|
290
290
|
readonly city: string;
|
|
291
291
|
}): DiscoverResult;
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Placeholder version string for the meta package. The authoritative
|
|
295
|
+
* package version lives in `package.json#version` (currently
|
|
296
|
+
* `0.1.0-rc.7`); this constant has not been bumped. Sibling packages
|
|
297
|
+
* (`@mostlyrightmd/core` / `weather` / `markets`) each export their own
|
|
298
|
+
* `version` constant, exposed here via the namespaced module objects.
|
|
299
|
+
*/
|
|
293
300
|
declare const version = "0.0.0";
|
|
294
301
|
|
|
295
302
|
export { type DiscoverResult, type DiscoverRow, MODE2_SOURCES, type Mode2Source, type ResearchBySourceOptions, type ResearchOptions, SELECTOR_NAMES, SOURCE_ALIASES, type SelectorArgs, type SelectorName, type StationOverrideWarning, annotateSettlesFor, assertSourceIdentity, buildOverrideWarning, discover, isMode2Source, research, researchBySource, resolveCity, resolveContract, validateSelectors, version };
|