// Session 48 — name normalization applied at every layer (snapshot grades, // game-card strips, scan grid). const fs = require('fs'); const path = require('path'); const WEB = path.join(__dirname, '..', '..', 'web', 'src'); const snap = require('../../src/services/snapshotService'); const slate = require('../../web/src/lib/slateAdapter'); function memCache() { const store = {}; return { store, cacheGet: async (k) => (k in store ? store[k] : null), cacheSet: async (k, v) => { store[k] = v; } }; } describe('Phase 2a — snapshot normalizes + dedupes grades at the source', () => { const baseGrades = [ { player: 'Matthew Liberatore', stat_type: 'strikeouts', line: 5.5, direction: 'over', grade: 'A', confidence: 80 }, { player: 'Matt Liberatore', stat_type: 'strikeouts', line: 3.5, direction: 'over', grade: 'B', confidence: 60 }, { player: 'A.J. Ewing', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'B+', confidence: 70 }, { player: 'AJ Ewing', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'C', confidence: 55 }, ]; const deps = (cache) => ({ getOdds: async () => ({ sport: 'mlb', props: [{ player: 'x', stat_type: 'hits', line: 1.5 }], provider: 'test' }), gradeAndCacheSlate: async (_s, _p, opts) => { await opts.cacheSet('grades:x', { grades: baseGrades }); return { written: true, count: baseGrades.length }; }, resolveStats: async () => ({ found: false }), classify: require('../../src/services/archetypeService').classify, cacheGet: cache.cacheGet, cacheSet: cache.cacheSet, now: () => '2026-06-19T20:00:00Z', nowMs: () => 1, }); it('writes ONE grade per normalized player+stat (highest confidence), names normalized', async () => { const cache = memCache(); await snap.runSnapshot('mlb', deps(cache)); const grades = cache.store['snapshot:mlb:latest'].grades; // Liberatore: one strikeouts grade (the A/conf80), Ewing: one hits grade (B+/conf70) expect(grades).toHaveLength(2); const lib = grades.find((g) => g.stat_type === 'strikeouts'); expect(lib.grade).toBe('A'); expect(lib.player).toBe('Matthew Liberatore'); // normalized display const ew = grades.find((g) => g.stat_type === 'hits'); expect(ew.player).toBe('AJ Ewing'); // de-dotted }); }); describe('Phase 2b — game-card strips show one row per stat', () => { it('collapses variant prop dupes into one strip, one row per stat', () => { const strips = slate.buildPlayerStripsFromProps([ { player: 'Matt Liberatore', stat_type: 'strikeouts', line: 5.5 }, { player: 'Matthew Liberatore', stat_type: 'strikeouts', line: 3.5 }, { player: 'Matthew Liberatore', stat_type: 'hits_allowed', line: 4.5 }, ], {}, {}); expect(strips).toHaveLength(1); // strikeouts deduped to one row; hits_allowed separate const ks = strips[0].props.filter((p) => p.stat === 'Ks'); expect(ks).toHaveLength(1); expect(strips[0].props).toHaveLength(2); }); it('prefers the graded prop over an awaiting one for the same stat', () => { const gradeIndex = slate.indexGrades([{ player: 'AJ Ewing', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'A', gradedAt: { line: 1.5, timestamp: '2026-06-19T18:00:00Z' } }]); const strips = slate.buildPlayerStripsFromProps([ { player: 'A.J. Ewing', stat_type: 'hits', line: 1.5 }, // matches grade { player: 'AJ Ewing', stat_type: 'hits', line: 1.5 }, // variant, ungraded dup ], gradeIndex, {}); expect(strips).toHaveLength(1); expect(strips[0].props).toHaveLength(1); expect(strips[0].props[0].grade).toBe('A'); // kept the graded one }); }); describe('Phase 2c — scan player grid groups by nameKey', () => { const src = fs.readFileSync(path.join(WEB, 'app', 'scan', 'page.tsx'), 'utf8'); it('groups tonightsPlayers by nameKey + displays normalized', () => { expect(src).toContain('nameKey(p.player)'); expect(src).toContain('normalizeName(p.player).display'); }); });