Phase A #2: the champion grade (l5/l20 result-based form) is a HYPOTHESIS that
contact quality predicts better — unmeasured on our props, with zero settled
p_win yet. Swapping l5/l20 (the champion's two heaviest ±1.0 factors) blind
could degrade the core grade undetectably for weeks. So this NOMINATES contact
quality as a second challenger, records what it WOULD project per prop, and lets
the settled ledger decide. Nothing users see changes; the champion is untouched.
- src/services/contactChallenger.js — pure, mirrors challengerProjection. Log-
odds lean (capped, never a re-forecast) from SEASON contact quality vs league
percentiles. Metric→prop mapping is the whole game: barrel_pct→HR,
hard_hit_pct→TB/doubles, k_pct-INVERSE→hits (singles resolve on contact
frequency, not barrels), k_pct→batter K. rbi/runs/walks ABSTAIN (opportunity/
discipline — no clean contact predictor). Honest-absent: thin (<50 PA)/absent/
unmapped/non-batter → p_win_contact NULL (no projection), never a fallback;
"measured but unremarkable" is distinct (equals champion, delta 0).
- Wired in snapshotService AFTER arch-v1, reusing the already-loaded statcast
rows; its own try so a second challenger can't break the pipeline. Reads
g.p_win, never writes it.
- Retained SEPARATELY on the ledger (p_win_contact/contact_delta/
contact_adjustments/contact_version='contact-v1') so each challenger's marginal
contribution is measured independently; ledger_entries.stat gives per-prop-type
segmentation. Migration 031 (applied to prod).
Phase 0 (prod-verified): statcast_aggregates is SEASON cumulative (not rolling),
48h stale now but season-scoped so ~8 PA/600 is negligible; 100% of graded
hitters covered, 92% at ≥50 PA; no xBA/xwOBA in the feed. Forward-only,
version-stamped (contact_version null on pre-nomination rows). Promotion is a
LATER decision on settled evidence, per prop type — never asserted here.
contactChallenger 14/14; snapshot/ledger/arch-v1 suites 80 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj