a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere.
drydown.social
1import { AtpBaseClient } from '../client/index'
2import type { UserPreferencesForScoring } from './reviewUtils'
3import { cache, TTL } from '../services/cache'
4import { DEFAULT_PREFERENCES } from '../data/preferenceDefinitions'
5
6/**
7 * Fetches another user's preference settings from their PDS.
8 * Returns null if the fetch fails (private settings, not found, etc.)
9 * Falls back to cache if available.
10 */
11export async function fetchUserPreferences(
12 did: string,
13 pdsUrl: string
14): Promise<UserPreferencesForScoring | null> {
15 const cacheKey = `preferences:${did}`
16
17 // Check cache first
18 const cached = cache.get<UserPreferencesForScoring>(cacheKey)
19 if (cached) {
20 return cached
21 }
22
23 try {
24 // Create unauthenticated PDS client for public reads
25 const pdsClient = new AtpBaseClient({ service: pdsUrl })
26
27 // Fetch the settings record (rkey is always 'self' for user settings)
28 const response = await pdsClient.social.drydown.settings.get({
29 repo: did,
30 rkey: 'self',
31 })
32
33 if (!response.value) {
34 return null
35 }
36
37 const settings = response.value
38 const preferences: UserPreferencesForScoring = {
39 presenceStyle: settings.presenceStyle,
40 longevityPriority: settings.longevityPriority,
41 complexityPreference: settings.complexityPreference,
42 scoringApproach: settings.scoringApproach,
43 }
44
45 // Cache the result
46 cache.set(cacheKey, preferences, TTL.SETTINGS)
47
48 return preferences
49 } catch (error) {
50 // Settings not found, private, or network error - return null
51 console.debug('Could not fetch user preferences:', error)
52 return null
53 }
54}
55
56/**
57 * Calculates compatibility score between two users' preferences.
58 * Returns a percentage from 0-100 indicating how aligned their preferences are.
59 *
60 * Algorithm:
61 * 1. Calculate absolute distance for each of 4 preference values (each on 1-5 scale)
62 * 2. Average the distances (max possible avg = 4)
63 * 3. Convert to percentage: ((4 - avgDistance) / 4) * 100
64 *
65 * Examples:
66 * - Identical preferences: avgDistance = 0 → 100%
67 * - Opposite preferences: avgDistance = 4 → 0%
68 * - Mixed preferences: avgDistance = 0.75 → 81%
69 */
70export function calculateCompatibilityScore(
71 userPrefs: UserPreferencesForScoring,
72 otherPrefs: UserPreferencesForScoring
73): number {
74 // Use DEFAULT_PREFERENCES if any preference is undefined
75 const distances = [
76 Math.abs((userPrefs.presenceStyle ?? DEFAULT_PREFERENCES.presenceStyle) - (otherPrefs.presenceStyle ?? DEFAULT_PREFERENCES.presenceStyle)),
77 Math.abs((userPrefs.longevityPriority ?? DEFAULT_PREFERENCES.longevityPriority) - (otherPrefs.longevityPriority ?? DEFAULT_PREFERENCES.longevityPriority)),
78 Math.abs((userPrefs.complexityPreference ?? DEFAULT_PREFERENCES.complexityPreference) - (otherPrefs.complexityPreference ?? DEFAULT_PREFERENCES.complexityPreference)),
79 Math.abs((userPrefs.scoringApproach ?? DEFAULT_PREFERENCES.scoringApproach) - (otherPrefs.scoringApproach ?? DEFAULT_PREFERENCES.scoringApproach)),
80 ]
81
82 const avgDistance = distances.reduce((sum, d) => sum + d, 0) / 4
83 const compatibility = ((4 - avgDistance) / 4) * 100
84
85 return Math.round(compatibility)
86}
87
88/**
89 * Returns a friendly label describing the compatibility level.
90 */
91export function getCompatibilityLabel(score: number): string {
92 if (score >= 90) return 'Highly Similar'
93 if (score >= 75) return 'Similar Tastes'
94 if (score >= 50) return 'Somewhat Different'
95 if (score >= 25) return 'Different Tastes'
96 return 'Very Different'
97}