From ec815b0e3730ad44be557e302bffa325914d8d4e Mon Sep 17 00:00:00 2001 From: Kev Date: Sun, 2 Aug 2026 01:15:51 -0400 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc --- specs/MASTER-PLAN.md | 37 ++++++------ specs/matchup-axis.md | 133 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 specs/matchup-axis.md diff --git a/specs/MASTER-PLAN.md b/specs/MASTER-PLAN.md index 8f8dd8e..80d0282 100644 --- a/specs/MASTER-PLAN.md +++ b/specs/MASTER-PLAN.md @@ -29,28 +29,27 @@ stay provisional until re-run** · documented ≠ verified. ## ▶ NEXT EXECUTABLE ORDER -**BUILD THE MATCHUP (PLATOON) AXIS on `challengerProjection` (arch-v1).** -Sourcing verified 2026-08-01 by VALUE, not endpoint presence: +**CONNECT THE STILL-DORMANT LAYERS** — similarity, Bayesian, and the distribution +ladder (`projection/distribution.js`, proj-v1.1, built and unconnected). Same +pattern that just worked three times: audit what is actually firing, repair the +joins, add as a labelled challenger, prove on an axis-filtered holdout. -| input | coverage | source | -|---|---:|---| -| opposing probable starter | **29/30 team-sides** | schedule `hydrate=probablePitcher` | -| pitcher hand (L/R) | **1342/1342** | `pitchHand.code` | -| batter hand (L/R/S) | **1342/1342** | `batSide.code` (L 416 / R 848 / S 78) | +*Blocked on nothing.* The alternative next order is +**`archetype_x_archetype`** — the matchup ladder's upper rung, which needs the +opposing starter classified per game. -**One shared dependency unlocks it:** `/sports/1/players` — a list we already -fetch and cache — carries `currentTeam.id`, `batSide` **and** `pitchHand`. -**SOURCE-LINEUPS-first is NOT needed.** Archetype-level reach on the opposing -starter is also available (the SP resolves to a player id, so the existing -classifier applies) — noted, not built. +### THREE challengers accruing in parallel — do NOT re-run early +Verified firing on a real prod snapshot (293 grades), not inferred: -*Blocked on nothing.* +| axis | coverage | mean \|nudge\| | holdout query | +|---|---:|---:|---| +| **environment** | **84.6%** | 0.057 | *(shares the arch-v1 pattern)* | +| **matchup** (`batter_own_split`) | **82.9%** | 0.015 | `scripts/matchup-axis-holdout.sql` | +| **opportunity** | 30.0% | 0.142 | `scripts/opportunity-axis-holdout.sql` | -### Accruing in parallel — do NOT re-run early -- **`opportunity_drift`** — live, 142 rows/slate, orthogonal (r ≈ 0 vs every - projection input). Verdict n-blocked; `scripts/opportunity-axis-holdout.sql`. -- **`environment`** — resolver fixed and verified (105/120), **first ledger write - is tomorrow's slate** (same-day rows dedupe by design and cannot backfill). +All three orthogonal (r ≈ 0 vs projection, `p_win`, line and each other). Each +promotes ONLY on its own axis-filtered holdout, and ONLY if **reliability AND +resolution** improve. **This is time, not code.** Both promote only on their own axis-filtered holdout, and only if **reliability AND resolution** improve. @@ -76,7 +75,7 @@ AND resolution** improve. | ✅ **calibration** | **DECIDED** — ruler-independent; MLB isotonic **DECIDED**, not provisional | `specs/mlb-recalibration-vs-consensus-ruler.md` | | ✅ **grade cap** | DONE — 25 → 500; board **7 → 365+** graded props | `specs/grade-cap-and-refusal-diagnosis.md` | | ✅ **S59 join invariant** | **ARMED** — root cause was `searchPlayer` returning `team: null` (`currentTeam` has no `name`); fail-safe: drops only on a positive not-in-game | `tests/unit/slateJoinInvariant.test.js` | -| **arch-v1 condition axes** | **env + matchup fired on 0/634 prod rows** — `team` was null on 416/416, so the venue join had no key. **Environment FIXED** (joins on the game; resolves 105/120). **Matchup still dead** — needs opposing SP + both hands, three separate absences | `specs/arch-v1-axis-audit.md` | +| ✅ **arch-v1 condition axes** | **BOTH NOW FIRING** — environment 84.6%, matchup 82.9% (`batter_own_split`). Was: env + matchup on 0/634 prod rows — `team` was null on 416/416, so the venue join had no key. **Environment FIXED** (joins on the game; resolves 105/120). **Matchup still dead** — needs opposing SP + both hands, three separate absences | `specs/arch-v1-axis-audit.md` | | **opportunity_drift** | built, orthogonal (r≈0), live as a challenger on 142 rows/slate | verdict n-blocked; `scripts/opportunity-axis-holdout.sql` | | **opportunity layer** | **NOT BUILT** — `ab_per_game` is display-only; engine1 has no usage factor; MLB batting order unavailable in every wired source | Step 0 stopped before wiring. Recommended instead: an `opportunity_drift` axis on the EXISTING `challengerProjection` (arch-v1). See `specs/connect-opportunity-step0.md` | | **MLB board size** | **7 → 365 graded props** (52×) in 114s | the 25-cap discarded 95.7% of the slate. Raised to 500 on measured cost. Refusals were **43.8% deliberate policy suppression**, not a data gap | diff --git a/specs/matchup-axis.md b/specs/matchup-axis.md new file mode 100644 index 0000000..41aa3d3 --- /dev/null +++ b/specs/matchup-axis.md @@ -0,0 +1,133 @@ +# 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).