Skip to content

feat(psychometric): recover ESEM loadings and refuse reverse DSEM lags - #119

Merged
seonghobae merged 5 commits into
mainfrom
agent/psychometric-esem-dsem-fit
Aug 24, 2026
Merged

feat(psychometric): recover ESEM loadings and refuse reverse DSEM lags#119
seonghobae merged 5 commits into
mainfrom
agent/psychometric-esem-dsem-fit

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • ADR 0005 still required a CPU f64 ESEM/DSEM fit path that does not treat raw topic proportions as Euclidean indicators.
  • Adds standalone psychometric_fit: OLS recovers known exploratory cross-loadings from admitted ALR/ILR/logistic-normal coordinates; recovered RMSE is below a zero-loading collapse.
  • DSEM lagged paths are admitted only when the predictor is strictly earlier in event time.
  • A good global fit cannot reclassify formative or network constructs as reflective.
  • Does not recreate psychometric_core (feat(psychometric): posterior ESEM input gates with true-parameter RMSE #49 owns input gates) and does not allocate migration 0007/0008 (#45 still owns 0007).

Claim boundary

  • OLS loading recovery and event-time lag gates only. Not rotation, posterior pooling, invariance, or a full ESEM/DSEM sampler.
  • Not a certification, attestation, or causal-identification claim.

Test plan

  • RED: esem_dsem_fit_contract failed before the crate existed
  • cargo test -p psychometric_fit --offline --lib --tests
  • cargo clippy -p psychometric_fit --all-targets --offline -- -D warnings
  • cargo llvm-cov -p psychometric_fit authored lines 108/108; nightly-2026-08-01 branches 30/30
  • docstring, workspace, and documentation gates
  • Exact-head CI and independent review

Summary by CodeRabbit

  • 새 기능

    • CPU 기반 ESEM 요인 적재량 복구와 RMSE 평가를 지원합니다.
    • 이벤트 시간 순서를 검증하는 DSEM 지연 경로 복구를 추가했습니다.
    • 잘못된 좌표, 입력값, 차원, 표본 수 및 역방향 시간 경로를 명확한 오류로 처리합니다.
    • 지원하지 않는 구성개념의 반사형 재해석을 차단합니다.
  • 문서

    • 기능 범위, 검증 기준, 아키텍처 및 구현 상태 문서를 업데이트했습니다.
  • 테스트

    • 정상적인 ESEM/DSEM 사례와 다양한 잘못된 입력에 대한 검증을 추가했습니다.

Add a standalone psychometric_fit crate that recovers exploratory
cross-loadings on a CPU f64 OLS path from admitted log-ratio coordinates
and refuses non-forward event-time DSEM lags. Does not recreate
psychometric_core or allocate migration 0008.
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 93fb5915-ebd1-4647-8c56-d99a4a343760

📥 Commits

Reviewing files that changed from the base of the PR and between ab78015 and 16ed894.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • ARCHITECTURE.md
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • crates/psychometric_fit/Cargo.toml
  • crates/psychometric_fit/src/error.rs
  • crates/psychometric_fit/src/fit.rs
  • crates/psychometric_fit/src/lib.rs
  • crates/psychometric_fit/tests/crate_contract.rs
  • crates/psychometric_fit/tests/esem_dsem_fit_contract.rs
  • docs/TRACEABILITY.md
  • docs/adr/0005-posterior-esem-dsem.md
  • docs/adr/README.md
  • docs/research/esem-dsem-fit.md
  • docs/research/standards-and-literature.md
  • docs/validation/temporal-event-foundation.md
  • scripts/check_workspace_contract.py

📝 Walkthrough

Walkthrough

새로운 psychometric_fit Rust crate가 workspace에 추가되었습니다. crate는 좌표와 구성개념을 검증하고, CPU f64 ESEM 적재량 복구와 event-time DSEM 지연 경로를 제공합니다. 단위 테스트와 통합 테스트가 정상 및 fail-closed 동작을 검증합니다.

Changes

psychometric_fit 적합 기능

Layer / File(s) Summary
워크스페이스 및 공개 계약
Cargo.toml, crates/psychometric_fit/Cargo.toml, crates/psychometric_fit/src/lib.rs, README.md, ARCHITECTURE.md, scripts/check_workspace_contract.py
psychometric_fit crate를 workspace와 기본 멤버에 추가했습니다. 매니페스트, lint 설정, 공개 API 재수출, workspace 문서와 계약 검증 목록을 추가했습니다.
ESEM/DSEM 적합 엔진
crates/psychometric_fit/src/error.rs, crates/psychometric_fit/src/fit.rs, docs/research/esem-dsem-fit.md, docs/research/standards-and-literature.md, docs/adr/0005-posterior-esem-dsem.md, docs/adr/README.md, docs/TRACEABILITY.md, docs/validation/temporal-event-foundation.md, CHANGELOG.md
좌표와 구성개념 분류를 정의했습니다. raw proportion과 허용되지 않는 reflective 재해석을 거부합니다. 최대 두 요인의 CPU f64 OLS ESEM 적재량 복구, RMSE 계산, 엄격한 event-time DSEM 지연 경로를 구현했습니다. 오류 유형과 관련 문서를 추가했습니다.
crate 계약 검증
crates/psychometric_fit/tests/crate_contract.rs, crates/psychometric_fit/tests/esem_dsem_fit_contract.rs, crates/psychometric_fit/src/error.rs, crates/psychometric_fit/src/fit.rs
crate 이름, 알려진 적재량 복구, ILR·logistic-normal 좌표, 순방향 DSEM 경로, 오류 조건, 구성개념 판정, 안정적인 문자열을 검증합니다.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant admit_fit_coordinates
  participant recover_esem_loadings
  participant loading_recovery_rmse
  Caller->>admit_fit_coordinates: FitCoordinateKind 검증
  admit_fit_coordinates-->>Caller: 좌표 적합성 결과
  Caller->>recover_esem_loadings: factor_scores와 indicators 전달
  recover_esem_loadings-->>Caller: 복구된 loading matrix 반환
  Caller->>loading_recovery_rmse: truth와 recovered 비교
  loading_recovery_rmse-->>Caller: RMSE 반환
Loading
✨ 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 agent/psychometric-esem-dsem-fit

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.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d94e3ee-819c-4a2b-997e-8b201edd8589

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

# Conflicts:
#	CHANGELOG.md
#	docs/validation/temporal-event-foundation.md
@seonghobae
seonghobae marked this pull request as ready for review August 20, 2026 18:54
@seonghobae

Copy link
Copy Markdown
Contributor Author

Current HEAD 2ea08a87be3c87d7d8c400addcdf0fd3e8e830d0 includes current main, the CPU f64 ESEM/DSEM acceptance contract, and the dynamic workspace crate-count quality fix. Verified at this HEAD: cargo fmt --all -- --check; cargo test -p psychometric_fit --offline (8 tests including known-loading RMSE and forward event-time contracts); Python quality suite (89 tests); workspace, documentation, Rust docstring, and diff checks. Please provide the required independent approval; merge remains gated until current-head required checks and approval are present.

@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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@seonghobae

Copy link
Copy Markdown
Contributor Author

Current-head review refresh for 2ea08a8:

  • ESEM loading recovery is measured against known truth, while reverse DSEM lags are rejected so temporal direction is not silently inverted.
  • git diff --check, documentation validation, workspace contract, docstring contract, and cargo fmt --all -- --check passed locally.
  • Current hosted checks are pass=15, pending=1, fail=0; merge remains withheld until the pending exact-head required check completes and qualifying approvals exist.
  • No new actionable source defect was found at this exact head; no approval is being self-issued.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Rebased current head 47ab763 onto origin/main. The changelog conflict retains both feature and current-main entries; inherited documentation trailing whitespace was removed. Local merge-tree, git diff --cached --check, and cargo fmt --all -- --check pass. Exact-head hosted checks and required independent approvals remain required before protected merge.

@seonghobae
seonghobae enabled auto-merge (squash) August 24, 2026 00:57
@github-actions
github-actions Bot disabled auto-merge August 24, 2026 08:18
…em-dsem-fit

# Conflicts:
#	ARCHITECTURE.md
#	CHANGELOG.md
#	Cargo.lock
#	Cargo.toml
#	README.md
#	docs/TRACEABILITY.md
#	docs/adr/0005-posterior-esem-dsem.md
#	docs/adr/README.md
#	docs/research/standards-and-literature.md
#	docs/validation/temporal-event-foundation.md
#	scripts/check_workspace_contract.py
#	tests/quality/test_check_docstrings.py
@seonghobae
seonghobae merged commit 6d40f74 into main Aug 24, 2026
18 of 22 checks passed

@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 3 new potential issues.

Open in Devin Review

Comment on lines +259 to +290
fn invert_gram(gram: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, PsychometricFitError> {
match gram.len() {
1 => {
let value = gram[0][0];
if value <= 0.0 {
return Err(PsychometricFitError::SingularDesign);
}
Ok(vec![vec![require_finite(1.0 / value)?]])
}
2 => {
let a = gram[0][0];
let b = gram[0][1];
let c = gram[1][0];
let d = gram[1][1];
let determinant = require_finite(a * d - b * c)?;
if determinant.abs() <= 0.0 {
return Err(PsychometricFitError::SingularDesign);
}
Ok(vec![
vec![
require_finite(d / determinant)?,
require_finite(-b / determinant)?,
],
vec![
require_finite(-c / determinant)?,
require_finite(a / determinant)?,
],
])
}
_ => Err(PsychometricFitError::InvalidNumericInput),
}
}

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: Singular-design guard only rejects exactly-zero determinant, not near-singular

invert_gram rejects a design only when the 1x1 value is <= 0.0 (fit.rs) or the 2x2 determinant is exactly zero (determinant.abs() <= 0.0 at fit.rs). A numerically near-singular (ill-conditioned but nonzero-determinant) Gram matrix passes this gate and yields large but finite loadings that are only rejected if they overflow to non-finite via require_finite. For a crate that advertises a fail-closed SingularDesign error, a relative/condition-number tolerance would be more robust. This is a defensible reference-path design choice (the contract text says "singular") rather than a bug, so I did not flag it, but reviewers may want to confirm the intended tolerance behavior.

Open in Devin Review

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

Comment on lines +191 to +206
pub fn recover_dsem_lagged_path(
predictor_event_time: i64,
outcome_event_time: i64,
predictor: &[f64],
outcome: &[f64],
) -> Result<f64, PsychometricFitError> {
if outcome_event_time <= predictor_event_time {
return Err(PsychometricFitError::ReverseEventTimePath);
}
let loadings = recover_esem_loadings(
&[predictor.to_vec()],
&[outcome.to_vec()],
FitCoordinateKind::LogisticNormal,
)?;
Ok(loadings[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.

📝 Info: DSEM lag path hardcodes LogisticNormal and does not gate raw-proportion inputs

recover_dsem_lagged_path always calls recover_esem_loadings with FitCoordinateKind::LogisticNormal (fit.rs). Because the function signature takes no coordinate kind, a caller cannot indicate that the predictor/outcome series are raw simplex proportions, so the RawProportionForbidden gate that protects recover_esem_loadings is effectively bypassed for DSEM lag recovery. This is consistent with the crate's current narrow API (the caller is expected to supply already-transformed coordinates), so I did not flag it as a bug, but if DSEM inputs can ever originate from untransformed proportions this gate should be surfaced in the signature.

Open in Devin Review

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

Comment on lines +119 to +154
pub fn recover_esem_loadings(
factor_scores: &[Vec<f64>],
indicators: &[Vec<f64>],
kind: FitCoordinateKind,
) -> Result<Vec<Vec<f64>>, PsychometricFitError> {
admit_fit_coordinates(kind)?;
if factor_scores.len() > 2 {
return Err(PsychometricFitError::InvalidNumericInput);
}
let observation_count = factor_scores.first().map_or(0, Vec::len);
let mut centered_factors = Vec::new();
for values in factor_scores {
if observation_count < 2 || values.len() != observation_count {
return Err(PsychometricFitError::InvalidNumericInput);
}
centered_factors.push(center(values)?);
}
if centered_factors.is_empty() {
return Err(PsychometricFitError::InvalidNumericInput);
}
let mut loadings = Vec::new();
for values in indicators {
if values.len() != observation_count {
return Err(PsychometricFitError::InvalidNumericInput);
}
let centered_indicator = center(values)?;
loadings.push(ordinary_least_squares_loadings(
&centered_factors,
&centered_indicator,
)?);
}
if loadings.is_empty() {
return Err(PsychometricFitError::InvalidNumericInput);
}
Ok(loadings)
}

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: OLS regression recovers loadings only for exact/noiseless linear combinations

recover_esem_loadings recovers the loading matrix as OLS regression coefficients of each indicator on the factor scores (fit.rs). This exactly recovers the true loadings only when the indicator is an exact linear combination of the provided factors (the noiseless test setup). With measurement error or omitted factors, the OLS coefficients are the best linear predictor, not necessarily the structural ESEM loadings — and the crate does no rotation or posterior propagation. The docstrings and esem-dsem-fit.md explicitly scope this out, so it is not a bug, but consumers should not treat these coefficients as full ESEM loadings on noisy data.

Open in 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