Event admission gate: an unresolved or contradicted game does not earn a Read
A fractured identity keeps two bad records from merging. It does not make an unknown game true. Until now a prop with an unresolved or verified-impossible event still continued into grading under that synthetic key; it no longer does. - event_binding_status contract: RESOLVED / UNRESOLVED / AMBIGUOUS / CONTRADICTED / UNSUPPORTED. CONTRADICTED and UNRESOLVED stay distinct — one means we know the association is wrong, the other that we do not know. - ONE admission gate (gradeSlateService.admitForGrading), before dedupe and before grading. Admission requires RESOLVED *and* a canonical_event_id; a legacy derived game_id can never satisfy it. Rejected props are returned, not discarded, so retention keeps them as evidence. - Roster evidence is now DATE-SCOPED. statsapi honours ?date= and it changes the answer (Joe Mack is on the 2026-08-26 Marlins roster, absent on 2026-04-15). Evidence that does not describe the slate's date can only yield UNRESOLVED, never CONTRADICTED — uncertainty must not become an accusation. - A mis-nested market is REFUSED, never re-bound. Knowing Joe Mack is a Marlin does not license moving a provider record into the Marlins game; that would invent provenance. Measured on the real 2026-08-26 slate: 2,878 props -> 2,874 admitted, 4 rejected (EVENT_PLAYER_TEAM_CONTRADICTION), each player's correct game still resolving. Doubleheader 824514/824478 both remain independently RESOLVED and admitted. No change to probability, projection, side, grade, confidence, ranking, normalization or calibration. Lineage remains disabled. Suite 380/5,061/0 from the release worktree; web tsc exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
@@ -732,3 +732,193 @@ describe('IMPOSSIBLE-BINDING INVARIANT — the two verified regressions', () =>
|
||||
expect(built.index.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EVENT ADMISSION — a fractured identity is not model admission', () => {
|
||||
const gs = require('../../src/services/gradeSlateService');
|
||||
const admit = (props) => gs.__internals.admitForGrading(props, 'mlb');
|
||||
const prop = (o) => ({ player: 'P', stat_type: 'hits', line: 0.5, book: 'draftkings', ...o });
|
||||
|
||||
test('RESOLVED with a canonical id is admitted', () => {
|
||||
const r = admit([prop({ event_binding_status: 'RESOLVED', canonical_event_id: 'mlb:gamepk:1' })]);
|
||||
expect(r.admitted).toHaveLength(1);
|
||||
expect(r.rejected).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('RESOLVED WITHOUT a canonical id is still rejected', () => {
|
||||
// The status alone is not the credential; the id has to be there.
|
||||
const r = admit([prop({ event_binding_status: 'RESOLVED' })]);
|
||||
expect(r.admitted).toHaveLength(0);
|
||||
expect(r.rejected).toHaveLength(1);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['UNRESOLVED', 'EVENT_UNRESOLVED'],
|
||||
['AMBIGUOUS', 'EVENT_AMBIGUOUS'],
|
||||
['CONTRADICTED', 'EVENT_PLAYER_TEAM_CONTRADICTION'],
|
||||
])('%s does NOT grade and is labelled %s', (status, reason) => {
|
||||
const r = admit([prop({ event_binding_status: status })]);
|
||||
expect(r.admitted).toHaveLength(0);
|
||||
expect(r.rejected[0].event_rejection_reason).toBe(reason);
|
||||
expect(r.rejected[0].event_admission).toBe('REJECTED');
|
||||
});
|
||||
|
||||
test('a legacy derived game_id can never satisfy the gate', () => {
|
||||
const r = admit([prop({ event_binding_status: 'UNRESOLVED', game_id: 'mlb:2026-08-26:A@B' })]);
|
||||
expect(r.admitted).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('a sport with no resolver is unchanged', () => {
|
||||
expect(admit([prop({ event_binding_status: 'UNSUPPORTED' })]).admitted).toHaveLength(1);
|
||||
expect(admit([prop({})]).admitted).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('rejected props are RETAINED for diagnostics, not discarded', () => {
|
||||
const r = admit([prop({ event_binding_status: 'CONTRADICTED' })]);
|
||||
expect(r.rejected).toHaveLength(1);
|
||||
expect(r.reasons.EVENT_PLAYER_TEAM_CONTRADICTION).toBe(1);
|
||||
});
|
||||
|
||||
test('the gate runs BEFORE dedupe and grading, exactly once', () => {
|
||||
const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8');
|
||||
expect(src.indexOf('admitForGrading(props, sport)')).toBeLessThan(src.indexOf('dedupeProps(gate.admitted'));
|
||||
// One admission seam, not scattered checks.
|
||||
expect((src.match(/admitForGrading\(/g) || []).length).toBe(2); // definition + call
|
||||
});
|
||||
});
|
||||
|
||||
describe('ROSTER TEMPORAL SEMANTICS — evidence must describe the right moment', () => {
|
||||
const { nameKey } = require('../../src/utils/playerName');
|
||||
const SLATE = [
|
||||
{ gamePk: 823825, gameNumber: 1, gameDate: '2026-08-26T22:40:00Z', home: { team: 'Miami Marlins', teamId: 146 }, away: { team: 'Boston Red Sox', teamId: 111 } },
|
||||
{ gamePk: 824878, gameNumber: 1, gameDate: '2026-08-26T23:15:00Z', home: { team: 'Atlanta Braves', teamId: 144 }, away: { team: 'Los Angeles Dodgers', teamId: 119 } },
|
||||
];
|
||||
const TEAMS = new Map([[nameKey('Joe Mack'), new Set(['MIA'])]]);
|
||||
const misNested = {
|
||||
player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'pinnacle',
|
||||
home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
|
||||
game_date: '2026-08-26', game_time: '2026-08-26T23:15:00Z',
|
||||
};
|
||||
|
||||
test('date-valid evidence yields CONTRADICTED', () => {
|
||||
const r = E.resolveEvent('mlb', misNested, SLATE, TEAMS, true);
|
||||
expect(r.event_binding_status).toBe(E.BINDING_STATUS.CONTRADICTED);
|
||||
});
|
||||
|
||||
test('evidence that is NOT date-valid yields UNRESOLVED, never an accusation', () => {
|
||||
const r = E.resolveEvent('mlb', misNested, SLATE, TEAMS, false);
|
||||
expect(r.event_binding_status).toBe(E.BINDING_STATUS.UNRESOLVED);
|
||||
expect(r.reason).toBe('participant_evidence_not_date_valid');
|
||||
// Either way it does not grade — but the two verdicts stay distinct.
|
||||
expect(r.canonical_event_id).toBeNull();
|
||||
});
|
||||
|
||||
test('evidenceIsDateValid requires date-scoping AND a matching date', () => {
|
||||
expect(E.evidenceIsDateValid({ date_scoped: true, as_of_date: '2026-08-26' }, '2026-08-26')).toBe(true);
|
||||
expect(E.evidenceIsDateValid({ date_scoped: true, as_of_date: '2026-08-01' }, '2026-08-26')).toBe(false);
|
||||
expect(E.evidenceIsDateValid({ date_scoped: false, as_of_date: null }, '2026-08-26')).toBe(false);
|
||||
expect(E.evidenceIsDateValid(null, '2026-08-26')).toBe(false);
|
||||
});
|
||||
|
||||
test('the roster fetch is scoped to the slate date', async () => {
|
||||
const seen = [];
|
||||
await E.buildPlayerTeamIndex(SLATE, {
|
||||
asOfDate: '2026-08-26',
|
||||
getTeamRoster: async (id, asOf) => { seen.push(asOf); return [{ id, name: 'Joe Mack' }]; },
|
||||
});
|
||||
expect(new Set(seen)).toEqual(new Set(['2026-08-26']));
|
||||
});
|
||||
|
||||
test('an unscoped index reports date_scoped false', async () => {
|
||||
const b = await E.buildPlayerTeamIndex(SLATE, {
|
||||
getTeamRoster: async (id) => [{ id, name: 'Joe Mack' }],
|
||||
});
|
||||
expect(b.stats.date_scoped).toBe(false);
|
||||
expect(E.evidenceIsDateValid(b.stats, '2026-08-26')).toBe(false);
|
||||
});
|
||||
|
||||
test('the adapter passes the date through to statsapi', () => {
|
||||
const src = fs.readFileSync(path.join(ROOT, 'src/services/adapters/mlbStatsAdapter.js'), 'utf8');
|
||||
expect(src).toMatch(/asOfDate \? `&date=/);
|
||||
expect(src).toMatch(/mlbstats:roster:\$\{teamId\}:\$\{season\}:\$\{asOfDate \|\| 'now'\}/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MIS-NESTED MARKETS ARE REFUSED, NEVER RE-BOUND', () => {
|
||||
const { nameKey } = require('../../src/utils/playerName');
|
||||
const SLATE = [
|
||||
{ gamePk: 823825, gameNumber: 1, gameDate: '2026-08-26T22:40:00Z', home: { team: 'Miami Marlins', teamId: 146 }, away: { team: 'Boston Red Sox', teamId: 111 } },
|
||||
{ gamePk: 824878, gameNumber: 1, gameDate: '2026-08-26T23:15:00Z', home: { team: 'Atlanta Braves', teamId: 144 }, away: { team: 'Los Angeles Dodgers', teamId: 119 } },
|
||||
{ gamePk: 824234, gameNumber: 1, gameDate: '2026-08-26T17:10:00Z', home: { team: 'Detroit Tigers', teamId: 116 }, away: { team: 'Tampa Bay Rays', teamId: 139 } },
|
||||
{ gamePk: 824555, gameNumber: 1, gameDate: '2026-08-26T23:40:00Z', home: { team: 'Chicago White Sox', teamId: 145 }, away: { team: 'Texas Rangers', teamId: 140 } },
|
||||
];
|
||||
const TEAMS = new Map([
|
||||
[nameKey('Joe Mack'), new Set(['MIA'])],
|
||||
[nameKey('Yandy Diaz'), new Set(['TB'])],
|
||||
]);
|
||||
|
||||
test('JOE MACK: contradicted, not re-bound to the Marlins game', () => {
|
||||
const r = E.resolveEvent('mlb', {
|
||||
player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'pinnacle',
|
||||
home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
|
||||
game_date: '2026-08-26', game_time: '2026-08-26T23:15:00Z',
|
||||
}, SLATE, TEAMS, true);
|
||||
expect(r.event_binding_status).toBe('CONTRADICTED');
|
||||
// The refusal must NOT quietly relocate the prop to gamePk 823825. The
|
||||
// provider record is invalid; inventing a different event is inventing
|
||||
// provenance.
|
||||
expect(r.canonical_event_id).toBeNull();
|
||||
expect(r.canonical_event_id).not.toBe('mlb:gamepk:823825');
|
||||
});
|
||||
|
||||
test('YANDY DIAZ: contradicted, not re-bound to the Rays game', () => {
|
||||
const r = E.resolveEvent('mlb', {
|
||||
player: 'Yandy Diaz', stat_type: 'total_bases', line: 0.5, book: 'pinnacle',
|
||||
home_team: 'Chicago White Sox', away_team: 'Texas Rangers',
|
||||
game_date: '2026-08-26', game_time: '2026-08-26T23:40:00Z',
|
||||
}, SLATE, TEAMS, true);
|
||||
expect(r.event_binding_status).toBe('CONTRADICTED');
|
||||
expect(r.canonical_event_id).toBeNull();
|
||||
expect(r.canonical_event_id).not.toBe('mlb:gamepk:824234');
|
||||
});
|
||||
|
||||
test('each player CORRECTLY associated market remains eligible', () => {
|
||||
const gs = require('../../src/services/gradeSlateService');
|
||||
const good = [
|
||||
{ player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'draftkings', home_team: 'Miami Marlins', away_team: 'Boston Red Sox', game_time: '2026-08-26T22:40:00Z' },
|
||||
{ player: 'Yandy Diaz', stat_type: 'total_bases', line: 0.5, book: 'draftkings', home_team: 'Detroit Tigers', away_team: 'Tampa Bay Rays', game_time: '2026-08-26T17:10:00Z' },
|
||||
];
|
||||
E.attachEventIdentity('mlb', good, SLATE, TEAMS, true);
|
||||
for (const p of good) expect(p.event_binding_status).toBe('RESOLVED');
|
||||
expect(gs.__internals.admitForGrading(good, 'mlb').admitted).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('the mis-nested props do not survive admission', () => {
|
||||
const gs = require('../../src/services/gradeSlateService');
|
||||
const bad = [
|
||||
{ player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'pinnacle', home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers', game_time: '2026-08-26T23:15:00Z' },
|
||||
{ player: 'Yandy Diaz', stat_type: 'total_bases', line: 0.5, book: 'pinnacle', home_team: 'Chicago White Sox', away_team: 'Texas Rangers', game_time: '2026-08-26T23:40:00Z' },
|
||||
];
|
||||
E.attachEventIdentity('mlb', bad, SLATE, TEAMS, true);
|
||||
const gate = gs.__internals.admitForGrading(bad, 'mlb');
|
||||
expect(gate.admitted).toHaveLength(0);
|
||||
expect(gate.rejected).toHaveLength(2);
|
||||
expect(gate.reasons.EVENT_PLAYER_TEAM_CONTRADICTION).toBe(2);
|
||||
});
|
||||
|
||||
test('DOUBLEHEADER survives the stricter gate — both games still admitted', () => {
|
||||
const gs = require('../../src/services/gradeSlateService');
|
||||
const DH = [
|
||||
{ gamePk: 824514, gameNumber: 1, gameDate: '2026-08-17T17:40:00Z', home: { team: 'Cincinnati Reds', teamId: 113 }, away: { team: 'St. Louis Cardinals', teamId: 138 } },
|
||||
{ gamePk: 824478, gameNumber: 2, gameDate: '2026-08-17T22:40:00Z', home: { team: 'Cincinnati Reds', teamId: 113 }, away: { team: 'St. Louis Cardinals', teamId: 138 } },
|
||||
];
|
||||
const teams = new Map([[nameKey('Andre Pallante'), new Set(['STL'])]]);
|
||||
const props = ['2026-08-17T17:40:00Z', '2026-08-17T22:40:00Z'].map((t) => ({
|
||||
player: 'Andre Pallante', stat_type: 'strikeouts', line: 4.5, book: 'draftkings',
|
||||
home_team: 'Cincinnati Reds', away_team: 'St. Louis Cardinals', game_time: t,
|
||||
}));
|
||||
E.attachEventIdentity('mlb', props, DH, teams, true);
|
||||
expect(props.map((p) => p.event_binding_status)).toEqual(['RESOLVED', 'RESOLVED']);
|
||||
expect(new Set(props.map((p) => p.canonical_event_id)).size).toBe(2);
|
||||
expect(gs.__internals.admitForGrading(props, 'mlb').admitted).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user