a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere. drydown.social

Scoring System Documentation#

Last Updated: 2026-03-04

Overview#

Drydown 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.

Core Concept#

Users set preferences (1-5 scale) that map to their "ideal" ratings. When viewing a review, the system:

  1. Calculates the distance between each review property and the user's ideal
  2. Applies a power 1.5 curve to convert distance → fit percentage
  3. Weights each property by importance
  4. Sums weighted fits to produce final score (0-5 scale)

Preference Mapping#

presenceStyle → Projection/Sillage Ideals#

Value Statement Ideal Projection Ideal Sillage
1 "Skin scent only — just for me" 1 1
2 "Close company — noticeable only up close" 2 2
3 "Balanced — depends on the occasion" 3 3
4 "Room presence — I enjoy being noticed" 4 4
5 "Announce myself — I like to make an entrance" 5 5

longevityPriority → Longevity Ideal#

Value Statement Ideal Longevity
1 "Not important — I enjoy reapplying" 1
2 "Nice to have — but not a dealbreaker" 2
3 "Moderately important" 3
4 "Very important — I want it to last" 4
5 "Essential — all-day performance" 5

complexityPreference → Complexity Ideal#

Value Statement Ideal Complexity
1 "Simple and focused — clean scents" 1
2 "Mostly simple — with maybe a twist" 2
3 "Balanced — both simple and complex" 3
4 "Layered — discovering notes" 4
5 "Intricate — depth and evolution" 5

scoringApproach → Property Importance Adjustments#

Value Statement Effect
1 "Pure instinct — gut reaction matters most" overallRating importance × 1.3
2 "Mostly instinct — but I consider details" overallRating importance × 1.3
3 "Balanced — feeling and analysis equal" No adjustments
4 "Mostly analytical — weigh each aspect" Technical properties × 1.1-1.2
5 "Fully analytical — numbers tell the story" Technical properties × 1.1-1.2

Technical properties boosted (scoringApproach ≥ 4):

  • complexity × 1.2
  • longevity × 1.2
  • sillage × 1.1
  • midProjection × 1.1
  • openingProjection × 1.1

Quality Rating Ideals#

All quality ratings always use ideal = 5:

  • openingRating
  • drydownRating
  • endRating
  • overallRating

Everyone wants "best" quality, so these properties measure distance from perfection.

Property Importance Rankings#

Base importance values (before scoringApproach adjustments):

Rank Property Importance Category
1 midProjection 1.5 Performance
2 openingProjection 1.45 Performance
3 complexity 1.4 Technical
4 longevity 1.35 Technical
5 sillage 1.3 Performance
6 drydownRating 1.25 Quality
7 openingRating 1.2 Quality
8 endRating 1.15 Quality
9 overallRating 1.1 Gut check

Design rationale:

  • Projection metrics matter most (user feedback priority)
  • Technical properties (complexity, longevity) come next
  • Quality ratings follow
  • Overall gut check is least important (captured by other ratings)

Distance to Fit Conversion#

The power 1.5 curve penalizes mismatches moderately (more than linear, less than squared):

normalizedDistance = Math.abs(actual - ideal) / 4
fit = Math.pow(1 - normalizedDistance, 1.5)
Distance Normalized Fit % Interpretation
0 0.00 100% Perfect match
1 0.25 65% Close match
2 0.50 35% Moderate mismatch
3 0.75 13% Poor match
4 1.00 0% Opposite of ideal

Score Calculation Algorithm#

function calculateIdealBasedScore(review, preferences) {
  // 1. Map preferences → ideal scores
  const idealScores = preferencesToIdealScores(preferences)

  // 2. Adjust importance based on scoringApproach
  const importance = adjustImportanceForScoringApproach(
    preferences.scoringApproach
  )

  // 3. Calculate weighted fit for each property
  let totalWeightedFit = 0
  let totalImportance = 0

  for (const property of ALL_PROPERTIES) {
    if (review[property] === undefined) continue

    const distance = Math.abs(review[property] - idealScores[property])
    const normalizedDistance = distance / 4
    const fit = Math.pow(1 - normalizedDistance, 1.5)
    const weightedFit = fit * importance[property]

    totalWeightedFit += weightedFit
    totalImportance += importance[property]
  }

  // 4. Normalize to 0-5 scale
  const score = (totalWeightedFit / totalImportance) * 5
  return Math.round(score * 1000) / 1000
}

Personalized Score Display#

Users can toggle between two score perspectives:

"Their perspective" (scoreLens='theirs')#

Shows the reviewer's original score (stored in weightedScore field).

"Your perspective" (scoreLens='mine')#

Recalculates the score using the viewer's ideal preferences.

Special case: Own reviews always show original score (no recalculation).

API Reference#

Core Functions#

calculateIdealBasedScore(review, preferences): number#

Calculates score based on distance from user's ideal preferences.

Parameters:

  • review: Review record with rating properties
  • preferences: UserPreferencesForScoring object

Returns: Score from 0-5 (rounded to 3 decimal places)

getPersonalizedScore(review, userPreferences?, isOwnReview): { score, isPersonalized }#

Get display score with personalization support.

Parameters:

  • review: Review record
  • userPreferences: Optional user preferences
  • isOwnReview: Whether review belongs to current user

Returns:

  • score: Calculated score (0-5)
  • isPersonalized: Whether score was recalculated with user preferences

Helper Functions#

preferencesToIdealScores(preferences): IdealScores#

Converts user preferences to ideal score values.

adjustImportanceForScoringApproach(scoringApproach): PropertyImportance#

Adjusts property importance based on scoring approach preference.

Examples#

Example 1: Matching Preferences#

User preferences:

{
  presenceStyle: 5,        // "Announce myself"
  longevityPriority: 5,    // "All-day essential"
  complexityPreference: 4, // "Layered"
  scoringApproach: 3       // Balanced
}

Review ratings:

{
  openingProjection: 5,  // Room-filling
  midProjection: 5,      // Strong presence
  sillage: 5,            // Noticeable trail
  longevity: 5,          // All-day lasting
  complexity: 4,         // Very layered
  // ... other ratings
}

Result: High score (4-5 range) because review closely matches user ideals.

Example 2: Mismatched Preferences#

User preferences:

{
  presenceStyle: 1,        // "Skin scent only"
  longevityPriority: 2,    // "Nice to have"
  complexityPreference: 1, // "Simple and focused"
  scoringApproach: 1       // "Pure instinct"
}

Same review as above

Result: Low score (2-3 range) because review is opposite of user ideals (projection distance = 4, complexity distance = 3).

Example 3: Partial Mismatch#

User preferences:

{
  presenceStyle: 5,        // Matches review
  longevityPriority: 1,    // "Not important"
  complexityPreference: 5, // "Intricate"
  scoringApproach: 3
}

Review ratings:

{
  openingProjection: 5,  // ✓ Matches ideal
  midProjection: 5,      // ✓ Matches ideal
  sillage: 5,            // ✓ Matches ideal
  longevity: 5,          // ✗ Doesn't matter (user ideal = 1)
  complexity: 2,         // ✗ Poor match (user ideal = 5)
  // ... other ratings
}

Result: Moderate score (3-4 range). Projection matches perfectly, but longevity "overperformance" and low complexity reduce fit.

Migration from Weight-Based System#

No migration needed! The new system:

  • Uses the same 4 preferences (no schema changes)
  • Keeps the same UI (no interface changes)
  • Maintains backward compatibility (existing data works unchanged)

The only change is how preferences are interpreted by the scoring algorithm.

See Also#