Per-sport rank guard + edge diagnostic-only display + delta report

DELTA MEASURED on live prod grades (live ordering unchanged): MLB 7/8
props move (87.5%), mean 2.5 places, TOP READ CHANGES (corey seager hits
1.5 under -> jake burger hits 0.5 over). WNBA 25/25 move, mean 4.1, max 12.
This is a large re-ordering, not a tweak.

Caveat recorded rather than buried: MLB had only 8 graded props at
measurement time. The percentages are real; the sample is one small slate.
Re-run before the flip -- it is one call.

PER-SPORT DOCTRINE ENFORCED IN CODE. WNBA moves the most and must NOT
adopt this: its p_win is anti-predictive, so ranking that board by p_win
would sort it by a signal measured to point the WRONG WAY -- worse than
the incumbent, not better. A comment would not have stopped a future flip
from going global, so FORECAST_RANKED_SPORTS = Set(['mlb']) gates the
forecast_rank stamp, with tests asserting no sport inherits MLB's result.
A sport joins only by passing its own holdout.

EDGE IS NOW DIAGNOSTIC-ONLY IN DISPLAY. MobileEdgeBoard.EdgeCell rendered
green (--g-a) for positive edge and red (--miss) for negative. Two things
were wrong: green/red IS a quality claim on a quantity that does not
predict, and ROW-GRAMMAR reserves red for settled-negative ONLY -- a
negative diagnostic is not a settled loss. Now neutral mono with a
diagnostic tooltip; header reads "MKT GAP · DIAGNOSTIC". The number is
still shown -- no display went blank. DeskShowcase neutralised likewise.

PINNACLE LOGGED, NOT ENSHRINED. Per the order, "market-not-sharp" is
PENDING-RECOVERY rather than a confirmed permanent limitation. The single
question for PropLine is in BLOCKERS.md with its evidence, and MASTER-PLAN
now carries the pending status instead of the permanent claim.

Live sorts remain byte-identical: selectTopGrades, flattenToEdgeBoard and
topGradedService all still call the incumbent.

Gates: 4,041 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:29:20 -04:00
parent 86d123945c
commit ef4ac60b81
8 changed files with 242 additions and 10 deletions
+5 -1
View File
@@ -20,7 +20,7 @@ const { indexRosterLogs, attachLast10Dots } = require('../services/last10Dots');
// viewer. This endpoint is PUBLIC, so the Session-66 gate on /api/analyze was
// being bypassed here on every graded row. Same layer as the CLV gate.
const { stripModelPrice, gateItemizedGrades, liveLockedSummary, freeSample, entitledToItemizedGrades } = require('../utils/snapshotGating');
const { rankByForecast, gradeKey } = require('../utils/gradeRanking');
const { rankByForecast, gradeKey, ranksOnForecast } = require('../utils/gradeRanking');
const { resolveTierFromRequest } = require('../utils/requestTier');
const router = express.Router();
@@ -121,6 +121,10 @@ router.get('/:sport', async (req, res) => {
// and accepted for the top-graded board.
const stampForecastRank = (grades) => {
if (!Array.isArray(grades) || grades.length === 0) return grades;
// PER-SPORT DOCTRINE, enforced: only sports that passed their OWN holdout
// may be ranked by forecast. WNBA's p_win is anti-predictive, so stamping
// it would hand a future flip a wrong-way ordering for that board.
if (!ranksOnForecast(sport)) return grades;
const ranked = rankByForecast(grades);
const pos = new Map();
ranked.forEach((g, i) => pos.set(gradeKey(g), i + 1));
+15
View File
@@ -141,6 +141,20 @@ function rankByForecast(grades, limit) {
return limit == null ? out : out.slice(0, Math.max(0, limit));
}
/**
* WHICH SPORTS MAY RANK ON THE FORECAST — per-sport doctrine, enforced in code.
*
* MLB only. WNBA's p_win is ANTI-PREDICTIVE on its own data (it abstains), so
* ranking WNBA by p_win would sort that board by a signal measured to point the
* wrong way — worse than the incumbent, not better. A comment would not have
* stopped a future flip from applying this globally; this does.
*
* A sport joins this set only by passing its OWN holdout: honest calibration
* AND surviving resolution. Never by inheriting MLB's result.
*/
const FORECAST_RANKED_SPORTS = Object.freeze(new Set(['mlb']));
const ranksOnForecast = (sport) => FORECAST_RANKED_SPORTS.has(String(sport || '').toLowerCase());
/** Stable identity for a grade row, for comparing two orderings. */
function gradeKey(g) {
if (!g) return '';
@@ -204,4 +218,5 @@ function rankingDelta(grades, topN = 10) {
module.exports = {
GRADE_RANK, gradeRankOf, strictNum, takeablePWin, descNullsLast, rankGrades,
rankByForecast, rankingDelta, gradeKey,
FORECAST_RANKED_SPORTS, ranksOnForecast,
};