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,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