尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Storm-signal detection + confidence-scored trip-opportunity alert engine for storm-chasing skiers. Built from BUILD_PRIMER: powder-alert-platform :: PART_1 (the primer's own working title — left as-written, a historical citation, not the product's current name). Named Powder Seeker once a product name was decided — domain powderseeker.us — see ROADMAP.md's Phase D for the rename and everywhere it touched.
This is a working, from-scratch implementation — schema, ingestion, scoring engine, alerting, public site, and admin panel — built against placeholder env vars. No live Supabase/Clerk/Resend/Vercel/Instagram/Travelpayouts credentials were provisioned in this build session, and outbound network to api.open-meteo.com / NRCS's wcc.sc.egov.usda.gov was unreachable from the sandbox, so several integration surfaces are built to documented API conventions rather than live-validated. Everything is flagged below and in code comments — search for ASSUMPTION FLAG and VERIFICATION FLAG in src/lib.
npm install
cp .env.example .env.local # fill in real Supabase/Clerk/Resend/etc keys
supabase/migrations/ in order (0001_init.sql through 0013_cron_runs.sql) via the SQL editor or supabase db push.npm run seed:resorts — upserts the full US Epic + Ikon resort roster (src/lib/config/resorts-seed.ts), then runs the elevation onboarding check, SNOTEL station resolution, NWS gridpoint resolution, and the Mount Mansfield snow-stake historical backfill for Stowe (item 15 below) against live Open-Meteo/NRCS/NWS/NCEI APIs, then seeds+activates a baseline model_versions row if none exists yet (ROADMAP Phase B.1).npm run dev for local dev, or deploy to Vercel and let vercel.json's cron schedule drive ingestion/scoring/alerts.npm run test (vitest — scoring engine unit tests) and npm run typecheck before shipping changes.npm run backtest:slr-reanalysis -- --years=10 and npm run backtest:reconciliation -- --years=4 bootstrap Phase B's calibration with years of historical storm data instead of waiting out a live season — see ROADMAP.md's Phase B section. Add --version=<model_versions.id> to either to backtest under a specific draft version instead of whatever's currently active (ROADMAP Phase B.1).hfvcwrzjekzrpyerefao): migrations 0001–0013 are applied (0012–0013 on 2026-10-18), the 65-resort roster is seeded, and the v1 — baseline model version is active. They were applied as hand-split execute_sql chunks, because the MCP apply_migration call kept timing out in the agent sandbox. That means Supabase's own migration history table is empty, so supabase db push would try to re-run everything. Mark them applied with supabase migration repair --status applied 0001 … 0013 before pushing with the CLI. Seeding went through SQL, not npm run seed:resorts, so the elevation check, SNOTEL/NWS resolution, and the Mansfield backfill have not run yet. Run npm run seed:resorts once SUPABASE_SERVICE_ROLE_KEY is available. It's idempotent: the upsert is a no-op and the baseline version already exists.CRON_SECRET). The secret ones (SUPABASE_SERVICE_ROLE_KEY, Clerk, Resend, NCEI, Travelpayouts, Instagram) still have to be added in the Vercel dashboard. PUBLIC_BASE_URL points at https://www.powderseeker.us, because the *.vercel.app URLs sit behind Vercel deployment protection and aren't publicly reachable../scripts/run-local-model.sh (or npm run model:local) does the whole "download the data and compute the model" pass from a laptop. It installs Node 24 if needed, runs npm ci, asks once for SUPABASE_SERVICE_ROLE_KEY (saved to the git-ignored .env.local), and runs seed:resorts. It then runs both backtests for every resort that has a SNOTEL station: 40 years of the ERA5 SLR backtest and 5 years of the forecast-archive backtest. After that it generates calibration recommendations and writes CSV copies of accuracy_log, snotel_actuals and calibration_recommendations to local-data/. No Python or local database is needed.
It fetches in water-year chunks (Oct 1 to Sep 30) rather than one request per resort, because Open-Meteo weights a request by the date range it covers: a single 40-year request is about 1,000 calls, more than the free tier's per-minute limit. A full run needs roughly 50k calls against a 10k/day free limit, so the script stops with exit code 2 when it hits the daily limit. Run it again the next day and it resumes from local-data/progress.json. Flags: --years=N, --forecast-years=N, --resort=slug, --version=<model_versions.id>, --parallel=N (resorts at once, default 4), --skip-seed, --skip-slr, --skip-forecast. Resorts run in parallel behind one shared pacer set just under Open-Meteo's per-minute and per-hour caps, and every Supabase write is batched (500 rows per request instead of one per day), which was the main slowdown from a home connection. Parallelism can't get past the 10k/day cap, so the full run still spans several days. The same archive request also saves daily humidity, wind, sun, pressure and cloud cover to reanalysis_daily (migration 0014) for the planned natural base-depth model.
Without a laptop: .github/workflows/historical-model.yml runs the same driver on GitHub Actions every day at 06:00 UTC (or on demand from the Actions tab, with optional extra flags). It needs one repository secret, SUPABASE_SERVICE_ROLE_KEY. Each run goes until Open-Meteo's daily limit, saves progress.json in the Actions cache, and the next day's run resumes. CSV exports are kept as a run artifact for 30 days.
On Windows, double-click scripts\run-local-model.bat instead (it works on its own too, e.g. from Downloads; it then clones the repo to %USERPROFILE%\stormchaser). It installs Git and Node LTS with winget if missing, asks for the key with hidden input and has PowerShell write it straight to .env.local, then offers two choices: 1 = seed plus a quick 1-year SLR check (--skip-forecast --years=1), 2 = the full run, or resume from progress.json. Choice 2 skips the seed step once a run has started. No WSL needed.
Schema — supabase/migrations/. Matches BUILD_PRIMER section 4 with RLS added (section 10.5: public read on resorts/snow_scores/alerts_fired, service-role-only writes) and a couple of refinements: resorts.slr_calibration_multiplier (section 5.3 calibration hook), resorts.elevation_check_* + snotel_station_* + nws_grid_* (onboarding checks), accuracy_log for the backtest loop, and benchmark_forecasts for the public-baseline comparison pipeline (section 9 below).
Ingestion — src/lib/ingestion/. Open-Meteo per-model, per-elevation-band pulls (open-meteo.ts, ingest-models.ts), the elevation onboarding sanity check (elevation.ts), SNOTEL ground truth (snotel.ts), the Mount Mansfield snow-stake ground truth for Stowe specifically (mansfield-stake.ts — see item 15 below), and Travelpayouts indicative fares (fares.ts). Wired to Vercel Cron via src/app/api/cron/* + vercel.json.
Scoring engine — src/lib/scoring/. Multi-model reconciliation with lead-time-bucketed weights (reconciliation.ts), ensemble confidence with the cross-check downgrade (ensemble-confidence.ts), swappable SLR strategies (slr/ — Kuchera default, fixed-ratio baseline, Cobb-Waldstreicher stub, kuchera-wind — see item 13 below), composite powder_score (powder-score.ts), a generic wind-hold-probability estimate (wind-hold.ts), and the orchestrator (run.ts). The full reconcile→SLR→powder-score pipeline is a pure function, computeScoreFromInputs — computeScoreForResortDay is just a thin DB-fetching wrapper around it — specifically so the historical backtest pipelines (item 11 below) run the exact same scoring code the live cron does, not a reimplementation that could drift from it. Computes each resort's scoring day (and its own target-date list) on its own LOCAL timezone rather than UTC (see "Real bugs found by code review" below). All thresholds/weights are named constants in src/lib/config/constants.ts by default, but every pure function that reads one also accepts an optional config override — see item 12 below. 222 unit tests total across tests/.
trip_opportunity contract — src/lib/trip-opportunity.ts, matches section 6 exactly, no internal scoring details leaked.
See ROADMAP.md for what comes after PART_1 — operational-readiness steps, current progress against plan, and the prioritized non-coding punch list.
Status as of the WebSearch-assisted verification pass described in ROADMAP.md's progress log — direct WebFetch to every candidate primary-source domain (open-meteo.com, weather.gov, inscc.utah.edu, pivotalweather.com, wcc.sc.egov.usda.gov) returned EGRESS_BLOCKED in this sandbox, so nothing here is a confirmed live response — but WebSearch itself works and surfaced real, independently-converging corrections against two of these, which are now fixed:
src/lib/scoring/slr/kuchera.ts) — corrected this session. Two independent WebSearch queries both converged on the same formula from Veals et al. 2025, which has the slope magnitudes swapped from what was originally shipped (published: slope 2 above the 271.16K threshold, slope 1 below it; this codebase originally had -1.0/-2.6, i.e. backwards on both which branch is steeper and by how much). Fixed, with 2 new tests pinning the exact corrected values. Still short of reading the primary-source PDF directly (blocked) — worth doing if this matters enough to you, but meaningfully de-risked from "pure memory recall" to "two independent sources agree."src/lib/ingestion/snotel.ts) — partially corrected this session. WebSearch surfaced that this API has no server-side spatial/bounding-box filtering at all (the documented pattern is: fetch the full station list, filter client-side) — the original code's bBox param was almost certainly a no-op or an error. Fixed: now fetches the full SNTL network list once and does the nearest-match client-side, which the code was already set up to do downstream anyway. The endpoint path and field names (/stations, WTEQ, stationTriplet) remain unverified against a live response.src/lib/ingestion/open-meteo.ts) — spot-checked this session, looking good. WebSearch independently confirmed the dedicated model endpoints (/v1/ecmwf, /v1/dwd-icon, /v1/gem), the /v1/ensemble endpoint's required models= parameter, the elevation parameter's role (90m DEM grid-cell selection), and the specific hourly variable names used (freezing_level_height, temperature_850hPa, cloud_cover, wind_speed_10m) — all matched what this code already assumed. Not exhaustively checked (every variable name, the exact ensemble models= identifier string like gfs_seamless), but the structural assumptions held up.src/lib/ingestion/nws.ts) — unchanged, still only documented-convention-level confidence (higher than the others per NWS's API being older/more stable, but not independently re-checked this session beyond the original build).src/lib/ingestion/fares.ts) — corrected this session. The real response is {success, data: [...]} with data as a flat array (not nested by destination/flight-key as originally assumed), and the field names differ too: not , / not /. Fixed. wasn't confirmed present in the documented response — kept optional/nullable rather than assumed. Low real-world impact either way: section 2.4 already treats this whole enrichment as optional and non-blocking.The common thread: WebSearch (which goes through Anthropic's own infrastructure, not this sandbox's egress proxy) works even when WebFetch and raw curl to the same domains don't. If you want the remaining gaps closed fully, a session with real outbound network access (or you manually checking a couple of URLs) would let this go further than search-engine summaries.
These weren't WebSearch findings — logic bugs caught by re-reading the pipeline itself. Both fixed this session, before any live deployment, so no real historical data was ever affected — but either would have been a serious production incident if it had shipped.
Alert trigger could re-fire forever on a stale, long-resolved storm — the more severe of the two. lib/alerts/trigger.ts originally queried a resort's entire snow_scores history (no target_date filter at all) ordered by powder_score DESC first, computed_at DESC only as a tiebreaker. That finds whichever single row ever scored highest, including a date that's already passed and including a since-superseded forecast for that same date. Combined with the 24h rate limit resetting on its own, this would have re-sent an alert for the same long-past storm indefinitely, every 24 hours, forever — not a one-time nuisance but an ongoing credibility problem for the entire alerting feature, undermining the product's own stated transparency/trust positioning. The exact same bug pattern, lower-stakes but still real, existed in the public homepage's "best upcoming score" query (lib/public/resort-scores.ts) — it could display a stale, since-corrected score for a date still on the books. Both fixed: filter to target_date >= today, dedupe to the latest computed_at per date first, then pick the best among those current assessments — never let an old score outrank its own later correction. The trigger-side fix is a pure, exported function (selectCurrentBestQualifyingScore) specifically so this logic has real unit tests (7 of them, including the exact bug-reproduction scenario) rather than relying on manual inspection.
Local-day-boundary mismatch. lib/scoring/aggregate.ts and lib/ingestion/nws.ts originally computed each "target day"'s hour-range using UTC midnight, not the resort's own local midnight. Every seeded resort is in a US timezone 5-8 hours behind UTC, so this silently shifted which hourly forecast_pulls rows counted toward "today" by that many hours, for every resort, every day — overnight storms would get misattributed between adjacent ski-days. Fixed with a new dependency-free, Intl-based utility (src/lib/utils/timezone.ts, localDateRangeToUtc/utcInstantToLocalDateIso/localDayBoundsContaining) and threaded through lib/scoring/run.ts, lib/scoring/aggregate.ts, and lib/ingestion/nws.ts (the NWS benchmark had the identical bug — important since section A.1's whole point is comparing our forecast against NWS's for the same day, which wasn't true before this fix). 9 new tests, including the US DST spring-forward/fall-back transition days specifically (23-hour and 25-hour local days), since those are exactly where a naive "add 24h" implementation breaks and this one doesn't.
Same bug class, found again while building the resort detail page's "today"/"next 5 days" visuals — in two more places. lib/scoring/run.ts's nextNDates generated the scoring orchestrator's entire target-date list from UTC "today," computed once and reused for every resort regardless of timezone — for US resorts (always behind UTC), UTC's calendar date rolls over to "tomorrow" several hours before any US resort's local midnight, so during that multi-hour window (roughly evening through early morning local time, every single day), the date list started at local tomorrow and silently skipped generating a score for today's own date. lib/public/resort-scores.ts's getUpcomingScoresForResort had the identical root cause on the read side — target_date >= today compared against a UTC "today" that could already be tomorrow, excluding today's own row from "upcoming" during that same window. Both fixed the same way as the original instance: resolve "today" from the resort's own timezone (localTodayIso), not new Date().toISOString(). 3 + reused coverage in existing suites; see tests/scoring/run.test.ts for a test that reproduces the exact Eastern-vs-Pacific-vs-UTC discrepancy at one fixed instant.
The operator asked about comparing against OpenSnow directly. We're not doing that — see ROADMAP.md for the full reasoning, but short version: OpenSnow is a paid, proprietary consumer product with no public API and a ToS that doesn't permit automated access, even for paying subscribers. The BUILD_PRIMER already drew this line in section 2.3 ("do not scrape, do not reference their scoring methodology"). Instead, the public-baseline comparison pipeline (src/lib/ingestion/nws.ts) builds the same kind of comparison — "is our forecast better than a baseline" — against NWS's public-domain gridded data, which answers a more defensible question (are we beating a plain public forecast) without touching anyone's proprietary product. A manual, human-entered comparison log against OpenSnow (you read their number yourself, we store it) remains a legitimate option if wanted later — not built yet, no ToS issue since there's no automated access involved.
vercel.json schedules 10 jobs at hourly/6h/daily/weekly cadence. WebSearch confirmed (2026): Vercel's Hobby (free) tier allows at most 2 active cron jobs, each capped at once-per-day. This project's design needs Vercel Pro ($20/mo), not Hobby — this isn't a "maybe," the current vercel.json simply won't deploy as-is on Hobby. Budget for this in Phase 0 (see ROADMAP.md).OPEN_METEO_API_KEY is already wired as an env var for when that switch happens.npm audit — two critical RCEs patched this session. Between the original build (2026-08-22) and this update (2026-10-04), two critical, unauthenticated remote-code-execution advisories landed against Next.js itself (Windows-hosted servers; the AVIF image-optimization path). npm audit fix (non-breaking — stayed within the ^15.1.4 range, landed on 15.5.27) resolved both; verified clean build/typecheck/tests after. Remaining: 12 lower-severity advisories, mostly transitive through build tooling (postcss/sharp need Next 16, a breaking change, to fully clear — deferred, same reasoning as before) and one critical advisory in vitest's UI-server feature specifically, which this project doesn't use (package.json only ever runs vitest run, never vitest --ui) — real-world risk here is close to zero despite the severity label, but worth a vitest@5 bump eventually. Run npm audit yourself periodically; this class of finding changes over time, not just at initial build./admin visitor sees a 403 message but the HTTP response is a 200 (Next.js Server Components don't set a custom status without the forbidden()/unauthorized() APIs). Access is genuinely blocked either way; only the status code is cosmetically wrong.src/lib/scoring/slr/cobb-waldstreicher.stub.ts), per section 5.3 — needs vertical-motion/RH ingestion this pipeline doesn't pull yet.src/lib/scoring/seasonal-pattern.stub.ts), per section 1's explicit "stub a placeholder module, do not implement logic" directive — this was missing until a full primer audit caught it (see ).src/app/api/cron/*). The Express 5 reuse the primer calls for (section 3) lives at src/server/express-app.ts, mounted into Next via a node-mocks-http-based adapter (src/server/node-adapter.ts) at /api/express/* — used for the subscribe endpoint and the read-only trip-opportunities lookup, the two places that genuinely fit Express's middleware-and-router shape. Local dev + curl end-to-end tested; see comments in node-adapter.ts and express-app.ts for two real bugs hit and fixed along the way (node-mocks-http's default no-op EventEmitter stub, and finalhandler's req.unpipe() crash on a non-stream mock request).src/lib/scoring/slr/types.ts, src/lib/scoring/reconciliation.ts) per section 10, non-negotiable principle 2.trip_opportunity payload (section 6) is the only surface a future PART_2 booking layer would touch.Alerting — src/lib/alerts/ (trigger + 24h rate limit + Resend dispatch), src/lib/email/.
Public site — src/app/page.tsx is a ranked powder leaderboard (pass/region/window filters, a NOAA MRMS radar that opens on the leading region and re-centers on any resort you pick, top picks, and a ranked list with daily snow bars). src/app/resorts/[slug]/page.tsx has the resort's radar, heads-up callouts, a score breakdown, the snowfall chart, conditions, the forecast table and the webcam. No auth wall, per-request rendered (scores change hourly). Public styles live in src/app/public.css (light theme scoped under .ps); the admin pages keep the dark tokens in src/app/globals.css. Radar images come through /api/radar/frames and /api/radar/image (see src/lib/radar/noaa.ts); see ROADMAP Phase D.2.
Admin panel — src/app/admin/, Clerk-gated + ADMIN_USER_IDS owner allowlist, with a persistent nav across its three sections (Dashboard/Forecast accuracy/SLR calibration) and a dashboard stat row + status badges (src/components/status-badge.tsx) — full Phase D pass, not just color tokens. Generates a social graphic via @vercel/og (src/app/api/admin/graphic/[alertId]/route.tsx, matching the same visual system as the public site), drafts a caption (src/lib/social/caption.ts), and posts to Instagram via the two-call Graph API flow (src/lib/social/instagram.ts) only after manual approval (src/app/api/admin/post/[alertId]/route.ts).
Backtest/accuracy logging — src/lib/backtest/accuracy.ts, daily cron + a minimal /admin/backtest report.
Public-baseline comparison pipeline — src/lib/ingestion/nws.ts. Compares our scoring engine against NWS (api.weather.gov) gridded forecasts, both measured against SNOTEL ground truth via the same backtest/accuracy_log pipeline. This is deliberately not an OpenSnow integration — see "On OpenSnow" below.
Self-tuning SLR calibration (recommend-only) — src/lib/calibration/. ROADMAP Phase B. A weekly cron proposes a bounded change to a resort's slr_calibration_multiplier from its pooled accuracy_log history (live samples plus the two historical backtest sources below); nothing is applied until approved at /admin/calibration. See ROADMAP.md for the guardrails (min sample size, max step, hard bounds) and why recommend-only is the starting mode. 8 unit tests in tests/calibration/.
Historical backtest pipelines — src/lib/backtest/slr-reanalysis.ts and src/lib/backtest/reconciliation-forecast-archive.ts, run via npm run backtest:slr-reanalysis -- --years=N / npm run backtest:reconciliation -- --years=N. Let Phase B calibration bootstrap from years of real storm history instead of waiting out a live season — see ROADMAP.md's Phase B section for the full design (why two pipelines, what each validates and doesn't, how they feed into calibration) and src/lib/ingestion/open-meteo-historical.ts for the two new Open-Meteo hosts involved. Results show up in /admin/backtest's own section, kept separate from live accuracy. 34 unit tests across tests/backtest/ and related files.
Model versioning — src/lib/model-versions/ (resolve/create/activate/compare), src/lib/scoring/config.ts, /admin/model-versions. ROADMAP Phase B.1. Named, timestamped, DB-stored snapshots of the scoring engine's tunable config (reconciliation weights, SLR strategy, powder-score weights, confidence thresholds) — exactly one active at a time, read fresh by live scoring every run. Admins can save a draft, backtest under it (--version=<id> on either script above) before ever activating it, compare accuracy stats across versions, and roll back by activating an older one — same code path as activating a draft, deliberately. Calibration approval (item 10) now creates+activates a version instead of silently overwriting a resort's multiplier, so every calibration change has a rollback point. See ROADMAP.md's Phase B.1 section for the full design, including what's deliberately out of scope (no automatic activation, no config-diff view).
Parallel model scoring ("shadow mode") — supabase/migrations/0009_shadow_scoring.sql, src/lib/model-versions/shadow.ts, src/lib/scoring/run.ts. ROADMAP Phase B.2. Lets any number of draft model_versions be scored on the exact same live forecast inputs as the active version, every cron run, so a season's worth of real comparison data accumulates without ever touching what the public sees or what triggers alerts. A version is either active (the one production path — public site, alerts) or is_shadow (scored in parallel for comparison only) — never both, enforced by a DB CHECK constraint. Every snow_scores/accuracy_log row is tagged is_primary at write time; the public read paths (src/lib/public/resort-scores.ts) and the alert trigger (src/lib/alerts/trigger.ts) filter on is_primary = true so a shadow version's score can never leak out or fire an alert. Toggle shadowing per draft from /admin/model-versions; the existing compare view there (item 12) is what you read the results with — no separate UI needed. Ships with one real second model to shadow out of the box: kuchera-wind (src/lib/scoring/slr/kuchera-wind.ts), a wind-adjusted SLR strategy — see ROADMAP.md's Phase B.2 section for why it's framed as a hypothesis to validate this season, not a reproduction of any published model.
Per-resort model precision — supabase/migrations/0010_resort_scoring_overrides.sql, resolveScoringConfigForResort in src/lib/scoring/config.ts. ROADMAP Phase B.3, requested directly ("I want the model to be as precise as possible so code the individual resorts"). Until this, every field in a model_versions row (which weather models to trust at which lead time, SLR strategy, confidence thresholds, ...) applied uniformly to every resort — the one exception was the existing per-resort SLR multiplier (item 12). model_versions.resort_scoring_overrides generalizes that pattern to the rest of the config: resort_id -> a PARTIAL scoring config, merged field-by-field onto that same version's own global config (never straight to the compiled-in defaults) by resolveScoringConfigForResort, called per-resort by the live scoring orchestrator, both historical backtest scripts, and every shadow version — so a resort-specific override is honored everywhere a version's config is used, not just in the live cron. No resort is differentiated out of the box (every version defaults every resort's override to empty, inheriting the global config unchanged) — this ships the capability, not fabricated per-resort tuning invented without evidence. Set overrides from /admin/model-versions's new-draft form (a second, optional JSON textarea); the comparison view there gained an optional per-resort filter so you can check whether an override is actually helping that resort specifically, not just the all-resorts blend.
Mount Mansfield snow-stake ground truth (Stowe-specific) — supabase/migrations/0011_mansfield_stake.sql, src/lib/ingestion/mansfield-stake.ts. Requested directly ("can u integrate data from the Mansfield snow stake"). A resort-specific ground-truth source, not a generalized second network like SNOTEL's nearest-match search: Mount Mansfield is Stowe's own mountain, and the stake has a daily NWS-read record back to 1954 — more locally specific than any nearest-station match could offer for a Vermont resort (SNOTEL has no Northeast coverage at all). Pulls from NOAA's NCEI GHCND archive (public-domain federal data, same legal category as SNOTEL — unlike a UVM-hosted mirror of the same stake found during research, which is CC BY-NC-SA/non-commercial and was deliberately avoided) via the documented CDO v2 API (NCEI_CDO_TOKEN — free, self-service, see .env.example). Writes into the same snotel_actuals table SNOTEL uses, tagged source='ghcnd', reusing the exact same depth-delta-to-snowfall transformation (computeDepthChangeSeries) rather than a parallel pipeline. resorts.ghcnd_station_id (Stowe's seeded value) is explicitly flagged as an unverified candidate — found via a single forum-post citation, not confirmed against NCEI's own station catalog, since every *.noaa.gov host (including a direct fetch attempt at the raw ONI/GHCND data) returned a DNS failure from this sandbox. Degrades gracefully if wrong (no rows written, not a crash) — confirm before fully trusting it.
Mansfield / ENSO correlation research — src/lib/analysis/mansfield-enso.ts, src/lib/config/enso-winters.ts, run via npm run analyze:mansfield-enso. Requested directly alongside item 15 ("pay particular attention to ENSO behavior around the stake as well"). Groups the stake's real historical record into Jul-Jun winter seasons and computes honest mean/min/max seasonal snowfall per ENSO category, joined against enso-winters.ts — a deliberately partial table of only the handful of strong/very-strong El Niño and La Niña winters this session could cross-reference with real confidence via WebSearch, not NOAA's full 1950-present series (NOAA's own ONI data file was unreachable from this sandbox even via a direct fetch attempt — see that file's header for the full sourcing account). Every number this produces is real and computed, never fabricated, but sample sizes per category are small (a handful of seasons) — the script's own output says so explicitly rather than presenting an illustrative comparison as a statistically powered finding. Not wired into live scoring or alerts.
Regional model weighting — src/lib/config/resort-regions.ts, src/lib/config/regional-model-weights.ts, src/lib/model-versions/regional-overrides.ts, npm run create:regional-weighting-draft. ROADMAP Phase B.5, requested directly ("we should be able to individually select the best weather model for each mtn... to mix and match at the resort level. As well as regionally... develop the best regional model u can"). The per-resort mechanism (item 14) already supported this — what was missing was an actual reasoned starting point and a way to apply it to many resorts at once. Classifies the full 65-resort roster into 5 climate/storm-track regions (not an arbitrary grid — genuinely different storm mechanisms: first-range-hit Pacific systems, moisture-depleted interior/continental systems, nor'easter-driven East Coast systems, lake-effect-influenced Midwest), then applies a WebSearch-grounded (not fabricated, not yet accuracy-data-validated) model-weighting hypothesis per region — e.g. a modest HRRR nudge in complex mountain terrain at short lead times (real literature support, SNOTEL-verified), a modest additional ECMWF nudge for the Northeast reflecting its documented-but-imperfect East Coast storm-track reputation. One region (Midwest/Great Lakes) is deliberately left unweighted — lake-effect snow isn't resolved well by any of these synoptic-scale models, and no regional hypothesis was invented just to fill in every region. npm run create:regional-weighting-draft expands this into a shadowable draft model_versions row via the existing resort_scoring_overrides mechanism — never auto-activated or auto-shadowed, same "every activation is an explicit admin click" principle as everywhere else.
Recency weighting and ENSO-aware calibration — supabase/migrations/0012_enso_calibration.sql, src/lib/enso/oni.ts, src/lib/calibration/generate.ts, src/lib/scoring/enso-multiplier.ts, /api/cron/enso. ROADMAP Phase B.6. Calibration samples are weighted by age (half weight at 10 years). Every sample is tagged El Niño / La Niña / neutral from NOAA's ONI, and a resort gets a separate phase multiplier only when its error in that phase is clearly different from neutral winters. Live scoring and the backtests use the phase multiplier in winters of that phase.
valuepricedepart_datereturn_datedeparture_atreturn_atairlinesrc/lib/social/instagram.ts) — partially corrected this session. The two-call parameter names (image_url, caption, creation_id) are confirmed correct as originally built. Two things fixed: the hardcoded API version (v21.0) was likely close to Meta's ~2-year deprecation window by now — made configurable via IG_GRAPH_API_VERSION. There's also a real fork in Meta's current product line between graph.facebook.com (the classic Instagram Graph API via a Facebook-connected account — matches the primer's "Tester role"/dev-mode-app description, kept as the default) and graph.instagram.com (a newer, separate product). Confirm which one your actual app setup uses via IG_GRAPH_API_BASE_URL if needed./data endpoint (src/lib/ingestion/snotel.ts) — corrected this session. Param names/order (stationTriplets, elements, duration, beginDate, endDate) confirmed matching. The duration value was sent as "DAILY" uppercase; documented values are lowercase (daily, hourly, ...) — fixed.batch.send size limit (src/lib/alerts/dispatch.ts) — real bug, fixed this session. Confirmed via WebSearch: max 100 emails per batch call. The original code sent one unchunked batch for all matching subscribers — would have hard-failed the moment any single resort passed 100 subscribers. Fixed: chunks into groups of ≤100, sends all chunks even if one fails, tracks actual delivered count.src/lib/config/resorts-seed.ts): lat/lng/elevation for the full US Epic + Ikon roster (51 resorts) compiled from training knowledge, not a live source. npm run seed:resorts runs each one through the Open-Meteo Elevation API onboarding check (section 2.1a) and flags >150m discrepancies — review flagged resorts before trusting their scores. mid elevation is a base/summit midpoint interpolation, not a measured mid-mountain figure.src/lib/scoring/aggregate.ts and run.ts.src/lib/scoring/wind-hold.ts) — deliberately generic, not resort-specific. This codebase has no per-resort/per-lift wind-hold threshold data (each resort's own operational policy, not published anywhere this pipeline ingests), so the estimate is a documented logistic curve over forecast wind gust (see the constants' header comment in src/lib/config/constants.ts), surfaced in the UI as an estimate, never a guarantee a lift will or won't run.resorts.webcam_url) — operator-curated, same pattern as elevation_base_m/slr_calibration_multiplier, not scraped. Requested directly ("pull from resort pages"), but automated scraping of ~40+ different resort sites' webcam pages (different structures, JS-rendered players, real ToS risk per-site) is exactly the kind of scraping-at-scale tradeoff already declined once this session for OpenSnow — see "On OpenSnow" below. Every resort currently has webcam_url = null until manually populated; the page renders a "not linked yet" state until then.computeDepthChangeSeries in src/lib/ingestion/snotel.ts. Same western-US-only SNOTEL coverage caveat as the existing ground-truth pipeline applies here too.src/lib/ingestion/open-meteo-historical.ts, feeding the Phase B backtest pipelines) — not verified against a live response, same EGRESS_BLOCKED situation as every other Open-Meteo host this sandbox tried all session. WebSearch confirmed the Historical Weather API (archive-api.open-meteo.com/v1/archive, ERA5 reanalysis back to 1940) officially supports temperature_2m/precipitation/cloud_cover/wind_speed_10m as named hourly variables — this module deliberately requests only temperature_2m+precipitation for it, since pressure-level variable support on that specific endpoint couldn't be confirmed and an invalid param could fail the whole request (the existing Kuchera surface-temp fallback already handles the no-pressure-levels case, so this isn't a new simplification). The Historical Forecast API (historical-forecast-api.open-meteo.com/v1/forecast) is less confirmed: one third-party source described it as accepting "the same parameters as the forecast API, plus start_date/end_date," but the exact models= identifier strings (HISTORICAL_FORECAST_MODEL_IDS in the same file) are a best-effort mapping, not a confirmed list — flagged clearly in-code, one place to fix if any model ID is wrong, and HRRR is deliberately excluded (no source confirmed archive coverage for it, unlike GFS/ICON).RECONCILIATION_BACKTEST_NOMINAL_LEAD_TIME_HOURS) rather than claiming to validate the T48_120/T120_PLUS weight buckets specifically — see src/lib/backtest/reconciliation-forecast-archive.ts's header for the full reasoning.ROADMAP.mdresorts table, never hardcoded elsewhere.