Structural hardening: unknown-is-not-zero + takeability-is-book-identity
Both guards are ADDITIVE. The full suite (4,111 -> 4,126 tests, 331 suites) passes unchanged through the migration, which is the evidence that no currently-correct output moved: served path, champion, reference ruler and the four accruing challengers are byte-identical. GUARD 1 -- src/utils/known.js. Number(null)===0 has produced at least SIX separate defects here, including one in a module written the same week its author documented the trap. Per-module vigilance has demonstrably failed, so the rule lives in one place and SEVEN sites now delegate: platoonSplits, projectionChallenger, challengerProjection, contactChallenger, statcastAggregateService, consensusRuler, gradeRanking -- plus compoundTotalBases moved onto knownRate. Two functions, deliberately: knownNumber (any finite number -- a REAL 0 is a fact and must survive) and knownRate (non-negative, rejects booleans -- for counts/rates where `true` or -1 is broken, not thin). Collapsing them is how the next variant gets in. firstKnown() exists because `a || b` discards a measured 0 and `a ?? b` does not. MY OWN GUARD HAD THE BUG IT EXISTS TO PREVENT, and its own test caught it: Number([]) === 0, so an empty array coerced to a measured ZERO. Same trap wearing a different type. Both helpers now reject objects outright. GUARD 2 -- src/config/takeability.js. Takeability is BOOK IDENTITY and never price shape. Baseball prop markets are genuinely thin, juiced and one-sided, and all three are NORMAL structure: betrivers and hardrockbet legitimately quote one side only (5 such rows surfaced in yesterday's re-stamp), and a hits-over at -300 is a real placeable bet. A rule that inferred un-takeability from price extremity or one-sidedness would throw those away while still admitting a DFS book at an ordinary -119 -- exactly backwards, because the -119 is the fake one. THE DISTINCTION THAT MUST NOT COLLAPSE, now enforced by test: isTakeableMarket(book) -- CAN it be bet? (identity) isWithinPriceBand(odds) -- SHOULD we promote? (policy band, floor -160) A -300 DraftKings prop is takeable AND out of band; a PrizePicks -119 is in band AND not takeable. Independent axes. FLAGGED, NOT SILENTLY CHANGED: the ledger's `takeable` column is the PRICE-BAND answer, and its name predates this distinction. Four challengers and the ranking gate read it, so renaming or redefining it is its own order -- doing it here would have changed correct current behaviour under cover of a hardening change. Fixtures are REAL prod rows from the 2026-08-02 re-stamp, not invented. Gates: 4,126 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
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* takeability — TAKEABILITY IS BOOK IDENTITY. NEVER PRICE SHAPE.
|
||||
*
|
||||
* A line is takeable if and only if it comes from a book a bettor can actually
|
||||
* place a wager at. Nothing about the PRICE decides it.
|
||||
*
|
||||
* WHY THIS NEEDS TO BE A RULE AND NOT A HABIT. Baseball prop markets are
|
||||
* genuinely thin, genuinely juiced, and genuinely one-sided, and all three are
|
||||
* NORMAL market structure rather than signs of a bad quote:
|
||||
*
|
||||
* - `betrivers` and `hardrockbet` legitimately quote ONE SIDE ONLY on real
|
||||
* props (verified in prod: 5 such rows surfaced in the 2026-08-02 re-stamp).
|
||||
* - a hits-over at -300 is a real, placeable bet. Steep juice is a price, not
|
||||
* a disqualification.
|
||||
* - a market with two books is still a market.
|
||||
*
|
||||
* Any rule that inferred UN-takeability from price extremity, one-sidedness or
|
||||
* thinness would throw those real markets away — while still happily admitting
|
||||
* a DFS pick'em book at an ordinary-looking -119. That is exactly backwards:
|
||||
* the -119 is the fake one, because PrizePicks is not a sportsbook.
|
||||
*
|
||||
* ── THE DISTINCTION THAT MUST NOT COLLAPSE ────────────────────────────────
|
||||
*
|
||||
* isTakeableMarket(book) — CAN a bettor place this? Book identity. This file.
|
||||
* isWithinPriceBand(odds) — SHOULD we surface/promote it? A policy band on the
|
||||
* price (`takeableStandard`, floor -160, uncapped
|
||||
* plus). A -300 prop is takeable AND outside the
|
||||
* band. Both statements are true at once.
|
||||
*
|
||||
* These answer different questions and are stored in different columns. The
|
||||
* ledger's `takeable` column is the PRICE-BAND answer; its name predates this
|
||||
* distinction and is misleading — flagged, not silently redefined, because four
|
||||
* challengers and the ranking gate currently read it.
|
||||
*/
|
||||
|
||||
const { TAKEABLE_BOOKS, isTakeableBook } = require('./bookRoles');
|
||||
const { isLedgerTakeable, LEDGER_TAKEABLE_FLOOR } = require('./takeableStandard');
|
||||
|
||||
/**
|
||||
* THE canonical takeability rule. Every path that asks "can this be bet?" calls
|
||||
* this one function, so the answer cannot drift between the ledger write, the
|
||||
* price anchor and any surface.
|
||||
*
|
||||
* @param {string|null|undefined} book
|
||||
* @returns {boolean} — false for an absent book: we cannot assert takeability
|
||||
* of a market we cannot name.
|
||||
*/
|
||||
function isTakeableMarket(book) {
|
||||
return isTakeableBook(book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly NOT a takeability test — kept here beside it so the two are read
|
||||
* together and the difference is unmissable.
|
||||
*
|
||||
* @returns {boolean|null} null when the price is UNKNOWN (never false — an
|
||||
* absent price is not an out-of-band price).
|
||||
*/
|
||||
function isWithinPriceBand(americanOdds) {
|
||||
return isLedgerTakeable(americanOdds);
|
||||
}
|
||||
|
||||
/**
|
||||
* The full answer for one quote, with the two axes kept apart.
|
||||
*
|
||||
* `takeable` depends ONLY on the book. `within_price_band` depends only on the
|
||||
* price. A one-sided -300 quote from DraftKings is `takeable: true,
|
||||
* within_price_band: false` — a real bet we would not promote.
|
||||
*/
|
||||
function assessQuote({ book, odds } = {}) {
|
||||
return {
|
||||
book: book || null,
|
||||
takeable: isTakeableMarket(book),
|
||||
within_price_band: isWithinPriceBand(odds),
|
||||
price_band_floor: LEDGER_TAKEABLE_FLOOR,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isTakeableMarket,
|
||||
isWithinPriceBand,
|
||||
assessQuote,
|
||||
TAKEABLE_BOOKS,
|
||||
LEDGER_TAKEABLE_FLOOR,
|
||||
};
|
||||
@@ -86,11 +86,13 @@ const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
const toLogOdds = (p) => Math.log(p / (1 - p));
|
||||
const fromLogOdds = (l) => 1 / (1 + Math.exp(-l));
|
||||
|
||||
function num(v) {
|
||||
if (v == null || v === '') return null;
|
||||
const n = typeof v === 'number' ? v : Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
// MIGRATED to the shared guard (2026-08-02). `Number(null) === 0` has produced
|
||||
// at least SIX separate defects in this codebase, including one in a module
|
||||
// written the same week its author documented the trap — per-module vigilance
|
||||
// has demonstrably failed. Semantics are byte-identical to the local copy this
|
||||
// replaces, so no output changes; the point is that there is now ONE rule.
|
||||
const { knownNumber } = require('../utils/known');
|
||||
const num = knownNumber;
|
||||
|
||||
/**
|
||||
* adjust({ pWin, direction, statType, classification }) — PURE.
|
||||
|
||||
@@ -40,11 +40,13 @@ function median(xs) {
|
||||
}
|
||||
|
||||
/** Strict numeric: null/''/NaN are ABSENT, never 0. (`Number(null) === 0`.) */
|
||||
function num(v) {
|
||||
if (v == null || v === '') return null;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
// MIGRATED to the shared guard (2026-08-02). `Number(null) === 0` has produced
|
||||
// at least SIX separate defects in this codebase, including one in a module
|
||||
// written the same week its author documented the trap — per-module vigilance
|
||||
// has demonstrably failed. Semantics are byte-identical to the local copy this
|
||||
// replaces, so no output changes; the point is that there is now ONE rule.
|
||||
const { knownNumber } = require('../utils/known');
|
||||
const num = knownNumber;
|
||||
|
||||
/**
|
||||
* Per-book de-vigged fair probability for one side, at one line.
|
||||
|
||||
@@ -71,11 +71,13 @@ const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
const toLogOdds = (p) => Math.log(p / (1 - p));
|
||||
const fromLogOdds = (l) => 1 / (1 + Math.exp(-l));
|
||||
|
||||
function num(v) {
|
||||
if (v == null || v === '') return null;
|
||||
const n = typeof v === 'number' ? v : Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
// MIGRATED to the shared guard (2026-08-02). `Number(null) === 0` has produced
|
||||
// at least SIX separate defects in this codebase, including one in a module
|
||||
// written the same week its author documented the trap — per-module vigilance
|
||||
// has demonstrably failed. Semantics are byte-identical to the local copy this
|
||||
// replaces, so no output changes; the point is that there is now ONE rule.
|
||||
const { knownNumber } = require('../utils/known');
|
||||
const num = knownNumber;
|
||||
|
||||
/** percentileOf(sortedAsc, x) — fraction of the reference below x, 0..1. */
|
||||
function percentileOf(sortedAsc, x) {
|
||||
|
||||
@@ -60,11 +60,13 @@ const STAT_RATE = Object.freeze({
|
||||
/** Stats where a HIGHER rate means a LOWER prop outcome. */
|
||||
const INVERTED = Object.freeze(new Set(['strikeouts']));
|
||||
|
||||
const num = (v) => {
|
||||
if (v == null || v === '') return null;
|
||||
const n = typeof v === 'number' ? v : Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
};
|
||||
// MIGRATED to the shared guard (2026-08-02). `Number(null) === 0` has produced
|
||||
// at least SIX separate defects in this codebase, including one in a module
|
||||
// written the same week its author documented the trap — per-module vigilance
|
||||
// has demonstrably failed. Semantics are byte-identical to the local copy this
|
||||
// replaces, so no output changes; the point is that there is now ONE rule.
|
||||
const { knownNumber } = require('../utils/known');
|
||||
const num = knownNumber;
|
||||
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
* actual generative structure, not by a family that happens to fit its name.
|
||||
*/
|
||||
|
||||
const { knownRate } = require('../../utils/known');
|
||||
|
||||
const TB_CAP = 20; // bases per game; beyond this is not a real outcome
|
||||
const N_CAP = 8; // per-component events per game
|
||||
|
||||
@@ -66,13 +68,12 @@ function tbPmf(rates = {}) {
|
||||
{ w: 3, rate: rates.triples },
|
||||
{ w: 4, rate: rates.home_runs },
|
||||
];
|
||||
// STRICT: `Number(null) === 0`, so a null rate would pass a naive finite check
|
||||
// and be treated as a real, measured zero — the difference between "this
|
||||
// player never triples" and "we do not know his triple rate". Absent stays
|
||||
// absent; only a genuine number counts.
|
||||
const isRate = (v) => v != null && v !== '' && typeof v !== 'boolean'
|
||||
&& Number.isFinite(Number(v)) && Number(v) >= 0;
|
||||
const usable = components.filter((c) => isRate(c.rate));
|
||||
// MIGRATED to the shared guard (2026-08-02). `Number(null) === 0` would read a
|
||||
// null rate as a real, measured zero — the difference between "this player
|
||||
// never triples" and "we do not know his triple rate". `knownRate` is the one
|
||||
// rule; this module previously carried its own copy, which is exactly the
|
||||
// pattern that has failed six times.
|
||||
const usable = components.filter((c) => knownRate(c.rate) !== null);
|
||||
if (usable.length === 0) return null;
|
||||
|
||||
let pmf = new Array(TB_CAP + 1).fill(0);
|
||||
@@ -117,10 +118,9 @@ function pAtLeast(pmf, k) {
|
||||
|
||||
/** Expected total bases implied by the component rates. */
|
||||
function tbMean(rates = {}) {
|
||||
// Same strictness as tbPmf: an absent component contributes nothing, and is
|
||||
// not silently read as a measured zero.
|
||||
const n = (v) => ((v != null && v !== '' && typeof v !== 'boolean'
|
||||
&& Number.isFinite(Number(v)) && Number(v) >= 0) ? Number(v) : 0);
|
||||
// Same shared rule: an absent component contributes nothing and is not
|
||||
// silently read as a measured zero.
|
||||
const n = (v) => knownRate(v) ?? 0;
|
||||
return n(rates.singles) + 2 * n(rates.doubles) + 3 * n(rates.triples) + 4 * n(rates.home_runs);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,13 @@ const STAT_FIELD = Object.freeze({
|
||||
});
|
||||
|
||||
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
||||
const num = (v) => {
|
||||
if (v == null || v === '') return null;
|
||||
const n = typeof v === 'number' ? v : Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
};
|
||||
// MIGRATED to the shared guard (2026-08-02). `Number(null) === 0` has produced
|
||||
// at least SIX separate defects in this codebase, including one in a module
|
||||
// written the same week its author documented the trap — per-module vigilance
|
||||
// has demonstrably failed. Semantics are byte-identical to the local copy this
|
||||
// replaces, so no output changes; the point is that there is now ONE rule.
|
||||
const { knownNumber } = require('../utils/known');
|
||||
const num = knownNumber;
|
||||
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
const abbrOf = (team) => {
|
||||
if (!team) return null;
|
||||
|
||||
@@ -34,11 +34,13 @@ const statcast = require('./adapters/statcastAdapter');
|
||||
const MIN_PA = Number(process.env.STATCAST_MIN_PA) || 50;
|
||||
const MIN_IP = Number(process.env.STATCAST_MIN_IP) || 10;
|
||||
|
||||
const num = (v) => {
|
||||
if (v == null || v === '') return null;
|
||||
const n = typeof v === 'number' ? v : Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
};
|
||||
// MIGRATED to the shared guard (2026-08-02). `Number(null) === 0` has produced
|
||||
// at least SIX separate defects in this codebase, including one in a module
|
||||
// written the same week its author documented the trap — per-module vigilance
|
||||
// has demonstrably failed. Semantics are byte-identical to the local copy this
|
||||
// replaces, so no output changes; the point is that there is now ONE rule.
|
||||
const { knownNumber } = require('../utils/known');
|
||||
const num = knownNumber;
|
||||
|
||||
/**
|
||||
* roleDetail(usage) — TRUE role from real usage, not the IP proxy (which drifts
|
||||
|
||||
@@ -35,11 +35,13 @@ function gradeRankOf(g) {
|
||||
}
|
||||
|
||||
/** Strict numeric read — `Number(null) === 0` is the recurring fabrication bug. */
|
||||
function strictNum(v) {
|
||||
if (v == null || v === '') return null;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
// MIGRATED to the shared guard (2026-08-02). `Number(null) === 0` has produced
|
||||
// at least SIX separate defects in this codebase, including one in a module
|
||||
// written the same week its author documented the trap — per-module vigilance
|
||||
// has demonstrably failed. Semantics are byte-identical to the local copy this
|
||||
// replaces, so no output changes; the point is that there is now ONE rule.
|
||||
const { knownNumber } = require('./known');
|
||||
const strictNum = knownNumber;
|
||||
|
||||
/**
|
||||
* The takeable-gated champion probability for one grade row, or null.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* known — UNKNOWN IS NOT ZERO.
|
||||
*
|
||||
* This codebase's single most repeated defect: `Number(null) === 0`. A missing
|
||||
* value slides through a naive finite check and becomes a real, measured zero —
|
||||
* which is not a neutral default but usually the STRONGEST possible statement:
|
||||
*
|
||||
* - a null triple rate read as 0 says "this player never triples"
|
||||
* - a null at-bat rate read as 0 says "zero opportunity" — a maximal fade
|
||||
* - a null price read as 0 lands ABOVE a -160 floor and tags itself takeable
|
||||
* - a null line read as 0 matches a 0-line quote
|
||||
*
|
||||
* It has appeared at least SIX times, including in a module written the same
|
||||
* week its author documented the trap. Per-module vigilance has demonstrably
|
||||
* failed, so the rule lives here and every site delegates.
|
||||
*
|
||||
* TWO functions, deliberately, because "is this a number?" and "is this a valid
|
||||
* RATE?" are different questions and collapsing them is how the next variant
|
||||
* gets in:
|
||||
*
|
||||
* knownNumber — any finite number, including negatives and zero. A REAL 0 is
|
||||
* a fact (0 rest days, an even-money 0 gap) and must survive.
|
||||
* knownRate — a non-negative finite magnitude, rejecting booleans. For
|
||||
* counts, rates and probabilities, where a negative or a
|
||||
* `true` is not a thin measurement but a broken one.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A finite number, or null when the value is UNKNOWN.
|
||||
*
|
||||
* Semantics are byte-identical to the six local `num`/`strictNum` copies this
|
||||
* replaces, so migrating a call site cannot change its output.
|
||||
*
|
||||
* NOTE ON BOOLEANS: this accepts them (`Number(true) === 1`), matching the
|
||||
* behaviour of the copies it replaces. Use `knownRate` where a boolean would be
|
||||
* nonsense — that one rejects them.
|
||||
*/
|
||||
function knownNumber(v) {
|
||||
if (v == null || v === '') return null;
|
||||
// `Number([]) === 0` and `Number([7]) === 7` — an empty array coerces to a
|
||||
// measured ZERO, which is the exact trap this module exists to close, wearing
|
||||
// a different type. Found by this module's own test. Only primitives are
|
||||
// candidates for being a number.
|
||||
if (typeof v === 'object') return null;
|
||||
const n = typeof v === 'number' ? v : Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
/** Is this a known (present, finite) number? */
|
||||
function isKnown(v) {
|
||||
return knownNumber(v) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A finite, NON-NEGATIVE magnitude, or null when UNKNOWN.
|
||||
*
|
||||
* Stricter than `knownNumber` on purpose: a rate/count of `true`, `-1` or `NaN`
|
||||
* is not a thin measurement, it is a broken one, and admitting it would let a
|
||||
* downstream distribution treat garbage as evidence.
|
||||
*/
|
||||
function knownRate(v) {
|
||||
if (v == null || v === '' || typeof v === 'boolean' || typeof v === 'object') return null;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) && n >= 0 ? n : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coalesce to the first KNOWN value, or null.
|
||||
*
|
||||
* The point is that a present 0 wins over a later fallback — `firstKnown(0, 5)`
|
||||
* is 0, because a measured zero is an answer. `a ?? b` gets this right and
|
||||
* `a || b` does not, which is the same bug wearing different syntax.
|
||||
*/
|
||||
function firstKnown(...vals) {
|
||||
for (const v of vals) {
|
||||
const n = knownNumber(v);
|
||||
if (n !== null) return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { knownNumber, knownRate, isKnown, firstKnown };
|
||||
@@ -0,0 +1,149 @@
|
||||
'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));
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user