# Data Models **Related Documents**: - [Lexicon Schemas](./lexicon-schemas.md) - AT Protocol schemas - [Storage Strategy](./storage-strategy.md) - Where data is stored - [Requirements](../product/requirements.md) - Validation rules ## Overview TypeScript interfaces and types that map to AT Protocol lexicons and application state. **Status**: 🔴 Planned --- ## Core Types ### FragranceReview Maps to `social.drydown.review` lexicon: ```typescript interface FragranceReview { // Metadata (not in AT Protocol record) id?: string // Local ID for in-progress reviews uri?: string // AT Protocol record URI: at://did/collection/rkey // Fragrance info fragrance: { name: string // 1-200 chars, required brand?: string // 0-100 chars, optional year?: number // 1000-2100, optional } // Timestamps (ISO 8601 format) createdAt: string // When Stage 1 started stage2CompletedAt?: string // When Stage 2 completed stage3CompletedAt?: string // When Stage 3 completed completedAt?: string // When all stages done // Ratings stage1: Stage1Ratings stage2?: Stage2Ratings stage3?: Stage3Ratings // Review content textReview?: string // Max 275 graphemes calculatedRating?: number // 0-5, to 3 decimal places // Sharing sharedToBluesky: boolean sharedAt?: string // ISO 8601, when last shared } ``` ### Stage Ratings ```typescript interface Stage1Ratings { openingAppeal: number // 1-5, integer projection: number // 1-5, integer firstImpression: number // 1-5, integer } interface Stage2Ratings { heartAppeal: number // 1-5, integer heartProjection: number // 1-5, integer skipped: boolean } interface Stage3Ratings { stillDetectable: number // 1-5, integer (longevity) sillage: number // 1-5, integer skinChemistry: number // 1-5, integer complexity: number // 1-5, integer gutScore: number // 1-5, integer } ``` ### UserSettings Maps to `social.drydown.settings` lexicon: ```typescript interface UserSettings { // Metadata createdAt: string // ISO 8601 updatedAt: string // ISO 8601 // Rating weights ratingWeights: RatingWeights // App preferences blueskyClient: 'default' | 'blacksky' | 'graysky' } interface RatingWeights { openingAppeal: number // 0-10, default 1.0 projection: number // 0-10, default 1.0 firstImpression: number // 0-10, default 1.0 heartAppeal: number // 0-10, default 1.0 heartProjection: number // 0-10, default 1.0 stillDetectable: number // 0-10, default 1.0 sillage: number // 0-10, default 1.0 skinChemistry: number // 0-10, default 1.0 complexity: number // 0-10, default 1.0 gutScore: number // 0-10, default 1.0 } const DEFAULT_WEIGHTS: RatingWeights = { openingAppeal: 1.0, projection: 1.0, firstImpression: 1.0, heartAppeal: 1.0, heartProjection: 1.0, stillDetectable: 1.0, sillage: 1.0, skinChemistry: 1.0, complexity: 1.0, gutScore: 1.0 } ``` --- ## Utility Functions ### Calculate Final Rating ```typescript function calculateFinalRating( review: FragranceReview, weights: RatingWeights ): number { let totalWeightedScore = 0 let totalWeights = 0 // Stage 1 (always present) if (review.stage1) { totalWeightedScore += review.stage1.openingAppeal * weights.openingAppeal totalWeights += weights.openingAppeal totalWeightedScore += review.stage1.projection * weights.projection totalWeights += weights.projection totalWeightedScore += review.stage1.firstImpression * weights.firstImpression totalWeights += weights.firstImpression } // Stage 2 (if not skipped) if (review.stage2 && !review.stage2.skipped) { totalWeightedScore += review.stage2.heartAppeal * weights.heartAppeal totalWeights += weights.heartAppeal totalWeightedScore += review.stage2.heartProjection * weights.heartProjection totalWeights += weights.heartProjection } // Stage 3 (always present if review completed) if (review.stage3) { totalWeightedScore += review.stage3.stillDetectable * weights.stillDetectable totalWeights += weights.stillDetectable totalWeightedScore += review.stage3.sillage * weights.sillage totalWeights += weights.sillage totalWeightedScore += review.stage3.skinChemistry * weights.skinChemistry totalWeights += weights.skinChemistry totalWeightedScore += review.stage3.complexity * weights.complexity totalWeights += weights.complexity totalWeightedScore += review.stage3.gutScore * weights.gutScore totalWeights += weights.gutScore } // Handle edge case: all weights are 0 if (totalWeights === 0) { return 0 } const rating = totalWeightedScore / totalWeights // Round to 3 decimal places return Math.round(rating * 1000) / 1000 } ``` ### Format Rating for Display ```typescript function formatRatingPrecise(rating: number): string { return `${rating.toFixed(3)} / 5.000` } function formatRatingStars(rating: number): string { const rounded = Math.round(rating) const filled = '★'.repeat(rounded) const empty = '☆'.repeat(5 - rounded) return filled + empty } ``` ### Count Graphemes ```typescript function countGraphemes(text: string): number { const segmenter = new Intl.Segmenter() return Array.from(segmenter.segment(text)).length } function validateTextLength(text: string, maxGraphemes: number): boolean { return countGraphemes(text) <= maxGraphemes } ``` --- ## State Management Types ### App State ```typescript interface AppState { // Auth session: OAuthSession | null isInitializing: boolean // Reviews inProgressReviews: FragranceReview[] completedReviews: FragranceReview[] // Settings userSettings: UserSettings // UI loading: boolean error: string | null } ``` --- **Related Documents**: - [Lexicon Schemas](./lexicon-schemas.md) - AT Protocol record definitions - [Storage Strategy](./storage-strategy.md) - Data persistence - [Requirements](../product/requirements.md) - Validation rules