tidaldb/.sdlc/features/m9-community-profile-sync/design.md

3.4 KiB

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

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:

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.