@@ -1,16 +1,15 @@
// Package recsettings is the DB-backed home of the recommendation
// tuning knobs (#1250) : the two scoring-weight profiles (radio /
// daily_mix) and the taste-profile build settings (engagement
// half-life + completion curve). It follows the coverart
// tuning knobs: the scoring-weight profiles (radio, daily_mix,
// songs_like), Discover's settings, and the taste-profile build
// settings (engagement half-life + completion curve). It follows the coverart
// SettingsService pattern — boot reconcile seeds shipped defaults for
// missing rows, values are cached under a RWMutex, and every change
// takes effect live (the daily_mix profile + taste config are pushed
// into package playlists; radio reads the cache per request).
//
// Framing (decision #1247): this is the defaults-discovery lab. The
// operator turns knobs to FIND good values; found-good values get
// baked back into the Shipped* functions below as new shipped
// defaults. End users and other operators should never need the card.
// This is a lab for finding good defaults: an admin turns knobs to find
// good values, and those get baked back into the Shipped* functions below
// as new shipped defaults. A typical install never needs to touch it.
package recsettings
import (
@@ -35,24 +34,23 @@ const (
ScopeRadio = "radio"
ScopeDailyMix = "daily_mix"
ScopeTaste = "taste"
// ScopeDiscover is the Discover request surface (#2377) . Its own scope
// rather than columns on taste: SnoozeDays lives here, and a snooze must
// never be read as taste signal (#2374) — filing it under taste would put
// it one careless join from the leak that design forbids .
// ScopeDiscover is the Discover request surface. Its own scope rather
// than columns on taste: SnoozeDays lives here, and a snooze must never
// be read as taste signal — filing it under taste would put it one
// careless join from that leak.
ScopeDiscover = "discover"
// ScopeSongsLike is the "Songs like {X}" surface (#3881). It shared
// daily_mix with For-You until 2026-09-10, and that sharing WAS the bug:
// the two surfaces want opposite things. For-You answers "what will they
// enjoy today" and is supposed to roam; Songs-like answers "what sounds
// like THIS" and is the tightest surface in the product. One set of
// weights cannot serve both, and the broad answer was winning.
// ScopeSongsLike is the "Songs like {X}" surface. It is separate from
// daily_mix because the two want opposite things: For-You answers "what
// will they enjoy today" and is supposed to roam; Songs-like answers
// "what sounds like THIS" and is the tightest surface in the product. One
// set of weights cannot serve both.
ScopeSongsLike = "songs_like"
)
// TasteTuning is the tunable subset of taste.Config: the engagement
// half-life, the completion→engagement curve points, the enriched-tag
// weight (how much folksonomy tags count vs raw ID3 genre, #1520 ), and the
// era-facet weight (how strongly a decade-play imprints, #1530 ).
// weight (how much folksonomy tags count vs raw ID3 genre), and the
// era-facet weight (how strongly a decade-play imprints).
type TasteTuning struct {
HalfLifeDays float64
EngagementHardSkip float64
@@ -63,12 +61,10 @@ type TasteTuning struct {
MoodScale float64
}
// ShippedRadioWeights are the shipped radio-profile defaults (moved
// here from config.RecommendationConfig — YAML is bootstrap-only,
// rule: config in UI). Radio is seed-directed (the user picked a
// direction), so taste is a lighter nudge than in the daily mixes .
// ContextTimeWeight starts uniform (1.0) across both profiles pending
// trend data (#1531); split them once the metrics view justifies it.
// ShippedRadioWeights are the shipped radio-profile defaults. Radio is
// seed-directed (the user picked a direction), so taste is a lighter nudge
// than in the daily mixes. ContextTimeWeight is 1.0 in both profiles; the
// metrics view is what would justify splitt ing them .
func ShippedRadioWeights ( ) recommendation . ScoringWeights {
return recommendation . ScoringWeights {
BaseWeight : 1.0 ,
@@ -99,12 +95,11 @@ func ShippedDailyMixWeights() recommendation.ScoringWeights {
}
}
// ShippedSongsLikeWeights are the shipped songs_like-profile defaults
// (#3881). The whole point is that SIMILARITY DOMINATES; every other
// profile balances it against taste and engagement, and this one must not.
// ShippedSongsLikeWeights are the shipped songs_like-profile defaults. The
// whole point is that SIMILARITY DOMINATES; every other profile balances it
// against taste and engagement, and this one must not.
//
// The failure being corrected, arithmetic from the daily_mix profile that
// this surface used to share:
// Under the daily_mix weights, for example:
//
// unrelated track, liked, not played recently → 1.0 + 2.0 + 1.0 = 4.0
// PERFECT similarity match, not liked → 1.0 + 1.5 = 2.5
@@ -132,12 +127,10 @@ func ShippedDailyMixWeights() recommendation.ScoringWeights {
// stays 2.0 because a track the user skips is still unwanted no matter how
// similar it is.
//
// These are DEFAULTS, not settings (rule 25) — the operator turns them in the
// admin tuning card and good values get baked back here. They are a
// defensible starting point rather than a measured optimum: the per-arm fill
// rates and the real sim_score distribution are still unknown (#3879), and
// `likes_overlap` contributes a flat 0.3 that a high SimilarityWeight
// amplifies. Expect to move these once that lands.
// These are DEFAULTS, not settings: an admin turns them in the tuning card
// and good values get baked back here. They are a defensible starting point
// rather than a measured optimum, and `likes_overlap` contributes a flat 0.3
// that a high SimilarityWeight amplifies.
func ShippedSongsLikeWeights ( ) recommendation . ScoringWeights {
return recommendation . ScoringWeights {
BaseWeight : 1.0 , // same for all candidates; differentiates nothing
@@ -169,12 +162,12 @@ func shippedWeightsFor(scope string) (recommendation.ScoringWeights, bool) {
return recommendation . ScoringWeights { } , false
}
// DiscoverTuning is the tunable set for the Discover request surface (#2377) .
// DiscoverTuning is the tunable set for the Discover request surface.
type DiscoverTuning struct {
// TagOverlapWeight scales the taste-tag term: score × (1 + w × overlap).
// 0 disables it and restores pure similarity ranking.
TagOverlapWeight float64
// SnoozeDays is the default "not right now" duration (#2374) .
// SnoozeDays is the default "not right now" duration.
SnoozeDays float64
}
@@ -183,12 +176,11 @@ type DiscoverTuning struct {
// TagOverlapWeight 1.0 lets a perfect tag match at most double a candidate's
// similarity score — enough to reorder the deck meaningfully, not enough for a
// popular-tag coincidence to beat a genuinely strong similarity match. It is a
// starting point for the tuning lab , not a tuned value: the honest way to pick
// it is the metrics trend view after some real use.
// starting point, not a tuned value: the way to pick it is the metrics trend
// view after some real use.
//
// SnoozeDays 90 matches the operator's approved shape: long enough that a
// parked suggestion stops nagging, short enough that a taste shift brings it
// back on its own.
// SnoozeDays 90: long enough that a parked suggestion stops nagging, short
// enough that a taste shift brings it back on its own.
func ShippedDiscoverTuning ( ) DiscoverTuning {
return DiscoverTuning {
TagOverlapWeight : 1.0 ,
@@ -337,7 +329,7 @@ func (s *Service) Taste() TasteTuning {
// Discover returns the cached Discover-tuning values. Read per request by the
// suggestions handler, so an admin change takes effect on the next refresh
// with no restart (rule #25) .
// with no restart.
func ( s * Service ) Discover ( ) DiscoverTuning {
s . mu . RLock ( )
defer s . mu . RUnlock ( )