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
This commit is contained in:
Kev
2026-08-01 01:55:43 -04:00
parent ef4ac60b81
commit 6c97f59546
9 changed files with 222 additions and 90 deletions
+32 -7
View File
@@ -385,6 +385,11 @@ function buildPlayerStripsFromProps(gameProps, gradeIndex, deltaIndex, now = Dat
// Rev 3 flat edge-board (mobile screen 01) — the ranked hero figure.
// Strict null (never 0-coerced) so an absent edge sorts last, not top.
edge: (rec.edge_pct != null && Number.isFinite(Number(rec.edge_pct))) ? Number(rec.edge_pct) : null,
// Server-computed forecast ordinal (p_win-first). Stamped BEFORE the
// model-price strip, so it reaches every tier where raw p_win cannot.
// Present only for sports whose own model has passed — absent elsewhere,
// which is exactly how the boards fall back.
forecastRank: (rec.forecast_rank != null && Number.isFinite(Number(rec.forecast_rank))) ? Number(rec.forecast_rank) : null,
// A1 S3 — the prop's own book + the best available price across the
// game's book rows for the graded side (null unless ≥2 books at the
// same current line disagree — see detectBestBook).
@@ -470,6 +475,14 @@ const strictNum = (v) => (v == null || v === '' ? null : (Number.isFinite(Number
* in a different base" (Session 67). Those callers legitimately get null here
* and fall through to the signed edge.
*/
/** Ascending comparator (rank 1 is best) that always sorts a null LAST. */
function ascNullsLast(a, b) {
if (a == null && b == null) return 0;
if (a == null) return 1;
if (b == null) return -1;
return a - b;
}
function takeablePWin(g) {
const { isTakeable } = require('./valueState');
const p = strictNum(g && g.p_win);
@@ -513,19 +526,24 @@ function descNullsLast(a, b) {
*/
function selectTopGrades(grades, limit = 10) {
const arr = (Array.isArray(grades) ? grades : []).filter((g) => g && g.grade);
// FLIPPED 2026-08-01 — forecast order FIRST when the server supplied it, and
// the EDGE KEY IS GONE. `forecast_rank` is p_win-first and is stamped only for
// sports whose own model has passed; when absent (every not-built-yet sport)
// this falls through to the unchanged grade-tier chain. Edge is removed
// outright: corr(edge, outcome) = -0.010 / -0.022 on n=200 settled MLB rows,
// against corr(p_win, outcome) = +0.26.
const scored = arr.map((g, idx) => ({
g,
idx,
fr: strictNum(g.forecast_rank),
rank: gradeRankOf(g.grade),
conf: numOr(g.confidence, -1),
pWin: takeablePWin(g),
// SIGNED — no abs(). null (absent) sorts last, not first.
edge: strictNum(g.edge),
}));
scored.sort((a, b) => a.rank - b.rank
scored.sort((a, b) => ascNullsLast(a.fr, b.fr)
|| 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);
}
@@ -691,6 +709,7 @@ function flattenToEdgeBoard(cards) {
// — treat as absent so the board neither displays nor RANKS on a fake
// edge. When absent, rows fall through to the grade-rank tiebreak.
edge: (p.edge != null && Number.isFinite(p.edge) && Math.abs(p.edge) <= EDGE_BOARD_SANE_MAX) ? p.edge : null,
forecastRank: (p.forecastRank != null && Number.isFinite(p.forecastRank)) ? p.forecastRank : null,
sport: c.sport || '',
away,
home,
@@ -704,10 +723,16 @@ function flattenToEdgeBoard(cards) {
}
}
}
// FLIPPED 2026-08-01 — this board no longer ranks on edge. Edge was its
// PRIMARY key, so the entire mobile board was ordered by a quantity measured
// not to predict (corr -0.010 / -0.022 vs corr(p_win) = +0.26).
// Now: server forecast order first (p_win-first; MLB-only, absent elsewhere),
// then grade rank. Edge is still carried and still DISPLAYED as a labelled
// diagnostic — it just no longer decides what a user sees first.
rows.sort((a, b) => {
const ea = a.edge == null ? -Infinity : a.edge;
const eb = b.edge == null ? -Infinity : b.edge;
if (eb !== ea) return eb - ea;
const fa = a.forecastRank == null ? Infinity : a.forecastRank;
const fb = b.forecastRank == null ? Infinity : b.forecastRank;
if (fa !== fb) return fa - fb;
return edgeGradeRank(a.grade) - edgeGradeRank(b.grade);
});
return rows.map((r, i) => ({ ...r, rank: i + 1 }));