Retention: model_snapshots live + base64 SSH key support
RETENTION (Phase 2, priority zero). History starts compounding tonight. migration 025 model_snapshots — APPLIED to prod. Append-only, one row per graded prop PER SIDE PER CYCLE, with a unique index on (snapshot_id, player_key, stat, line, side) so a retried cycle cannot duplicate. RLS on, service-role writes only. What it captures that the ledger never did: - features jsonb — the model's INPUTS. Without these a backtest can only grade our own homework; with them any future model can be replayed against the exact conditions this one faced. - REFUSALS (refused + refusal_reason). The ledger drops them, so a gate refusing props that would have WON is invisible — unmeasurable lost edge. Captured via a new onGraded hook in gradeSlateService that fires with BOTH sides before any filtering. - grade_11, the pre-collapse grade. The 4-letter map throws away the entire live C-/C/C+/B- range. - model_version + code_sha on every row. ledger_entries mixes pre/post-fix grades with no marker and cannot be separated retroactively. - p_win / ev_pct / fair_odds / takeable / value — none of which any permanent store held. Wiring: analyzeViaEngine1 attaches _features/_grade_11 (underscore = internal); gradeSlateService fires onGraded then STRIPS them so they never reach a cache or API payload; snapshotService builds rows and persists best-effort. Retention reuses the LEDGER's dateET/gameIdFor helpers so rows share the ledger's natural key exactly — otherwise the settle pass could never join outcomes onto them. Rows are written BEFORE the empty- slate early return: a slate that refused everything is exactly the case worth recording. CONTRACT HELD: retention is injectable and every path is caught. persist() returns errors, never throws; a missing Supabase client is SKIPPED, not an error. A retention failure can never break a snapshot. BACKUP: backup-db.sh now accepts BACKUP_SSH_KEY as base64 (recommended — survives env-var newline mangling, which is how injected SSH keys usually break silently) OR raw PEM, detected by decoding and looking for the PEM header. Verified both forms detect correctly against a real generated key. Suite 279/3325 green, build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
-- 025 — MODEL SNAPSHOTS (retention store). Phase 2, priority zero.
|
||||
--
|
||||
-- Why this exists: as of 2026-07-20 the ONLY store of model history was
|
||||
-- ledger_entries (640 rows, 6 game days) and it holds no model INPUTS. Every
|
||||
-- other warehouse table in the schema is empty. That means we could score the
|
||||
-- grades we emitted but could NOT replay a different model against the same
|
||||
-- conditions — which is the only question a backtest exists to answer.
|
||||
--
|
||||
-- This table captures inputs AND outputs at lock time, immutably, so history
|
||||
-- starts compounding tonight.
|
||||
--
|
||||
-- Append-only: ONE ROW PER GRADED PROP PER SNAPSHOT CYCLE. A re-grade at the
|
||||
-- next cycle writes a NEW row on purpose — that is what lets us ask whether the
|
||||
-- 14:00 read or the 22:00 read was sharper.
|
||||
|
||||
create table if not exists public.model_snapshots (
|
||||
id bigserial primary key,
|
||||
|
||||
-- provenance — never mix model eras (ledger_entries already has this
|
||||
-- contamination with no marker; everything from here is stamped)
|
||||
snapshot_id uuid not null,
|
||||
captured_at timestamptz not null,
|
||||
cycle_hour_utc smallint,
|
||||
model_version text not null,
|
||||
code_sha text,
|
||||
|
||||
-- identity (mirrors the ledger natural key so outcomes can be stamped)
|
||||
sport text not null,
|
||||
game_id text not null,
|
||||
game_date date not null,
|
||||
player_key text not null,
|
||||
player_name text not null,
|
||||
team text,
|
||||
opponent text,
|
||||
stat text not null,
|
||||
line numeric not null,
|
||||
side text not null,
|
||||
|
||||
-- MARKET — real book numbers captured at a timestamp, never model output
|
||||
book text,
|
||||
book_odds integer,
|
||||
over_odds integer,
|
||||
under_odds integer,
|
||||
fair_odds integer,
|
||||
fair_prob numeric,
|
||||
overround numeric,
|
||||
devig_method text,
|
||||
|
||||
-- MODEL OUTPUT
|
||||
grade text,
|
||||
grade_11 text,
|
||||
confidence numeric,
|
||||
confidence_basis text,
|
||||
p_win numeric,
|
||||
ev_pct numeric,
|
||||
projection numeric,
|
||||
edge_pct numeric,
|
||||
takeable boolean,
|
||||
value boolean,
|
||||
archetype text,
|
||||
|
||||
-- REFUSALS ARE TRAINING DATA. The ledger drops them entirely, so "was the
|
||||
-- gate right to refuse this?" is currently unanswerable. A too-aggressive
|
||||
-- gate costs real edge and is invisible without these rows.
|
||||
refused boolean not null default false,
|
||||
refusal_reason text,
|
||||
|
||||
-- THE COUNTERFACTUAL ENABLER. Schema-free so a new feature never needs a
|
||||
-- migration. Nulled after 90 days by the retention sweep; scalars stay forever.
|
||||
features jsonb,
|
||||
|
||||
-- OUTCOME — stamped later by the settle pass, per cycle row
|
||||
outcome text,
|
||||
actual_value numeric,
|
||||
settled_at timestamptz,
|
||||
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- One row per prop per cycle; a retry of the same cycle must not duplicate.
|
||||
create unique index if not exists model_snapshots_cycle_prop_uniq
|
||||
on public.model_snapshots (snapshot_id, player_key, stat, line, side);
|
||||
|
||||
create index if not exists model_snapshots_date_sport_idx
|
||||
on public.model_snapshots (game_date, sport);
|
||||
create index if not exists model_snapshots_player_idx
|
||||
on public.model_snapshots (player_key, stat, game_date);
|
||||
create index if not exists model_snapshots_version_idx
|
||||
on public.model_snapshots (model_version, game_date);
|
||||
-- Settle pass looks up unsettled rows by natural key.
|
||||
create index if not exists model_snapshots_settle_idx
|
||||
on public.model_snapshots (game_date, player_key, stat, line, side)
|
||||
where outcome is null;
|
||||
|
||||
comment on table public.model_snapshots is
|
||||
'Append-only model history: inputs (features) + outputs per graded prop per snapshot cycle. Feeds the backtest harness, calibration, and metric validation. Features nulled after 90 days; scalars kept forever.';
|
||||
comment on column public.model_snapshots.features is
|
||||
'Full feature vector at lock time. THE counterfactual enabler — without it a backtest can only grade our own homework. Nulled after 90 days by the retention sweep.';
|
||||
comment on column public.model_snapshots.refused is
|
||||
'True when the engine refused to grade. Refusals are training data: the only way to detect a gate that is refusing props that would have won.';
|
||||
comment on column public.model_snapshots.model_version is
|
||||
'Which engine produced this row. A backtest that mixes model eras is worthless. ledger_entries lacks this and is permanently contaminated across the 2026-07-19 fix boundary.';
|
||||
|
||||
-- RLS: service-role writes only, same posture as ledger_entries. This is
|
||||
-- internal model telemetry — no client reads it.
|
||||
alter table public.model_snapshots enable row level security;
|
||||
Reference in New Issue
Block a user