Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
3b0f46e
test(membership): reject unsupported CWC membership structures (#604)
seonghobae Sep 19, 2026
02ebd23
feat(membership): add owner-issued single-membership admission (#604)
seonghobae Sep 19, 2026
efcdc60
feat(membership): expose single-membership admission failure (#604)
seonghobae Sep 19, 2026
f4ca5a6
feat(membership): export single-membership owner admission (#604)
seonghobae Sep 19, 2026
2233c2c
test(membership): expose rejected disjoint re-entry
seonghobae Sep 19, 2026
71c85c7
fix(membership): preserve disjoint same-identity re-entry
seonghobae Sep 19, 2026
86808b8
test(membership): cover reverse-order re-entry
seonghobae Sep 19, 2026
f5cc802
docs(membership): align duplicate error with interval semantics
seonghobae Sep 19, 2026
ce85ad1
test(membership): reject super-unit affiliation weights (#607)
seonghobae Sep 19, 2026
2843a28
test(membership): fail closed on invalid deserialized weights (#607)
seonghobae Sep 19, 2026
32f6973
fix(membership): bound affiliation weights to unit interval (#607)
seonghobae Sep 19, 2026
247e290
docs(membership): state bounded weight invariant (#607)
seonghobae Sep 19, 2026
f7d6817
docs(membership): currentize invalid-weight error boundary (#607)
seonghobae Sep 19, 2026
9928f99
refactor(membership): align validated serde path with repository styl…
seonghobae Sep 19, 2026
23a8af5
test(membership): refuse partial rows as nested ICC structure (#608)
seonghobae Sep 19, 2026
4b65bd3
fix(membership): refuse partial weights in nested ICC classification …
seonghobae Sep 19, 2026
7e52f86
test(membership): preserve full cross-classification boundary (#608)
seonghobae Sep 19, 2026
cb2abf7
test(membership): expose mixed MMMC classification loss #609
seonghobae Sep 19, 2026
edaddb1
fix(membership): preserve mixed MMMC design classification #609
seonghobae Sep 19, 2026
b653eed
test(membership): keep pure cross-classification control full-weight …
seonghobae Sep 19, 2026
49bce45
test(membership): reject overfull same-role share budget (#610)
seonghobae Sep 19, 2026
01c6f72
fix(membership): enforce overlapping role share budget (#610)
seonghobae Sep 19, 2026
fa91ab8
test(membership): pin aggregate role-share budget edge cases (#610)
seonghobae Sep 19, 2026
766dfaf
style(membership): align role-share repair formatting
seonghobae Sep 19, 2026
1f2b113
test(membership): reject heterogeneous roles in nested ICC (#611)
seonghobae Sep 19, 2026
29d38e3
fix(membership): preserve population classification dimension (#611)
seonghobae Sep 19, 2026
2b87d3e
test(membership): preserve heterogeneous mixed design signal (#611)
seonghobae Sep 19, 2026
ab37ccb
test(membership): reject rounded overfull share budgets (#612)
seonghobae Sep 19, 2026
2c27874
fix(membership): compare share budgets exactly (#612)
seonghobae Sep 19, 2026
5fe191b
refactor(membership): avoid lossy exponent cast (#612)
seonghobae Sep 19, 2026
3ee43cc
test(membership): reject zero-share affiliation
seonghobae Sep 19, 2026
064d5e3
fix(membership): reject zero-share active assignments
seonghobae Sep 19, 2026
c7defa2
docs(membership): align public weight domain with zero-share refusal
seonghobae Sep 19, 2026
e84e38b
test(membership): RED longitudinal observation design owner contract …
seonghobae Sep 21, 2026
91222b5
feat(membership): add missing observation membership refusal (#640)
seonghobae Sep 21, 2026
faed96e
feat(membership): classify longitudinal observation support (#640)
seonghobae Sep 21, 2026
4f21fc4
feat(membership): export longitudinal observation design contract (#640)
seonghobae Sep 21, 2026
8500104
test(membership): cover longitudinal observation accessors (#640)
seonghobae Sep 21, 2026
ff2270a
docs(changelog): record longitudinal membership support contract (#640)
seonghobae Sep 21, 2026
708642e
docs(research): trace longitudinal membership support semantics (#640)
seonghobae Sep 21, 2026
3f044fd
test(membership): RED owner design wire vocabulary (#641)
seonghobae Sep 21, 2026
3721751
feat(membership): own design wire vocabulary (#641)
seonghobae Sep 21, 2026
09e4320
feat(membership): type unknown design wire refusal (#641)
seonghobae Sep 21, 2026
9cf7ac3
feat(membership): compile owner design wire contract (#641)
seonghobae Sep 21, 2026
1c9f1c8
test(membership): RED explicit design vocabulary version (#641)
seonghobae Sep 21, 2026
1fad4ef
feat(membership): version owner design wire vocabulary (#641)
seonghobae Sep 21, 2026
544cfb9
feat(membership): export design wire version (#641)
seonghobae Sep 21, 2026
3b27cba
docs(membership): record design wire owner contract (#641)
seonghobae Sep 21, 2026
caa1473
test(membership): RED bind design version and name (#642)
seonghobae Sep 21, 2026
b0cb903
feat(membership): bind design wire coordinate (#642)
seonghobae Sep 21, 2026
a9eccde
feat(membership): export design wire coordinate (#642)
seonghobae Sep 21, 2026
ccecb46
docs(changelog): record design wire coordinate (#642)
seonghobae Sep 21, 2026
5517890
test(membership): RED require owner-derived design coordinate (#643)
seonghobae Sep 21, 2026
951a9e1
fix(membership): issue wire coordinates from owner state (#643)
seonghobae Sep 21, 2026
3d10f8e
feat(membership): export owner-issued wire classifier (#643)
seonghobae Sep 21, 2026
8d2f836
docs(changelog): require owner-issued design coordinates (#643)
seonghobae Sep 21, 2026
a574f01
test(membership): require distinct owner classification authority (#644)
seonghobae Sep 21, 2026
7470d6c
fix(membership): separate parsed wire from owner classification (#644)
seonghobae Sep 21, 2026
16a6d87
feat(membership): export typed classification authority (#644)
seonghobae Sep 21, 2026
25bb4e7
docs(changelog): record membership authority type boundary (#644)
seonghobae Sep 21, 2026
aef1fa2
docs(changelog): align wire coordinate with authority boundary (#644)
seonghobae Sep 21, 2026
1af5d3a
test(membership): bind classification to event-time support (#645)
seonghobae Sep 21, 2026
35ed8b1
fix(membership): retain classified support window (#645)
seonghobae Sep 21, 2026
fa8ffdf
docs(changelog): record membership support-window binding (#645)
seonghobae Sep 21, 2026
7a71fef
test(membership): bind exact observation support identity (#646)
seonghobae Sep 21, 2026
4c27462
build(membership): add sha2 for support provenance (#646)
seonghobae Sep 21, 2026
76ac744
fix(membership): bind canonical support digest (#646)
seonghobae Sep 21, 2026
2f34c69
feat(membership): export support digest contract (#646)
seonghobae Sep 21, 2026
35e5578
test(membership): assert support digest version (#646)
seonghobae Sep 21, 2026
c7740c8
docs(changelog): record canonical membership support digest (#646)
seonghobae Sep 21, 2026
e36dae4
refactor(membership): keep digest framing architecture-stable (#646)
seonghobae Sep 21, 2026
477bea0
test(membership): cover support topology canonicalization (#646)
seonghobae Sep 21, 2026
539c5ee
refactor(membership): canonicalize only reachable topology keys (#646)
seonghobae Sep 21, 2026
ba3d8a6
test(membership): require reconstructable support wire (#649)
seonghobae Sep 21, 2026
b7d3dad
feat(membership): add reconstructable support wire (#649)
seonghobae Sep 21, 2026
bfc3936
fix(membership): harden support wire implementation (#649)
seonghobae Sep 21, 2026
d4e0593
feat(membership): export reconstructable support wire (#649)
seonghobae Sep 21, 2026
9ab91b2
test(membership): align support wire contract fixture (#649)
seonghobae Sep 21, 2026
5584129
docs(membership): record reconstructable support wire (#649)
seonghobae Sep 21, 2026
d8e70c2
test(membership): reject detached support design labels (#649)
seonghobae Sep 21, 2026
1716b2f
refactor(membership): share canonical design signals with support wir…
seonghobae Sep 21, 2026
b6adb39
fix(membership): validate support wire design against topology (#649)
seonghobae Sep 21, 2026
df4026b
docs(membership): record topology-bound support wire (#649)
seonghobae Sep 21, 2026
e425c70
test(membership): cover support wire accessors (#649)
seonghobae Sep 21, 2026
0355549
test(membership): require owner-issued input support coordinates (#650)
seonghobae Sep 21, 2026
e2e8eb2
feat(membership): issue input support coordinates (#650)
seonghobae Sep 21, 2026
ce4d068
feat(membership): export support coordinate mapping (#650)
seonghobae Sep 21, 2026
2eca153
docs(membership): record owner-issued input coordinate mapping (#650)
seonghobae Sep 21, 2026
2774e1c
test(membership): reject unreachable support-wire states (#651)
seonghobae Sep 21, 2026
fff822d
refactor(membership): share exact weight-budget predicate (#651)
seonghobae Sep 21, 2026
8a73734
fix(membership): validate support wire against admission invariants (…
seonghobae Sep 21, 2026
3177e98
docs(membership): record reachable support-wire invariants (#651)
seonghobae Sep 21, 2026
028a066
test(membership): reject detached support digest (#652)
seonghobae Sep 21, 2026
4e5b64e
fix(membership): bind support wire content digest (#652)
seonghobae Sep 21, 2026
39ef9f7
feat(membership): export support wire digest contract (#652)
seonghobae Sep 21, 2026
a66613c
test(membership): cover recomputable support wire digest (#652)
seonghobae Sep 21, 2026
32871b5
docs(membership): distinguish source and wire support digests (#652)
seonghobae Sep 21, 2026
0bee645
test(membership): reject contradictory duplicate support topology (#653)
seonghobae Sep 21, 2026
8cd8a6d
fix(membership): enforce duplicate support coordinate determinism (#653)
seonghobae Sep 21, 2026
ef50885
docs(changelog): record duplicate support coordinate guard (#653)
seonghobae Sep 21, 2026
f5670df
style(membership): restore support wire source newline (#653)
seonghobae Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.d/membership-design-authority-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Membership

- Separate deserialized `MembershipDesignWire` coordinates from owner-derived longitudinal classification authority. `classify_membership_observations_wire` now returns `MembershipDesignClassification`, whose private state can only be issued after canonical Membership classification at each observation event time; parsing a supported `{version, name}` pair remains a non-authoritative serialization boundary.
5 changes: 5 additions & 0 deletions CHANGELOG.d/membership-design-support-window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Changed

- Bind owner-derived longitudinal Membership design classifications to the exact classified support cardinality and its earliest/latest event times. These coordinates are derived from the same `MembershipObservation` slice used by canonical classification and are not caller-supplied.
- Keep parsed `MembershipDesignWire` values free of analytical authority. Support count and time bounds belong only to `MembershipDesignClassification` issued after canonical Membership classification.
- Treat count plus event-time bounds as provenance coordinates, not complete cohort reconstruction: different supports can share the same summary, so released analysis projections still require privacy-appropriate observation-support evidence and exact binding where scientific reconstruction depends on it.
1 change: 1 addition & 0 deletions CHANGELOG.d/membership-design-wire-coordinate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `membership_core::MembershipDesignWire` binds the Membership-owned design vocabulary version and design name as one typed serialization coordinate. Supported names round-trip only under `tepp.membership_design.v1`; mismatched versions fail with `UnsupportedWireVersion` and unknown names fail with `UnknownMembershipDesign`. Parsed wire coordinates are non-authoritative. New analytical classification authority is returned separately as `MembershipDesignClassification` by `classify_membership_observations_wire`, which derives it from canonical Membership state; consumers may project its `.wire()` only after that owner-side classification.
1 change: 1 addition & 0 deletions CHANGELOG.d/membership-design-wire-vocabulary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `membership_core::MembershipDesign` now owns a versioned, closed wire vocabulary (`tepp.membership_design.v1`) with exact snake_case names and fail-closed parsing. Longitudinal consumers can bind the owner-derived design without reimplementing Membership semantics or accepting caller-asserted design labels.
1 change: 1 addition & 0 deletions CHANGELOG.d/membership-longitudinal-observation-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `membership_core` now classifies cross-classified, multiple-membership, and classification-heterogeneous structure over explicit `(MemberId, EventTime)` observation support. Every requested observation must have active Membership-owned context at its own event time; missing rows fail closed instead of disappearing into an atomistic longitudinal design. This is a domain admission contract, not a new estimator or a consumer-authorized design label.
5 changes: 5 additions & 0 deletions CHANGELOG.d/membership-observation-support-digest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Changed

- Bind each owner-derived longitudinal Membership classification to a versioned SHA-256 identity for the exact opaque observation multiset and the active Membership topology resolved at each observation event time.
- Canonicalize observation and active-assignment ordering while preserving duplicate observation multiplicity; frame variable-length fields and hash exact binary64 membership-weight bits so equivalent input ordering cannot change support identity and distinct same-window supports cannot alias silently.
- Keep the support digest as provenance identity only. Released analytical contracts must still carry privacy-appropriate, reconstructable event-time/window/cohort/support coordinates and verify that representation against the owner digest rather than treating a digest as reconstruction evidence.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Membership support duplicate-coordinate determinism

`tepp.membership_observation_support_projection.v1` now rejects a parsed payload when the same projection-local `(member_ordinal, event_time)` coordinate is repeated with different active Membership assignments. Canonical owner state resolves one topology at one member/event-time coordinate, so contradictory duplicate rows are unreachable even when each row is individually well formed and the aggregate design label remains plausible.

Byte-identical duplicate rows remain valid and multiplicity-sensitive. This repair does not deduplicate observations, change the wire schema, make parsed wire authoritative, or alter Membership estimator arithmetic.
8 changes: 8 additions & 0 deletions CHANGELOG.d/membership-observation-support-wire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Changed

- Add a Membership-owned `tepp.membership_observation_support_projection.v1` wire projection for longitudinal support. The owner path resolves every observation at its own event time, preserves repeated-member/repeated-group linkage through projection-local ordinals, retains role and exact binary64 weight bits, and carries the source-support digest issued by canonical Membership classification.
- The opaque-source `support_sha256` remains an owner provenance fingerprint and is intentionally not recomputable after raw `MemberId`/`GroupId` UUID redaction. A separate versioned `tepp.membership_observation_support_projection_digest.v1` SHA-256 is recomputed from the complete privacy-reduced projection state, including the source-support digest, so valid-looking source digests and reconstructable coordinates cannot drift independently under the supported parser contract. This content digest is not a signature and does not turn parsed bytes into owner authority.
- Canonical JSON omits raw `MemberId`/`GroupId` UUID text and rejects unsupported versions, unknown fields, timezone aliases, malformed weights, noncanonical ordering, invalid local-ordinal spaces, detached wire-content digests, and design labels that do not match the reconstructed role/group/weight topology. The parser reuses Membership's canonical design-signal classifier rather than maintaining a second topology interpretation.
- Parsed support rows are also constrained to states reachable through canonical Membership admission: a role/group edge cannot occur twice at one observation, and known same-role shares may not exceed unity under the same exact represented-binary64 budget predicate used by `MembershipNetwork`.
- The owner projection also returns a non-wire coordinate slice parallel to the caller's admitted observation order, containing only the Membership-issued local member ordinal and exact event time. Downstream owners can bind their own document/evidence identity without re-sorting raw member IDs; canonical support bytes remain input-order insensitive and duplicate observations remain multiplicity-sensitive.
- This is privacy reduction for scientific reconstruction, not anonymity. Parsed wire values remain non-authoritative serialization data; new analytical authority is issued only through canonical Membership state.
1 change: 1 addition & 0 deletions crates/membership_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ publish = false
[dependencies]
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
temporal_core = { path = "../temporal_core", version = "=0.2.0" }
uuid.workspace = true

Expand Down
85 changes: 85 additions & 0 deletions crates/membership_core/src/admission.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Fail-closed admission for estimators that only support full single membership.

use crate::{
GroupId, MemberId, MembershipError, MembershipNetwork, MembershipRole, MembershipWeight,
};
use temporal_core::EventTime;

/// Owner-issued proof that one member has exactly one active, full-weight membership.
///
/// This value is intentionally constructed only from [`MembershipNetwork`] state. Consumers that
/// implement a nested or single-membership estimator can require this admission instead of trusting
/// a caller-selected cluster key or silently flattening cross-classified / multiple-membership
/// structure.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SingleMembershipAdmission {
member_id: MemberId,
group_id: GroupId,
role: MembershipRole,
weight: MembershipWeight,
event_time: EventTime,
}

impl SingleMembershipAdmission {
/// Return the admitted member identity.
#[must_use]
pub const fn member_id(self) -> MemberId {
self.member_id
}

/// Return the sole active group identity.
#[must_use]
pub const fn group_id(self) -> GroupId {
self.group_id
}

/// Return the sole active contextual role.
#[must_use]
pub const fn role(self) -> MembershipRole {
self.role
}

/// Return the full membership weight.
#[must_use]
pub const fn weight(self) -> MembershipWeight {
self.weight
}

/// Return the event time at which membership was admitted.
#[must_use]
pub const fn event_time(self) -> EventTime {
self.event_time
}
}

/// Admit one member for an estimator whose declared estimand supports only full single membership.
///
/// The admission is derived from canonical [`MembershipNetwork`] state. It fails closed when the
/// member is inactive, has more than one simultaneous active assignment (including same-group
/// cross-role assignments), or has a partial membership weight. A future cross-classified or
/// weighted estimator must use a separate versioned admission path rather than reusing this proof.
///
/// # Errors
///
/// Returns [`MembershipError::SingleMembershipProfileInapplicable`] whenever the active membership
/// structure cannot be represented without loss by a full single-membership estimator.
pub fn admit_single_membership(
network: &MembershipNetwork,
member_id: MemberId,
event_time: EventTime,
) -> Result<SingleMembershipAdmission, MembershipError> {
let active = network.active_memberships_for(member_id, event_time);
let [assignment] = active.as_slice() else {
return Err(MembershipError::SingleMembershipProfileInapplicable);
};
if assignment.weight().value().to_bits() != 1.0_f64.to_bits() {
return Err(MembershipError::SingleMembershipProfileInapplicable);
}
Ok(SingleMembershipAdmission {
member_id,
group_id: assignment.group_id(),
role: assignment.role(),
weight: assignment.weight(),
event_time,
})
}
Loading
Loading