d103ecf4c3
BYTE-IDENTICAL. The audit found no consumer getting the wrong axis, so this is a disambiguation, not a bug fix. 4,131 tests / 331 suites green. STEP 1 AUDIT -- and the order's premise was wrong in a useful way: the four accruing challengers read the flag ZERO times (not four) the ranking gate wants PROMOTION, gets promotion [correct] the ledger column holds the LEDGER band, consumed as such the UI (LiveHeroProp) TYPES a `takeable` field it never renders THERE ARE THREE DEFINITIONS, NOT TWO -- and I only found the third by tracing the ranking gate: 1. IDENTITY can it be bet? book identity (takeability) 2. LEDGER BAND worth recording? odds >= -160, UNCAPPED plus 3. PROMOTION worth crowning? -160..+200, i.e. band PLUS a ceiling (2) and (3) genuinely disagree, and I measured it rather than asserting it: 439 rows -- 28.2% of all takeable=true ledger rows -- carry prices above +200, up to +1300. A +1300 longshot is a real bet worth RECORDING and not one worth CROWNING. Both are correct for their own purpose. THE DANGER WAS NEVER THE LOGIC. It was that three questions shared one word, so a reader could not tell which answer they held -- and hits, which must model thin/juiced/one-sided REAL markets, would have been the next reader to guess wrong. RESOLUTION: all three now have distinct names in config/takeability.js; gradeRanking calls isWithinPromotionBand so its intent is self-evident (a test pins it byte-identical to the old valueEngine call across the whole price range); the ledger dual-writes within_price_band with `takeable` kept as a documented DEPRECATED MIRROR so nothing breaks. Column comments in the database now say what each column actually holds. I did NOT redefine `takeable` in place. Four readers and a ranking gate sit on it, and silently changing its meaning under cover of a naming change is exactly the class of move this session keeps removing. Gates: 4,131 tests / 331 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
192 lines
8.8 KiB
JavaScript
192 lines
8.8 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* STRUCTURAL GUARDS (2026-08-02) — two recurring failure families, locked.
|
||
*
|
||
* Both are the same principle: never silently convert something ABSENT or THIN
|
||
* into a false DEFINITE value.
|
||
*
|
||
* GUARD 1 unknown ≠ zero (`Number(null) === 0`, 6× recurring)
|
||
* GUARD 2 takeability = book identity, never price shape
|
||
*
|
||
* The fixtures are REAL rows from prod (2026-08-02 re-stamp), not invented.
|
||
*/
|
||
|
||
const { knownNumber, knownRate, isKnown, firstKnown } = require('../../src/utils/known');
|
||
const { isTakeableMarket, isWithinPriceBand, assessQuote } = require('../../src/config/takeability');
|
||
|
||
// ─────────────────────────────── GUARD 1 ───────────────────────────────
|
||
describe('GUARD 1 — unknown is not zero', () => {
|
||
it('every absent form is UNKNOWN, never 0', () => {
|
||
for (const v of [null, undefined, '', NaN, 'abc', {}, []]) {
|
||
expect(knownNumber(v)).toBeNull();
|
||
expect(isKnown(v)).toBe(false);
|
||
}
|
||
});
|
||
|
||
it('a REAL zero survives — 0 rest days is a fact, not an absence', () => {
|
||
expect(knownNumber(0)).toBe(0);
|
||
expect(isKnown(0)).toBe(true);
|
||
expect(knownRate(0)).toBe(0);
|
||
});
|
||
|
||
it('Infinity is not a measurement', () => {
|
||
expect(knownNumber(Infinity)).toBeNull();
|
||
expect(knownNumber(-Infinity)).toBeNull();
|
||
});
|
||
|
||
it('knownRate additionally rejects booleans and negatives', () => {
|
||
expect(knownRate(true)).toBeNull(); // Number(true) === 1
|
||
expect(knownRate(false)).toBeNull(); // Number(false) === 0 — the trap
|
||
expect(knownRate(-0.5)).toBeNull();
|
||
expect(knownNumber(-0.5)).toBe(-0.5); // a signed gap is legitimately negative
|
||
});
|
||
|
||
it('firstKnown prefers a present 0 over a later fallback (|| gets this wrong)', () => {
|
||
expect(firstKnown(0, 5)).toBe(0);
|
||
expect(firstKnown(null, 5)).toBe(5);
|
||
expect(firstKnown(null, undefined, '')).toBeNull();
|
||
});
|
||
|
||
it('REAL FIXTURE — a null triple rate must not read as "never triples"', () => {
|
||
const tb = require('../../src/services/projection/compoundTotalBases');
|
||
// Same components, one with an UNKNOWN triple rate vs a MEASURED zero.
|
||
const unknown = tb.tbPmf({ singles: 0.6, doubles: 0.2, triples: null, home_runs: 0.15 });
|
||
const zero = tb.tbPmf({ singles: 0.6, doubles: 0.2, triples: 0, home_runs: 0.15 });
|
||
// Both are computable, and an unknown component contributes nothing —
|
||
// but it must do so by being SKIPPED, not by being asserted as zero.
|
||
expect(unknown).not.toBeNull();
|
||
expect(zero).not.toBeNull();
|
||
// The guard that matters: no component usable at all → null, not a flat curve.
|
||
expect(tb.tbPmf({ singles: null, doubles: null, triples: null, home_runs: null })).toBeNull();
|
||
expect(tb.tbPmf({ singles: false })).toBeNull();
|
||
});
|
||
|
||
it('the migrated sites all delegate to the ONE rule', () => {
|
||
// Semantics must be identical across every migrated module, or the
|
||
// migration reintroduced drift.
|
||
const mods = [
|
||
require('../../src/utils/gradeRanking').strictNum,
|
||
require('../../src/services/consensusRuler').__internals.num,
|
||
];
|
||
for (const fn of mods) {
|
||
expect(fn(null)).toBeNull();
|
||
expect(fn('')).toBeNull();
|
||
expect(fn(0)).toBe(0);
|
||
expect(fn('2.5')).toBe(2.5);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ─────────────────────────────── GUARD 2 ───────────────────────────────
|
||
describe('GUARD 2 — takeability is BOOK IDENTITY, never price shape', () => {
|
||
it('REAL FIXTURE — one-sided takeable markets are TAKEABLE', () => {
|
||
// Verified in prod: betrivers and hardrockbet quote one side only on real
|
||
// props (5 such rows surfaced in the 2026-08-02 re-stamp). One-sidedness is
|
||
// normal baseball market structure, not a defect.
|
||
expect(isTakeableMarket('betrivers')).toBe(true);
|
||
expect(isTakeableMarket('hardrockbet')).toBe(true);
|
||
// A one-sided quote: the other side is genuinely absent, and takeability
|
||
// does not depend on it at all.
|
||
expect(assessQuote({ book: 'betrivers', odds: 6600 }).takeable).toBe(true);
|
||
expect(assessQuote({ book: 'hardrockbet', odds: 400 }).takeable).toBe(true);
|
||
});
|
||
|
||
it('REAL FIXTURE — an extreme price is still TAKEABLE (steep juice is a price)', () => {
|
||
// Ohtani hits-over at -266 and Schwarber hits-over at -209 are real,
|
||
// placeable DraftKings bets.
|
||
for (const odds of [-266, -209, -300, -1400, 4900]) {
|
||
expect(assessQuote({ book: 'draftkings', odds }).takeable).toBe(true);
|
||
}
|
||
});
|
||
|
||
it('REAL FIXTURE — a DFS book at an ordinary -119 is NOT takeable', () => {
|
||
// The ordinary-looking price is the fake one: PrizePicks is not a
|
||
// sportsbook. Book identity decides, price shape never does.
|
||
for (const book of ['prizepicks', 'underdog', 'sleeper', 'dabble']) {
|
||
expect(isTakeableMarket(book)).toBe(false);
|
||
expect(assessQuote({ book, odds: -119 }).takeable).toBe(false);
|
||
}
|
||
});
|
||
|
||
it('exchanges and offshore are not takeable however normal the price looks', () => {
|
||
for (const book of ['kalshi', 'novig', 'smarkets', 'bovada', 'onexbet', 'pinnacle']) {
|
||
expect(isTakeableMarket(book)).toBe(false);
|
||
}
|
||
});
|
||
|
||
it('an ABSENT book is not takeable — we cannot assert a market we cannot name', () => {
|
||
for (const b of [null, undefined, '', 'not_a_book']) expect(isTakeableMarket(b)).toBe(false);
|
||
});
|
||
|
||
it('THE DISTINCTION: takeable and price-band are independent axes', () => {
|
||
// A -300 DraftKings prop is a REAL bet we would not promote. Both true.
|
||
const steep = assessQuote({ book: 'draftkings', odds: -300 });
|
||
expect(steep.takeable).toBe(true);
|
||
expect(steep.within_price_band).toBe(false);
|
||
|
||
const normal = assessQuote({ book: 'draftkings', odds: -110 });
|
||
expect(normal.takeable).toBe(true);
|
||
expect(normal.within_price_band).toBe(true);
|
||
|
||
// ...and a DFS book inside the band is still not takeable.
|
||
const dfs = assessQuote({ book: 'prizepicks', odds: -119 });
|
||
expect(dfs.takeable).toBe(false);
|
||
expect(dfs.within_price_band).toBe(true);
|
||
});
|
||
|
||
it('an UNKNOWN price is not an out-of-band price', () => {
|
||
expect(assessQuote({ book: 'draftkings', odds: null }).within_price_band).toBeNull();
|
||
expect(assessQuote({ book: 'draftkings' }).takeable).toBe(true); // book still decides
|
||
});
|
||
|
||
it('no drift: the canonical rule IS bookRoles.isTakeableBook', () => {
|
||
const { isTakeableBook } = require('../../src/config/bookRoles');
|
||
for (const b of ['draftkings', 'prizepicks', 'pinnacle', 'betrivers', null]) {
|
||
expect(isTakeableMarket(b)).toBe(isTakeableBook(b));
|
||
}
|
||
});
|
||
});
|
||
|
||
// ──────────────────── THREE BANDS, THREE NAMES ────────────────────
|
||
describe('the three questions that used to share the word "takeable"', () => {
|
||
const { isTakeableMarket, isWithinPriceBand, isWithinPromotionBand, assessQuote } =
|
||
require('../../src/config/takeability');
|
||
|
||
it('a +1300 longshot: real bet, worth RECORDING, not worth CROWNING', () => {
|
||
// The measured disagreement: 439 prod rows (28.2% of takeable=true) sit
|
||
// above +200. All three answers differ in intent, none is wrong.
|
||
expect(isTakeableMarket('draftkings')).toBe(true); // can be bet
|
||
expect(isWithinPriceBand(1300)).toBe(true); // worth recording
|
||
expect(isWithinPromotionBand(1300)).toBe(false); // not worth crowning
|
||
});
|
||
|
||
it('a -300 favourite: real bet, outside BOTH bands', () => {
|
||
expect(isTakeableMarket('draftkings')).toBe(true);
|
||
expect(isWithinPriceBand(-300)).toBe(false);
|
||
expect(isWithinPromotionBand(-300)).toBe(false);
|
||
});
|
||
|
||
it('the ledger band is UNCAPPED upward; the promotion band has a ceiling', () => {
|
||
expect(isWithinPriceBand(200)).toBe(true);
|
||
expect(isWithinPromotionBand(200)).toBe(true);
|
||
expect(isWithinPriceBand(201)).toBe(true); // still recordable
|
||
expect(isWithinPromotionBand(201)).toBe(false); // ceiling bites here
|
||
});
|
||
|
||
it('the ranking gate uses the PROMOTION band — byte-identical to before', () => {
|
||
const { takeablePWin } = require('../../src/utils/gradeRanking');
|
||
const { isTakeable } = require('../../src/config/valueEngine');
|
||
for (const odds of [-160, -161, -110, 100, 200, 201, 1300]) {
|
||
const g = { p_win: 0.6, book_odds: odds };
|
||
// gate open iff the OLD valueEngine rule said so — no behaviour moved.
|
||
expect(takeablePWin(g) !== null).toBe(isTakeable(odds));
|
||
}
|
||
});
|
||
|
||
it('assessQuote reports all three axes so a caller cannot pick the wrong one blind', () => {
|
||
const q = assessQuote({ book: 'draftkings', odds: 1300 });
|
||
expect(q).toMatchObject({ takeable: true, within_price_band: true, within_promotion_band: false });
|
||
});
|
||
});
|