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:
@@ -445,10 +445,17 @@ async function getModelAggregate(opts = {}) {
|
|||||||
const since = new Date(nowMs - AGG_WINDOW_DAYS * 24 * 3600 * 1000).toISOString().slice(0, 10);
|
const since = new Date(nowMs - AGG_WINDOW_DAYS * 24 * 3600 * 1000).toISOString().slice(0, 10);
|
||||||
|
|
||||||
let settledQ = sb.from('ledger_entries')
|
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 = opts.userId ? settledQ.eq('user_id', opts.userId) : settledQ.is('user_id', null);
|
||||||
settledQ = settledQ
|
settledQ = settledQ
|
||||||
.not('outcome', 'is', null)
|
.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)
|
.gte('game_date', since)
|
||||||
.limit(AGG_FETCH_LIMIT);
|
.limit(AGG_FETCH_LIMIT);
|
||||||
if (opts.sport) settledQ = settledQ.eq('sport', String(opts.sport).toLowerCase());
|
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')
|
let pendingQ = sb.from('ledger_entries')
|
||||||
.select('id', { count: 'exact', head: true });
|
.select('id', { count: 'exact', head: true });
|
||||||
pendingQ = opts.userId ? pendingQ.eq('user_id', opts.userId) : pendingQ.is('user_id', null);
|
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.sport) pendingQ = pendingQ.eq('sport', String(opts.sport).toLowerCase());
|
||||||
if (opts.playerKey) pendingQ = pendingQ.eq('player_key', opts.playerKey);
|
if (opts.playerKey) pendingQ = pendingQ.eq('player_key', opts.playerKey);
|
||||||
if (opts.team) pendingQ = pendingQ.eq('team', opts.team);
|
if (opts.team) pendingQ = pendingQ.eq('team', opts.team);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function mockChain() {
|
|||||||
is(col, val) { b._filters.push(['is', col, val]); return b; },
|
is(col, val) { b._filters.push(['is', col, val]); return b; },
|
||||||
not(col, op, val) { b._filters.push(['not', col, op, val]); return b; },
|
not(col, op, val) { b._filters.push(['not', col, op, val]); return b; },
|
||||||
gte(col, val) { b._filters.push(['gte', col, val]); return b; },
|
gte(col, val) { b._filters.push(['gte', col, val]); return b; },
|
||||||
|
gt(col, val) { b._filters.push(['gt', col, val]); return b; },
|
||||||
ilike(col, val) { b._filters.push(['ilike', col, val]); return b; },
|
ilike(col, val) { b._filters.push(['ilike', col, val]); return b; },
|
||||||
order() { return b; },
|
order() { return b; },
|
||||||
limit() {
|
limit() {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function mockChain(table) {
|
|||||||
b.is = rec('is');
|
b.is = rec('is');
|
||||||
b.not = rec('not');
|
b.not = rec('not');
|
||||||
b.gte = rec('gte');
|
b.gte = rec('gte');
|
||||||
|
b.gt = rec('gt');
|
||||||
b.order = () => b;
|
b.order = () => b;
|
||||||
b.maybeSingle = () => {
|
b.maybeSingle = () => {
|
||||||
mockState.filters.push([table, b._filters]);
|
mockState.filters.push([table, b._filters]);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ function mockChain(table) {
|
|||||||
b.is = rec('is');
|
b.is = rec('is');
|
||||||
b.not = rec('not');
|
b.not = rec('not');
|
||||||
b.gte = rec('gte');
|
b.gte = rec('gte');
|
||||||
|
b.gt = rec('gt');
|
||||||
b.order = () => b;
|
b.order = () => b;
|
||||||
b.maybeSingle = () => {
|
b.maybeSingle = () => {
|
||||||
mockState.filters.push([table, b._filters]);
|
mockState.filters.push([table, b._filters]);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function makeSb(captured, rows) {
|
|||||||
b.is = rec('is');
|
b.is = rec('is');
|
||||||
b.not = rec('not');
|
b.not = rec('not');
|
||||||
b.gte = rec('gte');
|
b.gte = rec('gte');
|
||||||
|
b.gt = rec('gt');
|
||||||
b.order = () => b;
|
b.order = () => b;
|
||||||
b.limit = () => { captured.push(b._filters); return Promise.resolve({ data: rows, error: null, count: 0 }); };
|
b.limit = () => { captured.push(b._filters); return Promise.resolve({ data: rows, error: null, count: 0 }); };
|
||||||
b.then = (resolve, reject) => {
|
b.then = (resolve, reject) => {
|
||||||
@@ -37,6 +38,12 @@ describe('getModelAggregate scoping', () => {
|
|||||||
}
|
}
|
||||||
expect(agg.hit_pct).toBeNull();
|
expect(agg.hit_pct).toBeNull();
|
||||||
expect(agg.min_sample).toBe(ledgerService.MIN_AGG_SAMPLE);
|
expect(agg.min_sample).toBe(ledgerService.MIN_AGG_SAMPLE);
|
||||||
|
// 2026-07 — projection<=0 grades (the degradation blast radius) are excluded
|
||||||
|
// from the public record on BOTH queries (kept in the append-only ledger,
|
||||||
|
// just never counted). `.gt('model_value', 0)` also drops NULL model_value.
|
||||||
|
for (const filters of captured) {
|
||||||
|
expect(filters).toContainEqual(['gt', 'model_value', 0]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('userId scopes BOTH queries to that user and never touches the public scope', async () => {
|
test('userId scopes BOTH queries to that user and never touches the public scope', async () => {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function fakeSb() {
|
|||||||
not() { return b; },
|
not() { return b; },
|
||||||
lt() { return b; },
|
lt() { return b; },
|
||||||
gte() { return b; },
|
gte() { return b; },
|
||||||
|
gt() { return b; },
|
||||||
in(col, ids) {
|
in(col, ids) {
|
||||||
if (b._update) {
|
if (b._update) {
|
||||||
calls.updates.push({ values: b._update, ids });
|
calls.updates.push({ values: b._update, ids });
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ describe('ledgerService.settleLedger — WNBA settles vs the ESPN game log', ()
|
|||||||
upsert() { return Promise.resolve({ error: null }); },
|
upsert() { return Promise.resolve({ error: null }); },
|
||||||
update(v) { b._update = v; return b; },
|
update(v) { b._update = v; return b; },
|
||||||
select() { return b; }, eq() { return b; }, is() { return b; },
|
select() { return b; }, eq() { return b; }, is() { return b; },
|
||||||
not() { return b; }, lt() { return b; }, gte() { return b; }, order() { return b; },
|
not() { return b; }, lt() { return b; }, gte() { return b; }, gt() { return b; }, order() { return b; },
|
||||||
in(col, ids) {
|
in(col, ids) {
|
||||||
if (b._update) { calls.updates.push({ values: b._update, ids }); return Promise.resolve({ error: null }); }
|
if (b._update) { calls.updates.push({ values: b._update, ids }); return Promise.resolve({ error: null }); }
|
||||||
return terminal();
|
return terminal();
|
||||||
|
|||||||
Reference in New Issue
Block a user