3.3 KiB
3.3 KiB
Spec: Leave & Stop-Forward (m9-leave-revocation)
Problem
A user opts into a community personalization layer — their engagement signals fan out to shared cohort aggregates. When they leave, two things must happen immediately:
- Stop forwarding — new signals from this user no longer flow to community aggregates.
- Snapshot contribution boundary — record the timestamp at which forwarding ceased, so downstream retroactive-purge (m9-retroactive-purge) and re-materialization (m9-purge-rematerialization) can identify exactly which historical contributions belong to this user and up to what point.
Without this, a "leave" is silent — the system keeps accumulating the user's signal weight in community state indefinitely, violating the principle that personalization is user-owned and revocable (see VISION.md §Design Principles).
What This Feature Delivers
A single, explicit API call — db.leave_community_layer(user_id) — that:
- Atomically sets a durable community membership flag for the user to
Left, recording the leave timestamp as nanoseconds since Unix epoch. - Gates the
try_cohort_attributionandtry_community_forwardingpaths insignal_with_contextso signals from users with statusLeftskip cohort fan-out. - Exposes a re-enrollment path —
db.rejoin_community_layer(user_id)— that clears the gate and allows signal forwarding to resume. The previous leave snapshot is preserved in history so future purge requests can reference it. - Provides
db.community_layer_status(user_id)to query current status.
Scope
In scope:
CommunityMembershipstruct:{ user_id, status: Active | Left, left_at_ns: Option<u64> }- Durable persistence via
Tag::CommunityLeave = 0x11in the users storage backend - Gate in
try_cohort_attributionANDtry_community_forwarding: check membership status before fan-out db.leave_community_layer(user_id)— sets status toLeft, records timestampdb.rejoin_community_layer(user_id)— sets status toActive, preserves leave historydb.community_layer_status(user_id)— returns current status- In-memory
DashMap<u64, CommunityMembership>cache for O(1) gate check on the hot signal path - Restore membership map from durable storage on
TidalDb::from_parts
Out of scope:
- Retroactive purge of historical contributions (m9-retroactive-purge)
- Re-materialization of community aggregates (m9-purge-rematerialization)
- Community profile sync (m9-community-profile-sync)
API
pub fn leave_community_layer(&self, user_id: u64) -> crate::Result<()>;
pub fn rejoin_community_layer(&self, user_id: u64) -> crate::Result<()>;
pub fn community_layer_status(&self, user_id: u64) -> crate::Result<CommunityMembership>;
Data Model
pub enum MembershipStatus { Active, Left }
pub struct CommunityMembership {
pub user_id: u64,
pub status: MembershipStatus,
pub left_at_ns: Option<u64>,
}
Serialization: [status: 1 byte][left_at_ns: 8 bytes LE] — fixed 9-byte value.
Storage key: encode_key(EntityId::new(user_id), Tag::CommunityLeave, b"") in users storage.
Performance
- Gate check:
DashMap::get(user_id)— O(1), lock-free. Absent entry = Active (default). - leave/rejoin writes: one storage
put+ one DashMapinsert. Not on the hot path.