b06a84af80
The self-learning loop stopped two days ago and reported success the whole
time. 1,444 ledger rows from 2026-08-01 sit unsettled with settle_attempts=0
-- never even attempted -- and every accruing challenger has been starved of
settled sample as a result.
ROOT CAUSE. settleLedger fetched open ids, then REFETCHED the full rows with
.in('id', ids). PostgREST puts filters in the URL, so 500 UUIDs became an
18,499-character request that the fetch layer rejects with "TypeError: fetch
failed". The result was destructured as `const { data: rows } = ...` with NO
error binding, so rows came back null, the loop body never executed, and the
function returned {settled:0, voided:0, unrecoverable:0, pending:0} --
byte-identical to a clean "nothing to settle". Reproduced against prod before
changing anything.
WHY IT HID FOR TWO DAYS. It is volume-triggered. Daily volume ran 20-260 rows
and settled perfectly for weeks; 2026-08-01 was the first day past the 500-row
fetch limit. And the zero-settle ops alarm reads these very return values, so
pending:0 told the watchdog the backlog was empty -- the alarm built to catch
exactly this could not see it.
THE FIX. The refetch existed only to add game_date/settle_attempts/
dclv_computed_at. Selecting them in the first query removes the id list
entirely, so there is no URL to overflow at any volume. A failed fetch now
surfaces its error instead of being reported as an empty backlog.
captureClosing carried the same shape one level down -- .in('id', g.ids) on an
UPDATE, which fails identically once a single line|odds group gets large on a
big slate. Its id filters are now chunked at 100 (~3.7 KB).
Tests: the regression is locked by asserting settlement issues NO id-list
filter at 500 rows, and that a failed fetch is never reported as an empty
backlog -- the two properties that would have caught this. Two existing
suites asserted the old two-query shape and were updated to the real one.
4,159 tests green (332 suites); web build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
91 lines
3.8 KiB
JavaScript
91 lines
3.8 KiB
JavaScript
/**
|
|
* Session 64 — the CLV gate is SERVER-SIDE, at the data layer.
|
|
*
|
|
* A Free request must never RECEIVE dclv data. Client/CSS hiding is rejected:
|
|
* data that reaches the browser has left the building. These lock the gate and
|
|
* the surface audit, so a future column addition can't quietly leak.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { canAccess } = require('../../src/config/tiers');
|
|
|
|
const read = (f) => fs.readFileSync(path.join(__dirname, '..', '..', f), 'utf8');
|
|
|
|
describe('TIER capability', () => {
|
|
test('analyst and desk may see the badge; free may not', () => {
|
|
expect(canAccess('analyst', 'clv_badge')).toBe(true);
|
|
expect(canAccess('desk', 'clv_badge')).toBe(true);
|
|
expect(canAccess('free', 'clv_badge')).toBeFalsy();
|
|
expect(canAccess(undefined, 'clv_badge')).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('SERVER GATE — the data never leaves for an unentitled tier', () => {
|
|
const ledger = read('src/routes/ledger.js');
|
|
|
|
test('CLV columns are appended only via a capability check', () => {
|
|
expect(ledger).toMatch(/canAccess\(tier, 'clv_badge'\)/);
|
|
expect(ledger).toMatch(/CLV_COLUMNS/);
|
|
});
|
|
|
|
test('the base column list does NOT contain dclv', () => {
|
|
// Assert on the literal itself — a nearby comment mentioning dclv is fine.
|
|
const m = ledger.match(/const ROW_COLUMNS = '([^']+)'/);
|
|
expect(m).toBeTruthy();
|
|
expect(m[1]).not.toMatch(/dclv/);
|
|
});
|
|
|
|
test('responses are ALSO stripped — defence in depth, not just the SELECT', () => {
|
|
expect(ledger).toMatch(/function stripClv/);
|
|
expect(ledger).toMatch(/stripClv\(data, req\)/);
|
|
});
|
|
|
|
test('de-vig internals (fair_lock/fair_close) are never SELECTED for clients', () => {
|
|
// They may (and should) appear in the strip list — that is the guard.
|
|
const sel = ledger.match(/const CLV_COLUMNS = '([^']+)'/);
|
|
expect(sel[1]).not.toMatch(/fair_lock|fair_close/);
|
|
expect(ledger).toMatch(/dclv_fair_lock, dclv_fair_close, \.\.\.rest/);
|
|
});
|
|
});
|
|
|
|
describe('SURFACE AUDIT — every channel CLV could leak through', () => {
|
|
const surfaces = {
|
|
'public profile (share link)': 'src/routes/profiles.js',
|
|
'snapshot / card feed': 'src/routes/snapshot.js',
|
|
'ticker feed': 'src/routes/ticker.js',
|
|
'share card / OG': 'src/routes/shareCard.js',
|
|
'widget (embeddable)': 'src/routes/widget.js',
|
|
'newsletter': 'src/services/newsletterService.js',
|
|
};
|
|
for (const [name, file] of Object.entries(surfaces)) {
|
|
test(`${name} carries NO CLV data`, () => {
|
|
expect(read(file)).not.toMatch(/dclv/);
|
|
});
|
|
}
|
|
|
|
test('no ledger read uses select("*") — a star would auto-leak new columns', () => {
|
|
for (const f of ['src/routes/ledger.js', 'src/routes/profiles.js', 'src/services/ledgerService.js']) {
|
|
const src = read(f);
|
|
const stars = src.match(/from\('ledger_entries'\)[\s\S]{0,60}?select\('\*'\)/g) || [];
|
|
expect(stars).toHaveLength(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('IMMUTABILITY — a shown badge never silently flips', () => {
|
|
const svc = read('src/services/ledgerService.js');
|
|
test('dclv is computed only when it has never been computed', () => {
|
|
expect(svc).toMatch(/row\.dclv_computed_at\s*\n?\s*\?\s*null/);
|
|
});
|
|
test('the settle read fetches dclv_computed_at so the guard can see it', () => {
|
|
// Asserted on the SETTLE SELECT itself rather than on an adjacent column
|
|
// pair: the settle read is now a single query (the id-refetch that used to
|
|
// follow it overflowed the URL and silently returned null), so column order
|
|
// changed while the requirement did not. What matters is that the column the
|
|
// immutability guard reads is actually fetched.
|
|
const settleSelect = svc.match(/\.select\('id, player_key[^']*'\)/g) || [];
|
|
expect(settleSelect.length).toBeGreaterThan(0);
|
|
expect(settleSelect.some((sel) => sel.includes('dclv_computed_at'))).toBe(true);
|
|
});
|
|
});
|