Files
vyndr/supabase/migrations/034_factor_inputs.sql
T
builtbykev f61ec6b391 Read integrity, as-of context, and the shadow matchup resolve (A1-A7)
Seven orders of measurement-first repair. The served grade does not move.

A0/A1 — the unordered page walk returned the right COUNT and the wrong ROWS:
410-617 of 2,490 duplicated with an equal number never returned, while
rows.length matched the server exactly. safePaginate orders on a real unique
key, verifies the tuple at runtime, and THROWS on a query error instead of
treating it as end-of-data. Both hits PROVES are withdrawn: they were drawn
through that reader, and defense_by_direction's distinct-n was likely below
the gate floor all along.

A2/A2b — rolled across every reader: 11 FAIL -> 0. Composite keys pulled from
pg_index (the context tables are dated-composite and had no single unique
column). The unordered helper is deleted, not parked.

A3 — ledgerService and retentionService defaulted the SAME env var to
DIFFERENT versions, so no ledger row ever carried the marker eligibility
requires. One source now. model_snapshots settlement moved onto the cron:
15,484 -> 28,894 settled, repaired-champion 0 -> 7,556.

A4 — hitsFactorContext takes an as-of cutoff. Refusal over reconstruction: no
row at-or-before the date means the factor does not apply, never the nearest
row. Live path unchanged, proven 400/400 on real rows.

A5 — factor_inputs freezes what the factor READ, never the multiplier, so an
audit can recompute and check. It also recorded the finding: the three hits
factors have NEVER fired. prop.opponent and prop.opposing_pitcher are read by
the resolver and written by nothing.

A6/A7 — matchupKeys resolves those keys from the posted lineup plus the
schedule's probable pitchers, and fires the factors into a SHADOW freeze:
248 fires on 308 props, 245 of which would move the grade. The served
forecast is untouched. specs/a8-shadow-factor-gate.md pre-registers the test
that decides whether they ever go live.

Nothing is turned on. CALIBRATION_DEPLOYED stays []. Both verdicts stay
withdrawn. 4,772 tests / 371 suites green, web build exit 0, read-integrity
harness 34/34.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:49:56 -04:00

24 lines
1.3 KiB
SQL

-- Migration 034: model_snapshots.factor_inputs — FREEZE THE FACTOR INPUTS (Fix A5).
--
-- A re-audit that joins context tables to a past grade RECONSTRUCTS what the
-- context probably was. This column carries what the grader ACTUALLY read, so a
-- row becomes independently checkable.
--
-- INPUTS, NOT OUTPUTS. It stores the raw values (spray shares, positional OAA,
-- pitcher hard-hit rate, platoon split counts, handedness) and NOT the resulting
-- multiplier. A stored multiplier can only be compared to itself; stored inputs
-- can be re-run through hitsFactors and checked.
--
-- 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.
--
-- NULLABLE and populated only on `hits` rows with a factor context, so the
-- storage cost is bounded. Adding a nullable column is metadata-only in
-- Postgres — no table rewrite — which matters while this database sits over its
-- free-tier size cap.
ALTER TABLE model_snapshots ADD COLUMN IF NOT EXISTS factor_inputs jsonb;
COMMENT ON COLUMN model_snapshots.factor_inputs IS
'Fix A5: raw inputs the hits factors read at grade time (never the multiplier). Recompute via factorFreeze.recompute().';