Ledger takeable tagging (deferred C2); efficiency challenger BLOCKED

Champion grade UNCHANGED. Push scoring untouched. Additive tags only — nothing
deleted, nothing re-settled.

PART A — THE EFFICIENCY CHALLENGER: BLOCKED, NOT BUILT.
Review Zero came back ABSENT on all three inputs:
  0.1 efficiency scores DO NOT EXIST (zero occurrences of market_efficiency /
      marketEfficiency / efficiency_score in src/ or web/src/).
  0.2 base thresholds DO NOT EXIST (engine1.js has zero `edge` references — the
      grade is not an edge-vs-threshold comparison; grade_thresholds.json holds
      PROBABILITY bands).
  0.3 the +/-0.05 additive efficiency nudge DOES NOT EXIST. The only 0.05s on
      the grade path are featureCache.teammate_absence_bump, a bvp_advantage
      cutoff, and p*0.9+0.05 inside probabilityEstimator (the 0.5*0.1 term of
      the shrink-toward-0.5). There is no additive scaling to replace.

So a challenger differing from the champion in EXACTLY ONE thing cannot be
constructed: there is no additive scaling to swap, no base threshold to
multiply, and engine1.js has zero `sport` references so market cannot reach the
grade. A threshold must exist first — that is R1 of
specs/full-output-grade-mapping.md, an explicitly held separate order. Shipping
R1+R4 together would make the Phase-3 delta report misleading: the re-letter
would be driven mostly by switching to probability grading while being
presented as the efficiency fix.

0.4 coverage: the spec names 5 scores; the live ledger has 11 markets and only
MLB total_bases maps to one. 9 of 11 have no score, so "all scored markets"
cannot be satisfied without inventing 9 numbers.

PART B — LEDGER TAKEABLE TAGGING: BUILT (the deferred C2).
New src/config/takeableStandard.js: floor on the minus side, UNCAPPED plus.
Deliberately NOT valueEngine.isTakeable (the -160..+200 PROMOTION band) — a
+400 prop is not promotable but IS takeable; a test asserts the two diverge on
the plus side and agree at the floor so they can never quietly merge. Absent
price returns null, never false (Number(null) === 0 would tag a missing price
takeable). The floor is POLICY not derived (C1 could not derive one) and is
labelled so; each row records takeable_floor so a re-derivation can re-tag.

Migration 034 (applied + tracked): ledger_entries.takeable boolean +
takeable_floor numeric, nullable, partial index. Forward tagging in
ledgerService at row build; backfill in one statement.
Result: 1254 rows, 1246 tagged (781 takeable / 465 below floor), 8 NULL with
null_despite_price = 0 (the NULLs are genuinely priceless rows). Settled 1163
and graded 1254 unchanged.

PART C — the model-version boundary tag is DELIBERATELY NOT APPLIED: no scaling
change shipped, so no boundary exists, and stamping one would mark a model
transition that never happened. modelEras.js is its home when a real one lands.

Floor: 312 suites / 3890 tests green (8 new), web 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-07-31 00:19:13 -04:00
parent a6afac43cc
commit 2bfaeff572
6 changed files with 292 additions and 1 deletions
+122
View File
@@ -0,0 +1,122 @@
# SPEC — LEDGER TAKEABLE TAGGING (built) + MULTIPLICATIVE EFFICIENCY SCALING (BLOCKED)
2026-07-31. Champion grade UNCHANGED. Push scoring untouched.
---
## PART A — THE EFFICIENCY CHALLENGER: **BLOCKED, NOT BUILT**
### Review Zero came back ABSENT on all three inputs
- **0.1 efficiency scores — DO NOT EXIST.** Zero occurrences of `market_efficiency`,
`marketEfficiency`, or `efficiency_score` in `src/` or `web/src/`. `src/config/` holds
no efficiency file (the one `0.80` there is `providers.js WARN_PCT`, a quota warning).
- **0.2 base thresholds — DO NOT EXIST.** There is no "MLB A: 5% edge / NBA A: 7% edge"
table. `engine1.js` contains **zero `edge` references**; the grade is not an
edge-vs-threshold comparison. `grade_thresholds.json` holds PROBABILITY bands.
- **0.3 the ±0.05 additive nudge — DOES NOT EXIST.** Searched the whole grade path. The
only `0.05` values are `featureCache.teammate_absence_bump` (a feature), a
`bvp_advantage` cutoff for a matchup letter, and `p*0.9 + 0.05` inside
`probabilityEstimator` (the `0.5 × 0.1` term of the shrink-toward-0.5, not efficiency).
**There is no additive efficiency application to replace.** No market-conditional logic
exists in `engine1.js` or `analyzeViaEngine1.js` at all.
### Why the challenger cannot be built as specified
The order requires a challenger differing from the champion in **EXACTLY ONE thing —
multiplicative vs additive scaling**. That construction is impossible here:
- there is no additive scaling to swap,
- there is no base threshold to multiply,
- and `engine1.js` has **zero `sport` references**, so market/sport cannot reach the grade.
To have a threshold to scale, the challenger must FIRST grade on probability-vs-threshold —
which is rule **R1 of `specs/full-output-grade-mapping.md`**, an explicitly held separate
order. Shipping R1+R4 together would (a) violate the isolate-one-variable requirement and
(b) make the Phase-3 delta report **misleading**: the re-letter would be driven mostly by
switching to probability grading, presented as the efficiency fix. Kev would be approving
the wrong attribution. **That is why this half stopped rather than guessed.**
### 0.4 — coverage gap that would sink the build anyway
The spec names 5 scores (NFL passing .85, NBA points .80, MLB K .60, MLB TB .55, NBA role
.55). The live public ledger contains **11 markets**: MLB hits (376 rows), doubles (247),
total_bases (44), stolen_bases (17), earned_runs (6), home_runs (4), outs (2); WNBA points
(246), rebounds (156), assists (142), threes (10). **Only MLB total_bases maps to a specced
score.** MLB K's and NFL passing do not appear at all; WNBA is not in the spec list. So
**9 of 11 live markets have NO score** — and the guardrail ("all scored markets, or it's
incoherent") cannot be satisfied without inventing 9 numbers. Inventing them would be
fabricated calibration.
**What Part A needs before it can run:** (1) R1 probability grading, so a threshold exists;
(2) efficiency scores for the markets we actually grade — ideally **FIT from each sport's
own accrued record**, not hand-set (that is R4's stated design).
---
## PART B — LEDGER TAKEABLE TAGGING: **BUILT** (the deferred C2)
Independent of Part A: it needs no threshold, no scaling, no grade change.
### The standard (`src/config/takeableStandard.js`)
isLedgerTakeable(american) = american >= LEDGER_TAKEABLE_FLOOR // default -160
// FLOOR on the minus side, UNCAPPED on the plus side.
**🔴 It is deliberately NOT `valueEngine.isTakeable`.** Those answer different questions:
| | shape | question |
|---|---|---|
| `valueEngine.isTakeable` | 160..+200, **bounded both ends** | "may we PROMOTE this to the hero / top of board?" |
| `takeableStandard.isLedgerTakeable` | ≥ 160, **uncapped plus** | "was this a price a bettor could actually have TAKEN?" |
A **+400** prop is **NOT promotable but IS takeable** — both true at once. A unit test asserts
they diverge on the plus side and agree at the minus floor, so the two can never quietly merge.
**Absent price → `null`, never `false`.** Without the strict guard `Number(null) === 0` is
160 and a MISSING price would be tagged takeable — fabricated data in the record.
**The floor is POLICY, not derived, and is labelled so everywhere.** C1 (2026-07-30) could not
derive one: every price bucket's 95% ROI interval contained zero, the curve was non-monotone
(deepest buckets positive), and resolving a 5-point edge needs 6612,285 rows/bucket against
actual sizes of 871. 160 is held because it is directionally the better half of the only
available comparison (in-band ROI +3.84% vs out-of-band 0.04%, both spanning zero).
**Re-derivation trigger:** any negative bucket reaching n ≥ 300, or the end of the MLB regular
season — and adopt a derived floor only when a bucket's 95% CI excludes zero.
### Schema (migration 034, applied + tracked)
`ledger_entries.takeable boolean` + `takeable_floor numeric`, both nullable, plus a partial
index on `(sport, takeable) WHERE user_id IS NULL`. **`takeable_floor` records the floor each
row was judged under**, so a future re-derivation can re-tag safely instead of guessing which
standard a row was tagged by. Additive only — nothing deleted, nothing re-settled.
### Forward + backfill
Forward: `ledgerService` stamps `takeable`/`takeable_floor` at row build from the locked odds.
Backfill: every row with a parseable locked price, one statement, no outcome/grade/price touched.
**Result — 1,254 rows: 1,246 tagged (781 takeable / 465 below floor), 8 NULL, and
`null_despite_price = 0`** — i.e. every priced row is tagged and the 8 NULLs are genuinely
priceless rows (honest absence). One distinct floor (160). Settled 1,163 and graded 1,254
both unchanged.
---
## PART C — MODEL-VERSION BOUNDARY TAG: **DELIBERATELY NOT APPLIED**
Phase 2.4 asks for a boundary separating pre-fix (additive) from post-fix (multiplicative)
grades. **No scaling change shipped, so no such boundary exists.** Stamping one would mark a
model transition that never happened — a fabricated era in the permanent record, which is the
exact class of thing the Data Semantics Rule forbids. `src/config/modelEras.js` (Session 64)
is the correct home for it **on the day a real scaling change promotes**.
---
## VERIFY
Champion grade path untouched (`engine1`, `analyzeViaEngine1`, `probabilityEstimator`,
`gradeSlateService`, `grade_thresholds.json` — no diff). **312 suites / 3,890 tests green**
(8 new), **web build exit 0**. Grade visuals are auth-gated → Chrome audit; this order changed
nothing visual.
## TAGS
VERIFIED: efficiency scores/base thresholds/±0.05 nudge all absent; 11-market coverage gap;
tagging built + backfilled + integrity-checked. CANNOT DETERMINE: none. **BLOCKED: Part A
(the efficiency challenger) — needs R1 probability grading plus fitted per-market scores.**
+52
View File
@@ -0,0 +1,52 @@
'use strict';
/**
* THE LEDGER TAKEABLE STANDARD (2026-07-31, specs/takeable-tagging.md).
*
* 🔴 THIS IS NOT `config/valueEngine.isTakeable` AND MUST NOT BE CONFLATED WITH IT.
* They answer different questions and have DIFFERENT shapes:
*
* valueEngine.isTakeable — the PROMOTION BAND, -160..+200 (BOUNDED BOTH ENDS).
* "Which reads may we put on the hero / top of board?"
* It caps the plus side deliberately: a +400 longshot is
* not something we promote as the read of the day.
*
* THIS module — the LEDGER RECORD STANDARD: a FLOOR on the minus side,
* **UNCAPPED on the plus side** (founder-ratified shape).
* "Was this a price a bettor could actually have taken?"
* Plus-money is where genuine edge and ladder calls live,
* so it gets no ceiling.
*
* A +400 prop is NOT promotable but IS takeable. Both statements are true; keep the
* two functions separate.
*
* THE FLOOR IS A **POLICY** NUMBER, NOT A DERIVED ONE — say so wherever it surfaces.
* The C1 derivation (2026-07-30) could not derive a floor: on the clean population
* (296 decided MLB overs) EVERY price bucket's 95% ROI interval contained zero, the
* curve was non-monotone (the deepest buckets were the POSITIVE ones), and resolving a
* 5-point edge needs 661-2,285 rows per bucket against actual bucket sizes of 8-71.
* So -160 is inherited policy, held because it is directionally the better half of the
* only comparison available (in-band ROI +3.84% vs out-of-band -0.04%, both intervals
* spanning zero). Rows record the floor they were tagged under so a future
* re-derivation can re-tag safely.
*
* RE-DERIVATION TRIGGER: any negative price bucket reaching n >= 300, or the end of the
* MLB regular season, whichever comes first — and adopt a DERIVED floor only when a
* bucket's 95% CI excludes zero.
*/
/** Provisional policy floor (American odds). Env-tunable; changing it does NOT
* retroactively re-tag — `takeable_floor` on each row records what it was tagged under. */
const LEDGER_TAKEABLE_FLOOR = Number(process.env.LEDGER_TAKEABLE_FLOOR || -160);
/** Is this a price a bettor could actually have taken? Floor on the minus side,
* UNCAPPED on the plus side. Strict on input — `Number(null) === 0` would otherwise
* land a MISSING price above the floor and tag it takeable. */
function isLedgerTakeable(american) {
if (american == null || american === '') return null; // absent, not false
const a = Number(american);
if (!Number.isFinite(a)) return null;
return a >= LEDGER_TAKEABLE_FLOOR;
}
module.exports = { LEDGER_TAKEABLE_FLOOR, isLedgerTakeable, FLOOR_IS_POLICY_NOT_DERIVED: true };
+14
View File
@@ -29,6 +29,10 @@
const { nameKey, normalizeName } = require('../utils/playerName'); const { nameKey, normalizeName } = require('../utils/playerName');
const { settleResult, statValue, logRowOnDate } = require('./outcomeService'); const { settleResult, statValue, logRowOnDate } = require('./outcomeService');
// The LEDGER takeable standard (floor on the minus side, UNCAPPED plus) — a
// DIFFERENT question from valueEngine's -160..+200 promotion band. See
// config/takeableStandard.js for why the two must not be merged.
const { LEDGER_TAKEABLE_FLOOR, isLedgerTakeable: takeableFor } = require('../config/takeableStandard');
const CONFLICT = 'user_id,player_key,stat,line,side,game_id'; const CONFLICT = 'user_id,player_key,stat,line,side,game_id';
const UPSERT_CHUNK = 200; const UPSERT_CHUNK = 200;
@@ -236,6 +240,16 @@ function rowsFromSnapshot(sport, grades, oddsProps, nowIso) {
line, line,
side, side,
locked_odds: locked.odds != null ? String(locked.odds) : oddsForSide(prop, side), locked_odds: locked.odds != null ? String(locked.odds) : oddsForSide(prop, side),
// TAKEABLE TAG (2026-07-31, specs/takeable-tagging.md) — was this a price a
// bettor could actually have taken? FLOOR on the minus side, UNCAPPED plus.
// 🔴 NOT `valueEngine.isTakeable` (the -160..+200 PROMOTION band the hero and
// board rank on): a +400 prop is NOT promotable but IS takeable. Keep separate.
// The floor is POLICY, not derived (C1 could not derive one — every price
// bucket's ROI interval contained zero), so each row records the floor it was
// tagged under and a re-derivation can re-tag safely. Absent price → NULL, an
// honest absence, never false.
takeable: takeableFor(locked.odds != null ? locked.odds : oddsForSide(prop, side)),
takeable_floor: LEDGER_TAKEABLE_FLOOR,
book: (prop && prop.book) || g.book || null, book: (prop && prop.book) || g.book || null,
grade: g.grade, grade: g.grade,
edge: numOrNull(g.edge_pct), edge: numOrNull(g.edge_pct),
@@ -0,0 +1,28 @@
-- 034 — LEDGER TAKEABLE TAGGING (2026-07-31, specs/takeable-tagging.md)
--
-- Marks whether each ledger row's LOCKED price was one a bettor could actually
-- have taken, under the founder-ratified shape: a FLOOR on the minus side,
-- UNCAPPED on the plus side.
--
-- NOT the same as config/valueEngine.isTakeable (the -160..+200 PROMOTION band
-- used by the hero/board). A +400 prop is not promotable but IS takeable.
--
-- `takeable_floor` records the floor each row was tagged under, so a future
-- re-derivation (the floor is POLICY, not derived — see C1, 2026-07-30) can
-- re-tag safely without guessing what standard a row was judged by.
--
-- Additive only: nullable columns, no backfill of any existing value, nothing
-- deleted, nothing re-settled. NULL takeable = "no locked price to judge",
-- which is an honest absence, not false.
ALTER TABLE ledger_entries
ADD COLUMN IF NOT EXISTS takeable boolean,
ADD COLUMN IF NOT EXISTS takeable_floor numeric;
COMMENT ON COLUMN ledger_entries.takeable IS
'Locked price >= the policy floor (minus side), uncapped plus. NULL = no locked price. NOT the promotion band.';
COMMENT ON COLUMN ledger_entries.takeable_floor IS
'The policy floor (American odds) this row was tagged under. Policy, not derived — see specs/takeable-tagging.md.';
CREATE INDEX IF NOT EXISTS idx_ledger_takeable
ON ledger_entries (sport, takeable) WHERE user_id IS NULL;
+75
View File
@@ -0,0 +1,75 @@
/**
* Ledger takeable tagging (specs/takeable-tagging.md).
*
* The load-bearing property: this standard is a FLOOR with an UNCAPPED plus side,
* and it is DELIBERATELY NOT valueEngine's -160..+200 promotion band. A test that
* lets the two converge would erase the distinction the ledger depends on.
*/
const { LEDGER_TAKEABLE_FLOOR, isLedgerTakeable } = require('../../src/config/takeableStandard');
const { isTakeable: isPromotable } = require('../../src/config/valueEngine');
describe('ledger takeable standard — floor on minus, UNCAPPED plus', () => {
test('the floor is inclusive and a worse price is not takeable', () => {
expect(isLedgerTakeable(LEDGER_TAKEABLE_FLOOR)).toBe(true);
expect(isLedgerTakeable(LEDGER_TAKEABLE_FLOOR - 1)).toBe(false);
expect(isLedgerTakeable(-110)).toBe(true);
expect(isLedgerTakeable(-300)).toBe(false);
});
test('the PLUS side is UNCAPPED — this is the whole ratified shape', () => {
for (const p of [100, 200, 201, 400, 600, 5000]) {
expect(isLedgerTakeable(p)).toBe(true);
}
});
test('an ABSENT price is NULL, never false (Number(null) === 0 guard)', () => {
// Without the strict guard, Number(null) === 0 would be >= -160 and a MISSING
// price would be tagged takeable — fabricated data in the record.
expect(isLedgerTakeable(null)).toBeNull();
expect(isLedgerTakeable(undefined)).toBeNull();
expect(isLedgerTakeable('')).toBeNull();
expect(isLedgerTakeable('not-a-price')).toBeNull();
});
test('accepts the string prices the ledger actually stores', () => {
expect(isLedgerTakeable('-110')).toBe(true);
expect(isLedgerTakeable('+150')).toBe(true);
expect(isLedgerTakeable('-275')).toBe(false);
});
});
describe('it must NOT collapse into the promotion band', () => {
test('a long plus price is TAKEABLE but NOT promotable — both true at once', () => {
const longshot = 400;
expect(isLedgerTakeable(longshot)).toBe(true); // a bettor could take it
expect(isPromotable(longshot)).toBe(false); // we would not hero it
});
test('the two disagree on the plus side and AGREE on the minus floor', () => {
expect(isLedgerTakeable(-160)).toBe(true);
expect(isPromotable(-160)).toBe(true);
expect(isLedgerTakeable(-161)).toBe(false);
expect(isPromotable(-161)).toBe(false);
// divergence is confined to the plus side
expect(isLedgerTakeable(250)).toBe(true);
expect(isPromotable(250)).toBe(false);
});
});
describe('the pipeline row carries the tag and the floor it was judged under', () => {
const svc = require('../../src/services/ledgerService');
test('ledgerService requires the ledger standard, not the promotion band', () => {
const src = require('fs').readFileSync(
require('path').join(__dirname, '../../src/services/ledgerService.js'), 'utf8',
);
expect(src).toMatch(/require\('\.\.\/config\/takeableStandard'\)/);
expect(src).toMatch(/takeable: takeableFor\(/);
expect(src).toMatch(/takeable_floor: LEDGER_TAKEABLE_FLOOR/);
});
test('the service module loads and exposes its record builder', () => {
expect(typeof svc.recordPipelineGrades).toBe('function');
});
});
+1 -1
View File
File diff suppressed because one or more lines are too long