package sampling import ( "math" "time" "qbank/internal/models" ) const ( FloorWeight = 0.15 // mastered questions still appear at ~15% base rate RecencyCapDays = 30.0 // days until recency multiplier saturates RecencyMaxMult = 2.0 // peak recency multiplier UnseenBaseWeight = 0.5 // base weight for questions with no stats row ) // ComputeWeight returns the sampling weight for a question given its per-user // stat. A nil stat means the question has never been seen. func ComputeWeight(stat *models.UserQuestionStat, now time.Time) float64 { if stat == nil { // Unseen: mid-range base + full recency = 1.0 return UnseenBaseWeight * RecencyMaxMult } s := float64(stat.TimesSeen) c := float64(stat.TimesCorrect) // Laplace-smoothed error rate dampens noise from small samples. errorRate := (s - c + 1) / (s + 2) base := math.Max(FloorWeight, errorRate) var daysSince float64 if stat.LastSeenAt.Valid { daysSince = now.Sub(stat.LastSeenAt.Time).Hours() / 24 } else { daysSince = RecencyCapDays } recency := 1 + math.Min(daysSince/RecencyCapDays, 1.0) return base * recency }