a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere.
drydown.social
1# Scoring System Documentation
2
3**Last Updated:** 2026-03-04
4
5## Overview
6
7Drydown uses an **ideal score anchoring** system to calculate personalized review scores. Instead of multiplying weights, the system calculates how close a review is to the user's ideal preferences across all rating properties.
8
9## Core Concept
10
11Users set preferences (1-5 scale) that map to their "ideal" ratings. When viewing a review, the system:
121. Calculates the distance between each review property and the user's ideal
132. Applies a power 1.5 curve to convert distance → fit percentage
143. Weights each property by importance
154. Sums weighted fits to produce final score (0-5 scale)
16
17## Preference Mapping
18
19### presenceStyle → Projection/Sillage Ideals
20
21| Value | Statement | Ideal Projection | Ideal Sillage |
22|-------|-----------|------------------|---------------|
23| 1 | "Skin scent only — just for me" | 1 | 1 |
24| 2 | "Close company — noticeable only up close" | 2 | 2 |
25| 3 | "Balanced — depends on the occasion" | 3 | 3 |
26| 4 | "Room presence — I enjoy being noticed" | 4 | 4 |
27| 5 | "Announce myself — I like to make an entrance" | 5 | 5 |
28
29### longevityPriority → Longevity Ideal
30
31| Value | Statement | Ideal Longevity |
32|-------|-----------|-----------------|
33| 1 | "Not important — I enjoy reapplying" | 1 |
34| 2 | "Nice to have — but not a dealbreaker" | 2 |
35| 3 | "Moderately important" | 3 |
36| 4 | "Very important — I want it to last" | 4 |
37| 5 | "Essential — all-day performance" | 5 |
38
39### complexityPreference → Complexity Ideal
40
41| Value | Statement | Ideal Complexity |
42|-------|-----------|------------------|
43| 1 | "Simple and focused — clean scents" | 1 |
44| 2 | "Mostly simple — with maybe a twist" | 2 |
45| 3 | "Balanced — both simple and complex" | 3 |
46| 4 | "Layered — discovering notes" | 4 |
47| 5 | "Intricate — depth and evolution" | 5 |
48
49### scoringApproach → Property Importance Adjustments
50
51| Value | Statement | Effect |
52|-------|-----------|--------|
53| 1 | "Pure instinct — gut reaction matters most" | `overallRating` importance × 1.3 |
54| 2 | "Mostly instinct — but I consider details" | `overallRating` importance × 1.3 |
55| 3 | "Balanced — feeling and analysis equal" | No adjustments |
56| 4 | "Mostly analytical — weigh each aspect" | Technical properties × 1.1-1.2 |
57| 5 | "Fully analytical — numbers tell the story" | Technical properties × 1.1-1.2 |
58
59**Technical properties boosted (scoringApproach ≥ 4):**
60- `complexity` × 1.2
61- `longevity` × 1.2
62- `sillage` × 1.1
63- `midProjection` × 1.1
64- `openingProjection` × 1.1
65
66### Quality Rating Ideals
67
68All quality ratings always use ideal = 5:
69- `openingRating`
70- `drydownRating`
71- `endRating`
72- `overallRating`
73
74Everyone wants "best" quality, so these properties measure distance from perfection.
75
76## Property Importance Rankings
77
78Base importance values (before scoringApproach adjustments):
79
80| Rank | Property | Importance | Category |
81|------|----------|------------|----------|
82| 1 | midProjection | 1.5 | Performance |
83| 2 | openingProjection | 1.45 | Performance |
84| 3 | complexity | 1.4 | Technical |
85| 4 | longevity | 1.35 | Technical |
86| 5 | sillage | 1.3 | Performance |
87| 6 | drydownRating | 1.25 | Quality |
88| 7 | openingRating | 1.2 | Quality |
89| 8 | endRating | 1.15 | Quality |
90| 9 | overallRating | 1.1 | Gut check |
91
92**Design rationale:**
93- Projection metrics matter most (user feedback priority)
94- Technical properties (complexity, longevity) come next
95- Quality ratings follow
96- Overall gut check is least important (captured by other ratings)
97
98## Distance to Fit Conversion
99
100The power 1.5 curve penalizes mismatches moderately (more than linear, less than squared):
101
102```typescript
103normalizedDistance = Math.abs(actual - ideal) / 4
104fit = Math.pow(1 - normalizedDistance, 1.5)
105```
106
107| Distance | Normalized | Fit % | Interpretation |
108|----------|------------|-------|----------------|
109| 0 | 0.00 | 100% | Perfect match |
110| 1 | 0.25 | 65% | Close match |
111| 2 | 0.50 | 35% | Moderate mismatch |
112| 3 | 0.75 | 13% | Poor match |
113| 4 | 1.00 | 0% | Opposite of ideal |
114
115## Score Calculation Algorithm
116
117```typescript
118function calculateIdealBasedScore(review, preferences) {
119 // 1. Map preferences → ideal scores
120 const idealScores = preferencesToIdealScores(preferences)
121
122 // 2. Adjust importance based on scoringApproach
123 const importance = adjustImportanceForScoringApproach(
124 preferences.scoringApproach
125 )
126
127 // 3. Calculate weighted fit for each property
128 let totalWeightedFit = 0
129 let totalImportance = 0
130
131 for (const property of ALL_PROPERTIES) {
132 if (review[property] === undefined) continue
133
134 const distance = Math.abs(review[property] - idealScores[property])
135 const normalizedDistance = distance / 4
136 const fit = Math.pow(1 - normalizedDistance, 1.5)
137 const weightedFit = fit * importance[property]
138
139 totalWeightedFit += weightedFit
140 totalImportance += importance[property]
141 }
142
143 // 4. Normalize to 0-5 scale
144 const score = (totalWeightedFit / totalImportance) * 5
145 return Math.round(score * 1000) / 1000
146}
147```
148
149## Personalized Score Display
150
151Users can toggle between two score perspectives:
152
153### "Their perspective" (scoreLens='theirs')
154Shows the reviewer's original score (stored in `weightedScore` field).
155
156### "Your perspective" (scoreLens='mine')
157Recalculates the score using the viewer's ideal preferences.
158
159**Special case:** Own reviews always show original score (no recalculation).
160
161## API Reference
162
163### Core Functions
164
165#### `calculateIdealBasedScore(review, preferences): number`
166Calculates score based on distance from user's ideal preferences.
167
168**Parameters:**
169- `review`: Review record with rating properties
170- `preferences`: `UserPreferencesForScoring` object
171
172**Returns:** Score from 0-5 (rounded to 3 decimal places)
173
174#### `getPersonalizedScore(review, userPreferences?, isOwnReview): { score, isPersonalized }`
175Get display score with personalization support.
176
177**Parameters:**
178- `review`: Review record
179- `userPreferences`: Optional user preferences
180- `isOwnReview`: Whether review belongs to current user
181
182**Returns:**
183- `score`: Calculated score (0-5)
184- `isPersonalized`: Whether score was recalculated with user preferences
185
186### Helper Functions
187
188#### `preferencesToIdealScores(preferences): IdealScores`
189Converts user preferences to ideal score values.
190
191#### `adjustImportanceForScoringApproach(scoringApproach): PropertyImportance`
192Adjusts property importance based on scoring approach preference.
193
194## Examples
195
196### Example 1: Matching Preferences
197
198**User preferences:**
199```typescript
200{
201 presenceStyle: 5, // "Announce myself"
202 longevityPriority: 5, // "All-day essential"
203 complexityPreference: 4, // "Layered"
204 scoringApproach: 3 // Balanced
205}
206```
207
208**Review ratings:**
209```typescript
210{
211 openingProjection: 5, // Room-filling
212 midProjection: 5, // Strong presence
213 sillage: 5, // Noticeable trail
214 longevity: 5, // All-day lasting
215 complexity: 4, // Very layered
216 // ... other ratings
217}
218```
219
220**Result:** High score (4-5 range) because review closely matches user ideals.
221
222### Example 2: Mismatched Preferences
223
224**User preferences:**
225```typescript
226{
227 presenceStyle: 1, // "Skin scent only"
228 longevityPriority: 2, // "Nice to have"
229 complexityPreference: 1, // "Simple and focused"
230 scoringApproach: 1 // "Pure instinct"
231}
232```
233
234**Same review as above**
235
236**Result:** Low score (2-3 range) because review is opposite of user ideals (projection distance = 4, complexity distance = 3).
237
238### Example 3: Partial Mismatch
239
240**User preferences:**
241```typescript
242{
243 presenceStyle: 5, // Matches review
244 longevityPriority: 1, // "Not important"
245 complexityPreference: 5, // "Intricate"
246 scoringApproach: 3
247}
248```
249
250**Review ratings:**
251```typescript
252{
253 openingProjection: 5, // ✓ Matches ideal
254 midProjection: 5, // ✓ Matches ideal
255 sillage: 5, // ✓ Matches ideal
256 longevity: 5, // ✗ Doesn't matter (user ideal = 1)
257 complexity: 2, // ✗ Poor match (user ideal = 5)
258 // ... other ratings
259}
260```
261
262**Result:** Moderate score (3-4 range). Projection matches perfectly, but longevity "overperformance" and low complexity reduce fit.
263
264## Migration from Weight-Based System
265
266No migration needed! The new system:
267- Uses the same 4 preferences (no schema changes)
268- Keeps the same UI (no interface changes)
269- Maintains backward compatibility (existing data works unchanged)
270
271The only change is how preferences are interpreted by the scoring algorithm.
272
273## See Also
274
275- [Implementation Summary](../IMPLEMENTATION_SUMMARY.md)
276- [Preference Definitions](../src/data/preferenceDefinitions.ts)
277- [Review Utilities](../src/utils/reviewUtils.ts)