Build step 3 of 8 — Phase EJ (umbrella #281)
Build doc: documentation/plans/evolve-judging-plan.md §7 Step EJ.3
What to build
run_regression_eval rolls back on wins_new < games // 2 + 1 (src/orchestrator/evolve.py L746, L782) — a truly-neutral promotion is destroyed 50% of the time at n=5, and draws/crashes count against the new parent because _count_wins (L450-463) shrinks the decided count while the majority bar stays at 3.
Build src/orchestrator/gate_stats.py:
posterior_prob_worse(wins: int, losses: int) -> float returns P(p < 0.5 | Beta(1+wins, 1+losses)) via the exact identity I_0.5(a, b) = Σ_{k=a}^{a+b-1} C(a+b-1, k)·0.5^(a+b-1) (integer params, math.comb, no scipy / no new deps);
one_sided_rollback(wins_new, wins_prior, *, min_decided=4, threshold=0.85) -> tuple[bool, str] returns (rollback?, reason). At n=5 decided: rollback only on 0-5 (P≈0.984) and 1-4 (P≈0.891); 2-3 (P≈0.656) keeps. Fewer than min_decided decided → keep (fail-open) with an explanatory reason.
Add rule: str = "majority" to run_regression_eval with the early-stop threshold DERIVED from the active rule (the hardcoded stop-at-majority logic at L743-772 is wrong for the one-sided rule and at n≠5). Thread scripts/evolve.py --regression-rule {majority,one-sided} (default majority, byte-identical).
One-sided semantics: rollback iff decided ≥ 4 AND P(p<0.5) ≥ 0.85.
Existing context
gate_stats.py is a new leaf module — single source of truth for both the gate and its tests (.claude/rules/code-quality.md one-source-of-truth). Phase R can extend it later.
run_regression_eval read in full L687-808; needed = games // 2 + 1 (L746), rolled_back = wins_new < needed (L782), early-stop locks once either side hits majority (L743-772).
_count_wins (L450-463) excludes draws/crashes from both tallies → undecided games count against the new parent (the draw/crash-forces-rollback bias EJ.3 fixes by conditioning on decided games).
- Only caller of
run_regression_eval: scripts/evolve.py (1 call site) + tests (grep-confirmed, no other consumers). Default rule="majority" keeps signatures compatible.
Files to modify/create
src/orchestrator/gate_stats.py (new)
src/orchestrator/evolve.py (rule param in run_regression_eval; rule-derived early-stop)
scripts/evolve.py (--regression-rule flag, threaded to run_regression_eval)
tests/test_gate_stats.py (new); tests/test_evolve.py (extend regression cases)
Done when
Exact-value tests for the posterior table (0-5→rollback, 1-4→rollback, 2-3→keep at n=5; correct behavior at n=3/7/9; draw-heavy records keep under the min-decided floor); early-stop fires at rule-correct thresholds in both rules; rule="majority" decisions byte-identical to today across a golden table of all (wins_new, wins_prior) pairs at n=5 and n=9. uv run pytest / mypy src bots --strict / ruff check . clean; test count ≥ baseline.
Flags (recommended)
--reviewers code
Depends on
none
Parallel-safe with
none — strictly sequential (edits scripts/evolve.py argparse+loop shared with sibling EJ steps; plan §4 mandates serial execution). Note: EJ.4 depends on this step.
Produces
src/orchestrator/gate_stats.py; rule param in run_regression_eval; --regression-rule flag; tests/test_gate_stats.py; extended tests/test_evolve.py regression cases
Operator workflow notes (autonomous + UI-bundle + parallel)
- Runs end-to-end via
/build-step; the Done when is the gate.
- No
--ui — pure statistics + backend.
- Serial: shared
scripts/evolve.py; EJ.4 builds on this.
Synced from documentation/plans/evolve-judging-plan.md by /repo-sync at c6099d9
Build step 3 of 8 — Phase EJ (umbrella #281)
Build doc: documentation/plans/evolve-judging-plan.md §7 Step EJ.3
What to build
run_regression_evalrolls back onwins_new < games // 2 + 1(src/orchestrator/evolve.py L746, L782) — a truly-neutral promotion is destroyed 50% of the time at n=5, and draws/crashes count against the new parent because_count_wins(L450-463) shrinks the decided count while the majority bar stays at 3.Build
src/orchestrator/gate_stats.py:posterior_prob_worse(wins: int, losses: int) -> floatreturns P(p < 0.5 | Beta(1+wins, 1+losses)) via the exact identityI_0.5(a, b) = Σ_{k=a}^{a+b-1} C(a+b-1, k)·0.5^(a+b-1)(integer params,math.comb, no scipy / no new deps);one_sided_rollback(wins_new, wins_prior, *, min_decided=4, threshold=0.85) -> tuple[bool, str]returns (rollback?, reason). At n=5 decided: rollback only on 0-5 (P≈0.984) and 1-4 (P≈0.891); 2-3 (P≈0.656) keeps. Fewer thanmin_decideddecided → keep (fail-open) with an explanatory reason.Add
rule: str = "majority"torun_regression_evalwith the early-stop threshold DERIVED from the active rule (the hardcoded stop-at-majority logic at L743-772 is wrong for the one-sided rule and at n≠5). Thread scripts/evolve.py--regression-rule {majority,one-sided}(defaultmajority, byte-identical).One-sided semantics: rollback iff decided ≥ 4 AND P(p<0.5) ≥ 0.85.
Existing context
gate_stats.pyis a new leaf module — single source of truth for both the gate and its tests (.claude/rules/code-quality.mdone-source-of-truth). Phase R can extend it later.run_regression_evalread in full L687-808;needed = games // 2 + 1(L746),rolled_back = wins_new < needed(L782), early-stop locks once either side hits majority (L743-772)._count_wins(L450-463) excludes draws/crashes from both tallies → undecided games count against the new parent (the draw/crash-forces-rollback bias EJ.3 fixes by conditioning on decided games).run_regression_eval: scripts/evolve.py (1 call site) + tests (grep-confirmed, no other consumers). Defaultrule="majority"keeps signatures compatible.Files to modify/create
src/orchestrator/gate_stats.py(new)src/orchestrator/evolve.py(ruleparam inrun_regression_eval; rule-derived early-stop)scripts/evolve.py(--regression-ruleflag, threaded torun_regression_eval)tests/test_gate_stats.py(new);tests/test_evolve.py(extend regression cases)Done when
Exact-value tests for the posterior table (0-5→rollback, 1-4→rollback, 2-3→keep at n=5; correct behavior at n=3/7/9; draw-heavy records keep under the min-decided floor); early-stop fires at rule-correct thresholds in both rules;
rule="majority"decisions byte-identical to today across a golden table of all (wins_new, wins_prior) pairs at n=5 and n=9.uv run pytest/mypy src bots --strict/ruff check .clean; test count ≥ baseline.Flags (recommended)
--reviewers codeDepends on
none
Parallel-safe with
none — strictly sequential (edits
scripts/evolve.pyargparse+loop shared with sibling EJ steps; plan §4 mandates serial execution). Note: EJ.4 depends on this step.Produces
src/orchestrator/gate_stats.py;ruleparam inrun_regression_eval;--regression-ruleflag;tests/test_gate_stats.py; extendedtests/test_evolve.pyregression casesOperator workflow notes (autonomous + UI-bundle + parallel)
/build-step; theDone whenis the gate.--ui— pure statistics + backend.scripts/evolve.py; EJ.4 builds on this.Synced from documentation/plans/evolve-judging-plan.md by /repo-sync at c6099d9