Disambiguate takeable: THREE questions shared one word, now three names

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
This commit is contained in:
Kev
2026-08-02 18:19:17 -04:00
parent 8c764c22a4
commit d103ecf4c3
4 changed files with 94 additions and 6 deletions
+42
View File
@@ -147,3 +147,45 @@ describe('GUARD 2 — takeability is BOOK IDENTITY, never price shape', () => {
}
});
});
// ──────────────────── 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 });
});
});