89a2977f57
Kev's call: the 30D accuracy surfaces must read TRUTH, not a cache that can't be filtered. My earlier degraded-row exclusion only touched getModelAggregate (Postgres); the public buckets/badge still read outcomeService (Redis outcome log), which counts degraded projection-0 outcomes and has no field to filter on. - /api/accuracy (AccuracyBadge) + /api/ledger/accuracy (buckets/ModelRecord) now source from the clean Postgres ledger aggregate via new ledgerService.getAccuracyView + accuracyBucketsFromAgg (model_value > 0 excludes degraded rows). Same response shapes → no frontend change. Redis outcome log is now read by nothing public; it can age out or be rebuilt. - BEAT CLOSE is a MEASURED-WRONG ZERO: captureClosing re-records the locked line as the "closing" line, so clv is flat on the whole sample and beat_close reads 0% (comparing a number to itself). Full write-up: specs/audit-data/ clv-capture-broken.md (the fix belongs to C4). Until then, beat_close_pct + clv_distribution are SUPPRESSED at the source (getModelAggregate, gated by clvCaptureReliable() / CLV_CAPTURE_RELIABLE=1). Every public surface already renders BEAT CLOSE only when non-null, so they all hide it now — no wrong zero anywhere. HIT RATE (real) is unaffected. Suite 271/3261 green, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
42 lines
2.2 KiB
Markdown
42 lines
2.2 KiB
Markdown
# CLV Capture Broken — C4 Finding (2026-07-17)
|
|
|
|
Source: Truth-Everywhere Part 2, item 7. Escalated from the "0% BEAT CLOSE"
|
|
observation on the public accuracy surfaces.
|
|
|
|
## The bug
|
|
Every settled ledger row has `closing_line == locked_line` (and
|
|
`closing_odds == locked_odds`), so the computed `clv` is 0/flat on the ENTIRE
|
|
sample. `beat_close_pct` therefore reads a fabricated-looking **0%** — it is
|
|
comparing a number to itself, not measuring closing-line value.
|
|
|
|
Evidence (live `/api/ledger/model`, 2026-07-17): sampled rows show
|
|
`line == closing_line`, `clv: null`/0, `clv_result: null`/flat across the board.
|
|
|
|
## Root cause (to fix in C4)
|
|
`ledgerService.captureClosing` overwrites today's unsettled rows'
|
|
`closing_line`/`closing_odds` on every snapshot with the CURRENT feed values.
|
|
The intent (S58) was "the last write before game start is the close." But in
|
|
practice the captured value equals the locked line every time — either the
|
|
lines genuinely don't move in the captured window, or captureClosing is reading
|
|
the same feed field the lock came from and writing it back unchanged. Net: the
|
|
"closing" column is a copy of the lock, so CLV is structurally always 0.
|
|
|
|
## The fix (C4 — not done here)
|
|
Capture the REAL last line before game start into a SEPARATE field, never
|
|
defaulted to the lock:
|
|
- Record `closing_line` only from a distinct closing snapshot (the last feed
|
|
read before first pitch/tip), and only when it actually differs from the
|
|
lock — otherwise leave it null (absent, not a copy).
|
|
- CLV = signed(locked - closing) by side, computed only when a real, distinct
|
|
closing line exists. No close captured ⇒ CLV null for that row (honest), not 0.
|
|
- Verify against a few known line moves before trusting the aggregate.
|
|
|
|
## Interim (shipped this pass, item 7)
|
|
`beat_close_pct` and `clv_distribution` are SUPPRESSED at the source
|
|
(`getModelAggregate`, gated by `clvCaptureReliable()` /
|
|
`CLV_CAPTURE_RELIABLE=1`). Every public surface (ModelRecord, ledger MODEL tab,
|
|
public profiles, OG images) already renders BEAT CLOSE only when non-null, so
|
|
they all hide it now — no measured-wrong zero on any public surface. HIT RATE
|
|
(which is real) is unaffected. Flip `CLV_CAPTURE_RELIABLE=1` once C4 lands and
|
|
real closes are verified.
|