Build /api/props/top-graded server selector: rank with p_win, serve without it

New READ endpoint. No grade, ledger row, lock_line, or scoring write. Push
scoring untouched.

REVIEW ZERO CORRECTED THE PREMISE: the handler NEVER EXISTED in any commit
(searched git rev-list --all for a /top-graded definition in src/ — zero hits).
Not "removed" — the three axios callers (cheatsheetGenerator, gradeOfTheDay,
widget) and the Next proxy were written against a phantom endpoint, so those
three content generators have silently received [] for their entire life.
Contract recovered from the four consumers, not guessed: {props:[...]},
?sport=UPPERCASE (absent = all sports, which gradeOfTheDay relies on) + ?limit,
rows carrying player/stat/line/direction/sport/grade/confidence? plus the
player_name/stat_type aliases and game_id.

POPULATED-PATH RISK FOUND: the board's populated branch had never run in prod,
and dashboard/page.tsx:463 calls g.stat.replace(/_/g,' ') UNGUARDED (g.player
also feeds the row key, /scan URL and heading; sport must be UPPERCASE for
SportPill). toRow requires non-empty string player+stat and a finite line,
uppercases sport, and DROPS unrenderable rows — a shorter board beats a broken
one.

THE LEAK BOUNDARY (why this is server-side): the browser cannot rank on p_win
for all tiers because stripModelPrice deliberately withholds it from unentitled
tiers. Order of operations is
  read cache -> RANK with p_win (every tier) -> map rows incl. model fields
    -> stripModelPrice(rows, tier) -> serialize
so a free caller receives the paid RANKING without the paid VALUES. Tier comes
from resolveTierFromRequest, which FAILS CLOSED to 'free'. Cache-Control is
private under a bearer token, public otherwise (the /api/snapshot precedent).

ONE SHARED DEFINITION, no drift: new src/utils/gradeRanking.js
(takeablePWin/descNullsLast/rankGrades). heroPropService now imports
takeablePWin instead of its inline copy (behaviour unchanged — it was that
logic verbatim); the selector imports rankGrades; web/src/lib/slateAdapter
keeps its mirror (the browser cannot import src/, S25) and a test cross-checks
the two on identical fixtures (playerName.js precedent). Board is grade-first
("top GRADES"), hero is p_win-first ("top read") — they differ BY DESIGN and
agree within the leading tier.

HONEST LIMIT: the Next proxy (cachedBackendJson) sends no Authorization header
and caches under a shared key, so via the dashboard every viewer gets the
free-tier payload — correct order, no paid values. That is the SAFE behaviour;
forwarding auth into a shared cache is exactly how a paid payload leaks to
anonymous viewers. Per-tier delivery through the proxy needs a tier-keyed cache
and is not done here.

Verified on real prod snapshot data (anonymous path): MLB 8 props, WNBA 10,
0 paid-field leaks, render-contract safe on every row, sport uppercase.

Floor: 311 suites / 3882 tests green (18 new — leak test uses POPULATED p_win,
not today's nulls: entitled gets p_win and it drove the order, unentitled gets
a byte-identical order with all five MODEL_FIELDS absent and no trace in
JSON.stringify, while book/fair market facts survive). Web build exit 0.
Dashboard visual is auth-gated -> tagged for the Chrome audit, not faked.

Held: edge_pct rescale/retirement (Order B); board columns/contract unchanged;
tier-keyed proxy caching.

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 22:05:30 -04:00
parent 69feab4d25
commit 72a14dc4cd
10 changed files with 672 additions and 8 deletions
+99
View File
@@ -0,0 +1,99 @@
'use strict';
/**
* gradeRanking (2026-07-29, specs/top-graded-selector.md) — THE ONE definition of
* how a board of graded props is ordered. Extracted so the hero, the server
* selector, and the client board cannot drift apart.
*
* WHO USES THIS:
* - `services/heroPropService` — imports `takeablePWin` (its "top READ" rule
* is p_win-FIRST, so it uses the primitive, not `rankGrades`).
* - `services/topGradedService` — imports `rankGrades` (its "top GRADES" board
* is grade-FIRST). Those two leading picks may legitimately differ; they
* agree WITHIN the leading grade tier.
* - `web/src/lib/slateAdapter.selectTopGrades` — a MIRROR, because the browser
* cannot import `src/` (the Session-25 rule). `tests/unit/gradeBoardSort`
* cross-checks the two on identical fixtures — the `playerName.js` precedent.
* If you change the order here, change it there IN THE SAME COMMIT.
*
* WHY p_win AND NOT ev_pct/edge_pct: ev_pct is NULL on served grades and
* `Number(null) === 0` made every prop tie at 0 (the hero bug); edge_pct is a
* price-free (projline)/line artifact whose scale is a function of line size.
* p_win is the only signal whose takeable-MLB-over CLV survived the skew audit.
*/
const { isTakeable } = require('../config/valueEngine');
const GRADE_RANK = Object.freeze({
'A+': 0, A: 1, 'A-': 2, 'B+': 3, B: 4, 'B-': 5, 'C+': 6, C: 7, 'C-': 8, D: 9, F: 10,
});
/** Grade letter → sortable tier rank (lower = better). Unknown → 99. */
function gradeRankOf(g) {
const k = String(g == null ? '' : g).trim().toUpperCase();
return GRADE_RANK[k] !== undefined ? GRADE_RANK[k] : 99;
}
/** Strict numeric read — `Number(null) === 0` is the recurring fabrication bug. */
function strictNum(v) {
if (v == null || v === '') return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
/**
* The takeable-gated champion probability for one grade row, or null.
*
* The takeable filter is MANDATORY: raw p_win crowns 300 chalk, which is not
* the product. Band = `config/valueEngine.isTakeable` (160..+200), the same
* definition the takeable-edge proof and the over-side skew audit used.
* Price falls back to the LOCKED odds (`gradedAt.odds`) when `book_odds` is absent.
*/
function takeablePWin(g) {
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;
}
/**
* rankGrades — "top GRADES" order: grade tier → confidence → takeable-gated
* p_win → SIGNED edge → stable input order. Nulls sort LAST on both signals.
*
* SCALES ARE NEVER MIXED: p_win (0..1) is only ever compared against p_win and
* edge (%) only against edge. Comparing 0.62 against 62 is not a comparison.
*
* Rows without a grade are dropped (a board of ungraded rows is not a board).
* `limit` omitted → the whole ranked list (callers slice).
*/
function rankGrades(grades, limit) {
const arr = (Array.isArray(grades) ? grades : []).filter((g) => g && g.grade);
const scored = arr.map((g, idx) => ({
g,
idx,
rank: gradeRankOf(g.grade),
conf: strictNum(g.confidence) == null ? -1 : strictNum(g.confidence),
pWin: takeablePWin(g),
edge: strictNum(g.edge != null ? g.edge : g.edge_pct),
}));
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);
const out = scored.map((s) => s.g);
return limit == null ? out : out.slice(0, Math.max(0, limit));
}
module.exports = {
GRADE_RANK, gradeRankOf, strictNum, takeablePWin, descNullsLast, rankGrades,
};