checkpoint: chain shadow, WNBA possession feed, baseball chain
Backup commit of uncommitted working-tree state found during Legion recon (Tony resurrection, STEP 0). This work existed only on the laptop disk. - chain shadow accrual + probe script (038_chain_shadow.sql) - WNBA possession feed: ESPN adapter, usage service, verify script (039_wnba_player_game.sql) - baseball chain - retention/snapshot service updates, tableKeys, matchupKeys - specs: chain-v1, wnba-possession-feed, wnba-source-survey - unit tests for the above Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnvJAkC3h5QGmb6dipoiWn
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
-- Migration 038: model_snapshots.chain_shadow — the SHADOW CHAIN read.
|
||||
--
|
||||
-- The chain (src/services/model/chain.js) computes a probability by chaining
|
||||
-- base-event atoms: p per plate appearance from the PA outcome tree, chained
|
||||
-- over expected plate appearances. It is SERVED BY NOTHING. The counter
|
||||
-- (probabilityEstimator) remains the only thing a user ever sees.
|
||||
--
|
||||
-- WHY A COLUMN AT ALL. Evidence you cannot adjudicate is not evidence. A chain
|
||||
-- probability stored on its own could only ever be compared to itself. This
|
||||
-- column stores the chain's number NEXT TO the number it must beat, on a row
|
||||
-- that already carries the result:
|
||||
--
|
||||
-- chain_shadow.chain_p the chain's probability, SIDE-ALIGNED
|
||||
-- chain_shadow.counter_p the served counter's p_win for that same side
|
||||
-- model_snapshots.outcome written by the ordinary settle pass
|
||||
--
|
||||
-- Those three make the triple. Without all three on one row, the chain could be
|
||||
-- described but never judged.
|
||||
--
|
||||
-- SIDE ALIGNMENT IS LOAD-BEARING. The chain computes P(over the line); p_win is
|
||||
-- expressed for the graded side. An under row stores 1 - p_over. Storing the raw
|
||||
-- over-probability against an under row would invert every later comparison,
|
||||
-- silently.
|
||||
--
|
||||
-- IT IS LABELLED UN-SERVABLE IN THE DATA, not only in a comment: every block
|
||||
-- carries status 'UN-SERVABLE' and servable=false, because it was produced with
|
||||
-- requireCalibrated=false and the chain has never passed a calibration gate.
|
||||
--
|
||||
-- A SEPARATE COLUMN, not extra keys inside `features` — champion-ablation.js
|
||||
-- iterates every key of `features` for its residual scan, so widening it would
|
||||
-- silently enlarge that multiple-comparisons denominator. Same reasoning as 034.
|
||||
--
|
||||
-- NULLABLE and populated only on MLB `hits` rows the chain could read, so the
|
||||
-- storage cost is bounded. Adding a nullable column is metadata-only in
|
||||
-- Postgres — no table rewrite — which matters while this database sits near its
|
||||
-- free-tier size cap.
|
||||
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS chain_shadow jsonb;
|
||||
|
||||
COMMENT ON COLUMN model_snapshots.chain_shadow IS
|
||||
'Chain v1 SHADOW: side-aligned chain_p + the served counter_p, forming the (chain_p, counter_p, outcome) triple with model_snapshots.outcome. UN-SERVABLE (requireCalibrated=false); read by no serving path.';
|
||||
@@ -0,0 +1,106 @@
|
||||
-- Migration 039: wnba_player_game — the WNBA possession / usage / minutes feed.
|
||||
--
|
||||
-- The chain's basketball chainFn is `usage x possessions x efficiency`. None of
|
||||
-- those three existed for WNBA anywhere in this codebase; this is the feed that
|
||||
-- makes the slot fillable. It is scoped to exactly those inputs plus the
|
||||
-- game-state the `redistribute` hook reads — NOT a general WNBA stats dump.
|
||||
-- (closing_captures grew to 4.2M rows of something nothing read; the lesson is
|
||||
-- to ingest for a named consumer or not at all.)
|
||||
--
|
||||
-- ── POINT-IN-TIME IS STRUCTURAL, NOT A LATER RETROFIT ──────────────────────
|
||||
-- `statcast_aggregates` stores a SEASON AGGREGATE upserted in place, so every
|
||||
-- prior version is destroyed and a point-in-time question is unanswerable from
|
||||
-- it — which is why `statcast_history` had to be built afterwards, and why the
|
||||
-- skill backtest was honest only by accident.
|
||||
--
|
||||
-- This stores PER GAME rows. A completed box score never changes, so an as-of
|
||||
-- profile is `WHERE game_date < asOf` — a filter over immutable facts, not a
|
||||
-- dated snapshot of a mutable aggregate. There is nothing to overwrite, so there
|
||||
-- is nothing to retain a history OF.
|
||||
--
|
||||
-- STRICTLY `<`, never `<=`: a game played ON the as-of date may have tipped
|
||||
-- after grade time, and counting it would leak the evening being predicted into
|
||||
-- the prediction. Same rule as isPreGame in snapshotSettlementService.
|
||||
--
|
||||
-- ── THE KEY ────────────────────────────────────────────────────────────────
|
||||
-- (game_id, source_id) is unique: one row per player per game. `game_date` and
|
||||
-- `sport` lead the index because every read is date-bounded and single-sport,
|
||||
-- and safePaginate orders on the full tuple. Registered in src/utils/tableKeys.js
|
||||
-- — a stale entry there surfaces as a runtime duplicate-tuple throw.
|
||||
--
|
||||
-- ── DERIVED vs SERVED ──────────────────────────────────────────────────────
|
||||
-- ESPN returns the COMPONENTS (minutes, FGA, FTA, TOV, OREB and team totals),
|
||||
-- not the rates. `usage_rate`, `team_possessions`, `team_pace`, `ts_pct` and
|
||||
-- `efg_pct` are computed here from the standard box-score identities and stored
|
||||
-- so a chainFn read is one query. Every component is stored ALONGSIDE its
|
||||
-- derived value, so each rate is re-derivable and checkable rather than an
|
||||
-- unfalsifiable number (the factorFreeze rule: store inputs, not just outputs).
|
||||
CREATE TABLE IF NOT EXISTS wnba_player_game (
|
||||
sport text NOT NULL DEFAULT 'wnba',
|
||||
season integer,
|
||||
game_id text NOT NULL,
|
||||
game_date date NOT NULL,
|
||||
source_id text NOT NULL,
|
||||
player_key text NOT NULL,
|
||||
player_name text,
|
||||
team text,
|
||||
opponent text,
|
||||
home_away text,
|
||||
starter boolean,
|
||||
|
||||
-- raw box components (the usage/possession inputs)
|
||||
minutes numeric,
|
||||
points integer,
|
||||
fgm integer,
|
||||
fga integer,
|
||||
fg3m integer,
|
||||
fg3a integer,
|
||||
ftm integer,
|
||||
fta integer,
|
||||
reb integer,
|
||||
oreb integer,
|
||||
dreb integer,
|
||||
ast integer,
|
||||
tov integer,
|
||||
stl integer,
|
||||
blk integer,
|
||||
pf integer,
|
||||
plus_minus integer,
|
||||
|
||||
-- derived, per the identities in espnWnbaAdapter
|
||||
usage_rate numeric,
|
||||
ts_pct numeric,
|
||||
efg_pct numeric,
|
||||
|
||||
-- team possession context, denormalised so a chainFn read is ONE query
|
||||
team_minutes numeric,
|
||||
team_fga integer,
|
||||
team_fta integer,
|
||||
team_tov integer,
|
||||
team_oreb integer,
|
||||
team_possessions numeric,
|
||||
team_pace numeric,
|
||||
|
||||
-- game state — what the archetype `redistribute` hook reads (live in
|
||||
-- basketball: a blowout fades the star and feeds the bench)
|
||||
team_score integer,
|
||||
opp_score integer,
|
||||
final_margin integer,
|
||||
|
||||
ingested_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (game_id, source_id)
|
||||
);
|
||||
|
||||
-- Every read is "this player, before this date" or "this slate's date range".
|
||||
CREATE INDEX IF NOT EXISTS wnba_player_game_asof_idx
|
||||
ON wnba_player_game (sport, player_key, game_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS wnba_player_game_date_idx
|
||||
ON wnba_player_game (sport, game_date);
|
||||
|
||||
COMMENT ON TABLE wnba_player_game IS
|
||||
'WNBA per-player-per-game possession/usage/minutes feed for the chain basketball chainFn. PER-GAME grain makes point-in-time native: an as-of profile is WHERE game_date < asOf (strictly), over immutable completed box scores. Scoped to chainFn inputs + redistribute game-state.';
|
||||
COMMENT ON COLUMN wnba_player_game.usage_rate IS
|
||||
'DERIVED (not served by ESPN): 100*((FGA+0.44*FTA+TOV)*(TmMIN/5))/(MIN*(TmFGA+0.44*TmFTA+TmTOV)). Components stored alongside so it is re-derivable.';
|
||||
COMMENT ON COLUMN wnba_player_game.team_possessions IS
|
||||
'DERIVED: FGA - OREB + TOV + 0.44*FTA. The 0.44 free-throw-trip coefficient is the only estimated term.';
|
||||
Reference in New Issue
Block a user