feat(taste): time-of-day / weekday context conditioning — #1531
test-go / test (push) Successful in 35s
test-web / test (push) Successful in 42s
test-go / integration (push) Successful in 4m46s

Milestone #160 Opt 3 (temporal half). A new additive scoring term that
boosts a candidate when its artist's play history concentrates in the
CURRENT daypart × weekday-type cell, in the user's local timezone.

- Migration 0046: recommendation_weight_profiles.context_time_weight
  (per-profile scoring weight, DEFAULT 1.0).
- Query ListArtistContextPlayCountsForUser: per-artist completed-play
  counts split by the current cell (daypart night[22,5)/morning[5,12)/
  afternoon[12,17)/evening[17,22) × weekday-vs-weekend) via
  started_at AT TIME ZONE users.timezone; 365-day window, skips excluded.
- internal/recommendation/context.go: LoadContextAffinity computes each
  artist's shrunk cell-share minus the user's baseline share, clamped to
  [-1,1]; sparse artists shrink toward baseline (pseudo-count 5), unknown
  artists → 0 (cold-start neutral).
- Score() gains context_affinity_score · ContextTimeWeight; both
  candidate loaders set it per candidate.
- Tuning lab: ContextTimeWeight threaded through recsettings + admin API
  + web card ("Time-of-day weight" row) + Go/web tests. Shipped 1.0 both
  profiles (uniform start, re-bakeable).

Device-class axis deferred to #1551 (needs a client_id → device-class
mapping that doesn't exist yet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 09:31:43 -04:00
parent 40384cc05e
commit 65dd132b3d
18 changed files with 437 additions and 119 deletions
@@ -0,0 +1,12 @@
-- 0046_context_time_weight.up.sql — time-of-day/weekday context conditioning
-- (#1531, milestone #160 Opt 3). Adds a per-profile scoring weight for the new
-- context-affinity term: how strongly a candidate is boosted when its artist's
-- play history concentrates in the current daypart × weekday-type cell (in the
-- user's local timezone). Mirrors the other ScoringWeights columns.
--
-- DEFAULT 1.0 backfills both existing profile rows to a modest on-value; the
-- Go ShippedRadioWeights/ShippedDailyMixWeights carry the same 1.0 so fresh
-- installs seed identically. Reconcile only seeds MISSING rows (ON CONFLICT DO
-- NOTHING), so existing rows rely on this DEFAULT until an operator resets.
ALTER TABLE recommendation_weight_profiles
ADD COLUMN context_time_weight double precision NOT NULL DEFAULT 1.0;