'use strict'; /** * TEMPLATE 1 — HOT HITTERS. * * Reads the REPAIRED full-season log, not a ten-game slice. "Hot" here means a * recent rate measured against that hitter's own season rate — which is only a * meaningful comparison now that the season rate is a season. */ const { knownNumber } = require('../../../utils/known'); /** A hitter must have this much history before we call him anything. */ const MIN_SEASON_GAMES = 20; const RECENT = 10; module.exports = { id: 'hot_hitters', sport: 'mlb', label: 'Hot Hitters', requires: ['count', 'hitters', 'window', 'date'], async pull(deps) { const rows = await deps.hitterForm(); // injected, read-only // ── EMPTY POOL IS NOT AN HONEST ABSENCE ──────────────────────────────── // The first run of this template emitted "no hitter is meaningfully hot" // while the real cause was a source holding fewer than 20 games for EVERY // player. That reads as a considered editorial judgement and is actually a // broken pull -- the exact failure class that has bitten this codebase four // times tonight, here wearing the costume of our own honesty copy. // // So the two are separated: no candidates at all is a SKIP with a reason; // candidates present but none hot is the honest absence. const candidates = (rows || []).length; const usable = (rows || []).filter((r) => knownNumber(r.season_games) !== null && r.season_games >= MIN_SEASON_GAMES && knownNumber(r.recent_rate) !== null && knownNumber(r.season_rate) !== null); const hot = usable .map((r) => ({ ...r, lift: r.recent_rate - r.season_rate })) .filter((r) => r.lift > 0) .sort((a, b) => b.lift - a.lift) .slice(0, 5); return { date: deps.date, window: RECENT, candidates, qualified: usable.length, count: hot.length || null, // zero hot hitters is an ABSENT list, not "0 hot hitters" hitters: hot.length ? hot : null, list: hot.map((h, i) => `${i + 1}. ${h.name} — ${Math.round(h.recent_rate * 100)}% last ${RECENT}, ${Math.round(h.season_rate * 100)}% season`).join('\n'), top_name: hot[0] ? hot[0].name : null, top_recent: hot[0] ? Math.round(hot[0].recent_rate * 100) : null, top_season: hot[0] ? Math.round(hot[0].season_rate * 100) : null, }; }, copy: () => `WHO'S ACTUALLY HOT — {date} {top_name} is hitting {top_recent}% over his last {window}. His season number is {top_season}%. That gap is the whole point. Everybody else is guessing at it. {list} Measured off full season logs, not a ten-game window that flatters whoever ran hot last week.`, card: () => ({ kind: 'list', title: "WHO'S ACTUALLY HOT", subtitle: 'LAST {window} vs SEASON · {date}', lines: [ { text: '{top_name}', style: 'lead' }, { text: '{top_recent}% last {window} · {top_season}% season', style: 'stat' }, { text: '', style: 'rule' }, { text: '{list}', style: 'body' }, ], footer: 'Rates measured from full season game logs.', }), absent: (gaps, facts) => { // Only speak if there was actually a pool to judge. if (!facts || !facts.qualified) { return { copy: null, card: null, skip: `no qualified hitters in the pool (candidates=${(facts && facts.candidates) || 0}, qualified=${(facts && facts.qualified) || 0}) — source problem, not a quiet night`, }; } return { copy: "No hitter is meaningfully hot tonight.\n\nWe could dress up a middling week as a streak. We don't.", card: { kind: 'absence', title: 'NOTHING HOT TONIGHT', subtitle: 'AND WE WILL SAY SO', lines: [{ text: 'No hitter cleared his own season rate by enough to name.', style: 'body' }], footer: 'An empty list is a real answer.' }, }; }, };