/* ============================================================ Session 70 — THE MEASUREMENT INSTRUMENT. Calibration needs p_win beside the outcome. Market-comparison needs p_win beside the close. Both need them ON THE SAME ROW. Before this, p_win lived only in a table that never settles and the close lived only in a table with no probability — so neither question was answerable. ============================================================ */ const ledger = require('../../src/services/ledgerService'); const GRADE = { player: 'Josh Bell', stat_type: 'hits', direction: 'over', grade: 'B', gradedAt: { line: 0.5, odds: -210, timestamp: '2026-07-21T03:00:00.000Z' }, p_win: 0.757, fair_prob: 0.633, edge_pct: 12, confidence: 63, archetype_axes: { blend: [{ label: 'BOMBER', axis: 'POWER', tier: 'elite' }] }, }; const PROP = { player: 'Josh Bell', stat_type: 'hits', game_time: '2026-07-21T23:05:00Z', book: 'betmgm', over_odds: -210, under_odds: 170 }; /** rowsFromSnapshot is internal; exercise it through the public writer. */ async function rowFor(grade, prop = PROP) { let captured = null; const sb = { from: () => ({ upsert: async (rows) => { captured = rows; return { error: null }; } }) }; await ledger.recordPipelineGrades('mlb', [grade], [prop], { sb, now: () => '2026-07-21T03:00:00.000Z' }); return captured && captured[0]; } describe('lock-time capture — the projection we ACTUALLY made', () => { it('writes p_win, the market at lock, and the archetype VECTOR onto the row', async () => { const r = await rowFor(GRADE); expect(r.p_win).toBe(0.757); expect(r.fair_prob_lock).toBe(0.633); expect(r.archetype_vector).toEqual(GRADE.archetype_axes); expect(r.projection_locked_at).toBe('2026-07-21T03:00:00.000Z'); }); it('lands them on the SAME record as the outcome — the join is the point', async () => { const r = await rowFor(GRADE); // One row carries identity + p_win + the settle target. Calibration and // market-comparison are then plain SQL, not a lossy in-memory join. for (const f of ['player_key', 'stat', 'line', 'side', 'game_date', 'p_win']) { expect(r[f]).toBeDefined(); } }); it('stores the VECTOR, not a label — a label cannot attribute anything', async () => { const withBlend = await rowFor({ ...GRADE, archetype_axes: undefined, archetype_blend: [{ archetype: 'BOMBER', weight: 1 }], archetype: 'BOMBER' }); expect(withBlend.archetype_vector.blend).toHaveLength(1); }); it('a grade with no archetype stores NULL, not an empty object', async () => { const r = await rowFor({ ...GRADE, archetype_axes: undefined, archetype_blend: undefined, archetype: undefined }); expect(r.archetype_vector).toBeNull(); }); it('a grade with no p_win stores NULL, never 0', async () => { const r = await rowFor({ ...GRADE, p_win: undefined, fair_prob: undefined }); expect(r.p_win).toBeNull(); expect(r.fair_prob_lock).toBeNull(); }); it('is IMMUTABLE — the writer never overwrites an existing lock', async () => { // recordPipelineGrades upserts with ignoreDuplicates, so a re-run cannot // rewrite p_win. Re-deriving at settle would measure a projection we never // made. let opts = null; const sb = { from: () => ({ upsert: async (_r, o) => { opts = o; return { error: null }; } }) }; await ledger.recordPipelineGrades('mlb', [GRADE], [PROP], { sb }); expect(opts.ignoreDuplicates).toBe(true); }); }); describe('attachClosingProb — the market half', () => { const makeSb = ({ rows, caps }) => { const updates = []; return { updates, from: (t) => ({ select: () => ({ is: function () { return this; }, eq: function () { return this; }, not: function () { return this; }, in: function () { return this; }, limit: async () => ({ data: t === 'ledger_entries' ? rows : caps, error: null }), }), update: (patch) => ({ eq: async (_c, id) => { updates.push({ id, patch }); return { error: null }; } }), }), }; }; const ROW = { id: 'r1', player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20' }; it('DE-VIGS the raw both-side close prices (the table stores no probability)', async () => { // closing_captures deliberately stores over_odds/under_odds, NOT a // probability. Selecting a `fair_prob` column from it returns nothing and // makes every row look closeless — that shipped once and marked rows // market-unavailable without ever reading a capture. const sb = makeSb({ rows: [ROW], caps: [ { player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -150, under_odds: 120, captured_at: '2026-07-20T20:00:00Z' }, { player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -210, under_odds: 170, captured_at: '2026-07-20T22:50:00Z' }, ], }); const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' }); expect(out.updated).toBe(1); // The LATER capture is the true close, de-vigged from -210/+170. expect(sb.updates[0].patch.closing_prob).toBe(0.647); }); it('IGNORES refused captures — a missed_reason is not a close', async () => { const sb = makeSb({ rows: [ROW], caps: [{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -210, under_odds: 170, captured_at: 'x', missed_reason: 'one_sided_price' }], }); const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' }); expect(out.updated).toBe(0); expect(sb.updates[0].patch.market_unavailable_reason).toBe('no_usable_close'); }); it('NEVER imputes a closing line — absent is marked, not filled', async () => { const sb = makeSb({ rows: [ROW], caps: [] }); await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' }); expect(sb.updates[0].patch.closing_prob).toBeUndefined(); expect(sb.updates[0].patch.market_unavailable_reason).toBe('no_usable_close'); }); it('does NOT declare a future game closeless — a close can still arrive', async () => { // Premature absence is as dishonest as imputation, in the other direction. const sb = makeSb({ rows: [{ ...ROW, game_date: '2026-07-25' }], caps: [] }); const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' }); expect(sb.updates).toHaveLength(0); expect(out.updated).toBe(0); }); it('a one-sided capture is NOT a close (de-vig needs both sides)', async () => { const sb = makeSb({ rows: [ROW], caps: [{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -210, under_odds: null, captured_at: 'x' }], }); await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' }); expect(sb.updates[0].patch.market_unavailable_reason).toBe('no_usable_close'); }); it('never asks closing_captures for a column it does not have', () => { const src = require('fs').readFileSync(require.resolve('../../src/services/ledgerService'), 'utf8'); const fn = src.slice(src.indexOf('async function attachClosingProb')); expect(fn).toMatch(/from\('closing_captures'\)[\s\S]{0,200}over_odds, under_odds/); expect(fn).not.toMatch(/closing_captures'\)[\s\S]{0,200}fair_prob,/); }); it('only considers rows that have no close yet (write-once)', () => { const src = require('fs').readFileSync(require.resolve('../../src/services/ledgerService'), 'utf8'); const fn = src.slice(src.indexOf('async function attachClosingProb')); expect(fn).toMatch(/\.is\('closing_prob', null\)/); }); // CLV instrument repair — the write-once verdict on market_unavailable_reason // was permanent, so a row wrongly declared closeless (truncated read / // premature declaration) could never recover even though its capture existed. it('RECOVERS a row previously declared market_unavailable when a real close now exists', async () => { const sb = makeSb({ rows: [{ ...ROW, market_unavailable_reason: 'no_usable_close' }], caps: [{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -210, under_odds: 170, captured_at: '2026-07-20T22:50:00Z' }], }); const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' }); expect(out.updated).toBe(1); expect(out.recovered).toBe(1); expect(sb.updates[0].patch.closing_prob).toBe(0.647); // the buggy verdict is CLEARED, not left stale beside a real close. expect(sb.updates[0].patch.market_unavailable_reason).toBeNull(); }); it('leaves an already-declared row untouched when there is STILL no capture (no churn, no re-write)', async () => { const sb = makeSb({ rows: [{ ...ROW, market_unavailable_reason: 'no_usable_close' }], caps: [] }); const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' }); expect(out.updated).toBe(0); expect(out.absent).toBe(0); expect(sb.updates).toHaveLength(0); // no write at all }); }); describe('what stays measurable when the market is absent', () => { it('calibration needs only p_win + outcome; market-comparison needs the close', () => { // Encoded as a contract check: the three fields are independent columns, so // a row missing the close still scores for calibration. const src = require('fs').readFileSync(require.resolve('../../src/services/ledgerService'), 'utf8'); expect(src).toMatch(/market_unavailable_reason/); expect(src).toMatch(/p_win: numOrNull\(g\.p_win\)/); }); }); describe('scheduler wiring', () => { const src = require('fs').readFileSync(require.resolve('../../src/snapshotScheduler'), 'utf8'); it('runs the close attach on every configured sport', () => { expect(src).toContain('attachClosingProb'); expect(src).toMatch(/for \(const sp of cadence\.ALL_SPORTS\)/); }); });