Platoon polarity: VERIFIED correct, pinned by a standing test
Report-only verification of the two unproven claims from the Session-77 wiring, which fired platoon on synthetic split-less hitters where the K=600 regression zeroed the multiplier and masked both direction and resolution. No product code changed — this adds one standing regression test. FIXTURE — Yordan Alvarez (LHB), real 2026 splits, deep both hands: 115 PA vs LHP at .529 slg, 327 PA vs RHP at .695, overall .652. The regression leaves a material multiplier both ways (0.97 vs LHP, 1.023 vs RHP), so unlike the last test this fixture can actually reveal direction. RESOLUTION — verified on the live slate that the pitcher-hand attached to a hitter is the OPPOSING team's probable, not his own. CLE (home) resolved to Minnesota's away starter 696070; MIN (away) resolved to Cleveland's home starter 800048. The chain — hitter's team, the game, the other team, that team's probable, that pitcher's hand — is correct, and it is pinned independently of direction because a backwards resolution is invisible on a neutral hitter. DIRECTION — deterministic L-vs-R on the frozen Alvarez fixture. Facing RHP nudges UP (1.023) because he slugs .695 there, above his .652 overall — a favorable opposite-hand matchup, exactly what platoon theory predicts for a left-handed bat. Facing LHP nudges DOWN (0.97). The two move opposite directions, and crucially the SPECIFIC sides are asserted, not merely "opposite" — a mirrored-but-inverted implementation would put RHP below 1 and fails here. Both- backwards is ruled out. The test also pins a REVERSE-split hitter, Brandon Nimmo, who hits better vs LHP than RHP. His multiplier goes up vs LHP, following his real numbers rather than a hardcoded LHB-vs-RHP assumption — proof the sign is data-driven, which is the correct design. VERDICT: PASS. Resolution correct, direction correctly signed against both the real split and platoon theory. Pinned by tests/unit/platoonPolarity.test.js so the polarity cannot silently regress — the opp_rank_stat lesson applied. Tests 3750 passed / 302 suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/* ============================================================
|
||||
Session 78 — PLATOON POLARITY LOCK (standing regression).
|
||||
|
||||
The opp_rank_stat lesson: polarity needs a PERMANENT assertion, not a
|
||||
one-time eyeball. A backwards opposing-pitcher resolution OR a backwards
|
||||
multiplier sign both look fine on a near-neutral hitter — so this pins BOTH
|
||||
on a real, materially-split hitter (Yordan Alvarez, LHB, 2026), with frozen
|
||||
numbers so it can never silently drift.
|
||||
|
||||
If this test ever fails, platoon has been broken in one of two ways and the
|
||||
Session-77 wiring can no longer be trusted — do not "fix the test", find the
|
||||
regression.
|
||||
============================================================ */
|
||||
|
||||
const pl = require('../../src/services/platoonSplits');
|
||||
const ec = require('../../src/services/environmentContext');
|
||||
|
||||
// Yordan Alvarez, real 2026 splits — deep both hands, textbook LHB platoon.
|
||||
// vs RHP he slugs far above his overall (advantage); vs LHP below (disadvantage).
|
||||
const ALVAREZ = {
|
||||
vsL: { pa: 115, slg: 0.529, avg: 0.250 },
|
||||
vsR: { pa: 327, slg: 0.695, avg: 0.290 },
|
||||
overall: { pa: 442, slg: 0.652, avg: 0.278 },
|
||||
};
|
||||
|
||||
describe('DIRECTION — signed against the real split AND platoon theory', () => {
|
||||
const est = (ph) => pl.platoonEstimate({ splits: ALVAREZ, batterHand: 'L', pitcherHand: ph, statType: 'total_bases' });
|
||||
|
||||
it('produces a MATERIAL multiplier both ways (fixture can reveal direction)', () => {
|
||||
// A neutral subject would reproduce the inconclusive Session-77 test.
|
||||
expect(est('L').material).toBe(true);
|
||||
expect(est('R').material).toBe(true);
|
||||
});
|
||||
|
||||
it('vs RHP nudges UP — favorable split (opposite hand) → over more likely', () => {
|
||||
// .695 slg vs RHP > .652 overall → favorable → UP. Backwards would be < 1.
|
||||
expect(est('R').multiplier).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('vs LHP nudges DOWN — unfavorable split (same hand)', () => {
|
||||
// .529 slg vs LHP < .652 overall → unfavorable → DOWN. Backwards would be > 1.
|
||||
expect(est('L').multiplier).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('the two hands move OPPOSITE directions', () => {
|
||||
const l = est('L').multiplier;
|
||||
const r = est('R').multiplier;
|
||||
expect((l < 1)).not.toBe((r < 1));
|
||||
});
|
||||
|
||||
it('BOTH-BACKWARDS is ruled out — a mirrored-but-wrong sign fails here', () => {
|
||||
// If the whole thing were inverted, RHP would be < 1 and LHP > 1. Assert the
|
||||
// exact expected side, not merely "opposite".
|
||||
expect(est('R').multiplier).toBeGreaterThan(1); // must be the RHP side that's up
|
||||
expect(est('L').multiplier).toBeLessThan(1); // and the LHP side that's down
|
||||
});
|
||||
|
||||
it('a REVERSE-split hitter follows his real numbers, not the platoon prior', () => {
|
||||
// Brandon Nimmo (LHB) hits BETTER vs LHP than RHP — unusual. The estimate
|
||||
// must follow HIS data (up vs LHP), proving the sign is data-driven and not
|
||||
// a hardcoded LHB-vs-RHP assumption.
|
||||
const nimmo = { vsL: { pa: 104, slg: 0.510 }, vsR: { pa: 301, slg: 0.405 }, overall: { pa: 405, slg: 0.432 } };
|
||||
const vsL = pl.platoonEstimate({ splits: nimmo, batterHand: 'L', pitcherHand: 'L', statType: 'total_bases' });
|
||||
const vsR = pl.platoonEstimate({ splits: nimmo, batterHand: 'L', pitcherHand: 'R', statType: 'total_bases' });
|
||||
expect(vsL.multiplier).toBeGreaterThan(1); // favorable vs LHP per HIS split
|
||||
expect(vsR.multiplier).toBeLessThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RESOLUTION — the OPPOSING team\'s pitcher, never the hitter\'s own', () => {
|
||||
// A backwards resolution runs platoon on the wrong hand — invisible on a
|
||||
// neutral hitter, so it is pinned independently of direction.
|
||||
const SCHEDULE = { games: [
|
||||
{ homeTeam: { name: 'Cleveland Guardians' }, awayTeam: { name: 'Minnesota Twins' }, gameTime: '2026-07-21T22:40Z', venue: 'Progressive Field' },
|
||||
] };
|
||||
const PITCHERS = { games: [
|
||||
{ home: { team: 'Cleveland Guardians', pitcherId: 800048 }, away: { team: 'Minnesota Twins', pitcherId: 696070 } },
|
||||
] };
|
||||
const PEOPLE = { people: [
|
||||
{ id: 800048, pitchHand: { code: 'L' } }, // CLE's own SP
|
||||
{ id: 696070, pitchHand: { code: 'R' } }, // MIN's own SP
|
||||
] };
|
||||
const fetchJson = async (url) => {
|
||||
if (url.includes('/pitchers')) return PITCHERS;
|
||||
if (url.includes('personIds=')) return PEOPLE;
|
||||
if (url.includes('open-meteo')) return { hourly: { time: [], temperature_2m: [] } };
|
||||
if (url.includes('statSplits')) return { stats: [] };
|
||||
return SCHEDULE;
|
||||
};
|
||||
|
||||
it('a HOME hitter faces the AWAY team\'s pitcher hand', async () => {
|
||||
const ctx = await ec.buildContext('mlb', { fetchJson, schedule: SCHEDULE, pitchers: PITCHERS, people: PEOPLE });
|
||||
// CLE hitter should resolve to MIN's pitcher (696070, R), NOT CLE's own (800048, L).
|
||||
const oppPid = ctx._internals.oppPitcherByTeam.get('CLE');
|
||||
expect(oppPid).toBe(696070);
|
||||
expect(ctx._internals.handById.get(oppPid)).toBe('R');
|
||||
});
|
||||
|
||||
it('an AWAY hitter faces the HOME team\'s pitcher hand', async () => {
|
||||
const ctx = await ec.buildContext('mlb', { fetchJson, schedule: SCHEDULE, pitchers: PITCHERS, people: PEOPLE });
|
||||
const oppPid = ctx._internals.oppPitcherByTeam.get('MIN');
|
||||
expect(oppPid).toBe(800048); // CLE's SP, not MIN's own
|
||||
expect(ctx._internals.handById.get(oppPid)).toBe('L');
|
||||
});
|
||||
|
||||
it('the two teams get DIFFERENT pitchers — never the same (own) arm', async () => {
|
||||
const ctx = await ec.buildContext('mlb', { fetchJson, schedule: SCHEDULE, pitchers: PITCHERS, people: PEOPLE });
|
||||
expect(ctx._internals.oppPitcherByTeam.get('CLE')).not.toBe(ctx._internals.oppPitcherByTeam.get('MIN'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user