// Lock-line capture — persist multi-book lines at the lock moment (measurement-only). // Locks: build correctness, honest-absent single-book, the STRUCTURAL FENCE (no mutation, // no grade-path read), and grade byte-identical through runSnapshot. const fs = require('fs'); const path = require('path'); const llc = require('../../src/services/lockLineCapture'); const snapshot = require('../../src/services/snapshotService'); const { nameKey } = require('../../src/utils/playerName'); // Judge TB across 3 books + Betts hits (1 book) + a NON-graded prop. const props = [ { player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, over_odds: -115, under_odds: -105, book: 'draftkings', game_time: '2026-07-29T23:00:00Z' }, { player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, over_odds: -110, under_odds: -110, book: 'betmgm', game_time: '2026-07-29T23:00:00Z' }, { player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, over_odds: -120, under_odds: 100, book: 'pinnacle', game_time: '2026-07-29T23:00:00Z' }, { player: 'Mookie Betts', stat_type: 'hits', line: 1.5, over_odds: 120, under_odds: -150, book: 'draftkings', game_time: '2026-07-29T23:00:00Z' }, { player: 'Not Graded', stat_type: 'hits', line: 0.5, over_odds: -110, under_odds: -110, book: 'draftkings', game_time: '2026-07-29T23:00:00Z' }, ]; const gradedKeys = new Set([`${nameKey('Aaron Judge')}|total_bases`, `${nameKey('Mookie Betts')}|hits`]); describe('buildLockRows', () => { it('emits one row per graded prop×book, both odds, sharp tag on pinnacle', () => { const rows = llc.buildLockRows('mlb', props, gradedKeys, { lockedAt: 'T0' }); const judge = rows.filter((r) => r.stat === 'total_bases'); expect(judge).toHaveLength(3); expect(judge.map((r) => r.book).sort()).toEqual(['betmgm', 'draftkings', 'pinnacle']); expect(judge.every((r) => r.over_odds != null && r.under_odds != null)).toBe(true); expect(judge.every((r) => r.locked_at === 'T0')).toBe(true); expect(judge.find((r) => r.book === 'pinnacle').line_type).toBe('sharp'); expect(judge.find((r) => r.book === 'draftkings').line_type).toBe('book'); expect(judge[0].player_key).toBe(nameKey('Aaron Judge')); }); it('HONEST-ABSENT: a single-book graded prop persists as ONE row, never a fake second', () => { const rows = llc.buildLockRows('mlb', props, gradedKeys, { lockedAt: 'T0' }); const betts = rows.filter((r) => r.stat === 'hits' && r.player_key === nameKey('Mookie Betts')); expect(betts).toHaveLength(1); expect(betts[0].book).toBe('draftkings'); }); it('excludes NON-graded props (only the locked props are persisted)', () => { const rows = llc.buildLockRows('mlb', props, gradedKeys, { lockedAt: 'T0' }); expect(rows.some((r) => r.player_key === nameKey('Not Graded'))).toBe(false); }); it('drops a book row with neither price (not a real line)', () => { const rows = llc.buildLockRows('mlb', [ { player: 'X', stat_type: 'hits', line: 0.5, over_odds: null, under_odds: null, book: 'draftkings' }, ], new Set([`${nameKey('X')}|hits`]), { lockedAt: 'T0' }); expect(rows).toHaveLength(0); }); }); describe('STRUCTURAL FENCE', () => { it('never mutates the props it reads (deep-frozen input)', () => { const frozen = Object.freeze(props.map((p) => Object.freeze({ ...p }))); expect(() => llc.buildLockRows('mlb', frozen, gradedKeys, { lockedAt: 'T0' })).not.toThrow(); }); it('no grade-path module reads lock_lines / lockLineCapture', () => { const gradePathFiles = [ 'src/services/gradeSlateService.js', 'src/services/ledgerService.js', 'src/services/challengerProjection.js', 'src/services/contactChallenger.js', 'src/services/projectionChallenger.js', 'src/services/intelligence/analyzeViaEngine1.js', ]; for (const rel of gradePathFiles) { const src = fs.readFileSync(path.join(__dirname, '../../', rel), 'utf8'); expect(src).not.toMatch(/lock_lines/); expect(src).not.toMatch(/lockLineCapture/); } }); }); describe('GRADE BYTE-IDENTICAL through runSnapshot', () => { function memCache() { const store = {}; return { store, cacheGet: async (k) => (k in store ? store[k] : null), cacheSet: async (k, v) => { store[k] = v; } }; } const grades = [ { player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'A', confidence: 71, edge_pct: 4.2 }, { player: 'Mookie Betts', stat_type: 'hits', line: 1.5, direction: 'under', grade: 'B', confidence: 60, edge_pct: 1.1 }, ]; const fakeGrade = () => async (_s, _p, opts) => { await opts.cacheSet('grades:x', { grades, updated_at: opts.now(), source: 'test' }); return { written: true, count: grades.length }; }; const baseOpts = (cache, lockDep) => ({ getOdds: async () => ({ props, provider: 'test' }), gradeAndCacheSlate: fakeGrade(), resolveStats: async () => ({ found: false }), classify: () => ({ primary: null }), cacheGet: cache.cacheGet, cacheSet: cache.cacheSet, now: () => '2026-07-29T00:00:00.000Z', nowMs: () => 1000, notify: async () => {}, retention: null, ledger: { recordPipelineGrades: async () => ({ written: 0 }), captureClosing: async () => {}, __internals: require('../../src/services/ledgerService').__internals }, refreshTeamStats: async () => null, buildEspnIndex: async () => ({}), gameBinder: { attachGameTimes: async () => ({ bound: 2, alreadyHad: 0, unresolved: 0, ambiguous: 0 }) }, lockLineCapture: lockDep, }); it('grades identical whether persist runs or is a no-op, and lock rows target graded props at ts', async () => { const captured = []; const withPersist = { buildLockRows: llc.buildLockRows, persist: async (rows) => { captured.push(...rows); return { written: rows.length, attempted: rows.length }; } }; const noop = { buildLockRows: () => [], persist: async () => ({ written: 0, attempted: 0 }) }; const c1 = memCache(); await snapshot.runSnapshot('mlb', baseOpts(c1, withPersist)); const c2 = memCache(); await snapshot.runSnapshot('mlb', baseOpts(c2, noop)); const strip = (snap) => JSON.stringify((snap.grades || []).map((g) => ({ player: g.player, stat_type: g.stat_type, line: g.line, direction: g.direction, grade: g.grade, gradedAt: g.gradedAt }))); expect(strip(c1.store['snapshot:mlb:latest'])).toBe(strip(c2.store['snapshot:mlb:latest'])); // lock rows were built for the graded props, timestamped at the lock moment (ts). expect(captured.length).toBeGreaterThan(0); expect(captured.every((r) => r.locked_at === '2026-07-29T00:00:00.000Z')).toBe(true); expect(new Set(captured.map((r) => r.player_key))).toEqual(new Set([nameKey('Aaron Judge'), nameKey('Mookie Betts')])); }); });