Grade-board sort: signed signal, takeable-gated p_win, missing sorts LAST

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
This commit is contained in:
Kev
2026-07-29 21:13:51 -04:00
parent 9b5235cf99
commit b85b351993
9 changed files with 886 additions and 11 deletions
+134
View File
@@ -0,0 +1,134 @@
/**
* 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\)/);
});
});