134 lines
3.6 KiB
TypeScript
134 lines
3.6 KiB
TypeScript
/**
|
|
* 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<string, number> = {
|
|
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<PostForRanking & { rank_score: number }> {
|
|
return posts
|
|
.map((post) => ({
|
|
...post,
|
|
rank_score: calculateRankingScore(post),
|
|
}))
|
|
.sort((a, b) => b.rank_score - a.rank_score);
|
|
}
|