Skip to content

audit: VerifierBoost bootstrap gate must beat a constant-mean baseline (#306) - #311

Merged
Birfy merged 2 commits into
mainfrom
fix/issue-306-boost-constant-baseline
Oct 16, 2026
Merged

Birfy merged 2 commits into
mainfrom
fix/issue-306-boost-constant-baseline

Conversation

@Birfy

@Birfy Birfy commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Closes #306

What

VerifierBoost.retrain() activated a curve whenever validation MSE beat the raw verifier's by min_gain, which a pure bias shift satisfies even with zero discriminating power. The gate now also requires error_after <= error_const * (1 - min_gain), where error_const is the validation MSE of the train slice's constant-mean predictor. A failure is recorded as a rejection ("no discrimination beyond a constant ..."), the boost stays inactive and version_tag() stays boost:inactive. A curve with < 2 distinct fitted levels is rejected explicitly as well.

BoostState gains error_const (default inf, so existing constructors keep working) and status() exposes it; existing keys are unchanged. Scope is confined to the gate in retrain (no change to eval_counts/full_eval delegation or the #304 items).

Tests

tests/test_verifier_boost.py: the issue's repro (stays inactive, reason mentions the constant baseline), a control (oracle = 1 iff f > 0.9, scores in [0.8, 1.0] still activates), status/BoostState compatibility, degenerate-curve rejection. Fragment: changelog.d/306-boost-constant-baseline.md. Full local pytest -q passes (one timing-sensitive test_era_algotune sandbox test failed under heavy shared load and passes in isolation).

@Birfy
Birfy enabled auto-merge (squash) October 16, 2026 12:13
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T12:16:36.227993Z 98b8859 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98b885998a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +268 to +269
if beats_raw and not beats_const:
return self._deactivate(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the measured baseline on rejection

When the new constant-baseline gate rejects a candidate, this call routes through _deactivate(), which constructs a state with the default error_const=inf. Consequently, retrain() and status() report that the baseline was not measured even though error_const was just calculated, making the newly exposed field unusable for diagnosing the rejection; preserve the computed baseline in the rejected BoostState.

Useful? React with 👍 / 👎.

@Birfy Birfy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review (head 98b8859): LGTM, approving in spirit. Posted as a comment because GitHub doesn't allow self-approval.

Correctness

  • The fix targets exactly what #306 describes. error_const is the validation MSE of the train-slice mean (ys is train-only), so validation labels don't leak into the baseline. Requiring error_after <= error_const * (1 - min_gain) alongside the raw test is the right gate: a pure bias shift can't beat a constant, but a curve that actually ranks can.
  • Order of checks: a curve that fails the raw test keeps the old reason, and one that passes raw but not constant gets the new "no discrimination beyond a constant" reason. Both rejections go through _deactivate, so _xs/_vals/_fit_hash are cleared and version_tag() stays boost:inactive. That means VerifierWatch no longer withdraws calibration on an uninformative change. 👍
  • The degenerate-curve guard is correct. Worth knowing: a single-level PAV curve is exactly the train mean, so error_after == error_const and the constant gate already rejects it whenever min_gain > 0. The new branch only matters when min_gain == 0 or error_const == 0, which is how the test exercises it. It's a fine belt-and-braces check, and a one-line comment saying so would help future readers.
  • BoostState.error_const = math.inf as a trailing default keeps the old positional constructors and consumers working, and status() only adds a key.

Tests

  • Locally on a fresh clone: tests/test_verifier_boost.py gives 23 passed. With boost.py reverted to main, all 4 new tests fail: the repro activates, and the control/status tests have no error_const. So they genuinely guard the change.
  • The control (oracle = 1 iff f > 0.9) proves the gate didn't just get tighter across the board. Asserting that cheap_eval is untouched on the rejection path is a nice touch.
  • I didn't finish a full local pytest -q run (the shared box is under load, and it was past 50% with all green when I stopped). CI build is green; the 3.9–3.13 tests were still running at review time.

Non-blocking suggestions

  1. Observability on rejection: _deactivate writes error_before/error_after/error_const = inf. So right after the new #306 rejection, status()["error_const"] reads "not measured" even though it was measured (the numbers only survive inside the reason string). The test even codifies "after a rejection the baseline is not measured". Consider letting _deactivate optionally carry the measured errors for the two gate rejections, so dashboards and the audit can tell "too few labels" apart from "measured and lost to a constant".
  2. Docs drift: the retrain() docstring ("activate only if the corrected validation error beats the raw by min_gain") and the module docstring around line 19 still describe a raw-only gate. Please add the constant-mean condition there.
  3. Follow-up idea (out of scope here): the gate is still a single holdout split. With small min_val_labels, both comparisons are noisy. A paired bootstrap or a minimum-val-size rule for the constant comparison could be its own issue.

The changelog fragment changelog.d/306-boost-constant-baseline.md (Fixed) matches the fragment convention. Scope stays within the gate, as the issue asked (no eval_counts/full_eval or #304 changes).

…ion (#306)

When the bootstrap gate rejects because a candidate fails the
constant-mean baseline (or the raw-error / degenerate checks after
those MSEs were computed), keep error_const / validation MSEs /
n_train / n_val on the rejected BoostState instead of _deactivate()'s
default inf, so status() can diagnose the rejection.

@Birfy Birfy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: LGTM (posted as COMMENT; self-APPROVE is often blocked). Reviewed at head dd9d47a.

Closes #306. The bootstrap gate now also requires the fitted curve to beat a constant-mean baseline by min_gain, and rejects degenerate single-level curves. BoostState / status() expose error_const.

Follow-up on this head addresses the Codex P2: when the constant-baseline / bootstrap / degenerate path rejects after MSEs were computed, _deactivate(..., error_const=...) retains the measured diagnostics instead of defaulting to inf ("not measured"). Tests cover pure-bias rejection with finite error_const / error_before / error_after / n_*, informative activation still passing both gates, status key surface, and degenerate rejection.

Fragment changelog.d/306-boost-constant-baseline.md is present; no CHANGELOG.md Unreleased edit.

Merge note: this and #312 both touch agentdescent/audit/boost.py and tests/test_verifier_boost.py — expect a conflict if merged in either order without rebasing the other.

@Birfy
Birfy merged commit 2b11ca0 into main Oct 16, 2026
7 checks passed
@Birfy
Birfy deleted the fix/issue-306-boost-constant-baseline branch October 16, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

audit.VerifierBoost: bootstrap gate activates a curve that is worse than a constant predictor (a pure bias shift passes 'validated')

1 participant