Skip to content

refactor(fitstats): move conditional Rasch M2 arithmetic to Rust - #807

Merged
seonghobae merged 3 commits into
mainfrom
fix/cmle-m2-rust-ownership
Aug 13, 2026
Merged

refactor(fitstats): move conditional Rasch M2 arithmetic to Rust#807
seonghobae merged 3 commits into
mainfrom
fix/cmle-m2-rust-ownership

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Buyer-visible numerical ownership gap

Protected main routes ordinary MMLE M2 through Rust/PyO3, but the public conditional-Rasch path m2(..., estimator="cmle") and direct m2_cmle_rasch() still execute result-affecting elementary-symmetric, derivative, covariance, projection, fit-index, and SRMSR arithmetic in Python/NumPy. That contradicts the production numerical ownership contract tracked in #627.

Intentional RED

Exact fail-first head b2a0bf9956fd223bc81b7e8382df043ad2adc1f1 adds only public-boundary regressions. They require both public CMLE entry paths to:

  • fail closed when the compiled core is unavailable;
  • delegate every result field through a dedicated m2_cmle_rasch_stat native entrypoint; and
  • preserve the existing M2Result, estimator label, and conditional-Rasch interpretation note.

Current protected behavior does not consult the Rust core, so the expected RED is the ownership assertion rather than fixture/setup/import failure. The full-pattern fixture contains every five-item response pattern and every raw-score category.

Minimum GREEN

  • port the existing conditional-Rasch elementary-symmetric probabilities, derivative matrix, covariance, projected M2, null model, fit indices, and SRMSR arithmetic to mlsirm-core;
  • expose one thin PyO3 entrypoint with bounded checked allocation and stable validation errors;
  • leave Python responsible for input validation, marshalling, and M2Result construction only;
  • keep any NumPy implementation explicitly private/reference-only and impossible to select through the public production path;
  • add direct Rust hand-calculation/error tests plus public parity/ownership tests;
  • preserve response conditioning, item-easiness centering, degrees of freedom, inference semantics, and existing public signatures;
  • add changelog and APA 7 doctoring after GREEN, then run exact-head Python/Rust/PyO3/package/fuzz/security/SAST/review gates.

No estimator-family redesign, hidden fallback, dependency, workflow, hosted-product, or release-version change belongs in this slice.

Advances #627.

Summary by CodeRabbit

  • Tests
    • Added coverage for conditional-Rasch M2 statistics, including bookkeeping, invariance to common item-easiness shifts, complete-response handling, and invalid input limits.
    • Added checks that public CMLE M2 APIs use the compiled Rust implementation and preserve returned statistics and metadata.
    • Added validation for failures when the required compiled core is unavailable.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 68 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d99c798-d449-48f2-a1c8-f3e43c2b1862

📥 Commits

Reviewing files that changed from the base of the PR and between 17620f7 and 8f850b6.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • crates/fast-mlsirm-py/src/lib.rs
  • crates/mlsirm-core/src/fitstats.rs
  • docs/changelog.d/807-cmle-m2-rust-ownership.md
  • python/fast_mlsirm/fitstats.py
  • tests/test_fitstats_cmle_m2_rust_ownership.py
📝 Walkthrough

Walkthrough

The pull request adds Rust integration tests for conditional-Rasch CMLE M2 and Python ownership-contract tests for its public APIs. The tests cover valid results, input constraints, Rust delegation, metadata preservation, and fail-closed behavior.

Changes

Conditional-Rasch CMLE M2 validation

Layer / File(s) Summary
Rust CMLE M2 integration coverage
crates/mlsirm-core/tests/fitstats_cmle_m2.rs
Tests validate result counts, finite statistics, common item-easiness shift invariance, complete-row filtering, score-category validation, and the 64-item limit.
Public API Rust ownership checks
tests/test_fitstats_cmle_m2_rust_ownership.py
Tests verify failure without the compiled Rust core and confirm that direct and routed APIs preserve returned statistics, CMLE metadata, and inference notes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: 🟡 Moderate · up to 17620

The current tests do not yet prove numerical correctness for CMLE M2 or fail-closed behavior through the public estimator route, so the PR is not merge-ready until those bounded regression tests are added.

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: moving conditional Rasch M2 arithmetic to Rust.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cmle-m2-rust-ownership

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.

Copy link
Copy Markdown
Contributor Author

@opencode-agent address

Operate only if the live Draft head is still exactly b2a0bf9956fd223bc81b7e8382df043ad2adc1f1; abort without writing if it moved.

Follow strict RED → GREEN on this existing branch:

  1. Build/install the current extension and run pytest -q tests/test_fitstats_cmle_m2_rust_ownership.py. Confirm the two ownership tests fail because public m2_cmle_rasch() bypasses _core_module() / m2_cmle_rasch_stat, not because of import, fixture, or unrelated setup failure. If the failure reason differs, repair only the test fixture/contract and re-observe the intended RED before production code.
  2. Port the existing conditional-Rasch M2 numerical body into mlsirm-core: log elementary-symmetric probabilities, conditional moments, finite-difference item contrasts, empirical raw-score nuisance columns, model/null covariance, projected M2, RMSEA2 interval, CFI/TLI and SRMSR. Preserve current equations, degrees of freedom, full-pattern conditioning, errors and interpretation boundaries; do not redesign the estimator.
  3. Expose a bounded m2_cmle_rasch_stat PyO3 entrypoint. Use checked products and fallible allocations; reject malformed/non-finite inputs with stable package-owned errors. Do not create per-call threads or a Python numerical fallback.
  4. Convert public m2_cmle_rasch() into validation/marshalling/result construction only. m2(..., estimator="cmle") must reuse that Rust-owned path. Any retained NumPy implementation must be explicitly private/reference-only and unreachable from public production dispatch.
  5. Add direct Rust hand-calculation, malformed-input, degrees-of-freedom and degenerate-pair tests; retain public parity/ownership tests; update changelog and APA 7 doctoring only after GREEN.
  6. Run focused Python/Rust/PyO3 tests, then complete repository Python coverage/docstrings, Rust workspace/all-targets, package/reinstall, explicit GPU no-skip, fuzz, Security Scan and SAST on the exact final head. Keep Draft; do not approve, merge, release, alter workflows/dependencies/versions, or touch unrelated paths.

Advance #627 only with exact-head evidence.

@seonghobae
seonghobae marked this pull request as ready for review August 13, 2026 00:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@crates/mlsirm-core/tests/fitstats_cmle_m2.rs`:
- Around line 40-45: Add a deterministic, independently hand-calculated CMLE M2
fixture in the test covering the existing result from the fit-statistics path,
and assert exact or appropriately tolerance-bounded expected values for m2, df,
p_value, rmsea2, and srmsr rather than only finiteness and shift invariance.
Keep the fixture and expected calculations grounded in the primary literature
supporting the fit-diagnostic scope.

In `@tests/test_fitstats_cmle_m2_rust_ownership.py`:
- Around line 42-43: Extend the existing Rust-core-unavailable test around
m2_cmle_rasch to also call fitstats.m2 with the CMLE estimator and require the
same RuntimeError. Keep the test under the same _core_module() monkeypatch and
use the existing responses, factor_id, and params fixtures to verify the routed
CMLE path fails closed.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fd80d394-ab0a-471c-9d3c-608316b28241

📥 Commits

Reviewing files that changed from the base of the PR and between d34a473 and 17620f7.

📒 Files selected for processing (2)
  • crates/mlsirm-core/tests/fitstats_cmle_m2.rs
  • tests/test_fitstats_cmle_m2_rust_ownership.py

Comment thread crates/mlsirm-core/tests/fitstats_cmle_m2.rs Outdated
Comment thread tests/test_fitstats_cmle_m2_rust_ownership.py
@seonghobae
seonghobae force-pushed the fix/cmle-m2-rust-ownership branch 3 times, most recently from 4bab11a to eb34c99 Compare August 13, 2026 02:01

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@seonghobae
seonghobae enabled auto-merge (squash) August 13, 2026 03:20
@seonghobae
seonghobae force-pushed the fix/cmle-m2-rust-ownership branch from 52961a5 to 8e6abe3 Compare August 13, 2026 04:32
Fail closed without the compiled core and delegate every public
conditional-Rasch M2 result field to m2_cmle_rasch_stat. Ports the
Haberman-style raw-score conditioning and Maydeu-Olivares–Joe projection
into mlsirm-core so production ownership matches ordinary m2_stat.

## changelog
- Public m2_cmle_rasch and m2(..., estimator="cmle") require Rust
- Native m2_cmle_rasch_stat owns elementary-symmetric and projected M2
@seonghobae
seonghobae force-pushed the fix/cmle-m2-rust-ownership branch from 8e6abe3 to 8f850b6 Compare August 13, 2026 05:43

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@seonghobae
seonghobae merged commit df71a7e into main Aug 13, 2026
36 checks passed
@seonghobae
seonghobae deleted the fix/cmle-m2-rust-ownership branch August 13, 2026 07:09
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.

1 participant