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:
@@ -65,12 +65,15 @@ async function main() {
|
||||
|
||||
// PARTICIPANT EVIDENCE — the same index production builds.
|
||||
const built = await E.buildPlayerTeamIndex(games, {
|
||||
getTeamRoster: async (id) => {
|
||||
const r = await (await fetch(`https://statsapi.mlb.com/api/v1/teams/${id}/roster?rosterType=active`)).json();
|
||||
asOfDate: DATE,
|
||||
getTeamRoster: async (id, asOf) => {
|
||||
const d = asOf ? `&date=${asOf}` : '';
|
||||
const r = await (await fetch(`https://statsapi.mlb.com/api/v1/teams/${id}/roster?rosterType=active${d}`)).json();
|
||||
return (r.roster || []).map((x) => ({ id: x.person && x.person.id, name: x.person && x.person.fullName }));
|
||||
},
|
||||
});
|
||||
const playerTeams = built.index;
|
||||
const evidenceDateValid = E.evidenceIsDateValid(built.stats, DATE);
|
||||
|
||||
// Reconstruct prop shapes. One row per (player, stat, line, book) — the side
|
||||
// is not part of the dedupe key, so both sides collapse to the same prop.
|
||||
@@ -92,7 +95,7 @@ async function main() {
|
||||
}
|
||||
// The schedule has one game per pair on a non-doubleheader date, so teams
|
||||
// alone resolve; no game_time is needed and none is reconstructed.
|
||||
const ident = E.attachEventIdentity(SPORT, props, games, playerTeams);
|
||||
const ident = E.attachEventIdentity(SPORT, props, games, playerTeams, evidenceDateValid);
|
||||
|
||||
const oldSet = new Set(props.map(oldKey));
|
||||
const newSet = new Set(props.map(newKey));
|
||||
@@ -112,7 +115,11 @@ async function main() {
|
||||
console.log(`participant evidence : ${built.stats.players} players / ${built.stats.teams} rosters`);
|
||||
console.log(`canonical event resolution : ${ident.canonical}/${ident.total}`
|
||||
+ ` (unresolved ${ident.unresolved}, unsupported ${ident.unsupported})`);
|
||||
console.log(`IMPOSSIBLE-BINDING refusals : ${ident.impossible}`);
|
||||
console.log(`evidence date-valid : ${evidenceDateValid} (as of ${DATE})`);
|
||||
console.log(`CONTRADICTED bindings : ${ident.impossible}`);
|
||||
const gate = require('../src/services/gradeSlateService').__internals.admitForGrading(props, SPORT);
|
||||
console.log(`ADMITTED to grading : ${gate.admitted.length}`);
|
||||
console.log(`REJECTED before grading : ${gate.rejected.length} ${JSON.stringify(gate.reasons)}`);
|
||||
if (Object.keys(ident.reasons).length) console.log(` unresolved reasons : ${JSON.stringify(ident.reasons)}`);
|
||||
console.log(`distinct events : ${new Set(props.map((p) => p.canonical_event_id).filter(Boolean)).size}`);
|
||||
console.log('');
|
||||
|
||||
Reference in New Issue
Block a user