sojorn/_legacy/supabase/functions/calculate-harmony/index.ts
2026-02-15 00:33:24 -06:00

216 lines
7.2 KiB
TypeScript

/**
* 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' },
});
}
});