Skip to content

Phase EJ Step 3: One-sided posterior rollback bar #284

Description

@aberson

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions