'use strict'; /** * lowParamService — the production side of the two-parameter correction. * * Mirrors calibrationService's interface so the serving path swaps cleanly, but * fits a Platt curve instead of an isotonic map. The reason for the swap is * capacity, not score: on 19 dates we cannot certify the stability of a map with * one free parameter per prediction level, and LODO turned out to have 1.4-9.3% * power to tell us otherwise. Two parameters cannot encode "this Tuesday was * odd", which is exactly the failure we cannot rule out for isotonic. * * Stated plainly because it is a judgement rather than a measurement: on the * held-out window isotonic scored BETTER than this on hits (+0.0028) and rbi * (+0.0042) and tied on total_bases. That window spans 2-4 date blocks, so it is * weak evidence either way, and it is consistent with a flexible map having * captured structure shared by fit and evaluation periods. * * Same point-in-time cut as before: fitted ONLY on games that are already over. */ const lp = require('./lowParamCalibrator'); const cal = require('./calibration'); const { paginate } = require('../../utils/safePaginate'); const { knownNumber } = require('../../utils/known'); const MIN_FIT = 200; const HOLDOUT_FRACTION = 0.35; /** Build from settled rows: fit on the older part, certify bands on the newer. */ function build(rows, opts = {}) { const clean = (rows || []) .map((r) => ({ p: knownNumber(r.p), won: knownNumber(r.won), date: String(r.date || '') })) .filter((r) => r.p !== null && (r.won === 0 || r.won === 1)) .sort((a, b) => a.date.localeCompare(b.date)); if (clean.length < (opts.minFit ?? MIN_FIT)) return null; const cut = Math.floor(clean.length * (1 - (opts.holdout ?? HOLDOUT_FRACTION))); const fitRows = clean.slice(0, cut); const certRows = clean.slice(cut); if (fitRows.length < (opts.minFit ?? MIN_FIT) || certRows.length < 50) return null; const model = lp.fitPlatt(fitRows, opts); // A refused fit (inverting or collapsed slope) yields no calibrator at all. if (!model || model.refused) return null; const corrected = certRows .map((r) => ({ ...r, p: lp.applyPlatt(model, r.p) })) .filter((r) => knownNumber(r.p) !== null); const bands = cal.certifyBands(corrected, { tolerance: opts.tolerance ?? 0.05, minBin: opts.minBin ?? 40, }); return { model, bands, fit_n: fitRows.length, certify_n: certRows.length, fitted_through: fitRows[fitRows.length - 1].date, shrinkage: model.shrinkage, calibrate(p) { const raw = knownNumber(p); if (raw === null) return { p_raw: null, p_calibrated: null, calibrated: false, reason: 'absent' }; const c = lp.applyPlatt(model, raw); if (c === null) return { p_raw: raw, p_calibrated: null, calibrated: false, reason: 'no_model_value' }; const inBand = cal.inCertifiedBand(bands, c); return { p_raw: raw, p_calibrated: Math.round(c * 1000) / 1000, calibrated: inBand, reason: inBand ? null : 'outside_certified_band', }; }, }; } function todayEt() { return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit', }).format(new Date()); } /** * The ROW LOAD — exported so the read-integrity harness measures THE REAL * FUNCTION rather than a restatement of its query. * * ── FIX A2 (2026-08-09) — THIS READ WAS 24.8% CORRUPT ──────────────────── * This is the PRIMARY calibrator (calibrationService is only its shadow), and it * carried the byte-identical defect A1 fixed there: an unordered `.range()` walk, * plus `if (error || !data) break` swallowing a failed read as end-of-data. * Measured on production: 617 of 2,490 rows returned twice, an equal number never * returned, with `rows.length` matching the server count exactly. * * Both are now `safePaginate` on the unique `id`. A throw means the read failed; * `null` from `fromLedger` still means "not enough settled history to fit". Those * are different states and collapsing them is what hid the defect. */ async function loadSettledRows(sb, { sport = 'mlb', stat = 'hits', before = null } = {}) { const cutoff = before || todayEt(); return paginate( () => sb.from('ledger_entries') .select('id, p_win, outcome, game_date, quarantine_reason') .eq('sport', sport).is('user_id', null).eq('stat', stat) .in('outcome', ['hit', 'miss']).not('p_win', 'is', null) .lt('game_date', cutoff), { key: 'id', pageSize: 1000, label: `lowParamService.fromLedger(${sport}/${stat})` }, ); } /** Load settled history and build, POINT-IN-TIME (strictly before today). */ async function fromLedger(sb, { sport = 'mlb', stat = 'hits', before = null, ...opts } = {}) { if (!sb) return null; const cutoff = before || todayEt(); const rows = await loadSettledRows(sb, { sport, stat, before: cutoff }); const clean = rows .filter((r) => !(r.quarantine_reason || '').startsWith('nontakeable_book')) .map((r) => ({ p: Number(r.p_win), won: r.outcome === 'hit' ? 1 : 0, date: String(r.game_date) })); const built = build(clean, opts); return built ? { ...built, cutoff } : null; } module.exports = { build, fromLedger, loadSettledRows, MIN_FIT, HOLDOUT_FRACTION };