/** * Grade-board sort (specs/grade-board-sort.md) — display ORDERING only. * * Locks the two defects that were wrong at ANY scale: * 1. abs() on an already-direction-signed edge ranked DISAGREEMENTS level with * agreements. * 2. Math.abs(-Infinity) === Infinity made a MISSING signal sort FIRST. * Plus: the takeable gate matches the hero, and p_win/edge scales never mix. */ const adapter = require('../../web/src/lib/slateAdapter'); const { isTakeable } = require('../../web/src/lib/valueState'); const { isTakeable: backendIsTakeable } = require('../../src/config/valueEngine'); const names = (rows) => rows.map((r) => r.player); describe('selectTopGrades — signed signal, missing sorts LAST', () => { test('a DISAGREEMENT does not outrank an AGREEMENT at equal grade+confidence', () => { // edge is signed by direction: positive = model agrees with the graded side. // Old |edge| ranked -80 (strong disagreement) above +10 (mild agreement). const grades = [ { player: 'disagrees', grade: 'B', confidence: 50, edge: -80 }, { player: 'agrees', grade: 'B', confidence: 50, edge: 10 }, ]; expect(names(adapter.selectTopGrades(grades, 10))).toEqual(['agrees', 'disagrees']); }); test('a MISSING signal sorts LAST and is still PRESENT (never dropped)', () => { const grades = [ { player: 'no-signal', grade: 'B', confidence: 50 }, { player: 'weak-but-real', grade: 'B', confidence: 50, edge: 0.1 }, { player: 'negative-but-real', grade: 'B', confidence: 50, edge: -5 }, ]; const out = names(adapter.selectTopGrades(grades, 10)); expect(out).toEqual(['weak-but-real', 'negative-but-real', 'no-signal']); expect(out).toHaveLength(3); // present, not dropped expect(out[out.length - 1]).toBe('no-signal'); }); test('null/empty-string edge is absent, NOT zero (Number(null) === 0 guard)', () => { const grades = [ { player: 'nullish', grade: 'B', confidence: 50, edge: null }, { player: 'empty', grade: 'B', confidence: 50, edge: '' }, { player: 'real-negative', grade: 'B', confidence: 50, edge: -1 }, ]; // A real negative beats two absents; absents keep input order at the bottom. expect(names(adapter.selectTopGrades(grades, 10))).toEqual(['real-negative', 'nullish', 'empty']); }); }); describe('selectTopGrades — takeable-gated p_win outranks edge, scales never mix', () => { test('takeable p_win row outranks an UNTAKEABLE higher-p_win chalk row', () => { const grades = [ { player: 'chalk', grade: 'B', confidence: 50, p_win: 0.92, book_odds: -300 }, // untakeable { player: 'takeable', grade: 'B', confidence: 50, p_win: 0.61, book_odds: -120 }, ]; expect(names(adapter.selectTopGrades(grades, 10))[0]).toBe('takeable'); }); test('p_win is preferred over edge, and a p_win row outranks an edge-only row', () => { const grades = [ { player: 'edge-only', grade: 'B', confidence: 50, edge: 300 }, { player: 'has-pwin', grade: 'B', confidence: 50, p_win: 0.55, book_odds: 100 }, ]; // 0.55 must NOT be compared against 300 — the p_win-bearing row wins on the // earlier key instead (scales never mixed in one comparator). expect(names(adapter.selectTopGrades(grades, 10))[0]).toBe('has-pwin'); }); test('among takeable p_win rows the HIGHER p_win wins', () => { const grades = [ { player: 'lower', grade: 'B', confidence: 50, p_win: 0.55, book_odds: -110 }, { player: 'higher', grade: 'B', confidence: 50, p_win: 0.71, book_odds: -110 }, ]; expect(names(adapter.selectTopGrades(grades, 10))[0]).toBe('higher'); }); test('gradedAt.odds is accepted as the price when book_odds is absent', () => { const grades = [ { player: 'via-gradedAt', grade: 'B', confidence: 50, p_win: 0.66, gradedAt: { odds: -115 } }, { player: 'edge-only', grade: 'B', confidence: 50, edge: 5 }, ]; expect(names(adapter.selectTopGrades(grades, 10))[0]).toBe('via-gradedAt'); }); test('the frontend takeable band MATCHES the backend hero gate exactly', () => { for (const price of [-400, -160, -159, -110, 0, 100, 200, 201, 500]) { expect(isTakeable(price)).toBe(backendIsTakeable(price)); } // and the strict-null contract both sides expect(isTakeable(null)).toBe(false); expect(backendIsTakeable(null)).toBe(false); }); test('grade tier still dominates every signal', () => { const grades = [ { player: 'B-strong', grade: 'B', confidence: 99, p_win: 0.99, book_odds: -110 }, { player: 'A-weak', grade: 'A', confidence: 1, edge: -50 }, ]; expect(names(adapter.selectTopGrades(grades, 10))[0]).toBe('A-weak'); }); }); describe('alt-line ladder — ordered highest-p_win-first via the monotonic line rule', () => { // P(stat >= k) is monotone non-increasing in k, so p_win-desc is line-ASC for an // over and line-DESC for an under. Assert the ordering the engine emits. const ladderOrder = (direction, lines) => { const rungs = lines.map((line) => ({ line })); return rungs .slice() .sort((a, b) => (String(direction).toLowerCase() === 'under' ? Number(b.line) - Number(a.line) : Number(a.line) - Number(b.line))) .map((r) => r.line); }; test('OVER: lowest line (highest p_win) first', () => { expect(ladderOrder('over', [1.5, 0.5, 2.5, 1, 2])).toEqual([0.5, 1, 1.5, 2, 2.5]); }); test('UNDER: highest line (highest p_win) first', () => { expect(ladderOrder('under', [1.5, 0.5, 2.5, 1, 2])).toEqual([2.5, 2, 1.5, 1, 0.5]); }); test('the engine sorts its ladder by that rule, NOT by edge_pct', () => { const src = require('fs').readFileSync( require('path').join(__dirname, '../../src/services/intelligence/analyzeViaEngine1.js'), 'utf8', ); // the old key must be gone from the ladder sort expect(src).not.toMatch(/sort\(\(a, b\) => \(Number\(b\.edge_pct\)/); expect(src).toMatch(/Number\(b\.line\) - Number\(a\.line\)/); }); });