65 lines
3.3 KiB
Markdown
65 lines
3.3 KiB
Markdown
# 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:
|
|
|
|
1. **Stop forwarding** — new signals from this user no longer flow to community aggregates.
|
|
2. **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:
|
|
|
|
1. Atomically sets a durable **community membership flag** for the user to `Left`, recording the leave timestamp as nanoseconds since Unix epoch.
|
|
2. Gates the `try_cohort_attribution` and `try_community_forwarding` paths in `signal_with_context` so signals from users with status `Left` skip cohort fan-out.
|
|
3. 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.
|
|
4. Provides `db.community_layer_status(user_id)` to query current status.
|
|
|
|
## Scope
|
|
|
|
**In scope:**
|
|
- `CommunityMembership` struct: `{ user_id, status: Active | Left, left_at_ns: Option<u64> }`
|
|
- Durable persistence via `Tag::CommunityLeave = 0x11` in the users storage backend
|
|
- Gate in `try_cohort_attribution` AND `try_community_forwarding`: check membership status before fan-out
|
|
- `db.leave_community_layer(user_id)` — sets status to `Left`, records timestamp
|
|
- `db.rejoin_community_layer(user_id)` — sets status to `Active`, preserves leave history
|
|
- `db.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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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 DashMap `insert`. Not on the hot path.
|