Blast radius: exclude projection<=0 grades from the public model record

The degraded grades (projection=0 → model_value=0) are already settled in the
append-only ledger and must NOT be deleted (Data Semantics law). But their
hit/miss is noise, not model skill — they never had a real projection. So
getModelAggregate now filters `.gt('model_value', 0)` on both the settled and
pending queries: the rows stay in ledger_entries, but leave the public hit_pct /
CLV / per-tier record. `.gt` also drops NULL model_value. Post-fix no such row
can be written (projection<=0 refuses), so this only sheds the historical set.

This is the functional form of the "marking" the work order asked for — the
degraded locks are effectively marked as non-counting without mutating history.

Test builder mocks gained `.gt`; a lock asserts the filter is applied to both
queries. Suite 269/3253 green, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-17 03:15:50 -04:00
parent 888d103f95
commit 9fc4edf3a9
7 changed files with 21 additions and 3 deletions
+9 -2
View File
@@ -445,10 +445,17 @@ async function getModelAggregate(opts = {}) {
const since = new Date(nowMs - AGG_WINDOW_DAYS * 24 * 3600 * 1000).toISOString().slice(0, 10);
let settledQ = sb.from('ledger_entries')
.select('outcome, clv_result, clv, player_key, grade');
.select('outcome, clv_result, clv, player_key, grade, model_value');
settledQ = opts.userId ? settledQ.eq('user_id', opts.userId) : settledQ.is('user_id', null);
settledQ = settledQ
.not('outcome', 'is', null)
// 2026-07 — a grade with a non-positive model_value had NO real projection
// (the pre-fix degradation). Those locks are kept in the append-only ledger
// but must not count toward the public model record — their hit/miss is
// noise, not model skill. `.gt` also excludes NULL model_value. Post-fix no
// such row can be written (projection<=0 now refuses), so this only filters
// the historical blast radius.
.gt('model_value', 0)
.gte('game_date', since)
.limit(AGG_FETCH_LIMIT);
if (opts.sport) settledQ = settledQ.eq('sport', String(opts.sport).toLowerCase());
@@ -460,7 +467,7 @@ async function getModelAggregate(opts = {}) {
let pendingQ = sb.from('ledger_entries')
.select('id', { count: 'exact', head: true });
pendingQ = opts.userId ? pendingQ.eq('user_id', opts.userId) : pendingQ.is('user_id', null);
pendingQ = pendingQ.is('outcome', null);
pendingQ = pendingQ.is('outcome', null).gt('model_value', 0); // same real-projection filter
if (opts.sport) pendingQ = pendingQ.eq('sport', String(opts.sport).toLowerCase());
if (opts.playerKey) pendingQ = pendingQ.eq('player_key', opts.playerKey);
if (opts.team) pendingQ = pendingQ.eq('team', opts.team);