feat(discover): taste-targeted novelty bucket + rebalance toward discovery
The 2-week metrics review found Discover beating the manual baseline on skip rate — which for a discovery surface means it plays it safe. On a single-user server it's effectively dormant + crude-random, and the per-user taste profile (taste_profile_tags, #796) went unused (#1254 gap). Add a fourth Discover candidate bucket, ListTasteUnheardTracksForDiscover: unheard / non-liked / non-quarantined tracks ranked by summed taste-tag weight over tracks.genre (split like the radio tag_overlap arm), md5 tiebreak. Its picks are stamped pick_kind = 'taste_unheard' (migration 0041 widens the CHECK on playlist_tracks + play_events, rule #36). Rebalance the slot allocation 40/30/30 → taste_unheard 35 / dormant 30 / cross_user 20 / random 15, and lead the interleave with taste_unheard so a track shared with another bucket keeps the taste stamp and the targeted-novelty arm stays measurable. Metrics label "Taste-matched" + order entry added to the single server-side pickKindLabels map, so web and Android surface the new breakdown row with no client change. Cold start (empty taste_profile_tags) yields an empty taste bucket that redistributes to the survivors, so Discover still fills. Scribe #1488. Companion review outcomes: Songs-like starvation already fixed (#1255); For You v2 ratified as-is (fresh-injection cost disproven). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -171,18 +171,19 @@ func (h *handlers) handleGetRecommendationMetrics(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// pickKindLabels is the display vocabulary for play_events.pick_kind
|
||||
// values (mirrors the CHECK in migration 0039). Every system mix that
|
||||
// values (mirrors the CHECK in migration 0041). Every system mix that
|
||||
// stamps provenance gets its breakdown from this one map — adding a
|
||||
// stamping mix needs no metrics change.
|
||||
var pickKindLabels = map[string]string{
|
||||
"taste": "Taste picks",
|
||||
"fresh": "Fresh picks",
|
||||
"dormant": "Dormant artists",
|
||||
"cross_user": "Liked by others",
|
||||
"random": "Random unheard",
|
||||
"tier1": "Tier 1 (exact)",
|
||||
"tier2": "Tier 2 (relaxed)",
|
||||
"tier3": "Tier 3 (stretched)",
|
||||
"taste": "Taste picks",
|
||||
"fresh": "Fresh picks",
|
||||
"dormant": "Dormant artists",
|
||||
"taste_unheard": "Taste-matched",
|
||||
"cross_user": "Liked by others",
|
||||
"random": "Random unheard",
|
||||
"tier1": "Tier 1 (exact)",
|
||||
"tier2": "Tier 2 (relaxed)",
|
||||
"tier3": "Tier 3 (stretched)",
|
||||
}
|
||||
|
||||
// pickKindOrder fixes breakdown row order; unattributed ("", i.e. NULL
|
||||
@@ -192,7 +193,7 @@ var pickKindLabels = map[string]string{
|
||||
// of silently shrinking. The DB CHECK gates pick_kind to exactly this
|
||||
// vocabulary, so iterating the list is exhaustive.
|
||||
var pickKindOrder = []string{
|
||||
"taste", "fresh", "dormant", "cross_user", "random",
|
||||
"taste", "fresh", "dormant", "taste_unheard", "cross_user", "random",
|
||||
"tier1", "tier2", "tier3", "",
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestBucketMetricsResponse_ForYouBreakdown(t *testing.T) {
|
||||
func TestBucketMetricsResponse_DiscoverBucketBreakdown(t *testing.T) {
|
||||
// Provenance is standard (#1270): Discover's bucket stamps surface as
|
||||
// a breakdown exactly like For You's taste/fresh — this is what makes
|
||||
// the 40/30/30 allocation judgeable instead of a guess.
|
||||
// the bucket allocation judgeable instead of a guess.
|
||||
src := func(s string) *string { return &s }
|
||||
kind := func(s string) *string { return &s }
|
||||
rows := []dbq.RecommendationSourceMetricsForUserRow{
|
||||
|
||||
Reference in New Issue
Block a user