-- matchup-axis-holdout.sql — per-axis proof, RUN WHEN n IS ADEQUATE.
--
-- Filtered to MATCHUP-CARRYING rows only. Including untouched rows would
-- dilute the comparison with rows where challenger === champion BY
-- CONSTRUCTION, biasing toward a false positive (the trap opportunity_drift
-- established).
--
-- MATCHUP'S OWN CONTRIBUTION IS KEPT VISIBLE, not just the combined challenger:
-- arch-v1 composes archetype + environment + opportunity + matchup into ONE
-- p_win_challenger, so a combined-only view cannot tell which axis earned the
-- movement. `matchup_nudge` is pulled out of the adjustments array so the axis
-- can be judged on its own terms and, if it is the one dragging, shelved alone.
--
-- BOTH reliability AND resolution must improve for the axis to promote.

with rows_ as (
  select
    l.game_date, l.id,
    l.p_win::numeric            champ,
    l.p_win_challenger::numeric chal,
    (l.outcome='hit')::int      won,
    (select (a->>'nudge')::numeric
       from jsonb_array_elements(l.challenger_adjustments) a
      where a->>'axis' = 'matchup' limit 1)  matchup_nudge,
    (select a->>'tier'
       from jsonb_array_elements(l.challenger_adjustments) a
      where a->>'axis' = 'matchup' limit 1)  matchup_tier
  from public.ledger_entries l
  where l.sport='mlb' and l.user_id is null
    and l.outcome in ('hit','miss')
    and l.p_win is not null and l.p_win_challenger is not null
    and l.challenger_adjustments::text like '%matchup%'
),
split as (
  select *, case when ntile(2) over (order by game_date, id) = 1 then 'train' else 'holdout' end split
  from rows_
),
b_champ as (select split, width_bucket(champ,0,1,10) bkt, count(*) n, avg(champ) pred, avg(won::numeric) actual from split group by 1,2),
b_chal  as (select split, width_bucket(chal ,0,1,10) bkt, count(*) n, avg(chal ) pred, avg(won::numeric) actual from split group by 1,2)
select
  s.split,
  count(*) n,
  count(distinct s.matchup_tier) tiers,
  round(avg(abs(s.matchup_nudge))::numeric,4) mean_abs_matchup_nudge,
  round((select sum(n*abs(pred-actual))/nullif(sum(n),0) from b_champ c where c.split=s.split),4) reliability_champion,
  round((select sum(n*abs(pred-actual))/nullif(sum(n),0) from b_chal  c where c.split=s.split),4) reliability_challenger,
  round(corr(s.champ, s.won::numeric)::numeric,4) resolution_champion,
  round(corr(s.chal , s.won::numeric)::numeric,4) resolution_challenger,
  -- does the matchup nudge ITSELF point the right way?
  round(corr(s.matchup_nudge, s.won::numeric)::numeric,4) matchup_nudge_vs_outcome,
  round(avg(s.won::numeric),4) base_rate
from split s group by s.split order by s.split desc;
