docs: wanted, dbtest, recommendation and playlists comments say what the code does (#6277)

The same rewrite as d191a282, for four packages whose comments carried the
most history: tuning stories, quotes from the operator, single-install
measurements in library_scale.go, and 'used to' accounts of bugs. The
constraint each story protected stays as a present-tense reason (why tier 3
is reduced not removed, why the randomised arms take an order seed, why the
seed artist is eligible in Songs-like). Comments naming system_cron.go and
config.RecommendationConfig, neither of which holds what they claimed, now
name the scheduler and internal/recsettings.

Checked: non-comment tokens identical to HEAD; gofmt clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 12:19:15 -04:00
co-authored by Claude Opus 5.5
parent d191a28217
commit fcf5b7e5a8
48 changed files with 391 additions and 501 deletions
+32 -57
View File
@@ -82,11 +82,11 @@ type CandidateSourceLimits struct {
TagOverlap int
LikesOverlap int
RandomFill int
// TasteOverlap (#796 phase 2b): tracks by the user's top positively-
// TasteOverlap: tracks by the user's top positively-
// weighted taste-profile artists. 0 disables the arm (e.g. cold-start
// users have an empty profile, so it contributes nothing anyway).
TasteOverlap int
// UserCoplay (#1533): tracks by artists co-played across the instance
// UserCoplay: tracks by artists co-played across the instance
// with the seed's artist (source='user_cooccurrence'). Empty on
// single-user servers, so it contributes nothing there.
UserCoplay int
@@ -105,21 +105,18 @@ func DefaultCandidateSourceLimits() CandidateSourceLimits {
}
}
// SongsLikeCandidateSourceLimits is the pool shape for "Songs like {X}"
// (#3881). Same total size as the default (~170) — the composition is what
// changes, shifted hard toward arms that actually measure distance from the
// seed.
// SongsLikeCandidateSourceLimits is the pool shape for "Songs like {X}".
// Same total size as the default (~170) — the composition is what changes,
// shifted hard toward arms that actually measure distance from the seed.
//
// The surface answers "what sounds like THIS", and it shared the default
// pool with For-You, which answers the much broader "what will they enjoy
// today". Under the default, 50 of ~170 candidates carried sim_score = 0 by
// construction — `taste_overlap` (tracks by the user's top taste artists) and
// `random_fill` (literally any track not already in the pool), both of which
// are seed-INDEPENDENT. Nearly a third of the pool had no relationship to the
// seed at all, and the operator saw it: "I was getting a seeming wide variety
// of music from each one when I was hoping to stay in a certain neighborhood."
// The surface answers "what sounds like THIS", where For-You answers the much
// broader "what will they enjoy today". In the default pool, 50 of ~170
// candidates carry sim_score = 0 by construction — `taste_overlap` (tracks by
// the user's top taste artists) and `random_fill` (any track not already in
// the pool), both seed-INDEPENDENT — so nearly a third of it has no
// relationship to the seed.
//
// TIERED, per rule 131 — a system mix degrades, it never vanishes:
// TIERED — a system mix degrades, it never vanishes:
//
// tier 1 lb_similar real track-level similarity. The exact promise.
// tier 2 similar_artist, tag_overlap, coplay, likes_overlap
@@ -129,43 +126,23 @@ func DefaultCandidateSourceLimits() CandidateSourceLimits {
//
// The tiering is enforced by SCORE rather than by a fallback ladder: each tier
// holds its own sim_score band (recommendation.sql; tier 1 above 0.5, tier 2
// at or below it, #6177), and tier-3 arms carry sim_score 0, so under SongsLike weights (SimilarityWeight 4.0,
// everything seed-independent demoted) they rank below any real match and
// surface only when tiers 1–2 cannot fill the mix. That is the rule's
// "fill from tier 1 first, reach down only when a tier cannot fill".
// at or below it), and tier-3 arms carry sim_score 0, so under the SongsLike
// weights (SimilarityWeight 4.0, everything seed-independent demoted) they
// rank below any real match and surface only when tiers 1–2 cannot fill the
// mix: fill from tier 1 first, reach down only when a tier cannot fill.
//
// Which is exactly why tier 3 is REDUCED rather than removed. Zeroing those
// two arms was the first instinct and it is the vanish-or-nothing shape rule
// 131 exists to forbid: a seed whose artist has thin ListenBrainz coverage
// would produce a short mix or none at all, and "no playlist" is a worse
// answer than "a few tracks further from the seed than we would like".
// Tier 3 is REDUCED, never removed. Zeroing those two arms would leave a seed
// whose artist has thin ListenBrainz coverage with a short mix or none at
// all, and "no playlist" is a worse answer than "a few tracks further from
// the seed than we would like". The trim is safe because those arms order by
// md5(id || seed): a smaller limit takes a smaller but REPRODUCIBLE slice, so
// same-day rebuilds still agree.
//
// The seed-independent arms are trimmed hardest, because on this surface they
// are noise: `taste_overlap` (tracks by the user's top taste artists) and
// `random_fill` (any track not already in the pool) both carry
// `0.0::float8 AS sim_score`, so nearly a third of the default pool had no
// relationship to the seed at all.
//
// THESE TRIMS WERE BLOCKED UNTIL #3889. `likes_overlap` and `random_fill`
// used to end in a bare `ORDER BY random()`, which made their output a stable
// SET only while the limit exceeded the eligible rows — so SHRINKING them
// changed pool membership between same-day rebuilds and broke daily
// determinism. Those arms now order by md5(id || seed), so a smaller limit
// takes a smaller but REPRODUCIBLE slice, and the trim is safe.
//
// Reduced, never removed. Rule 131: the two seed-independent arms are the
// tier-3 floor, and zeroing them would leave a seed with thin ListenBrainz
// coverage producing a short mix or none at all. The weights (SimilarityWeight
// 4.0, everything seed-independent demoted) keep them ranked last, so they
// surface only when the closer tiers cannot fill the mix.
//
// likes_overlap is cut hardest of the tier-2 arms for a specific reason: its
// SQL assigns a FLAT 0.3 sim_score (recommendation.sql) rather than measuring
// anything. It is a collaborative signal wearing similarity's clothes, and a
// raised SimilarityWeight amplifies it. The tier bands keep it below every
// ListenBrainz match (#6177); halved pending the fill-rate measurement in
// #3879, and the honest fix is still to stop it claiming a similarity score
// it never computed.
// likes_overlap is cut hardest of the tier-2 arms: its SQL assigns a FLAT 0.3
// sim_score (recommendation.sql) rather than measuring anything. It is a
// collaborative signal wearing similarity's clothes, and a raised
// SimilarityWeight amplifies it. The tier bands keep it below every
// ListenBrainz match.
func SongsLikeCandidateSourceLimits() CandidateSourceLimits {
return CandidateSourceLimits{
LBSimilar: 60, // tier 1 — doubled; the only arm that measures the seed
@@ -178,13 +155,13 @@ func SongsLikeCandidateSourceLimits() CandidateSourceLimits {
}
}
// LoadCandidatesFromSimilarity is M4c's primary candidate-pool loader.
// LoadCandidatesFromSimilarity is the primary candidate-pool loader.
// 5-way SQL UNION (LB-similar / similar-artist tracks / MB-tag overlap /
// likes-overlap / random fill) + dedup-by-max sim_score. Returns
// []Candidate (same shape as LoadCandidates) so Shuffle is unchanged.
//
// orderSeed decides whether the randomised arms repeat their draw — see
// Column12 below and #3889. Pass a stable per-(user, day) value where the
// Column12 below. Pass a stable per-(user, day) value where the
// selection must be reproducible, and a varying one where it should not be.
//
// Caller (radio handler) falls back to LoadCandidates on error.
@@ -213,10 +190,8 @@ func LoadCandidatesFromSimilarity(
Limit_5: int32(limits.RandomFill),
Limit_6: int32(limits.TasteOverlap),
Limit_7: int32(limits.UserCoplay),
// #3889. Four arms used to end in a bare ORDER BY random(), which made
// their output a stable SET only while the limit exceeded the eligible
// rows. They now order by md5(id || this), so the caller decides
// whether the draw repeats: a per-(user, day) seed for the system
// The randomised arms order by md5(id || this) rather than random(),
// so the caller decides whether the draw repeats: a per-(user, day) seed for the system
// mixes that promise daily determinism, a fresh one per radio request.
Column12: orderSeed,
})
@@ -282,7 +257,7 @@ func LoadCandidatesFromSimilarity(
}
// loadCandidateMoods fetches the enriched tags for the given candidate tracks
// and reduces each to its canonical mood buckets (internal/mood, #1534), so the
// and reduces each to its canonical mood buckets (internal/mood), so the
// scorer can apply the mood facet per candidate. Tracks with no mood-word tags
// are absent from the map (→ no mood signal). Empty input short-circuits.
func loadCandidateMoods(