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:
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user