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