Files
vyndr/specs/matchup-axis.md
builtbykev ec815b0e37 Matchup axis report + plan reconciled: three challengers now accruing
Records the verification that matters: firing measured on a real prod
snapshot rather than inferred. environment 248/293 (84.6%) -- also its
FIRST confirmed ledger write, which the previous session could only infer
-- and matchup 243/293 (82.9%) on tier batter_own_split. Both were 0/634.

Collinearity guard passed at n=243: r = -0.003 vs the projection, +0.074 vs
p_win, +0.003 vs line, -0.068 vs environment, -0.150 vs opportunity. The
axis is not re-encoding recent form. The nudge distribution is also the
right SHAPE -- mean +0.0007, 123 positive / 120 negative -- a balanced
two-sided signal; a one-sided distribution would have suggested a sign or
baseline error.

Plan reconciled in place: arch-v1 condition axes marked firing, three
challengers listed with coverage and their own holdout queries, and the
next-order pointer moved to connecting the still-dormant layers
(similarity, Bayesian, distribution ladder) with archetype_x_archetype as
the named alternative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-08-02 01:15:51 -04:00

5.5 KiB
Raw Permalink Blame History

MATCHUP / PLATOON AXIS — BUILT, FIRING ON PROD, VERDICT PENDING

Date: 2026-08-02 · ledger-only · champion p_win, ranking, calibration, the armed invariant and the two accruing verdicts untouched.

Gates: 4,093 tests / 328 suites green · next build exit 0 · firing verified on a real prod snapshot · collinearity guard passed.


THE HEADLINE: TWO DEAD AXES ARE NOW LIVE

Verified on an induced prod snapshot (293 grades), not on tests:

axis tier rows % of slate mean |nudge|
environment env 248 84.6% 0.0574
matchup batter_own_split 243 82.9% 0.0152
opportunity opportunity 88 30.0% 0.1419
(no axis at all) 32 10.9%

Both were 0/634 before. This is also the first confirmed ledger write of the environment axis, which the previous session could only infer.


WHAT WAS ACTUALLY BROKEN — three separate absences

The axis was already wired. It fired on nothing because every one of its three required joins was empty:

input was fix
oppPitcherByTeam 0 the self-origin /api/schedule/mlb/pitchers route returned nothing in prod → added the statsapi probable-pitcher hydrate as a fallback, mirroring the one the schedule step already uses. 29/30 team-sides, one free request
handById 0 follows from the above — the batched people call now has ids to ask about
bats 0/120 batter hand rode only on statcast aggregate rows, which don't cover the slate. The season player list we already fetch and cache carries batSide on 1342/1342 — a join, not a fetch

Switch-hitters (S, 78 of 1342) are preserved as-is; platoonSplits decides what to do with them, not the join.


THE FALLBACK LADDER — and a deliberate deviation

Shipped: batter_own_split — the hitter's OWN vs-L/vs-R line, regressed toward his own overall rate. Labelled on every adjustment.

league_generic is deliberately NOT implemented

platoonSplits already handles thin evidence by regressing toward the hitter's own rate — its own doc-comment argues that a hitter with no evidence of a split correctly gets no adjustment. A league split applied to that hitter models the league, not the player — the exact doctrine breach the order names in the same step it asks for the tier.

Adding it would have produced more firing rows and a weaker signal. Coverage is already 82.9% without it.

archetype_x_archetype is scoped, not built

It needs the opposing starter classified per game — real work, and its own order. The tier vocabulary is in place for it.

Neither absent tier is faked. No adjustment ever claims a rung it did not use.


COLLINEARITY GUARD — PASSED (n=243)

The ledger doesn't store l5/l20, so the guard uses the quantities they drive and that are stored:

correlation r
matchup nudge vs projection (model_value) 0.003
matchup nudge vs p_win +0.074
matchup nudge vs line +0.003
matchup nudge vs environment nudge 0.068
matchup nudge vs opportunity nudge 0.150

Orthogonal to recent form and to every other axis. It is not re-encoding something the projection already knows.

And the shape is right: mean nudge +0.0007, sd 0.0202, 123 positive / 120 negative. A balanced two-sided signal with no systematic tilt — which is what a platoon read should look like. A one-sided distribution would have suggested a sign or baseline error.

The mean |nudge| of 0.0152 is appropriately small: a platoon split is a real but modest effect, and it cannot outvote a measured archetype trait (0.110.22).


HOLDOUT — COMMITTED, VERDICT n-BLOCKED

scripts/matchup-axis-holdout.sql, filtered to matchup-carrying rows only — including untouched rows would dilute with rows where challenger ≡ champion by construction, biasing toward a false positive.

It keeps matchup's OWN nudge visible, not just the combined challenger. arch-v1 now composes four axes into one p_win_challenger, so a combined-only view cannot tell which axis earned the movement — or which one is dragging. The query pulls matchup_nudge out of the adjustments array and correlates it with the outcome directly, so the axis can be shelved alone.

Three challengers now accrue in parallel on one harness:

axis coverage verdict
opportunity 30.0% n-blocked — needs settled rows
environment 84.6% n-blocked — first writes confirmed today
matchup 82.9% n-blocked — first writes today

Each promotes only on its own axis-filtered holdout, and only if reliability AND resolution both improve.


WHAT IS UNTOUCHED

Champion p_win, the live grade path, ranking, calibration, the armed S59 invariant, and the two previously-accruing verdicts. All four axes write only to p_win_challenger / challenger_adjustments.

TAGS

VERIFIED: matchup fires 243/293 (82.9%) and environment 248/293 (84.6%) on a real prod snapshot · three joins repaired (SP 29/30, pitcher hand, batter hand 1342/1342) · collinearity r ≈ 0 against projection, p_win, line and both other axes · nudge distribution balanced 123/120.

NOT BUILT, BY DECISION: league_generic (models the league, not the player). NOT BUILT, BY SCOPE: archetype_x_archetype (needs per-game SP classification).