Files
vyndr/tests/unit/gradeBoardSort.test.js
T
builtbykev 6c97f59546 WNBA truth correction + THE p_win FLIP (live, rollback armed)
PART A -- WNBA TRUTH CORRECTION (no behaviour change).
WNBA does not "abstain" and is not "anti-predictive". The -0.12 that
produced those words was NBA-template machinery run on WNBA data -- WNBA
has never had its own archetypes, variables, conditions or calibration,
which is precisely the "sport stubbed in on another sport's template"
CLAUDE.md forbids. That is an UNBUILT MODEL'S EXPECTED FAILURE, not a
verdict on the sport; reading it as a verdict would quietly retire a sport
we never actually attempted. Its own build is QUEUED, after MLB.

The guard CODE is unchanged -- FORECAST_RANKED_SPORTS = {'mlb'} and the
inheritance test are correct live safety either way. Only the meaning is
corrected, and generalised into the doctrine-as-a-gate: a sport ranks on
p_win ONLY once its OWN model is built and shown to predict (calibration
AND resolution on its own holdout). Others are held out as NOT-BUILT,
never as failed. Re-labelled across gradeRanking, snapshot route, tests,
MASTER-PLAN and the challenger report.

PART B -- THE FLIP, gated on a full-slate re-run.

The re-run found something better than a bigger sample. An induced
snapshot graded 7 props: gradeAndCacheSlate runs with DEFAULT_LIMIT = 25
and ~72% of those refuse for insufficient_data, while 546 props are
gradeable. So 8 props IS the board, structurally -- not a small sample of
it. Logged as its own finding; the cap is a separate order.

For a statistically meaningful delta I used 11 real historical boards
(n=328, board sizes 14-57): 79.9% of rows move, mean 5.16 places per
board, TOP READ CHANGES ON 9 OF 11 BOARDS. The re-ordering holds at real
board size. Query committed.

FLIPPED:
- rankGrades drops its edge key (safe for every sport: removes a
  non-predictive tiebreak without putting p_win in front).
- selectTopGrades leads on forecast_rank, edge key removed.
- flattenToEdgeBoard sorts on forecastRank, not edge -- this board had
  edge as its PRIMARY key, so the whole mobile board was ordered by a
  quantity measured not to predict.
- forecast_rank threaded onto strip props.

Sports whose model is not built supply no forecast_rank, so their boards
fall through to the unchanged grade chain -- the fallback is the guard.

ROLLBACK ARMED: boards sort by forecast_rank WHEN PRESENT, so
FORECAST_RANK=0 reverts every surface on the next response -- no deploy,
no client release.

Edge is still computed, stored, carried and displayed as a labelled
diagnostic. Retired from ranking, not deleted.

Eight superseded tests updated to strictly stronger INVERSE properties --
they now fail if edge is ever re-introduced as a ranking key, which the
originals could not detect.

Gates: 4,045 tests / 323 suites green; next build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-08-01 01:55:43 -04:00

165 lines
7.3 KiB
JavaScript

/**
* 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);
// SUPERSEDED 2026-08-01 by the p_win flip. These three asserted that EDGE
// ordered the board (signed, nulls last). Edge no longer ranks anything:
// measured on n=200 settled MLB rows, corr(edge, outcome) = -0.010 under the
// incumbent ruler and -0.022 under the consensus ruler, against
// corr(p_win, outcome) = +0.26.
//
// The replacements are strictly STRONGER — they fail if edge is ever
// re-introduced as a ranking key, which the originals could not detect.
describe('selectTopGrades — EDGE CANNOT RANK (retired 2026-08-01)', () => {
test('flipping edge from -80 to +80 does NOT change the order', () => {
const a = [
{ player: 'first', grade: 'B', confidence: 50, edge: -80 },
{ player: 'second', grade: 'B', confidence: 50, edge: 10 },
];
const b = [
{ player: 'first', grade: 'B', confidence: 50, edge: 80 },
{ player: 'second', grade: 'B', confidence: 50, edge: -10 },
];
// Equal grade + confidence + no p_win -> stable input order, edge irrelevant.
expect(names(adapter.selectTopGrades(a, 10))).toEqual(['first', 'second']);
expect(names(adapter.selectTopGrades(b, 10))).toEqual(['first', 'second']);
});
test('a MISSING edge is no longer penalised — rows are still all PRESENT', () => {
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).toHaveLength(3); // never dropped
expect(out).toEqual(['no-signal', 'weak-but-real', 'negative-but-real']); // input order
});
test('forecast_rank LEADS the chain when the server supplied it', () => {
const grades = [
{ player: 'topGrade', grade: 'A', confidence: 90, forecast_rank: 3 },
{ player: 'topForecast', grade: 'C', confidence: 40, forecast_rank: 1 },
];
// p_win-first order wins over the letter: the letter measured r ~ 0.005 and
// is inverted, p_win measured +0.26.
expect(names(adapter.selectTopGrades(grades, 10))).toEqual(['topForecast', 'topGrade']);
});
test('a MISSING forecast_rank sorts LAST and falls back to the grade chain', () => {
const grades = [
{ player: 'noRank', grade: 'A', confidence: 90 },
{ player: 'ranked', grade: 'C', confidence: 40, forecast_rank: 2 },
];
expect(names(adapter.selectTopGrades(grades, 10))).toEqual(['ranked', 'noRank']);
});
test('forecast_rank null/empty is ABSENT, not 0 (Number(null) === 0 guard)', () => {
const grades = [
{ player: 'nullish', grade: 'B', confidence: 50, forecast_rank: null },
{ player: 'empty', grade: 'B', confidence: 50, forecast_rank: '' },
{ player: 'real', grade: 'C', confidence: 10, forecast_rank: 9 },
];
// rank 9 is a REAL rank and must beat two absents — a 0-coercion would
// have put the nulls first.
expect(names(adapter.selectTopGrades(grades, 10))[0]).toBe('real');
});
});
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\)/);
});
});