docs/DESIGN-qu100-ab-testing.md (§4.2, §10.3, §11.2) — approved 2026-07-03The A/B substrate must answer "is the challenger better?" — but better by what objective? Total return, Sharpe, dodged-loss, and drawdown give different winners. Today research/rewards/ is a stub: register(name, fn) with an empty REGISTRY and an implicit TradeRecord → float shape. There is no way to declare an objective's role (the thing being optimized vs a guardrail that blocks promotion), no input typing (a trade-series reward applied to a screener basket would silently compute nonsense), and no screener-shaped rewards at all.
@register("sharpe", input_type="basket", role="primary", direction="increase")
def sharpe(outcomes: BasketOutcomes) -> float: ...
REGISTRY ── keyed by name ──► {fn, input_type, role, direction, threshold}
score(candidate_type, reward_name) → refuses input_type mismatch
register becomes a decorator factory taking input_type (basket | trades), role (primary | guardrail | secondary), direction, and (guardrails) threshold. The as-built TradeRecord → float becomes the trades input case — existing signature is a stub with no callers, so this is safe.BasketOutcomes (frozen dataclass, pinned in design §10.3): days: list[BasketDay]; BasketDay = {date, symbols (rank order), fwd_return: {H → {symbol → float|None}}, regime}. None near window end, never 0.total_return, sharpe, hit_rate, max_drawdown (guardrail), turnover (guardrail), dodged_loss. Ratio rewards compose two aggregations.input_type — a loud ValueError, no silent mis-scoring.value vs threshold+direction); the promote-time gate consuming it is ab-registry-promote-84a7's scope.src/rainier/research/rewards/__init__.py — decorator factory + registry + refusal logic.src/rainier/research/rewards/basket.py — BasketOutcomes/BasketDay + the screener reward set.tests/research/test_rewards.py.basket reward → present in registry, selectable by name, metadata (role/direction) intact.trades reward scoring a basket candidate → ValueError (and vice versa).fwd_return=None entries excluded from aggregation, never treated as 0.BasketOutcomes → identical value across two calls.BasketOutcomes by hand.trades reward implementations beyond keeping the input-type seam working.Per the Subagent Dispatch Contract (~/.claude/CLAUDE.md): unit tests with the feature; codex + /review loops to clean; PR against main; PR URL in the return message.