Skip to content

feat(analysis): bind Rubin loading uncertainty to an analysis-run profile - #374

Draft
seonghobae wants to merge 2 commits into
mainfrom
feat/rubin-loading-uncertainty-analysis-run-gap-006
Draft

feat(analysis): bind Rubin loading uncertainty to an analysis-run profile#374
seonghobae wants to merge 2 commits into
mainfrom
feat/rubin-loading-uncertainty-analysis-run-gap-006

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

GAP-006 / #169 remaining operator-visible slice: jointly bind the already-merged psychometric_core posterior-draw OLS loading mean (recover_loading_point_estimate_mean) and Rubin (1996) total variance (combine_draw_level_ols_loadings) to an analysis_engine analysis-run output profile.

  • New profile rubin_loading_uncertainty_v1 / schema tepp.rubin_loading_uncertainty.v1 (ADR 0034; 0026–0033 remain on other live PRs).
  • Cutoff-filters observations by available_time against the request knowledge_cutoff.
  • Digest-bound terminal result with point-estimate mean and Rubin /Ū/B/T. Inference status is rubin_combined_ols_loadings_not_mislevy_pv.
  • Does not treat complete-data OLS draws as Mislevy person-level plausible values, invent an ESEM/DSEM estimator, restore a Driver p.16 std matrix, duplicate CWC, persist rows, or claim strong invariance.

This is not implemented-main. Exact-head Checks on this head only. Predecessor-head evidence does not transfer.

Does not duplicate:

Test plan

  • cargo test -p analysis_engine
  • cargo clippy -p analysis_engine --all-targets -- -D warnings
  • RUSTDOCFLAGS="-D warnings" cargo doc -p analysis_engine --no-deps
  • Exact-head Required Checks on this SHA
  • Two independent (non-author, non-bot) APPROVE reviews

Merge bar

Ruleset 18156473: two independent approvals + exact-head Checks. Do not self-approve. Do not --admin merge.


Devin Review

…file

Operators still cannot request the already-merged psychometric_core
draw-mean OLS loadings and Rubin T combination as a digest-bound
analysis-run output. Bind them jointly as rubin_loading_uncertainty_v1
/ tepp.rubin_loading_uncertainty.v1 (ADR 0034). Cutoff-filter
observations, refuse raw proportions and single-draw inputs, and keep
the claim boundary as complete-data OLS combination rather than
Mislevy person-level plausible values.

Not a new ESEM/DSEM estimator, not CWC, not a Driver p.16 std restore,
and not persistence.
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 56 minutes.

View limit details

Limit details: You’ve used the included review currently available.

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e02e482-2bf4-4202-8234-52eaa7f16407

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc02f5 and e41393e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • CHANGELOG.md
  • crates/analysis_engine/Cargo.toml
  • crates/analysis_engine/src/lib.rs
  • crates/analysis_engine/src/rubin_loading_artifact.rs
  • crates/analysis_engine/tests/rubin_loading_execution_contract.rs
  • crates/psychometric_core/src/error.rs
  • docs/TRACEABILITY.md
  • docs/adr/0034-rubin-loading-uncertainty-analysis-run.md
  • docs/adr/README.md
  • docs/doctoring/rubin-loading-uncertainty-analysis-run.md

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.

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 3 potential issues.

Devin Review

Comment on lines +169 to +174
|| !self.within_variance.is_finite()
|| self.within_variance < 0.0
|| !self.between_variance.is_finite()
|| self.between_variance < 0.0
|| !self.total_variance.is_finite()
|| self.total_variance < 0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Invalid uncertainty artifacts pass validation

validate accepts any nonnegative total variance without checking Rubin’s equation against its components and draw count. Consumers can accept false uncertainty as valid.

Prompt for agents
Strengthen RubinLoadingUncertaintyArtifact::validate in crates/analysis_engine/src/rubin_loading_artifact.rs so the serialized scientific fields are internally consistent. Recompute Rubin total variance from within_variance, between_variance, and draw_count using the same arithmetic contract and reject mismatches under an explicitly chosen serialization-safe comparison policy. Also test from_json and to_json with finite, nonnegative but inconsistent component values.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +67 to +80
#[test]
fn noiseless_draws_emit_digest_bound_point_mean_and_rubin_t() {
let request = request();
let accepted = accepted(&request);
let rows = noiseless_rows();
let execution = execute(
&request,
&accepted,
"snapshot-rubin-loading",
cutoff(),
IndicatorKind::AdditiveLogRatio,
&rows,
)
.expect("execution");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Profile recovery evidence remains incomplete

The profile adds cutoff filtering before estimation, but tests cover only one noiseless example. Add profile-level recovery and interval-coverage evidence for filtered data.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +51 to +53
if !factor_score.is_finite()
|| indicator_draws.is_empty()
|| indicator_draws.iter().any(|value| !value.is_finite())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 Unbounded draws enable memory exhaustion

RubinLoadingObservation::new accepts unlimited draws. Processing duplicates the full matrix, so oversized untrusted input can exhaust service memory.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 5 new potential issues.

Devin Review

Comment on lines +275 to +277
let digest = artifact
.sha256()
.expect("constructed Rubin artifact is valid and serializable");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Long identifiers crash completed runs

Identifiers over 256 bytes pass request validation but fail inside expect. A valid accepted run can panic instead of returning an error.

Suggested change
let digest = artifact
.sha256()
.expect("constructed Rubin artifact is valid and serializable");
let digest = artifact.sha256()?;
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +265 to +267
let combined =
combine_draw_level_ols_loadings(&eligible.factor_scores, &eligible.indicator_draws, kind)?;
let point_estimate_mean = combined.mean_loading;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Point estimates bypass robust recovery

Large canceling loadings make combined.mean_loading differ from the robust point-estimate helper. The artifact then reports the wrong point estimate.

Prompt for agents
In crates/analysis_engine/src/rubin_loading_artifact.rs, execute_rubin_loading_uncertainty_run must obtain point_estimate_mean by calling psychometric_core::recover_loading_point_estimate_mean on the eligible factor scores and transposed indicator draws. Keep combine_draw_level_ols_loadings for Rubin Q-bar/U-bar/B/T. Add a regression test with large canceling per-draw slopes to prove the point estimate follows the robust helper rather than combined.mean_loading.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +159 to +160
|| self.observation_count < 2
|| self.draw_count < 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Unbounded artifact counts are accepted

validate enforces minimum counts but no execution maximum. Imported artifacts can claim more observations and exclusions than any accepted run permits.

Prompt for agents
Make RubinLoadingUncertaintyArtifact::validate enforce the execution bounds represented by the schema. observation_count must not exceed MAX_EVIDENCE_UNITS, and observation_count plus excluded_after_cutoff_count must be checked for overflow and must not exceed MAX_EVIDENCE_UNITS. Define and enforce a bounded draw count as well. Add from_json tests for each oversized count and for overflowing count sums.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +137 to +142
pub fn to_json(&self) -> Result<String, AnalysisEngineError> {
self.validate()?;
let payload =
serde_json::to_string(self).map_err(|_| AnalysisEngineError::SerializationFailure)?;
return Ok(payload);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Serialization skips the declared size bound

from_json enforces 256 KiB, but to_json does not. Keep both public artifact directions symmetric as fields evolve.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +197 to +235
fn admit_observations_at_cutoff(
observations: &[RubinLoadingObservation],
knowledge_cutoff: KnowledgeCutoff,
) -> Result<EligibleRubinRows, AnalysisEngineError> {
if observations.len() > MAX_EVIDENCE_UNITS {
return Err(AnalysisEngineError::LimitExceeded);
}
let mut eligible = Vec::new();
let mut excluded_after_cutoff_count = 0_u64;
for observation in observations {
if observation.available_time.instant() <= knowledge_cutoff.instant() {
eligible.push(observation);
} else {
excluded_after_cutoff_count += 1;
}
}
if eligible.is_empty() {
return Err(AnalysisEngineError::Psychometric(
PsychometricError::InvalidNumericInput,
));
}
let draw_count = eligible[0].indicator_draws.len();
let mut factor_scores = Vec::with_capacity(eligible.len());
let mut indicator_draws = vec![Vec::with_capacity(eligible.len()); draw_count];
for observation in eligible {
if observation.indicator_draws.len() != draw_count {
return Err(AnalysisEngineError::Psychometric(
PsychometricError::InvalidNumericInput,
));
}
factor_scores.push(observation.factor_score);
for (draw_index, value) in observation.indicator_draws.iter().enumerate() {
indicator_draws[draw_index].push(*value);
}
}
#[rustfmt::skip]
let rows = EligibleRubinRows { factor_scores, indicator_draws, excluded_after_cutoff_count };
Ok(rows)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Cutoff filtering preserves draw alignment

Filtering precedes transposition, so every draw uses the same eligible rows. Unequal retained draw lengths fail before estimation.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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