MLB canonical event identity, impossible-binding refusal, event-aware dedupe, publication commit
Release-isolated slice built from 41ba38e. Ships ONLY the event-integrity +
publication + lineage-canary closure; the 90-path development tree stays
undeployed.
- canonical MLB event identity from statsapi gamePk (mlb:gamepk:<pk>), with
event_identity_source/method/version recorded. The id is canonical; the
binding is derived and says so.
- IMPOSSIBLE-BINDING REFUSAL. Verified in prod 2026-08-26: Joe Mack (Marlins)
was bound to Dodgers@Braves and Yandy Diaz (Rays) to Rangers@WhiteSox, both
from one book in the 01:00/03:01 UTC cycles after their own games began. Root
cause is source market data, not the binder. A prop whose player's team is not
an event participant now refuses; unknown team preserves uncertainty.
- event-aware dedupe: books still collapse, events no longer do. An unresolved
MLB event fractures rather than falling back to the collision-prone
date+teams key.
- publication commit moved AFTER the authoritative Redis slate write, with
exact parity-gap identity when the product publishes and the record does not.
- lineage dual-write behind LINEAGE_CANARY_SPORTS, DISABLED for this deploy.
Excluded deliberately: WNBA feed/chain, market ontology, PerformanceDistribution,
calibration certification, truth diagnostics, applyRevision Phase-1, and the
analyzeViaEngine1 confidence-rounding change (a served field).
Suite 380/5,040/0 from this worktree; web tsc exit 0; champion output identical
to production.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
@@ -77,13 +77,86 @@ const DEFAULT_TTL = 7200; // 2 hours — matches the spec's grades-cache TTL.
|
||||
// and before the limit — which makes the graded set byte-identical to what it
|
||||
// was before the widening. This gate lifts only when the MLB calibration is
|
||||
// re-run on the consensus ruler and v2 is promoted.
|
||||
/**
|
||||
* WHAT DUPLICATES IS THIS FUNCTION SUPPOSED TO REMOVE?
|
||||
*
|
||||
* ONE: the same proposition offered by SEVERAL BOOKS. Provider is collapsed on
|
||||
* purpose — grading the same player/stat/line once per book would multiply the
|
||||
* work with no new information, and the price anchor is chosen later.
|
||||
*
|
||||
* WHAT IT WAS ALSO REMOVING, WRONGLY
|
||||
*
|
||||
* The key was `player::stat_type::line` with NO event component, so the SAME
|
||||
* proposition in TWO DIFFERENT GAMES also collapsed — and first-row-wins
|
||||
* silently discarded the second real game.
|
||||
*
|
||||
* VERIFIED on production: 2026-08-17 St. Louis @ Cincinnati was a doubleheader
|
||||
* (gamePk 824514 / 824478). VYNDR recorded ONE derived id holding FOUR distinct
|
||||
* starting pitchers, i.e. both games merged. 19 doubleheaders / 38 games are
|
||||
* affected across the 2026 season to date.
|
||||
*
|
||||
* The key therefore adds the EVENT, and nothing else. Provider stays collapsed
|
||||
* because collapsing books is the function's actual job.
|
||||
*
|
||||
* `canonical_event_id` is used when identity resolved. When it did not, the
|
||||
* fallback is the legacy derived label, which reproduces the previous behaviour
|
||||
* exactly — it does not pretend to distinguish what it cannot.
|
||||
*/
|
||||
let fractureSeq = 0;
|
||||
|
||||
/**
|
||||
* The EVENT component of the dedupe key.
|
||||
*
|
||||
* -- FAIL CLOSED WHERE CANONICAL IDENTITY IS EXPECTED --------------------
|
||||
* A first version fell back to `legacy:{date}:{away}@{home}` whenever identity
|
||||
* did not resolve. That is precisely the collision-prone key this whole change
|
||||
* exists to remove: on a doubleheader it is byte-identical for two real games,
|
||||
* so an unresolved MLB prop could still merge two events.
|
||||
*
|
||||
* So for a sport where canonical identity is EXPECTED (MLB), an unresolved prop
|
||||
* FRACTURES: it gets a key unique to that row, which cannot merge with anything.
|
||||
* The cost is that books stop collapsing for that prop, so it may be graded more
|
||||
* than once. That is a coverage/efficiency cost, and it is the right direction:
|
||||
*
|
||||
* UNKNOWN EVENT IDENTITY MAY LOSE COVERAGE.
|
||||
* IT MAY NOT MERGE TWO REAL EVENTS.
|
||||
*
|
||||
* MEASURED: over MLB 2026-08-20..27, 101 of 101 team-pairs resolve from teams
|
||||
* alone; only a doubleheader pair needs the start time to disambiguate. So the
|
||||
* fracture path is rare in normal operation and bites only where identity is
|
||||
* genuinely unknown.
|
||||
*
|
||||
* -- SPORTS WITHOUT A RESOLVER ARE UNCHANGED ----------------------------
|
||||
* WNBA, NBA and the rest have no canonical adapter and never did. Fracturing
|
||||
* them would multiply their grading load for no safety gain, because their
|
||||
* identity was never canonical to begin with. They keep the legacy label, which
|
||||
* is the exact behaviour that existed before this change, and their lineage is
|
||||
* excluded from the canary for the same reason.
|
||||
*/
|
||||
function propositionEventKey(p) {
|
||||
if (p && p.canonical_event_id) return p.canonical_event_id;
|
||||
|
||||
const method = p && p.event_identity_method;
|
||||
const expectsCanonical = method && method !== 'UNSUPPORTED_SPORT';
|
||||
if (expectsCanonical) {
|
||||
// Unique per row: this prop can never share an event key with another.
|
||||
fractureSeq += 1;
|
||||
return `unresolved-event:${method}:${fractureSeq}`;
|
||||
}
|
||||
|
||||
const away = String((p && p.away_team) || '').replace(/\s+/g, '');
|
||||
const home = String((p && p.home_team) || '').replace(/\s+/g, '');
|
||||
const date = (p && p.game_date) || '';
|
||||
return `legacy:${date}:${away}@${home}`;
|
||||
}
|
||||
|
||||
function dedupeProps(props, limit) {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const p of props || []) {
|
||||
if (!p || !p.player || !p.stat_type || p.line == null) continue;
|
||||
if (!isModelBook(p.book)) continue;
|
||||
const key = `${p.player}::${p.stat_type}::${p.line}`;
|
||||
const key = `${propositionEventKey(p)}::${p.player}::${p.stat_type}::${p.line}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push(p);
|
||||
@@ -128,6 +201,13 @@ async function gradeBestSide(grade, prop, sport, opts = {}) {
|
||||
game_time: prop.game_time ?? null,
|
||||
home_team: prop.home_team ?? null,
|
||||
away_team: prop.away_team ?? null,
|
||||
// CANONICAL EVENT IDENTITY rides with the grade so retention and lineage
|
||||
// key on the real event rather than re-deriving a label from team names.
|
||||
canonical_event_id: prop.canonical_event_id ?? null,
|
||||
event_identity_source: prop.event_identity_source ?? null,
|
||||
event_identity_method: prop.event_identity_method ?? null,
|
||||
event_identity_version: prop.event_identity_version ?? null,
|
||||
event_occurrence: prop.event_occurrence ?? null,
|
||||
};
|
||||
const sides = await Promise.all([
|
||||
Promise.resolve()
|
||||
@@ -155,6 +235,14 @@ async function gradeBestSide(grade, prop, sport, opts = {}) {
|
||||
// Strip the internal retention fields so they never reach a cache or payload.
|
||||
delete winner._features;
|
||||
delete winner._grade_11;
|
||||
// PUBLICATION SIGNAL. `onGraded` above fired with both sides before any
|
||||
// filtering; this fires ONLY for the side that becomes the served Read, so
|
||||
// retention can tell a published claim from a captured model state. Measured:
|
||||
// 64.9% of captured rows describe a state no user ever saw.
|
||||
if (typeof opts.onPublished === 'function') {
|
||||
try { opts.onPublished(base, winner); } catch { /* never breaks the slate */ }
|
||||
}
|
||||
|
||||
// CARRY THE GAME (2026-08-01). The legacy grade shape drops home/away, so by
|
||||
// the time the challenger runs, nothing on the grade says WHICH GAME it is —
|
||||
// measured: `team` was null on 416/416 stored grades, so the park/weather
|
||||
|
||||
Reference in New Issue
Block a user