Files
vyndr/specs/takeable-tagging.md
builtbykev 2bfaeff572 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
2026-07-31 00:19:13 -04:00

6.8 KiB
Raw Permalink Blame History

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.