ec815b0e37
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
134 lines
5.5 KiB
Markdown
134 lines
5.5 KiB
Markdown
# 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.11–0.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).
|