194 lines
5.9 KiB
TypeScript
194 lines
5.9 KiB
TypeScript
/**
|
|
* Harmony Score Calculation
|
|
*
|
|
* Design intent:
|
|
* - Influence adapts; people are not judged.
|
|
* - Guidance replaces punishment.
|
|
* - Fit emerges naturally.
|
|
*
|
|
* Philosophy:
|
|
* - Score is private, decays over time, and is reversible.
|
|
* - Never bans or removes beliefs.
|
|
* - Shapes distribution width, not access.
|
|
*
|
|
* Inputs:
|
|
* - Blocks received (pattern-based, not single incidents)
|
|
* - Trusted reports (from high-harmony users)
|
|
* - Category friction (posting to sensitive categories with low CIS)
|
|
* - Posting cadence (erratic spikes vs steady participation)
|
|
* - Rewrite prompts triggered (content rejected for tone)
|
|
* - False reports made (reports that were dismissed)
|
|
*
|
|
* Effects:
|
|
* - Shapes distribution width (reach)
|
|
* - Adds gentle posting friction if low
|
|
* - Limits Trending eligibility
|
|
*/
|
|
|
|
export interface HarmonyInputs {
|
|
user_id: string;
|
|
blocks_received_7d: number;
|
|
blocks_received_30d: number;
|
|
trusted_reports_against: number;
|
|
total_reports_against: number;
|
|
posts_rejected_7d: number; // rewrite prompts triggered
|
|
posts_created_7d: number;
|
|
false_reports_filed: number; // reports dismissed after review
|
|
validated_reports_filed: number; // reports confirmed after review
|
|
days_since_signup: number;
|
|
current_harmony_score: number;
|
|
current_tier: string;
|
|
}
|
|
|
|
export interface HarmonyAdjustment {
|
|
new_score: number;
|
|
delta: number;
|
|
reason: string;
|
|
new_tier: string;
|
|
}
|
|
|
|
/**
|
|
* Calculate harmony score adjustment based on recent behavior
|
|
*/
|
|
export function calculateHarmonyAdjustment(inputs: HarmonyInputs): HarmonyAdjustment {
|
|
let delta = 0;
|
|
const reasons: string[] = [];
|
|
|
|
// 1. Blocks received (pattern-based)
|
|
// Single block = minor signal. Pattern of blocks = strong negative signal.
|
|
if (inputs.blocks_received_7d >= 3) {
|
|
delta -= 10;
|
|
reasons.push('Multiple blocks received recently');
|
|
} else if (inputs.blocks_received_30d >= 5) {
|
|
delta -= 5;
|
|
reasons.push('Pattern of blocks over time');
|
|
}
|
|
|
|
// 2. Trusted reports
|
|
// Reports from high-harmony users are strong negative signals
|
|
if (inputs.trusted_reports_against >= 2) {
|
|
delta -= 8;
|
|
reasons.push('Multiple reports from trusted users');
|
|
} else if (inputs.trusted_reports_against === 1) {
|
|
delta -= 3;
|
|
reasons.push('Report from trusted user');
|
|
}
|
|
|
|
// 3. Content rejection rate (rewrite prompts)
|
|
// High rejection rate indicates persistent tone issues
|
|
const rejectionRate =
|
|
inputs.posts_created_7d > 0 ? inputs.posts_rejected_7d / inputs.posts_created_7d : 0;
|
|
|
|
if (rejectionRate > 0.3) {
|
|
delta -= 6;
|
|
reasons.push('High content rejection rate');
|
|
} else if (rejectionRate > 0.1) {
|
|
delta -= 2;
|
|
reasons.push('Some content rejected for tone');
|
|
}
|
|
|
|
// 4. False reports filed
|
|
// Filing false reports is harmful behavior
|
|
if (inputs.false_reports_filed >= 3) {
|
|
delta -= 7;
|
|
reasons.push('Multiple false reports filed');
|
|
} else if (inputs.false_reports_filed >= 1) {
|
|
delta -= 3;
|
|
reasons.push('False report filed');
|
|
}
|
|
|
|
// 5. Positive signals: Validated reports
|
|
// Accurate reporting helps the community
|
|
if (inputs.validated_reports_filed >= 3) {
|
|
delta += 5;
|
|
reasons.push('Helpful reporting behavior');
|
|
} else if (inputs.validated_reports_filed >= 1) {
|
|
delta += 2;
|
|
reasons.push('Validated report filed');
|
|
}
|
|
|
|
// 6. Time-based trust growth
|
|
// Steady participation without issues slowly builds trust
|
|
if (inputs.days_since_signup > 90 && delta >= 0) {
|
|
delta += 2;
|
|
reasons.push('Sustained positive participation');
|
|
} else if (inputs.days_since_signup > 30 && delta >= 0) {
|
|
delta += 1;
|
|
reasons.push('Consistent participation');
|
|
}
|
|
|
|
// 7. Natural decay toward equilibrium (50)
|
|
// Scores gradually drift back toward 50 over time
|
|
// This ensures old negative signals fade
|
|
if (inputs.current_harmony_score < 45) {
|
|
delta += 1;
|
|
reasons.push('Natural recovery over time');
|
|
} else if (inputs.current_harmony_score > 60) {
|
|
delta -= 1;
|
|
reasons.push('Natural equilibrium adjustment');
|
|
}
|
|
|
|
// 8. Calculate new score with bounds [0, 100]
|
|
const new_score = Math.max(0, Math.min(100, inputs.current_harmony_score + delta));
|
|
|
|
// 9. Determine tier based on new score
|
|
let new_tier: string;
|
|
if (new_score >= 75) {
|
|
new_tier = 'established';
|
|
} else if (new_score >= 50) {
|
|
new_tier = 'trusted';
|
|
} else if (new_score >= 25) {
|
|
new_tier = 'new';
|
|
} else {
|
|
new_tier = 'restricted';
|
|
}
|
|
|
|
return {
|
|
new_score,
|
|
delta,
|
|
reason: reasons.join('; ') || 'No significant changes',
|
|
new_tier,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get user-facing explanation of harmony score (without revealing the number)
|
|
*/
|
|
export function getHarmonyExplanation(tier: string, score: number): string {
|
|
if (tier === 'restricted') {
|
|
return 'Your posts currently have limited reach. This happens when content patterns trigger community concerns. Your reach will naturally restore over time with calm participation.';
|
|
}
|
|
|
|
if (tier === 'new') {
|
|
return 'Your posts reach a modest audience while you build trust. Steady participation and helpful contributions will gradually expand your reach.';
|
|
}
|
|
|
|
if (tier === 'trusted') {
|
|
return 'Your posts reach a good audience. You have shown consistent, calm participation.';
|
|
}
|
|
|
|
if (tier === 'established') {
|
|
return 'Your posts reach a wide audience. You have built strong trust through sustained positive contributions.';
|
|
}
|
|
|
|
return 'Your reach is determined by your participation patterns and community response.';
|
|
}
|
|
|
|
/**
|
|
* Determine reach multiplier for feed algorithms
|
|
*/
|
|
export function getReachMultiplier(tier: string, score: number): number {
|
|
const baseMultiplier: Record<string, number> = {
|
|
restricted: 0.2,
|
|
new: 0.6,
|
|
trusted: 1.0,
|
|
established: 1.4,
|
|
};
|
|
|
|
// Fine-tune based on score within tier
|
|
const tierBase = baseMultiplier[tier] || 1.0;
|
|
const scoreAdjustment = (score - 50) / 200; // -0.25 to +0.25
|
|
|
|
return Math.max(0.1, tierBase + scoreAdjustment);
|
|
}
|