# 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::"`) - **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::"`: ``` 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>, } impl CommunityMembershipIndex { pub fn add(&self, user_id: u64, community: &str); pub fn get(&self, user_id: u64) -> Vec; 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.