Skip to content

feat(membership): recover nested ICC and refuse cross-classified collapse - #117

Merged
seonghobae merged 4 commits into
mainfrom
agent/membership-nested-icc-gate
Aug 24, 2026
Merged

feat(membership): recover nested ICC and refuse cross-classified collapse#117
seonghobae merged 4 commits into
mainfrom
agent/membership-nested-icc-gate

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

Summary

ADR 0003 increment from protected main (40adac9 / #44). Review wait and queued Checks on #45 are local; this slice does not recreate psychometric_core (#49), compute_backend (#51), or encrypted mapping persistence (waits for #45 / 0008).

  • CPU f64 unbalanced ANOVA recovers a known nested ICC (1/4) with computed RMSE versus that truth.
  • Zero within-cluster residual recovers ICC 1; equal cluster means recover ICC 0.
  • Cross-classified (two roles) and multiple-membership (two groups in one role) designs return NestedIccInapplicable.
  • Empty, inactive, duplicate, unknown, singleton-only, and constant samples fail closed.
  • No new migration (0007 remains owned by feat(persistence): retention deletion legal-hold SQL contracts (0007) #45).

Claim boundary

  • Membership design gate and nested ICC only. Not a full MMMC/HLM/ESEM/DSEM fit, not causal identification, not a GDPR/SOC 2/CSAP claim.

Test plan

  • RED: nested_icc_recovery_contract failed to compile (E0432/E0599) before the public API existed
  • cargo test -p membership_core --offline --lib --tests --doc
  • cargo clippy -p membership_core --all-targets --offline -- -D warnings
  • python3 scripts/check_docstrings.py, check_workspace_contract.py, validate_documentation.py
  • membership_core line coverage 188/188 and nightly branch coverage 50/50
  • Exact-head CI; independent review

…apse

Add a CPU f64 unbalanced ANOVA ICC for nested membership designs and fail
closed when the active design is cross-classified or multiple-membership.
No new migration (0007 remains owned by #45).
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 22 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: ca2192c2-9d88-4e3c-93ca-a05382bb4494

📥 Commits

Reviewing files that changed from the base of the PR and between c9da020 and d579229.

📒 Files selected for processing (12)
  • ARCHITECTURE.md
  • CHANGELOG.md
  • crates/membership_core/src/error.rs
  • crates/membership_core/src/icc.rs
  • crates/membership_core/src/lib.rs
  • crates/membership_core/tests/nested_icc_recovery_contract.rs
  • docs/TRACEABILITY.md
  • docs/adr/0003-relational-event-multiple-membership.md
  • docs/adr/README.md
  • docs/research/nested-icc-cross-classified-refusal.md
  • docs/research/standards-and-literature.md
  • docs/validation/temporal-event-foundation.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.

@seonghobae
seonghobae marked this pull request as ready for review August 20, 2026 18:57
@seonghobae

Copy link
Copy Markdown
Contributor Author

Current HEAD 293067ea08b5cce0a4484c73282fed4d43af6991 includes current main and the CPU f64 nested-ICC recovery/cross-classification refusal contract. Verified at this HEAD: cargo fmt --all -- --check; cargo test -p membership_core --offline (17 tests including RMSE/ICC recovery, temporal membership, and refusal edge cases); 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 293067e:

  • Nested ICC recovery remains explicit, while cross-classified structure is refused when a caller collapses it into a single hierarchy.
  • 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

Exact-head review receipt for PR #117 after normal main synchronization.

Current PR head: 8a5a3fa
Base protected main: c45be17

The branch was synchronized with protected main through a normal merge. The only merge conflict was CHANGELOG.md; both the nested ICC capability and naruon loopback capability were retained. Inherited documentation whitespace was corrected so the exact-head diff is clean.

Local proof at this exact HEAD: Rust 1.97.1 cargo fmt --check; cargo test --workspace --offline; documentation validation; workspace contract; git diff --check; CodeGraph sync; CodeReviewGraph update against protected main.

@cwl-noema-review @opencode-agent Please issue a formal independent review verdict for exactly 8a5a3fa. Review only; do not modify files, push, approve on behalf of another identity, or merge.

@seonghobae
seonghobae enabled auto-merge (squash) August 24, 2026 00:57
…ed-icc-gate

# Conflicts:
#	CHANGELOG.md
#	docs/TRACEABILITY.md
#	docs/adr/0003-relational-event-multiple-membership.md
@seonghobae
seonghobae merged commit 1808151 into main Aug 24, 2026
25 of 27 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 5 new potential issues.

Open in Devin Review

Comment on lines +120 to +126
match classify_members(network, instant, outcome_members.iter().copied())? {
MembershipDesign::Nested => {}
MembershipDesign::CrossClassified | MembershipDesign::MultipleMembership => {
return Err(MembershipError::NestedIccInapplicable);
}
}
anova_nested_icc(&groups)

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: Design classification differs in scope between public and ICC paths

classify_membership_design at icc.rs classifies over ALL members in the network, whereas the internal gate in nested_intraclass_correlation (icc.rs) classifies only over the members that supplied outcomes. This is intentional (the ICC only concerns members with outcomes), but it means a network the public classifier reports as CrossClassified can still yield a valid nested ICC if the supplied outcome members happen to each be singly nested. Reviewers relying on the public classifier as a pre-check should be aware the two answers can legitimately diverge.

Open in Devin Review

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

Comment on lines +108 to +127
for outcome in outcomes {
if !seen.insert(outcome.member_id()) {
return Err(MembershipError::DuplicateOutcomeMember);
}
let active = network.active_memberships_for(outcome.member_id(), instant);
if active.is_empty() {
return Err(MembershipError::UnknownOutcomeMember);
}
outcome_members.insert(outcome.member_id());
let group = active[0].group_id();
groups.entry(group).or_default().push(outcome.value());
}
match classify_members(network, instant, outcome_members.iter().copied())? {
MembershipDesign::Nested => {}
MembershipDesign::CrossClassified | MembershipDesign::MultipleMembership => {
return Err(MembershipError::NestedIccInapplicable);
}
}
anova_nested_icc(&groups)
}

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: Nested ICC gate builds cluster groups before design verification

In nested_intraclass_correlation (icc.rs), the per-group value map is populated using active[0].group_id() for each outcome member before classify_members confirms the design is nested. This is safe: if the design is cross-classified or multiple-membership, the function returns NestedIccInapplicable and the partially-built groups map is discarded. And when the design is Nested, each member has exactly one active membership (single role, single group), so active[0] is unambiguous. No ordering dependence on active_memberships_for's return order affects the result in the nested case.

Open in Devin Review

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

# ADR 0003 — Relational event ontology and time-varying multiple membership

**Decision status:** Accepted
**Implementation maturity:** partial — membership network, event mention/instance separation, and Kish ESS implemented-main; nested ICC with cross-classified/multiple-membership refusal is this increment; full multilevel/MMMC estimators and remaining persistence remain accepted-target

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Third Implementation maturity header stacked in ADR 0003

The PR adds another **Implementation maturity:** line to 0003-relational-event-multiple-membership.md on top of two already present, leaving three contradictory maturity headers in one ADR. The same stacking exists in the ADR README rows and appears to be an artifact of the stacked-PR workflow.

Open in Devin Review

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

Comment on lines +145 to +169
let mut groups_by_role: BTreeMap<MembershipRole, BTreeSet<crate::GroupId>> =
BTreeMap::new();
for assignment in active {
groups_by_role
.entry(assignment.role())
.or_default()
.insert(assignment.group_id());
}
for groups in groups_by_role.values() {
if groups.len() >= 2 {
return Ok(MembershipDesign::MultipleMembership);
}
}
if groups_by_role.len() >= 2 {
saw_cross = true;
}
}
if !saw_active {
return Err(MembershipError::InsufficientClusterStructure);
}
if saw_cross {
Ok(MembershipDesign::CrossClassified)
} else {
Ok(MembershipDesign::Nested)
}

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: Nested members in distinct role types treated as nested

classify_members at icc.rs only marks a design cross-classified when a single member holds two or more roles. If outcome members each hold exactly one membership but in different role types (e.g. one Author, one Department), the design is reported Nested and a nested ICC is computed by grouping on group_id. This is a degenerate population-level cross-classification that is not detected because cross-classification is defined per-member. Given the estimator groups purely by group id and each member is singly nested, this is defensible, but it is a subtle definitional choice worth confirming against ADR 0003 intent.

Open in Devin Review

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

Comment on lines +209 to +217
if sum_of_squares_between + sum_of_squares_within == 0.0 {
return Err(MembershipError::InsufficientClusterStructure);
}
let mean_square_between = sum_of_squares_between / (j - 1.0);
let mean_square_within = sum_of_squares_within / (n - j);
let harmonic_cluster_size = (n - sum_cluster_size_squared / n) / (j - 1.0);
let cluster_variance =
((mean_square_between - mean_square_within) / harmonic_cluster_size).max(0.0);
Ok(cluster_variance / (cluster_variance + mean_square_within))

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: Unbalanced ANOVA n₀ factor matches Snijders–Bosker and recovers balanced ICC exactly

harmonic_cluster_size = (n - sum_cluster_size_squared / n) / (j - 1.0) (icc.rs) is the Snijders–Bosker unbalanced cluster-size factor ñ = (M − Σn_j²/M)/(N−1). For the balanced 4×2 test it reduces to the per-cluster size (2), giving σ²_u=2/3, σ²_e=2, ICC=1/4, matching the contract test. All divisors are guarded: j-1 ≥ 1 (cluster_count ≥ 2), n-j > 0 (sample_size > cluster_count), and the denominator cluster_variance + mean_square_within can only be zero when both SSB and SSW are zero, which is caught by the earlier sum_of_squares_between + sum_of_squares_within == 0.0 guard. No division-by-zero or out-of-range ICC is reachable.

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