Skip to content

refactor(research): extract shared UCT kernel - #190

Merged
proerror77 merged 1 commit into
mainfrom
codex/unified-mcts-kernel-185
Jul 22, 2026
Merged

refactor(research): extract shared UCT kernel#190
proerror77 merged 1 commit into
mainfrom
codex/unified-mcts-kernel-185

Conversation

@proerror77

@proerror77 proerror77 commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Change contract

Extract deterministic UCT tree mechanics from continuous Formula MCTS into a domain-neutral research-core kernel while preserving Formula candidate generation, seeded selection, checkpoint wire format, and evaluation behavior.

Out of scope

Polymarket candidate/evaluator logic, LLM proposal policy, collector or snapshot changes, cloud execution, result publication, and checkpoint deletion.

Dependency or merge order

Parent PRD: #184. This PR implements #185 and is the stack base for #186 (Polymarket adapter). Merge order is #185 -> #186 -> #187 -> #188 -> #189. This layer compiles and remains fail-closed on its own.

Focused validation

  • cargo test --locked -p hft-search-kernel (5 passed)
  • cargo clippy --locked -p hft-search-kernel --all-targets -- -D warnings
  • cargo test --locked -p alpha-engine engines::mcts::tests --lib (8 passed)
  • cargo check --locked -p alpha-engine --no-default-features --lib
  • git diff --check
  • Fixed-seed golden covers propose -> observe -> checkpoint JSON -> restore -> same trace and next Formula candidate.
  • Counterexamples cover signed-zero UCT ordering, expandable cycles, checkpoint cycles, and reward overflow without partial mutation.

Rollout / rollback impact

No deployment or schema cutover. Reverting this PR restores Formula-local UCT mechanics. The existing checkpoint JSON shape and version remain unchanged.

Closes #185

Summary by CodeRabbit

  • New Features

    • Added a shared, deterministic search engine for tree validation, candidate selection, and reward propagation.
    • Improved checkpoint validation, including cycle and maximum-depth checks.
    • Added deterministic checkpoint round-trip behavior for consistent future proposals.
  • Documentation

    • Updated architecture documentation to describe the shared search component and engine responsibilities.
  • Bug Fixes

    • Improved handling of invalid search histories and prevented partial updates when validation fails.

@proerror77 proerror77 added the ready-for-agent Fully specified and safe for an autonomous agent label Jul 22, 2026
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new hft-search-kernel crate centralizes deterministic UCT mechanics, validation, statistics, selection, and backpropagation. The Formula MCTS engine adapts its nodes to the kernel and routes proposal, observation, restoration, and checkpoint validation through it.

Changes

UCT Kernel Extraction

Layer / File(s) Summary
Kernel contracts and UCT mechanics
rust_hft/Cargo.toml, rust_hft/research-core/search-kernel/*
The workspace adds the new crate, which defines deterministic RNG, UCT statistics, the UctNode contract, validation errors, tree validation, UCT selection, reward backpropagation, and unit tests.
Formula MCTS kernel integration
rust_hft/alpha-harness/engine/Cargo.toml, rust_hft/alpha-harness/engine/src/engines/*, rust_hft/ARCHITECTURE.md
Formula MCTS implements UctNode, uses shared selection and backpropagation, centralizes checkpoint validation, imports the shared RNG, and tests deterministic checkpoint parity and cyclic-history rejection.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FormulaMcts
  participant hft_search_kernel
  participant NodeTree
  FormulaMcts->>hft_search_kernel: select_expandable(NodeTree, root, exploration)
  hft_search_kernel->>NodeTree: validate tree and calculate UCT scores
  hft_search_kernel-->>FormulaMcts: return expandable node
  FormulaMcts->>hft_search_kernel: backpropagate(NodeTree, root, leaf, reward)
  hft_search_kernel->>NodeTree: update lineage statistics
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: extracting a shared UCT kernel for research/Formula MCTS.
Description check ✅ Passed The description matches the template well; only the optional Scope exception section is omitted.
Linked Issues check ✅ Passed The PR appears to satisfy #185: the kernel owns UCT logic, Formula delegates to it, parity tests exist, and invalid checkpoints fail closed.
Out of Scope Changes check ✅ Passed No obvious out-of-scope code changes are shown; edits stay focused on the shared UCT kernel and Formula MCTS adapter.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/unified-mcts-kernel-185

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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: 22475efeb0

ℹ️ 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".

None => break,
}
}
let _ = backpropagate(&mut self.nodes, 0, node_id, evaluation.score);

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 Propagate backpropagation failures

When a restored, otherwise accepted checkpoint has a node at u64::MAX visits or a finite reward total near overflow, backpropagate returns StatsOverflow without mutating the tree. Discarding that error after removing the pending candidate lets AutoResearchKernel checkpoint and persist the evaluation as successful even though its reward was permanently omitted; before this extraction, the resulting invalid statistics caused checkpointing to fail. Preserve the failure so the run remains fail-closed instead of silently corrupting search state.

AGENTS.md reference: AGENTS.md:L59-L61

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
rust_hft/Cargo.toml (1)

85-85: 🧹 Nitpick | 🔵 Trivial

Workspace graph changed — run the required metadata check.

Adding a new member changes the workspace dependency graph. As per path instructions, "after workspace graph changes, run cargo metadata --locked --no-deps" to confirm the graph resolves cleanly before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust_hft/Cargo.toml` at line 85, After adding the research-core/search-kernel
workspace member, run cargo metadata --locked --no-deps from the workspace root
and confirm it completes successfully. Address any metadata or lockfile
resolution errors before proceeding.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust_hft/alpha-harness/engine/src/engines/mcts.rs`:
- Around line 303-311: Update MCTS::observe so failures returned by
backpropagate are not silently discarded: capture the Result and emit an
appropriate error or warning with the candidate/node context when it fails,
while preserving the existing successful backpropagation path and
non-finite-score handling.

In `@rust_hft/research-core/search-kernel/src/lib.rs`:
- Around line 72-80: Update UctStats::validate to require best_reward to be
present exactly when visits is greater than zero, independently of total_reward;
retain the zero-visits requirement that total_reward is zero and best_reward is
absent. Also prevent direct Deserialize construction of UctStats, routing
deserialization through the validated from_parts path so all instances enforce
these invariants.

---

Nitpick comments:
In `@rust_hft/Cargo.toml`:
- Line 85: After adding the research-core/search-kernel workspace member, run
cargo metadata --locked --no-deps from the workspace root and confirm it
completes successfully. Address any metadata or lockfile resolution errors
before proceeding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15cec4e9-8f7d-4507-a348-b82c04b27f10

📥 Commits

Reviewing files that changed from the base of the PR and between ac7f656 and 22475ef.

⛔ Files ignored due to path filters (1)
  • rust_hft/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • rust_hft/ARCHITECTURE.md
  • rust_hft/Cargo.toml
  • rust_hft/alpha-harness/engine/Cargo.toml
  • rust_hft/alpha-harness/engine/src/engines/mcts.rs
  • rust_hft/alpha-harness/engine/src/engines/mod.rs
  • rust_hft/research-core/search-kernel/Cargo.toml
  • rust_hft/research-core/search-kernel/src/lib.rs

Comment on lines 303 to 311
fn observe(&mut self, proposal: &EngineProposal, evaluation: &CandidateEvaluation) {
let Some(mut node_id) = self.candidates.remove(&proposal.candidate_id) else {
let Some(node_id) = self.candidates.remove(&proposal.candidate_id) else {
return;
};
if !evaluation.score.is_finite() {
return;
}
loop {
let node = &mut self.nodes[node_id];
node.visits += 1;
node.total_reward += evaluation.score;
node.best_reward = Some(
node.best_reward
.map_or(evaluation.score, |best| best.max(evaluation.score)),
);
match node.parent {
Some(parent) => node_id = parent,
None => break,
}
}
let _ = backpropagate(&mut self.nodes, 0, node_id, evaluation.score);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

observe() silently drops backpropagate failures.

let _ = backpropagate(...) discards any UctError (e.g. StatsOverflow, InvalidTopology). This is fail-closed for data integrity (no partial mutation), but a genuine observation is lost with zero observability — nothing signals that a proposed candidate's evaluation never got recorded.

🔍 Suggested minimal observability fix
-        let _ = backpropagate(&mut self.nodes, 0, node_id, evaluation.score);
+        if let Err(error) = backpropagate(&mut self.nodes, 0, node_id, evaluation.score) {
+            tracing::warn!(%error, node_id, "MCTS observe: backpropagate rejected evaluation");
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn observe(&mut self, proposal: &EngineProposal, evaluation: &CandidateEvaluation) {
let Some(mut node_id) = self.candidates.remove(&proposal.candidate_id) else {
let Some(node_id) = self.candidates.remove(&proposal.candidate_id) else {
return;
};
if !evaluation.score.is_finite() {
return;
}
loop {
let node = &mut self.nodes[node_id];
node.visits += 1;
node.total_reward += evaluation.score;
node.best_reward = Some(
node.best_reward
.map_or(evaluation.score, |best| best.max(evaluation.score)),
);
match node.parent {
Some(parent) => node_id = parent,
None => break,
}
}
let _ = backpropagate(&mut self.nodes, 0, node_id, evaluation.score);
}
fn observe(&mut self, proposal: &EngineProposal, evaluation: &CandidateEvaluation) {
let Some(node_id) = self.candidates.remove(&proposal.candidate_id) else {
return;
};
if !evaluation.score.is_finite() {
return;
}
if let Err(error) = backpropagate(&mut self.nodes, 0, node_id, evaluation.score) {
tracing::warn!(%error, node_id, "MCTS observe: backpropagate rejected evaluation");
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust_hft/alpha-harness/engine/src/engines/mcts.rs` around lines 303 - 311,
Update MCTS::observe so failures returned by backpropagate are not silently
discarded: capture the Result and emit an appropriate error or warning with the
candidate/node context when it fails, while preserving the existing successful
backpropagation path and non-finite-score handling.

Comment on lines +72 to +80
fn validate(self) -> Result<(), UctError> {
if !self.total_reward.is_finite()
|| self.best_reward.is_some_and(|reward| !reward.is_finite())
|| (self.visits == 0) != (self.total_reward == 0.0 && self.best_reward.is_none())
{
return Err(UctError::InvalidStats);
}
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

UctStats::validate() doesn't fully enforce the visits↔best_reward invariant.

The check (self.visits == 0) != (self.total_reward == 0.0 && self.best_reward.is_none()) only catches inconsistency when total_reward == 0.0. A node with visits > 0, a nonzero total_reward, and best_reward == None passes validation undetected — and that exact shape is trivially reachable via a deserialized checkpoint (best_reward: null with nonzero visits/total_reward). Since validate_tree (used by both checkpoint validation and backpropagate) calls node.stats()UctStats::from_parts → this validate(), such a corrupted node is accepted rather than rejected, undermining the PR's fail-closed checkpoint acceptance criterion; the bogus best_reward then silently surfaces through trace()/MctsNodeSnapshot.

Separately, UctStats derives Deserialize directly (line 28) rather than only being constructible via from_parts; any future direct deserialization path would bypass this invariant entirely.

🛡️ Proposed fix for the invariant check
     fn validate(self) -> Result<(), UctError> {
         if !self.total_reward.is_finite()
             || self.best_reward.is_some_and(|reward| !reward.is_finite())
-            || (self.visits == 0) != (self.total_reward == 0.0 && self.best_reward.is_none())
+            || (self.visits == 0) != self.best_reward.is_none()
+            || (self.visits == 0 && self.total_reward != 0.0)
         {
             return Err(UctError::InvalidStats);
         }
         Ok(())
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn validate(self) -> Result<(), UctError> {
if !self.total_reward.is_finite()
|| self.best_reward.is_some_and(|reward| !reward.is_finite())
|| (self.visits == 0) != (self.total_reward == 0.0 && self.best_reward.is_none())
{
return Err(UctError::InvalidStats);
}
Ok(())
}
fn validate(self) -> Result<(), UctError> {
if !self.total_reward.is_finite()
|| self.best_reward.is_some_and(|reward| !reward.is_finite())
|| (self.visits == 0) != self.best_reward.is_none()
|| (self.visits == 0 && self.total_reward != 0.0)
{
return Err(UctError::InvalidStats);
}
Ok(())
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust_hft/research-core/search-kernel/src/lib.rs` around lines 72 - 80, Update
UctStats::validate to require best_reward to be present exactly when visits is
greater than zero, independently of total_reward; retain the zero-visits
requirement that total_reward is zero and best_reward is absent. Also prevent
direct Deserialize construction of UctStats, routing deserialization through the
validated from_parts path so all instances enforce these invariants.

@proerror77
proerror77 merged commit 9edf738 into main Jul 22, 2026
42 checks passed
@proerror77
proerror77 deleted the codex/unified-mcts-kernel-185 branch July 24, 2026 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-agent Fully specified and safe for an autonomous agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract a domain-neutral UCT kernel from continuous Formula MCTS

1 participant