/** * Ranking Algorithm for sojorn Feed * * Design intent: * - Attention moves slowly. * - Nothing competes for dominance. * - Clean content is protected from suppression. * * Principles: * - Saves > likes (intentional curation over quick reaction) * - Steady appreciation over time > viral spikes * - Low block rate = content is not harmful * - Low trusted report rate = content is genuinely clean * - Ignore comment count (we don't reward arguments) * - Ignore report spikes on high-CIS posts (brigading protection) */ export interface PostForRanking { id: string; created_at: string; cis_score: number; tone_label: string; save_count: number; like_count: number; view_count: number; author_harmony_score: number; author_tier: string; blocks_received_24h: number; trusted_reports: number; total_reports: number; } /** * Calculate calm velocity score * Measures steady appreciation rather than viral spikes */ function calculateCalmVelocity(post: PostForRanking): number { const ageInHours = (Date.now() - new Date(post.created_at).getTime()) / (1000 * 60 * 60); if (ageInHours === 0 || post.view_count === 0) return 0; // Saves are weighted 3x more than likes const engagementScore = post.save_count * 3 + post.like_count; // Engagement rate (relative to views) const engagementRate = engagementScore / Math.max(post.view_count, 1); // Calm velocity = steady engagement over time (not spiky) // Using logarithmic scaling to prevent runaway viral effects const velocity = Math.log1p(engagementRate * 100) / Math.log1p(ageInHours + 1); return velocity; } /** * Calculate safety score * Lower score = content has triggered negative signals */ function calculateSafetyScore(post: PostForRanking): number { let score = 1.0; // Penalize if blocks received in last 24h if (post.blocks_received_24h > 0) { score -= post.blocks_received_24h * 0.2; } // Penalize trusted reports heavily if (post.trusted_reports > 0) { score -= post.trusted_reports * 0.3; } // Ignore report spikes if CIS is high (brigading protection) if (post.cis_score < 0.7 && post.total_reports > 2) { score -= 0.15; } return Math.max(score, 0); } /** * Calculate author influence multiplier * Based on harmony score and tier */ function calculateAuthorInfluence(post: PostForRanking): number { const harmonyMultiplier = post.author_harmony_score / 100; // 0-1 range const tierMultiplier: Record = { new: 0.5, trusted: 1.0, established: 1.3, restricted: 0.2, }; return harmonyMultiplier * (tierMultiplier[post.author_tier] || 1.0); } /** * Calculate final ranking score for sojorn feed */ export function calculateRankingScore(post: PostForRanking): number { // Base score from content integrity const cisBonus = post.cis_score; // Tone eligibility (handled in feed query, but can boost here) const toneBonus = post.tone_label === 'positive' ? 1.2 : post.tone_label === 'neutral' ? 1.0 : 0.8; // Calm velocity (steady appreciation) const velocity = calculateCalmVelocity(post); // Safety (no blocks or trusted reports) const safety = calculateSafetyScore(post); // Author influence const influence = calculateAuthorInfluence(post); // Final score const score = cisBonus * toneBonus * velocity * safety * influence; return score; } /** * Rank posts for feed * Returns sorted array with scores attached */ export function rankPosts(posts: PostForRanking[]): Array { return posts .map((post) => ({ ...post, rank_score: calculateRankingScore(post), })) .sort((a, b) => b.rank_score - a.rank_score); }