104 lines
3.4 KiB
Markdown
104 lines
3.4 KiB
Markdown
# Design: Community Profile Sync
|
|
|
|
## Overview
|
|
|
|
Community Profile Sync adds opt-in routing of user engagement signals to named community personalization aggregates. When a user joins a community, their future `signal_with_context` writes are forwarded to the community's aggregate — a named partition of the existing `CohortSignalLedger` — in addition to the global ledger.
|
|
|
|
The design reuses existing infrastructure wherever possible:
|
|
- **Aggregate storage**: `CohortSignalLedger` (keyed by `"community::<name>"`)
|
|
- **Persistence layer**: `users_engine` fjall partition (new `Tag::CommunityMembership = 0x0E`)
|
|
- **Forwarding hook**: `try_community_forwarding()` called from `signal_with_context`
|
|
|
|
---
|
|
|
|
## Storage Layout
|
|
|
|
### Membership Records
|
|
|
|
```
|
|
Key: encode_key(EntityId::new(user_id), Tag::CommunityMembership, community_name.as_bytes())
|
|
Value: member_since_ns as [u8; 8] (little-endian u64)
|
|
```
|
|
|
|
**Tag assignment:** `Tag::CommunityMembership = 0x0E`
|
|
|
|
### Community Signal Aggregates
|
|
|
|
Aggregates are stored in the existing `CohortSignalLedger` with the key prefix `"community::<name>"`:
|
|
|
|
```
|
|
CohortSignalLedger key: ("community::jazz", entity_id, signal_type_id)
|
|
```
|
|
|
|
---
|
|
|
|
## New Components
|
|
|
|
### `Tag::CommunityMembership = 0x0E`
|
|
|
|
Added to `tidal/src/storage/keys.rs`.
|
|
|
|
### `CommunityMembershipIndex`
|
|
|
|
New module: `tidal/src/entities/community.rs`
|
|
|
|
```rust
|
|
pub struct CommunityMembershipIndex {
|
|
memberships: DashMap<u64, Vec<String>>,
|
|
}
|
|
|
|
impl CommunityMembershipIndex {
|
|
pub fn add(&self, user_id: u64, community: &str);
|
|
pub fn get(&self, user_id: u64) -> Vec<String>;
|
|
pub fn contains(&self, user_id: u64, community: &str) -> bool;
|
|
pub fn entry_count(&self) -> usize;
|
|
}
|
|
```
|
|
|
|
### `db/community.rs`
|
|
|
|
New module with public methods on `TidalDb`: `join_community`, `get_community_memberships`, `is_community_member`.
|
|
|
|
### `try_community_forwarding`
|
|
|
|
Private method in `db/signals.rs`, called from `signal_with_context` after `try_cohort_attribution`:
|
|
|
|
```rust
|
|
fn try_community_forwarding(&self, signal_type, entity_id, weight, timestamp, user_id) {
|
|
let memberships = self.community_membership.get(user_id);
|
|
if memberships.is_empty() { return; }
|
|
let Ok(type_id) = self.cohort_ledger.resolve_signal_type(signal_type) else { return; };
|
|
let ts_ns = timestamp.as_nanos();
|
|
for name in &memberships {
|
|
self.cohort_ledger.record(&format!("community::{name}"), entity_id, type_id, weight, ts_ns);
|
|
}
|
|
}
|
|
```
|
|
|
|
### `rebuild_community_memberships`
|
|
|
|
Added to `tidal/src/db/state_rebuild.rs`, called during open:
|
|
- Scans `Tag::CommunityMembership` keys from users_engine
|
|
- Populates `CommunityMembershipIndex`
|
|
|
|
---
|
|
|
|
## Changes by File
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `tidal/src/storage/keys.rs` | Add `Tag::CommunityMembership = 0x0E`; update `from_byte`, tests |
|
|
| `tidal/src/entities/community.rs` | New: `CommunityMembershipIndex` |
|
|
| `tidal/src/entities/mod.rs` | `pub mod community;` + re-export |
|
|
| `tidal/src/db/mod.rs` | Add `community_membership` field; wire in constructors |
|
|
| `tidal/src/db/community.rs` | New: `join_community`, `get_community_memberships`, `is_community_member` |
|
|
| `tidal/src/db/signals.rs` | Call `try_community_forwarding` in `signal_with_context` |
|
|
| `tidal/src/db/state_rebuild.rs` | `rebuild_community_memberships` called during open |
|
|
| `tidal/tests/m9_community_sync.rs` | New: 10 integration tests |
|
|
|
|
---
|
|
|
|
## Implementation Status
|
|
|
|
COMPLETE — All components implemented. 1299 lib tests pass. 10 integration tests pass. fmt/clippy clean.
|