352016790a
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
59 lines
3.8 KiB
SQL
59 lines
3.8 KiB
SQL
-- Migration 047: canonical event identity + publication commit (ADDITIVE ONLY).
|
|
--
|
|
-- Every statement is ADD COLUMN IF NOT EXISTS or CREATE INDEX. Nothing is
|
|
-- dropped, renamed, retyped or backfilled. Historical rows keep NULL.
|
|
--
|
|
-- ── WHY: EVENT LABELS ARE NOT EVENT IDENTITY ─────────────────────────────
|
|
-- `ledgerService.gameIdFor` derives `sport:date:away@home`. That has no
|
|
-- occurrence component, so both halves of a doubleheader produce a
|
|
-- BYTE-IDENTICAL string.
|
|
--
|
|
-- VERIFIED against statsapi, MLB 2026 through 2026-08-27: 19 doubleheaders /
|
|
-- 38 real games collapse into 19 derived labels. Real case 2026-08-17,
|
|
-- St. Louis @ Cincinnati: gamePk 824514 (game 1, 17:40Z) and 824478 (game 2,
|
|
-- 22:40Z). VYNDR recorded ONE id holding 171 ledger rows and FOUR distinct
|
|
-- starting pitchers -- both games merged into one event.
|
|
--
|
|
-- `canonical_event_id` is namespaced (`mlb:gamepk:824514`) so two sports'
|
|
-- numeric ids can never collide. The legacy `game_id` is KEPT for diagnostics
|
|
-- and compatibility and is explicitly not authoritative.
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS canonical_event_id text;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS event_identity_source text;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS event_identity_method text;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS event_identity_version text;
|
|
-- The source's own occurrence number (statsapi `gameNumber`). Never inferred.
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS event_occurrence integer;
|
|
|
|
-- ── WHY: PUBLISHED MUST MEAN COMMITTED ───────────────────────────────────
|
|
-- The authoritative served slate is ONE atomic Redis write,
|
|
-- `cacheSet('snapshot:{sport}:latest')`. Retention persists BEFORE it
|
|
-- (snapshotService:1137 vs :1152) and the winner is chosen earlier still, so
|
|
-- neither capture nor selection means published. `published_at` records the
|
|
-- commit instant and `publication_id` the slate it committed in -- the write is
|
|
-- slate-atomic, so publication is slate-level and every served row shares one.
|
|
--
|
|
-- `captured_at` answers when the model state was captured. `published_at`
|
|
-- answers when the composed Read became authoritative. They are different
|
|
-- questions and are stored separately.
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS published_at timestamptz;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS publication_id text;
|
|
|
|
-- Chronology is walked per canonical event.
|
|
CREATE INDEX IF NOT EXISTS model_snapshots_canonical_event_idx
|
|
ON model_snapshots (canonical_event_id) WHERE canonical_event_id IS NOT NULL;
|
|
-- Publication parity is read per slate.
|
|
CREATE INDEX IF NOT EXISTS model_snapshots_publication_idx
|
|
ON model_snapshots (publication_id) WHERE publication_id IS NOT NULL;
|
|
|
|
-- Ledger linkage to the canonical event, additive and read by nothing.
|
|
ALTER TABLE ledger_entries ADD COLUMN IF NOT EXISTS canonical_event_id text;
|
|
CREATE INDEX IF NOT EXISTS ledger_entries_canonical_event_idx
|
|
ON ledger_entries (canonical_event_id) WHERE canonical_event_id IS NOT NULL;
|
|
|
|
COMMENT ON COLUMN model_snapshots.canonical_event_id IS
|
|
'Namespaced canonical event identity, e.g. mlb:gamepk:824514. MLB only; other sports record UNSUPPORTED_SPORT rather than a guessed id. The legacy game_id is a derived LABEL and is not authoritative -- it cannot separate a doubleheader.';
|
|
COMMENT ON COLUMN model_snapshots.published_at IS
|
|
'When the composed Read became authoritative -- i.e. when the atomic slate write to snapshot:{sport}:latest succeeded. NOT the capture time, and never set by winner selection alone.';
|
|
COMMENT ON COLUMN model_snapshots.publication_id IS
|
|
'The slate whose atomic Redis write committed this row. Publication is slate-level because the write is.';
|