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
+1 -1
View File
File diff suppressed because one or more lines are too long
+62 -4
View File
@@ -454,10 +454,62 @@ const numOr = (v, fallback) => {
return Number.isFinite(n) ? n : fallback;
};
/** Strict numeric read — `Number(null) === 0` is the recurring fabrication bug. */
const strictNum = (v) => (v == null || v === '' ? null : (Number.isFinite(Number(v)) ? Number(v) : null));
/**
* The takeable-gated champion probability for one grade row, or null.
*
* Gate = `valueState.isTakeable` (160..+200), which mirrors
* `src/config/valueEngine.isTakeable` — the IDENTICAL band the hero ranks on
* (`heroPropService` HERO RULE v3), so the two ranking surfaces agree. The gate
* is mandatory: raw p_win crowns 300 chalk, which is not the product.
*
* p_win is ABSENT BY DESIGN for unentitled tiers — `utils/snapshotGating`
* strips it on the way out because "shipping p_win is shipping the model price
* in a different base" (Session 67). Those callers legitimately get null here
* and fall through to the signed edge.
*/
function takeablePWin(g) {
const { isTakeable } = require('./valueState');
const p = strictNum(g && g.p_win);
if (p == null) return null;
const price = strictNum(g && g.book_odds) ?? strictNum(g && g.gradedAt && g.gradedAt.odds);
if (price == null || !isTakeable(price)) return null;
return p;
}
/** Descending comparator that always sorts a null signal LAST (never first). */
function descNullsLast(a, b) {
if (a == null && b == null) return 0;
if (a == null) return 1;
if (b == null) return -1;
return b - a;
}
/**
* #13 — rank tonight's grades by TIER, then the VARYING signal so a leaderboard
* of near-identical rows stops being noise: confidence desc, then |edge| desc,
* stable by input order. Drops gradeless rows. Returns at most `limit`.
* of near-identical rows stops being noise. Drops gradeless rows. Returns at
* most `limit`.
*
* SORT FIX (2026-07-29, `specs/grade-board-sort.md`). The old key was
* `edge: Math.abs(numOr(g.edge, -Infinity))`, carrying two defects that were
* wrong at ANY scale — independent of edge_pct's separate retirement:
*
* 1. abs() ON AN ALREADY-SIGNED VALUE. `edge` is signed BY DIRECTION upstream
* (`edgePctFor`: over ? projline : lineproj), so POSITIVE = the model
* AGREES with the graded side. Ranking |edge| put the model's strongest
* DISAGREEMENTS level with its strongest agreements — the board promoted
* reads the model contradicts (177 public ledger rows carry a negative edge).
* 2. `Math.abs(-Infinity) === Infinity`, so a row with NO edge sorted FIRST.
* Absent data presented as the top pick — the `Number(null)` class again.
*
* Now: takeable-gated champion p_win first (so this board and the hero agree),
* then the SIGNED edge, each descending with MISSING SORTING LAST. Rows are
* never dropped for a missing signal — they rank at the bottom.
*
* SCALES ARE NEVER MIXED: p_win (0..1) is compared only against p_win, edge (%)
* only against edge. Comparing 0.62 against 62 is not a comparison.
*/
function selectTopGrades(grades, limit = 10) {
const arr = (Array.isArray(grades) ? grades : []).filter((g) => g && g.grade);
@@ -466,9 +518,15 @@ function selectTopGrades(grades, limit = 10) {
idx,
rank: gradeRankOf(g.grade),
conf: numOr(g.confidence, -1),
edge: Math.abs(numOr(g.edge, -Infinity)),
pWin: takeablePWin(g),
// SIGNED — no abs(). null (absent) sorts last, not first.
edge: strictNum(g.edge),
}));
scored.sort((a, b) => a.rank - b.rank || b.conf - a.conf || b.edge - a.edge || a.idx - b.idx);
scored.sort((a, b) => a.rank - b.rank
|| b.conf - a.conf
|| descNullsLast(a.pWin, b.pWin)
|| descNullsLast(a.edge, b.edge)
|| a.idx - b.idx);
return scored.slice(0, Math.max(0, limit)).map((s) => s.g);
}