feat(tuning): expose EnrichedTagScale in the tuning lab (#1520)
test-go / test (push) Successful in 35s
test-web / test (push) Successful in 41s
test-go / integration (push) Successful in 4m45s

Promote the enriched-tag weight (#1490) from a taste.Config default into
the DB-backed tuning lab so operators can dial how much folksonomy tags
count vs raw ID3 genre (rule #25).

- Migration 0044: taste_tuning.enriched_tag_scale (DEFAULT 0.5, backfills
  the existing row).
- recsettings: TasteTuning gains the field; seeded/read/updated through
  reconcile + persistTaste; applyTastePatch validates it to [0,1]
  (generic non-half-life clamp) and diffTaste audits it; TasteConfig maps
  it into the profile build.
- API: tasteTuningResp exposes enriched_tag_scale.
- Web tuning card: a data-driven "Enriched tag weight" knob (0 = genre
  only). Tests: recsettings persist+range, web fixture field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 07:51:36 -04:00
parent c30511e71b
commit 2b3be8311a
12 changed files with 61 additions and 8 deletions
+3
View File
@@ -129,6 +129,8 @@ func applyTastePatch(current TasteTuning, patch map[string]float64) (TasteTuning
target = &next.EngagementNeutral
case "engagement_full":
target = &next.EngagementFull
case "enriched_tag_scale":
target = &next.EnrichedTagScale
default:
return current, nil, fmt.Errorf("%w: %q", ErrUnknownField, field)
}
@@ -174,5 +176,6 @@ func diffTaste(a, b TasteTuning) []fieldChange {
add("engagement_hard_skip", a.EngagementHardSkip, b.EngagementHardSkip)
add("engagement_neutral", a.EngagementNeutral, b.EngagementNeutral)
add("engagement_full", a.EngagementFull, b.EngagementFull)
add("enriched_tag_scale", a.EnrichedTagScale, b.EnrichedTagScale)
return out
}
+8 -1
View File
@@ -38,12 +38,14 @@ const (
)
// TasteTuning is the tunable subset of taste.Config: the engagement
// half-life and the completion→engagement curve points.
// half-life, the completion→engagement curve points, and the enriched-tag
// weight (how much folksonomy tags count vs raw ID3 genre, #1520).
type TasteTuning struct {
HalfLifeDays float64
EngagementHardSkip float64
EngagementNeutral float64
EngagementFull float64
EnrichedTagScale float64
}
// ShippedRadioWeights are the shipped radio-profile defaults (moved
@@ -86,6 +88,7 @@ func ShippedTasteTuning() TasteTuning {
EngagementHardSkip: d.Engagement.HardSkip,
EngagementNeutral: d.Engagement.NeutralCompletion,
EngagementFull: d.Engagement.FullCompletion,
EnrichedTagScale: d.EnrichedTagScale,
}
}
@@ -133,6 +136,7 @@ func (s *Service) reconcile(ctx context.Context) error {
EngagementHardSkip: st.EngagementHardSkip,
EngagementNeutral: st.EngagementNeutral,
EngagementFull: st.EngagementFull,
EnrichedTagScale: st.EnrichedTagScale,
}); err != nil {
return fmt.Errorf("seed taste tuning: %w", err)
}
@@ -156,6 +160,7 @@ func (s *Service) reconcile(ctx context.Context) error {
EngagementHardSkip: tt.EngagementHardSkip,
EngagementNeutral: tt.EngagementNeutral,
EngagementFull: tt.EngagementFull,
EnrichedTagScale: tt.EnrichedTagScale,
}
s.mu.Unlock()
@@ -205,6 +210,7 @@ func (s *Service) TasteConfig() taste.Config {
NeutralCompletion: t.EngagementNeutral,
FullCompletion: t.EngagementFull,
}
cfg.EnrichedTagScale = t.EnrichedTagScale
return cfg
}
@@ -301,6 +307,7 @@ func (s *Service) persistTaste(
EngagementHardSkip: t.EngagementHardSkip,
EngagementNeutral: t.EngagementNeutral,
EngagementFull: t.EngagementFull,
EnrichedTagScale: t.EnrichedTagScale,
}); err != nil {
return fmt.Errorf("update taste tuning: %w", err)
}
+21
View File
@@ -182,6 +182,27 @@ func TestUpdateTaste_CurveOrderingEnforced(t *testing.T) {
}
}
func TestUpdateTaste_EnrichedTagScale(t *testing.T) {
pool := newPool(t)
s := newService(t, pool)
// In-range update persists into cache + the assembled taste config.
if err := s.UpdateTaste(context.Background(),
map[string]float64{"enriched_tag_scale": 0.8}); err != nil {
t.Fatalf("UpdateTaste: %v", err)
}
if got := s.Taste().EnrichedTagScale; got != 0.8 {
t.Errorf("Taste().EnrichedTagScale = %v, want 0.8", got)
}
if got := s.TasteConfig().EnrichedTagScale; got != 0.8 {
t.Errorf("TasteConfig().EnrichedTagScale = %v, want 0.8", got)
}
// Out of [0,1] rejects.
if err := s.UpdateTaste(context.Background(),
map[string]float64{"enriched_tag_scale": 1.5}); !errors.Is(err, ErrOutOfRange) {
t.Errorf("err = %v, want ErrOutOfRange for 1.5", err)
}
}
func TestReset_RestoresShippedAndAudits(t *testing.T) {
pool := newPool(t)
s := newService(t, pool)