b85b351993
Display ORDERING only. No grade, ledger row, lock_line, scoring, or edge_pct
scale/display change. Push scoring untouched.
Two defects removed from selectTopGrades (wrong at ANY scale, independent of
edge_pct's separate retirement):
1. edge: Math.abs(numOr(g.edge, -Infinity)) — abs() on an already-
direction-signed value ranked the model's strongest DISAGREEMENTS level
with its strongest agreements (177 public ledger rows carry a negative
edge; positive = the model AGREES with the graded side).
2. Math.abs(-Infinity) === Infinity, so a row with NO edge sorted FIRST —
absent data presented as the top pick (the Number(null) class).
New key: grade -> confidence -> takeable-gated p_win (nulls LAST) -> SIGNED
edge (nulls LAST) -> input order. Scales are never mixed in one comparator.
Takeable band = web valueState.isTakeable, asserted byte-equal to the hero's
config/valueEngine.isTakeable (-160..+200) incl. strict-null.
Alt-line ladder (analyzeViaEngine1:506) no longer sorts by edge_pct: ordered
highest-p_win-first derived analytically at zero added compute — 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. base stays marked; no consumer depends on
alt_lines[0]; deskShowcaseService.rungsOf already re-sorted by line.
THREE PREMISE BREAKS found report-first, before code:
- /api/props/top-graded 404s in prod (absent from src/) so the dashboard
board renders receipts/empty — the edge sort orders nothing there today.
The prior order's "97.3% of rows tie" was a LEDGER measurement wrongly
extrapolated to that board. Fix is correct-in-itself and lands when the
feed is restored.
- p_win cannot be a client-side key for all tiers: snapshotGating strips it
for unentitled tiers ("shipping p_win is shipping the model price").
Verified live: prod /api/snapshot carries p_win on 0/8 MLB, 0/25 WNBA.
- Ladder rungs carry no per-rung price, so the takeable gate is inapplicable.
Verified on real data, both sports, both paths: unentitled — WNBA (n=25)
ordering CHANGED, MLB (n=8) unchanged, signed edge non-increasing in every
(grade,confidence) tie group (20 pairs, 0 violations); entitled — 40 real
ledger rows with p_win+locked_odds, p_win-descending, untakeable chalk NOT
promoted (Trea Turner .757 @-275 does not beat Rhyne Howard .745 @-120)
(36 pairs, 0 violations).
Hero consistency, stated honestly: same signal + same gate, different
precedence BY CONTRACT (board = grade-tier-first "top GRADES"; hero =
p_win-first "top read"). Identical within the leading tier (verified); across
tiers the board may lead with an A the hero doesn't pick. Not a contradiction.
Floor: 310 suites / 3864 tests green, web build exit 0. Dashboard + Desk
visuals are auth/feed-gated -> tagged for the Chrome audit, no visual faked.
Held: edge_pct rescale/display retirement (Order B); building the missing
/api/props/top-graded selector; exposing p_win to unentitled tiers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
135 lines
5.9 KiB
JavaScript
135 lines
5.9 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);
|
|
|
|
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\)/);
|
|
});
|
|
});
|