/** * Harmony Score Calculation (Cron Job) * * This function runs periodically (e.g., daily) to recalculate harmony scores * for all users based on their recent behavior patterns. * * Design intent: * - Influence adapts automatically based on behavior. * - Scores decay over time (old issues fade). * - Changes are gradual, not sudden. * * Trigger: Scheduled via Supabase cron or external scheduler */ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createServiceClient } from '../_shared/supabase-client.ts'; import { calculateHarmonyAdjustment, type HarmonyInputs } from '../_shared/harmony.ts'; serve(async (req) => { try { // Verify this is a scheduled/cron request const authHeader = req.headers.get('Authorization'); const cronSecret = Deno.env.get('CRON_SECRET'); if (!authHeader || authHeader !== `Bearer ${cronSecret}`) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } const serviceClient = createServiceClient(); // 1. Get all users with their current trust state const { data: users, error: usersError } = await serviceClient .from('trust_state') .select('user_id, harmony_score, tier, counters') .order('user_id'); if (usersError) { console.error('Error fetching users:', usersError); return new Response(JSON.stringify({ error: 'Failed to fetch users' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); let updatedCount = 0; let errorCount = 0; // 2. Process each user for (const user of users) { try { // Gather behavior metrics for this user // Blocks received const { count: blocks7d } = await serviceClient .from('blocks') .select('*', { count: 'exact', head: true }) .eq('blocked_id', user.user_id) .gte('created_at', sevenDaysAgo); const { count: blocks30d } = await serviceClient .from('blocks') .select('*', { count: 'exact', head: true }) .eq('blocked_id', user.user_id) .gte('created_at', thirtyDaysAgo); // Reports against this user const { data: reportsAgainst } = await serviceClient .from('reports') .select('reporter_id, status') .or('target_type.eq.post,target_type.eq.comment') .in( 'target_id', serviceClient .from('posts') .select('id') .eq('author_id', user.user_id) .then((r) => r.data?.map((p) => p.id) || []) ); // Count trusted reports (from high-harmony reporters) let trustedReportsCount = 0; if (reportsAgainst) { for (const report of reportsAgainst) { const { data: reporterTrust } = await serviceClient .from('trust_state') .select('harmony_score') .eq('user_id', report.reporter_id) .single(); if (reporterTrust && reporterTrust.harmony_score >= 70) { trustedReportsCount++; } } } // Posts rejected in last 7 days (from audit log) const { data: rejectedPosts } = await serviceClient .from('audit_log') .select('id') .eq('actor_id', user.user_id) .eq('event_type', 'post_rejected') .gte('created_at', sevenDaysAgo); // Posts created in last 7 days const { count: postsCreated7d } = await serviceClient .from('posts') .select('*', { count: 'exact', head: true }) .eq('author_id', user.user_id) .gte('created_at', sevenDaysAgo); // Reports filed by user const { data: reportsFiled } = await serviceClient .from('reports') .select('id, status') .eq('reporter_id', user.user_id); const falseReports = reportsFiled?.filter((r) => r.status === 'dismissed').length || 0; const validatedReports = reportsFiled?.filter((r) => r.status === 'resolved').length || 0; // Days since signup const { data: profile } = await serviceClient .from('profiles') .select('created_at') .eq('id', user.user_id) .single(); const daysSinceSignup = profile ? Math.floor((Date.now() - new Date(profile.created_at).getTime()) / (1000 * 60 * 60 * 24)) : 0; // 3. Calculate harmony adjustment const inputs: HarmonyInputs = { user_id: user.user_id, blocks_received_7d: blocks7d || 0, blocks_received_30d: blocks30d || 0, trusted_reports_against: trustedReportsCount, total_reports_against: reportsAgainst?.length || 0, posts_rejected_7d: rejectedPosts?.length || 0, posts_created_7d: postsCreated7d || 0, false_reports_filed: falseReports, validated_reports_filed: validatedReports, days_since_signup: daysSinceSignup, current_harmony_score: user.harmony_score, current_tier: user.tier, }; const adjustment = calculateHarmonyAdjustment(inputs); // 4. Update trust state if score changed if (adjustment.delta !== 0) { const { error: updateError } = await serviceClient .from('trust_state') .update({ harmony_score: adjustment.new_score, tier: adjustment.new_tier, updated_at: new Date().toISOString(), }) .eq('user_id', user.user_id); if (updateError) { console.error(`Error updating trust state for ${user.user_id}:`, updateError); errorCount++; continue; } // 5. Log the adjustment await serviceClient.rpc('log_audit_event', { p_actor_id: null, // system action p_event_type: 'harmony_recalculated', p_payload: { user_id: user.user_id, old_score: user.harmony_score, new_score: adjustment.new_score, delta: adjustment.delta, old_tier: user.tier, new_tier: adjustment.new_tier, reason: adjustment.reason, }, }); updatedCount++; } } catch (userError) { console.error(`Error processing user ${user.user_id}:`, userError); errorCount++; } } return new Response( JSON.stringify({ success: true, total_users: users.length, updated: updatedCount, errors: errorCount, message: 'Harmony score recalculation complete', }), { status: 200, headers: { 'Content-Type': 'application/json' }, } ); } catch (error) { console.error('Unexpected error:', error); return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } });