Files
vyndr/tests/unit/retentionIdentity.test.js
T
builtbykev 8d741e3932 Pre-grading stage trace: record which branch loses the cohort
The acquisition recorder proved the previous diagnosis wrong: the 03:00 MLB
attempt acquired 6,365 props from a propline cache hit and CONTINUED. Zero
reached the first grading callback, and nothing durable says why.

WHY REPLAY WAS IMPOSSIBLE. The 03:00 input was the cache object written
02:20:24.889Z. ODDS_CACHE_TTL defaults to 3600s, so it expired ~03:20; it is
now gone. No historical copy exists: closing_captures holds no MLB rows past
01:40:52, model_snapshots none, and `bookprices:mlb` carries no timestamp the
book-comparison route exposes. Calling /api/odds/mlb was REFUSED — a cold cache
would fetch and fire recordDownstream -> gradeAndCacheSlate, writing product
state and manufacturing a false recovery. So the input is NOT RECOVERABLE and
no replay was attempted.

A SECOND CLAIM IS WITHDRAWN. The previous tranche concluded "all 6,365 props
were rejected at event admission", reasoning that dedupe cannot empty a
non-empty list. That is FALSE: `dedupeProps` also FILTERS — it drops props with
no player/stat_type/line and any prop whose book is not a MODEL book. A fully
admitted cohort can still dedupe to zero. Demonstrated in test. So the failing
branch was never established, only assumed — which is exactly what the order
forbade, and why DEDUPE_EMPTY is a first-class outcome here.

THE REGION IS SMALL AND EVERY BRANCH LOOKS IDENTICAL FROM OUTSIDE:
  identity annotation (runSnapshot, mlb only, own catch)
  -> admitForGrading (pure, can throw)
  -> dedupeProps     (pure, can throw, ALSO filters)
  -> mapLimit(gradeBestSide)   <- first onGraded-capable call
gradeAndCacheSlate swallows every throw in that region and returns the same
{written:false,count:0} it returns for an honest zero.

The recorder distinguishes them: READY_FOR_GRADING · ALL_REJECTED ·
IDENTITY_STAGE_ERROR · ADMISSION_STAGE_ERROR · DEDUPE_STAGE_ERROR ·
DEDUPE_EMPTY · NO_INPUT_PROPS · OTHER_PREGRADING_ERROR. An exception is never
folded into ALL_REJECTED, and with no admission evidence the classifier refuses
to classify at all — a test pins that.

EXCEPTION SEMANTICS UNCHANGED. Each stage is wrapped to record and then RETHROW
the identical error, so the enclosing best-effort catch still handles it exactly
as before: nothing caught that was not caught, nothing swallowed that was not
swallowed. Admission and dedupe rules, the impossible-binding invariant, event
identity, model books and the first-row-wins cap are untouched — the only
behavioural lines in the diff are `const gate/unique` becoming `let`.

Correlated to the SAME snapshot_attempt_id the acquisition recorder minted — not
a new run id — and stored under its own key `ops:pregrade:{sport}` so it can
never displace the acquisition record. Same atomic LPUSH/LTRIM pattern, bounded
per sport, scheduled-only, best-effort at the call site, auto-disabled under
test.

CAUGHT DURING BUILD: `savePgTrace` was defined and NEVER CALLED — the recorder
would have persisted nothing, the same "built, correct, never invoked" failure
this programme has hit before. A test now drives the real runSnapshot and
asserts a trace is persisted carrying the acquisition's attempt id.

Ten teeth, injections verified present, against a green baseline:
empty collector read as ALL_REJECTED (2) · admission throw reported as
admitted=0 (1) · dedupe throw reported as output=0 (1) · dedupe-to-zero
mislabeled as rejection (1) · identity exception hidden (1) · observer reorders
the candidate array (1) · trace failure changes product outcome (1) · intraday
overwrites scheduled (1) · different attempt id (2) · secret leak (1).
Restored byte-identically.

THREE OF THOSE LANDED AND PASSED FIRST TIME — coverage holes, not safe defects:
the dedupe-throw and identity-throw tests only exercised the recorder directly,
never the real path, and nothing asserted the CANDIDATE array is not reordered.
All three closed with real-path tests, then re-run failing.

Two stale source assertions updated with the reason recorded: both pinned the
exact `const gate = …` / opts-key order that the recording wrapper changed.

387 suites / 5,237 tests pass. web tsc exit 0. Lineage stays OFF.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
2026-08-28 00:41:34 -04:00

268 lines
12 KiB
JavaScript

'use strict';
/**
* SEMANTIC RETENTION PROPOSITION IDENTITY, and the database enforcement of it.
*
* The semantic question comes FIRST: when are two outbound rows the same
* retention proposition within one cycle? Answer — same cycle, same EVENT, same
* participant, same stat, same line, same side. Provider/book is deliberately
* absent: collapsing books is `dedupeProps`'s job and the price anchor is
* chosen later.
*
* The database index is ENFORCEMENT of that answer, never the definition of it.
* `model_snapshots_cycle_prop_uniq` carried no event component, so a real MLB
* doubleheader collapsed to one identity — verified on production: 2026-08-17
* St. Louis @ Cincinnati holds 1,738 retained rows under a single game_id, and
* replaying 1,000 real propositions across both real gamePks materialized 1,000
* of 2,000 rows under the old index and 2,000 of 2,000 under the new one.
*/
const fs = require('fs');
const path = require('path');
const retention = require('../../src/services/retentionService');
const ROOT = path.resolve(__dirname, '..', '..');
const SNAP = '11111111-1111-1111-1111-111111111111';
const G1 = 'mlb:gamepk:824514';
const G2 = 'mlb:gamepk:824478';
const row = (o = {}) => ({
snapshot_id: SNAP, game_id: 'mlb:2026-08-17:St.LouisCardinals@CincinnatiReds',
canonical_event_id: G1, player_key: 'nolan arenado', stat: 'hits', line: 0.5,
side: 'over', sport: 'mlb', ...o,
});
describe('SEMANTIC IDENTITY — the contract, stated before the schema', () => {
test('the conflict target names cycle, event, participant, stat, line, side', () => {
expect(retention.RETENTION_CONFLICT.split(',')).toEqual(
['snapshot_id', 'game_id', 'canonical_event_id', 'player_key', 'stat', 'line', 'side']);
});
test('BOOK is deliberately NOT part of identity', () => {
expect(retention.RETENTION_CONFLICT).not.toMatch(/book/);
// …and that is upstream's documented job, not an oversight.
const gs = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8');
expect(gs).toMatch(/Provider is collapsed on\s+\* purpose/);
});
test('SAME proposition -> same identity', () => {
expect(retention.rowIdentity(row())).toBe(retention.rowIdentity(row()));
});
test('DIFFERENT EVENT -> DISTINCT (the doubleheader)', () => {
expect(retention.rowIdentity(row({ canonical_event_id: G1 })))
.not.toBe(retention.rowIdentity(row({ canonical_event_id: G2 })));
});
for (const [what, patch] of [
['LINE', { line: 1.5 }],
['SIDE', { side: 'under' }],
['STAT', { stat: 'total_bases' }],
['PARTICIPANT', { player_key: 'paul goldschmidt' }],
['CYCLE', { snapshot_id: '22222222-2222-2222-2222-222222222222' }],
['GAME (no canonical)', { canonical_event_id: null, game_id: 'nba:2026-08-27:LAL@NYK' }],
]) {
test(`DIFFERENT ${what} -> DISTINCT`, () => {
expect(retention.rowIdentity(row())).not.toBe(retention.rowIdentity(row(patch)));
});
}
});
describe('SEMANTIC IDENTITY <=> DATABASE CONFLICT IDENTITY', () => {
test('the JS identity uses EXACTLY the columns the DB conflict target names', () => {
expect(retention.CONFLICT_IDENTITY).toEqual(retention.RETENTION_CONFLICT.split(','));
});
test('the migration creates an index on exactly those columns', () => {
const sql = fs.readFileSync(path.join(ROOT,
'supabase/migrations/048_retention_event_aware_identity_expand.sql'), 'utf8');
const stmt = sql.slice(sql.indexOf('create unique index'));
const cols = stmt.slice(stmt.indexOf('(snapshot_id'), stmt.indexOf('nulls not distinct'))
.replace(/[()\s]/g, '');
expect(cols.replace(/,$/, '')).toBe(retention.RETENTION_CONFLICT);
});
test('NULLS NOT DISTINCT is present, and it is load-bearing', () => {
// canonical_event_id is NULL for every non-MLB row. Under PostgreSQL's
// DEFAULT semantics two NULLs are DISTINCT, so the same NBA proposition
// would insert again on every retry — measured: 2 rows. NULLS NOT DISTINCT
// makes it 1.
const sql = fs.readFileSync(path.join(ROOT,
'supabase/migrations/048_retention_event_aware_identity_expand.sql'), 'utf8');
expect(sql).toMatch(/nulls not distinct/i);
});
});
describe('CANONICAL EVENT AVAILABILITY BY ROW CLASS', () => {
const gs = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8');
test('MLB: only RESOLVED rows WITH a canonical id are admitted', () => {
expect(gs).toMatch(/status === 'RESOLVED' && p\.canonical_event_id/);
});
test('UNRESOLVED / AMBIGUOUS / CONTRADICTED never reach retention at all', () => {
// They are rejected BEFORE grading, and onGraded is what feeds retention.
// Matches the CALL, not the assignment form: the pre-grading recorder
// wraps it in try/catch to record and rethrow, which changed `const gate =`
// into `gate = `. The invariant is that admission runs on (props, sport).
expect(gs).toMatch(/gate = admitForGrading\(props, sport\)/);
expect(gs).toMatch(/dedupeProps\(gate\.admitted, limit\)/);
expect(gs).not.toMatch(/dedupeProps\(gate\.rejected/);
});
test('a sport with no resolver is admitted unchanged — canonical stays NULL', () => {
expect(gs).toMatch(/!status \|\| status === 'UNSUPPORTED'/);
});
test('game_id is the always-present fallback, so identity is never NULL-only', () => {
// NOT NULL in the schema; measured 0 nulls / 0 empties across 328,262 rows.
const contract = JSON.parse(fs.readFileSync(path.join(ROOT,
'supabase/schema/model_snapshots.columns.json'), 'utf8'));
expect(contract.columns).toContain('game_id');
expect(retention.RETENTION_CONFLICT).toMatch(/game_id/);
});
});
describe('MIXED-FLEET BRIDGE', () => {
const err = (code, message) => ({ code, message });
test('a missing new index (pre-EXPAND) is bridged', () => {
expect(retention.isLegacyConflictBlock(
err('42P10', 'there is no unique or exclusion constraint matching the ON CONFLICT specification'))).toBe(true);
});
test('the LEGACY index rejecting a doubleheader row is bridged', () => {
expect(retention.isLegacyConflictBlock(
err('23505', 'duplicate key value violates unique constraint "model_snapshots_cycle_prop_uniq"'))).toBe(true);
});
test('a SUPERSEDES conflict is NOT bridged — the fork guard must still fire', () => {
// Also a 23505. Swallowing it would destroy the forked-history guard, which
// is why the bridge requires the legacy index to be NAMED.
expect(retention.isLegacyConflictBlock(
err('23505', 'duplicate key value violates unique constraint "model_snapshots_supersedes_unique"'))).toBe(false);
});
test('unrelated errors are never bridged', () => {
expect(retention.isLegacyConflictBlock(err('23502', 'null value in column "captured_at"'))).toBe(false);
expect(retention.isLegacyConflictBlock(err('PGRST204', "Could not find the 'x' column"))).toBe(false);
expect(retention.isLegacyConflictBlock(null)).toBe(false);
});
test('it targets the event-aware identity FIRST', async () => {
const seen = [];
const sb = { from: () => ({ upsert: async (rows, o) => { seen.push(o.onConflict); return { error: null }; } }) };
const res = await retention.upsertSnapshotChunk(sb, [row()], { ignoreDuplicates: true });
expect(seen).toEqual([retention.RETENTION_CONFLICT]);
expect(res.fellBack).toBe(false);
});
test('it retries the SAME chunk on the legacy target when bridged', async () => {
const seen = [];
const sb = {
from: () => ({
upsert: async (rows, o) => {
seen.push({ target: o.onConflict, n: rows.length });
return seen.length === 1
? { error: err('23505', 'duplicate key value violates unique constraint "model_snapshots_cycle_prop_uniq"') }
: { error: null };
},
}),
};
const chunk = [row({ canonical_event_id: G1 }), row({ canonical_event_id: G2 })];
const res = await retention.upsertSnapshotChunk(sb, chunk, { ignoreDuplicates: true });
expect(seen.map((s) => s.target)).toEqual([retention.RETENTION_CONFLICT, retention.LEGACY_CONFLICT]);
expect(seen[1].n).toBe(chunk.length); // the SAME chunk, whole
expect(res.error).toBeNull();
expect(res.fellBack).toBe(true);
});
test('a non-bridgeable error is returned, never retried', async () => {
const seen = [];
const sb = { from: () => ({ upsert: async (r, o) => { seen.push(o.onConflict); return { error: err('23502', 'null value') }; } }) };
const res = await retention.upsertSnapshotChunk(sb, [row()], {});
expect(seen).toHaveLength(1);
expect(res.error.code).toBe('23502');
expect(res.fellBack).toBe(false);
});
test('persist reports when the bridge fired — it is a bridge, not a resting place', async () => {
let first = true;
const sb = {
from: () => ({
upsert: async () => (first
? (first = false, { error: err('23505', 'duplicate key value violates unique constraint "model_snapshots_cycle_prop_uniq"') })
: { error: null }),
}),
};
const out = await retention.persist([row()], { getClient: () => sb });
expect(out.legacy_conflict_fallback).toBe(true);
expect(out.written).toBe(1);
});
test('EVERY model_snapshots write goes through the bridge', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/retentionService.js'), 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
// No hardcoded legacy target may survive outside the constant itself.
const hard = src.match(/onConflict: 'snapshot_id,player_key,stat,line,side'/g) || [];
expect(hard).toHaveLength(0);
const upserts = src.match(/\.upsert\(/g) || [];
const bridged = src.match(/upsertSnapshotChunk\(/g) || [];
// One .upsert lives inside the bridge helper (two calls: primary + fallback).
expect(upserts.length).toBe(2);
expect(bridged.length).toBeGreaterThanOrEqual(4); // definition + 3 call sites
});
});
describe('MIGRATION SAFETY', () => {
const expand = fs.readFileSync(path.join(ROOT,
'supabase/migrations/048_retention_event_aware_identity_expand.sql'), 'utf8');
const contract = fs.readFileSync(path.join(ROOT,
'supabase/migrations/049_retention_event_aware_identity_contract.sql'), 'utf8');
test('EXPAND is additive and idempotent, and retires nothing', () => {
expect(expand).toMatch(/create unique index if not exists/i);
expect(expand).not.toMatch(/drop\s+index/i);
expect(expand).not.toMatch(/alter table/i);
});
test('EXPAND performs no backfill', () => {
expect(expand).not.toMatch(/\bupdate\b/i);
expect(expand).not.toMatch(/\binsert\b/i);
});
test('CONTRACT only drops the legacy index, and documents its rollback', () => {
expect(contract).toMatch(/drop index if exists public\.model_snapshots_cycle_prop_uniq/i);
expect(contract).not.toMatch(/\bupdate\b/i);
expect(contract).toMatch(/ROLLBACK/);
expect(contract).toMatch(/APPLY THIS ONLY AFTER FLEET CONVERGENCE IS PROVEN/);
});
test('the two phases are separate files — they must not land together', () => {
expect(expand).not.toMatch(/drop index if exists public\.model_snapshots_cycle_prop_uniq/i);
});
});
describe('MATERIALIZATION DEFENCE SURVIVES THE REPAIR', () => {
test('the collision detector is still wired and still disqualifies a cohort', () => {
const rows = [row({ canonical_event_id: G1 }), row({ canonical_event_id: G2 })];
// Under the NEW identity these are two propositions, so no collision.
const exp = retention.expectedMaterialization(rows);
expect(exp.expected_identities).toBe(2);
expect(exp.collision_count).toBe(0);
// And a genuine duplicate still collides.
const dup = retention.expectedMaterialization([row(), row()]);
expect(dup.collision_count).toBe(1);
expect(retention.reconcileMaterialization({
expected: dup, actualIdentities: dup.identities, transportStatus: retention.TERMINAL.COMPLETE,
}).status).toBe(retention.MATERIALIZATION.COLLISION);
});
test('transport vs materialization, and per-sport evidence, are intact', () => {
expect(typeof retention.classifyPersist).toBe('function');
expect(typeof retention.reconcileMaterialization).toBe('function');
expect(typeof retention.lastRetention).toBe('function');
expect(retention.TERMINAL.FAILED_PARTIAL).toBeTruthy();
});
});