Product identity + widen books for DISPLAY, model input byte-identical
IDENTITY (CLAUDE.md top + MASTER-PLAN header). VYNDR is a PREDICTIVE MODEL: it projects what a player will DO and picks accurately. Market edge is a BYPRODUCT of a good prediction, never the success criterion. Success = the forecast is honest about its own confidence AND still ranks -- calibration and resolution, both. No edge/CLV term belongs in a pass/fail gate; they are diagnostics we report, not thresholds a model must clear. A model tuned to beat a closing line has been fitted to the market instead of to the game. Per-sport doctrine (Phillips 2022, classify by what players DO not by position): each sport is its own model -- own variables, archetypes, conditions, calibration, honest ceiling. Shared across sports: ONLY the Bayesian inference math. Truth Law: no fabricated data; honest-absent over invented; label limitations in-band; provisional stays provisional until re-run; documented is not verified. PHASE 2 -- AGGREGATOR WIDENING (live). normalizeProps now emits every DISPLAY book instead of 5 of 18. Before this we discarded 13 books of our own accord and 64.8% of the MLB slate was invisible to users. Every prop carries book_role (both/takeable/reference/dfs/offshore) so the display layer can say WHAT a price is -- a fixed-payout DFS number and a two-way sportsbook price are not interchangeable objects. Unknown books are still dropped. PHASE 3 -- MODEL GATE (the model does not move). bookRoles splits MODEL_BOOKS (the legacy allow-list, character for character) from DISPLAY_BOOKS. Both model paths re-filter before they pick a line: gradeSlateService.dedupeProps (before first-row-wins AND before the limit) and intradayRefreshService.indexOddsProps (which RE-GRADES at the current line -- without the gate, widening would have silently moved locked lines onto books the model has never been calibrated against). A test asserts the graded set is byte-identical through the widening. CURRENT_RULER_VERSION stays v1_first_book. The gate lifts only when the MLB calibration is re-run on the consensus ruler and v2 is promoted. HONEST FRAMING, recorded in the plan: this is an AGGREGATOR win and it does NOT fix the model. WNBA still abstains -- a model problem, not a coverage problem; it is better covered than MLB. MLB isotonic still provisional. The consensus is MARKET, not SHARP: pinnacle, matchbook and polymarket are 0% on both sports, so no sharp anchor exists in our feed. Two superseded tests updated to stronger properties rather than deleted: roleOf now names the KIND of book, and the normalizer test asserts the display set widens WHILE the model set does not. Gates: 4,027 tests / 322 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:
@@ -19,7 +19,13 @@ describe('bookRoles — the three-way split', () => {
|
||||
for (const dfs of roles.DFS_PLATFORMS) {
|
||||
expect(roles.REFERENCE_BOOKS.has(dfs)).toBe(false);
|
||||
expect(roles.EXCLUDED_FROM_PRICING.has(dfs)).toBe(true);
|
||||
expect(roles.roleOf(dfs)).toBe('excluded');
|
||||
// roleOf now names the KIND ('dfs'), which is stronger than 'excluded':
|
||||
// the display layer needs to say WHAT a price is, not just that it can't
|
||||
// price. Exclusion from pricing is asserted on the line above.
|
||||
expect(roles.roleOf(dfs)).toBe('dfs');
|
||||
expect(roles.isReferenceBook(dfs)).toBe(false);
|
||||
expect(roles.isModelBook(dfs)).toBe(false);
|
||||
expect(roles.isDisplayBook(dfs)).toBe(true); // shown for breadth, tagged
|
||||
}
|
||||
});
|
||||
|
||||
@@ -144,3 +150,32 @@ describe('consensusRuler — the incumbent it is challenging', () => {
|
||||
expect(compareRulers([q('prizepicks', 1.5, -119, -119)], 1.5, 'over', { allowedBooks: LIVE_ALLOWED }).delta_pts).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bookRoles — display vs model separation (Order Zero Phase 2/3)', () => {
|
||||
it('MODEL_BOOKS is the legacy allow-list, unchanged — the model must not move', () => {
|
||||
expect([...roles.MODEL_BOOKS].sort()).toEqual([
|
||||
'bet365', 'betmgm', 'betrivers', 'caesars', 'draftkings', 'fanatics',
|
||||
'fanduel', 'hardrockbet', 'pinnacle', 'pointsbet', 'thescore',
|
||||
].sort());
|
||||
});
|
||||
|
||||
it('DISPLAY_BOOKS is a strict superset of MODEL_BOOKS', () => {
|
||||
for (const b of roles.MODEL_BOOKS) expect(roles.DISPLAY_BOOKS.has(b)).toBe(true);
|
||||
expect(roles.DISPLAY_BOOKS.size).toBeGreaterThan(roles.MODEL_BOOKS.size);
|
||||
});
|
||||
|
||||
it('NO DFS or offshore book may reach the model, however visible it is', () => {
|
||||
for (const b of [...roles.DFS_PLATFORMS, ...roles.OFFSHORE_OR_INTL]) {
|
||||
expect(roles.isDisplayBook(b)).toBe(true);
|
||||
expect(roles.isModelBook(b)).toBe(false);
|
||||
expect(roles.isReferenceBook(b)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('an exchange is visible and may rule, but is never TAKEABLE', () => {
|
||||
for (const b of roles.EXCHANGES) {
|
||||
expect(roles.isDisplayBook(b)).toBe(true);
|
||||
expect(roles.isTakeableBook(b)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,27 +66,55 @@ describe('oddsNormalizer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out books not in the allowed set', () => {
|
||||
// SUPERSEDED 2026-08-01 (Order Zero). This used to assert bovada was
|
||||
// DROPPED. It is now emitted for DISPLAY — we were discarding 13 of the
|
||||
// feed's 18 books, which made 64.8% of the MLB slate invisible. The
|
||||
// property that replaces it is strictly stronger: the display set widens,
|
||||
// every row is TAGGED with what kind of price it is, an unknown book is
|
||||
// still dropped, and the MODEL set does not move.
|
||||
it('emits every DISPLAY book, tagged with its role, and still drops unknown books', () => {
|
||||
const { MODEL_BOOKS } = require('../../src/config/bookRoles');
|
||||
const event = makeEvent({
|
||||
bookmakers: [
|
||||
makeBookmaker('bovada', [
|
||||
bookmakers: ['bovada', 'draftkings', 'prizepicks', 'novig', 'not_a_real_book'].map((k) =>
|
||||
makeBookmaker(k, [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Jokic', -110, 26.5),
|
||||
makeOutcome('Under', 'Jokic', -110, 26.5),
|
||||
]),
|
||||
]),
|
||||
makeBookmaker('draftkings', [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Jokic', -110, 26.5),
|
||||
makeOutcome('Under', 'Jokic', -110, 26.5),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
])),
|
||||
});
|
||||
|
||||
const result = normalizeProps([event]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].book).toBe('draftkings');
|
||||
const byBook = Object.fromEntries(result.map((r) => [r.book, r.book_role]));
|
||||
|
||||
expect(byBook).toEqual({
|
||||
draftkings: 'both',
|
||||
bovada: 'reference',
|
||||
novig: 'reference',
|
||||
prizepicks: 'dfs', // shown for breadth, tagged, never a market price
|
||||
});
|
||||
expect(byBook.not_a_real_book).toBeUndefined(); // unknown books still dropped
|
||||
expect(ALLOWED_BOOKS).toBe(MODEL_BOOKS); // the alias still means MODEL
|
||||
});
|
||||
|
||||
it('MODEL INPUT IS BYTE-IDENTICAL despite the display widening', () => {
|
||||
// The grader re-filters to MODEL_BOOKS before first-row-wins, so widening
|
||||
// what the surfaces show cannot change a single graded prop. This is the
|
||||
// gate that lifts only when the consensus ruler is promoted.
|
||||
const { dedupeProps } = require('../../src/services/gradeSlateService').__internals;
|
||||
const event = makeEvent({
|
||||
bookmakers: ['prizepicks', 'novig', 'bovada', 'draftkings'].map((k) =>
|
||||
makeBookmaker(k, [
|
||||
makeMarket('player_points', [
|
||||
makeOutcome('Over', 'Jokic', -110, 26.5),
|
||||
makeOutcome('Under', 'Jokic', -110, 26.5),
|
||||
]),
|
||||
])),
|
||||
});
|
||||
|
||||
const graded = dedupeProps(normalizeProps([event]), 25);
|
||||
expect(graded).toHaveLength(1);
|
||||
expect(graded[0].book).toBe('draftkings'); // exactly what it was before
|
||||
});
|
||||
|
||||
it('maps every market key to its internal stat_type (NBA + soccer)', () => {
|
||||
|
||||
Reference in New Issue
Block a user