'use strict'; /** * The proven half of Link 2. * * Two grains were tested. These lock the one that earned a place and the * refusals that keep the other from creeping back in. */ const pq = require('../../src/services/model/penQuality'); const games = (n, q) => Array.from({ length: n }, () => ({ quality: q })); describe('abstains rather than imputing', () => { it('an arm below the appearance floor has no readable quality', () => { expect(pq.armQuality({ pa: 12, hits: 3 })).toBeNull(); expect(pq.armQuality({ pa: 60, hits: 12 })).toBeCloseTo(0.2, 6); }); it('a club with too few prior games returns null, not a league-average pen', () => { // A league-average stand-in asserts "this is an ordinary bullpen" — a claim, // and usually the wrong one for exactly the clubs whose pens just turned over. expect(pq.projectPen(games(3, 0.22))).toBeNull(); expect(pq.projectPen([])).toBeNull(); expect(pq.projectPen(null)).toBeNull(); expect(pq.projectPen(games(8, 0.22)).readable).toBe(true); }); it('unreadable quality produces no shift at all', () => { expect(pq.hitRateShift(null)).toBeNull(); expect(pq.hitRateShift(undefined)).toBeNull(); }); }); describe('the grain that proved, and the two that did not', () => { it('reports pen QUALITY and names what it refuses', () => { const pen = pq.projectPen(games(10, 0.24)); expect(pen.grain).toBe('pen_quality'); expect(pen.individual_arm_refused).toMatch(/17.2%/); expect(pen.archetype_refused).toMatch(/did not prove/); // The module must not offer an archetype — that grain did not earn one. expect(pen.archetype).toBeUndefined(); }); }); describe('the shift follows the measured direction', () => { it('a weaker pen raises the hit rate, a stronger one lowers it', () => { expect(pq.hitRateShift(0.26)).toBeGreaterThan(0); expect(pq.hitRateShift(0.19)).toBeLessThan(0); expect(pq.hitRateShift(pq.LEAGUE_PEN_QUALITY)).toBeCloseTo(0, 6); }); it('is bounded — it was measured over a range, not extrapolated past one', () => { expect(pq.hitRateShift(0.9)).toBeLessThanOrEqual(pq.MAX_SHIFT + 1e-9); expect(pq.hitRateShift(0.01)).toBeGreaterThanOrEqual(-pq.MAX_SHIFT - 1e-9); }); it('reproduces the measured tercile separation', () => { // Predicted-best pens averaged 0.2114 actual quality and a 0.2231 realized // hit rate; predicted-worst 0.2425 and 0.2501 — a 2.70pp gap. const gap = pq.hitRateShift(0.2425) - pq.hitRateShift(0.2114); expect(gap).toBeGreaterThan(0.02); expect(gap).toBeLessThan(0.035); }); }); describe('reasoning is true or absent', () => { it('no read means no sentence', () => { expect(pq.explain(null)).toBeNull(); expect(pq.explain({ readable: false })).toBeNull(); }); it('names the direction and how much was read', () => { expect(pq.explain(pq.projectPen(games(12, 0.25)))).toMatch(/weaker than league over 12 prior games/); expect(pq.explain(pq.projectPen(games(12, 0.20)))).toMatch(/stronger than league/); expect(pq.explain(pq.projectPen(games(12, 0.2261)))).toMatch(/league-average/); }); });