Files
vyndr/tests/unit/settledLiveGate.test.js
T
builtbykev 713f90183f Build 1 CORRECTED: itemized grades are PAID (live AND settled) — exploit killed
Serving/gating change only. src/services/ untouched: no grade, model or
settlement-logic change. Pricing = Build 2, migration = Build 3.

WHY THE PRIOR GATE WAS WRONG: freeing grades at resolution made the free tier a
ONE-DAY-DELAYED FEED OF THE WHOLE PRODUCT — settlement is nightly, so a bettor
watching one cycle behind got the entire method free. There is now NO
per-grade resolution flip: an itemized grade, tonight's or last week's, is
Analyst+.

FREE now gets, none of it itemizing the nightly slate:
  1. the full data aggregator (unchanged — schedule, per-book lines, stats,
     streaks, hubs)
  2. the AGGREGATE track record, which ALREADY EXISTS and is public:
     /api/accuracy (sample 937, byGrade tiers, per-sport mlb+wnba, min_sample 20)
     and /api/ledger/accuracy (per-grade buckets). The honest-record laws are
     already honored there — A/D/F return pct:null under the n>=20 threshold
     rather than a fake percentage.
  3. a CAPPED, day-rotated sample of resolved calls for texture: cap 3, stable
     within a day, rotates across days, and only RESOLVED rows are eligible so a
     live read can never be sampled. The cap is what kills the exploit — three
     rotating past calls cannot reconstruct a nightly slate, whereas the full
     settled list is the feed one cycle late.
  4. the locked shell of tonight's reads: they exist, and their shape.

EVERY itemized grade for an unentitled tier now loses grade, confidence,
confidence_basis, reasoning, kill_conditions_triggered, projection, edge_pct,
matchup_grade, form, alt_lines and kelly, and is stamped locked. Free-side DATA
survives so the board still reads as real: player, market, line, book_odds,
fair_odds (the de-vigged fair number is the free hook and is never the paywall),
season/last10 stats, archetype — and `outcome`, because a RESULT is a fact
rather than a judgment.

The tease stays aggregate-only (live_locked {count, tiers}) computed from the
ungated rows and never joined back to one, and no gated row carries a grade, so
nobody can work out which prop is the A.

Floor: 319 suites / 3970 tests green, 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 07:29:24 -04:00

88 lines
4.0 KiB
JavaScript

/**
* BUILD 1 CORRECTED — itemized grades are PAID (live AND settled).
* The property that matters: the one-day-behind exploit must be DEAD.
*/
const g = require('../../src/utils/snapshotGating');
const settled = { player: 'Settled Sam', stat_type: 'points', line: 12.5, book_odds: -110, fair_odds: -104,
season_avg: 14, grade: 'A', confidence: 75, reasoning: { summary: 'real why' },
kill_conditions_triggered: [{ code: 'X', reason: 'r' }], projection: 15, edge_pct: 20,
outcome: { result: 'hit', actual: 14 } };
const live = { player: 'Live Larry', stat_type: 'assists', line: 5.5, book_odds: -120, fair_odds: -112,
season_avg: 6, archetype: { primary: { name: 'CONDUCTOR' } }, grade: 'A', confidence: 75,
confidence_basis: 'grade_band', reasoning: { summary: 'secret why' },
kill_conditions_triggered: [{ code: 'Y', reason: 'secret' }], projection: 7.1, edge_pct: 29,
matchup_grade: 'B', form: 'hot', alt_lines: [{}], kelly: { units: 1 } };
const J = ['grade', 'confidence', 'confidence_basis', 'reasoning', 'kill_conditions_triggered',
'projection', 'edge_pct', 'matchup_grade', 'form', 'alt_lines', 'kelly'];
describe('THE EXPLOIT IS DEAD — settled itemized grades are PAID too', () => {
const out = g.gateItemizedGrades([settled, live], 'free');
test('a SETTLED grade is locked for free — no one-day-delayed feed', () => {
for (const f of J) expect(out[0][f]).toBeUndefined();
expect(out[0].locked).toBe(true);
});
test('a LIVE grade is locked for free', () => {
for (const f of J) expect(out[1][f]).toBeUndefined();
expect(out[1].locked).toBe(true);
});
test('NEITHER serialized row carries a trace of judgment', () => {
const json = JSON.stringify(out);
expect(json).not.toMatch(/real why|secret why|grade_band|hot/);
});
test('free-side DATA survives on both, so the board still reads as real', () => {
expect(out[0].player).toBe('Settled Sam');
expect(out[1].fair_odds).toBe(-112); // the fair leg is never the paywall
expect(out[1].season_avg).toBe(6);
expect(out[1].archetype).toBeTruthy();
expect(out[0].outcome).toEqual({ result: 'hit', actual: 14 }); // the RESULT is a fact, not judgment
});
});
describe('the free SAMPLE is a taste, not the archive', () => {
const pool = Array.from({ length: 40 }, (_, i) => ({ player: `P${i}`, grade: 'B', outcome: { result: 'hit' } }));
test('capped at 3 no matter how large the settled pool', () => {
expect(g.FREE_SAMPLE_CAP).toBe(3);
expect(g.freeSample(pool, '2026-07-31')).toHaveLength(3);
expect(g.freeSample(pool, '2026-07-31').length).toBeLessThan(pool.length);
});
test('rotates by day but is STABLE within a day', () => {
const a = g.freeSample(pool, '2026-07-30').map((x) => x.player);
const b = g.freeSample(pool, '2026-07-31').map((x) => x.player);
expect(a).toEqual(g.freeSample(pool, '2026-07-30').map((x) => x.player)); // stable
expect(a).not.toEqual(b); // rotates
});
test('only RESOLVED calls can be sampled — never a live read', () => {
expect(g.freeSample([live, settled], 'd')).toEqual([settled]);
expect(g.freeSample([live], 'd')).toEqual([]);
});
});
describe('entitled tiers get everything, untouched', () => {
test('analyst and desk get the array by reference — live AND archive', () => {
const rows = [settled, live];
expect(g.gateItemizedGrades(rows, 'analyst')).toBe(rows);
expect(g.gateItemizedGrades(rows, 'desk')).toBe(rows);
expect(g.entitledToItemizedGrades('free')).toBe(false);
expect(g.entitledToItemizedGrades(undefined)).toBe(false);
});
});
describe('the tease shows shape, never which prop', () => {
test('aggregate count + tiers, and no gated row carries a grade', () => {
const s = g.liveLockedSummary([settled, live, { grade: 'B' }]);
expect(s.count).toBe(2); // settled excluded from LIVE count
expect(s.tiers).toEqual({ A: 1, B: 1 });
for (const row of g.gateItemizedGrades([live, { grade: 'B' }], 'free')) {
expect(row.grade).toBeUndefined();
}
});
});