Repository navigation
membership: reject super-unit affiliation weights before longitudinal composition #607
Description
Finding
membership_core::MembershipWeight::new on protected main@a243f18da4a4ca8a8d068c39922537f1f8ed6ad0 accepts any finite non-negative f64, including values greater than 1.0. That contradicts the type's own scientific/domain contract: 1.0 is full affiliation and values in (0, 1) are partial multiple membership. A super-unit value such as 1.25 therefore enters canonical Membership state even though it is not a bounded affiliation share.
Review of the type exposed a second path through the same invariant: MembershipWeight derived transparent Deserialize, so JSON such as 1.25 or -0.25 reconstructed the private tuple field without calling MembershipWeight::new. Constructor-only validation would therefore leave a wire/domain bypass.
This matters for #604/#605. SingleMembershipAdmission already rejects anything except exact 1.0, but the canonical Membership owner still admitted invalid weights that a future weighted/cross-classified estimator or active_weight_by_role could consume. The correct owner invariant is a finite share in the closed unit interval [0, 1]; callers must not be silently clamped, normalized, or rescaled.
Ordinary-forward RED → repair lineage on #605
- RED
ce85ad1ce1b08e9f1e23ac4e23a08b1565493698expands the owner unit contract:0.0,0.5, and1.0remain valid, while1.0 + EPSILON, negative, infinities, and NaN must fail closed. - Wire/domain RED
2843a282ce81d691a60e734946b7814245c126c0requires invalid JSON weights to fail instead of bypassing the private-field constructor invariant. - Causal repair
32f6973cfe29a157ef370048f7ba83fa0a6fdd2fconstrainsMembershipWeight::newto finite[0, 1]and replaces derivedDeserializewith validation-preserving deserialization through the canonical constructor. It does not normalize, clamp, round, change feat(membership): add owner-issued single-membership admission #605 admission semantics, or add estimator arithmetic. - Doctoring
247e29070620faaf43a1d755e03350a19ed79589→f7d68179036e0f7e489b6e880290073820f03699currentizes the public export/error rustdoc. - Repository-style cleanup
9928f99bc37ee7584f45f0a31f47da44bfdff0eauses the existingserde::de::Error::custompattern without changing the rule.
The surviving Membership vehicle has since advanced for separate issue #608. Current Draft #605 exact head is 7e52f864e3294f412075916629a21e6799ed6888, base main@a243f18..., ahead 17 / behind 0. #607's valid source/test/doc delta survives on that head; exact-head evidence and review must therefore be acquired there rather than transferred from a predecessor.
Acceptance
Keep open until the surviving #605 head has exact Rust/rustdoc/tests/owned line+branch+edge coverage/security/CodeQL/Semgrep, qualifying independent approval, ordinary protected-main landing, and verified downstream inheritance by #604. No force push, destructive rebase, skip/xfail, tolerance weakening, normalization workaround, or gate weakening is authorized.
Activity
test(membership): reject super-unit affiliation weights (#607)
test(membership): fail closed on invalid deserialized weights (#607)
fix(membership): bound affiliation weights to unit interval (#607)
docs(membership): state bounded weight invariant (#607)
docs(membership): currentize invalid-weight error boundary (#607)
refactor(membership): align validated serde path with repository style (
seonghobae commentedon Sep 19, 2026
Surviving owner-head update: #605 is now exact
b653eed5963efde246a9224078e1a51d979c53e6after separate #608/#609 scientific findings. #607's bounded[0,1]constructor/serde invariant remains inherited on that head without normalization or clamp. Exact-head Rust/security/CodeQL/Semgrep plus independent review must be reacquired onb653eed...; predecessor receipts are diagnostic only.