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
90 lines
5.8 KiB
SQL
90 lines
5.8 KiB
SQL
-- Migration 045: append-only Read lineage (ADDITIVE ONLY).
|
|
--
|
|
-- Every statement is ADD COLUMN IF NOT EXISTS or CREATE INDEX. No column is
|
|
-- dropped, renamed, retyped or backfilled. No historical row is rewritten.
|
|
--
|
|
-- ── WHY THIS IS COLUMNS AND NOT A NEW CLAIM TABLE ──────────────────────────
|
|
-- `model_snapshots` is ALREADY an append-only store of published claims: it is
|
|
-- upserted with ignoreDuplicates on (snapshot_id, player_key, stat, line, side),
|
|
-- so a row is written once per snapshot cycle and never overwritten, and
|
|
-- `captured_at` is NOT NULL. Its only post-insert write is settlement
|
|
-- (outcome / actual_value / settled_at), which is not part of the claim.
|
|
--
|
|
-- MEASURED (mlb, 2026-08-26): 5,376 props, 4,286 with more than one row, and
|
|
-- 183 whose GRADE CHANGED across those rows. The immutable claims exist. What
|
|
-- was missing is the lineage over them, which is what these columns are.
|
|
--
|
|
-- Building a separate revision table would have duplicated a claim that is
|
|
-- already immutable, and given VYNDR two stores that can disagree.
|
|
--
|
|
-- ── THE ROW'S OWN id IS THE REVISION IDENTITY ─────────────────────────────
|
|
-- `model_snapshots.id` is a bigserial primary key on an append-only row. It is
|
|
-- already immutable and already unique, so a parallel text `revision_id` would
|
|
-- be a second spelling of the same fact. `supersedes_id` and `recaptures_id`
|
|
-- point at it directly.
|
|
--
|
|
-- ── CAPTURE IS NOT REVISION ───────────────────────────────────────────────
|
|
-- Most cycles re-observe an unchanged claim. `claim_digest` (sha256 over the
|
|
-- market terms plus gradeFreeze.SERVED_FIELDS) is what separates a genuine new
|
|
-- published state from a re-observation, so the chronology reports the ~183
|
|
-- revisions that happened rather than the ~13,000 captures that were written.
|
|
|
|
-- The logical Read: stable across cycles, and deliberately NOT derived from the
|
|
-- game_id string. Measured over 30,746 identity groups, 1,328 carried more than
|
|
-- one game_id spelling for the same real game while only 18 carried genuinely
|
|
-- different team pairs. Identity that included the raw spelling would have
|
|
-- fractured 1,310 Reads that are one Read.
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS read_id text;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS read_natural_key text;
|
|
|
|
-- The published state.
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS claim_digest text;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS revision_ordinal integer;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS supersedes_id bigint;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS recaptures_id bigint;
|
|
|
|
-- ORIGIN | REVISION | RECAPTURE. A row says which it is rather than leaving a
|
|
-- reader to infer it from ordinals.
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS lineage_action text;
|
|
-- LIVE | LEGACY_UNVERIFIED. Rows written before this migration keep NULL, which
|
|
-- is the honest value: their position in a chronology is unknown and is not
|
|
-- reconstructed.
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS lineage_state text;
|
|
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS lineage_version text;
|
|
|
|
-- Walking one Read's chronology in order.
|
|
CREATE INDEX IF NOT EXISTS model_snapshots_lineage_idx
|
|
ON model_snapshots (read_id, revision_ordinal) WHERE read_id IS NOT NULL;
|
|
-- Resolving a candidate's family on write.
|
|
CREATE INDEX IF NOT EXISTS model_snapshots_natural_key_idx
|
|
ON model_snapshots (read_natural_key) WHERE read_natural_key IS NOT NULL;
|
|
-- FORK PREVENTION, not merely fork detection.
|
|
--
|
|
-- Only ONE row may supersede a given parent. Two workers that each read the
|
|
-- same standing revision before either wrote cannot see one another, so no
|
|
-- in-memory check can catch that race — the second insert must fail, and this
|
|
-- is what makes it fail. Without it the chain could silently become
|
|
-- R1 -> {R2a, R2b} and no reader could say which claim VYNDR actually published.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS model_snapshots_supersedes_unique
|
|
ON model_snapshots (supersedes_id) WHERE supersedes_id IS NOT NULL;
|
|
|
|
-- ── LEDGER LINKAGE (SHADOW) ───────────────────────────────────────────────
|
|
-- A receipt should be able to name the EXACT revision it settles. These are
|
|
-- written in shadow alongside the existing settlement path and are read by
|
|
-- nothing; current settlement outcomes are unchanged. NULL on every existing
|
|
-- row, and no historical receipt is re-pointed.
|
|
ALTER TABLE ledger_entries ADD COLUMN IF NOT EXISTS read_id text;
|
|
ALTER TABLE ledger_entries ADD COLUMN IF NOT EXISTS read_revision_id bigint;
|
|
|
|
CREATE INDEX IF NOT EXISTS ledger_entries_read_id_idx
|
|
ON ledger_entries (read_id) WHERE read_id IS NOT NULL;
|
|
|
|
COMMENT ON COLUMN model_snapshots.read_id IS
|
|
'The stable identity of a logical Read across every published revision. Deliberately NOT derived from game_id: measured, 1,328 of 30,746 identity groups carry more than one game_id spelling for the same real game.';
|
|
COMMENT ON COLUMN model_snapshots.claim_digest IS
|
|
'sha256 over the market terms (line, side, book, locked_odds) plus gradeFreeze.SERVED_FIELDS. Separates a genuine new published claim from a re-observation of the standing one.';
|
|
COMMENT ON COLUMN model_snapshots.lineage_action IS
|
|
'ORIGIN | REVISION | RECAPTURE. Most cycles re-observe an unchanged claim; only a changed claim_digest advances the chain.';
|
|
COMMENT ON COLUMN ledger_entries.read_revision_id IS
|
|
'SHADOW: the exact model_snapshots row this receipt settles. Read by nothing; settlement outcomes are unchanged by migration 045.';
|