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
+24 -43
View File
@@ -11,44 +11,26 @@ import (
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
// Scaling the candidate pool to the library (#3880).
// Scaling the candidate pool to the library.
//
// DefaultCandidateSourceLimits returns what its own comment calls "the v1
// hardcoded constants per spec" — ~170 candidates, identical for a 500-track
// library and a 100,000-track one. Operator, 2026-09-10: "is the pool that we
// draw from somehow scaled to the amount of music in the library... my earlier
// understanding of the tuning and work may have been skewed by what was in my
// library."
//
// It was not, and the consequence compounds. The pool samples a shrinking
// FRACTION of the library as it grows — 17% of 1,000 tracks, 1.7% of 10,000,
// 0.17% of 100,000 — so the ceiling on how much of a collection can ever
// surface stays flat while the collection does not. RandomFill, whose entire
// job is exploration, becomes a thinner and noisier slice of a more diverse
// corpus at exactly the moment diversity rises.
// DefaultCandidateSourceLimits is ~170 candidates, whatever the library's
// size. Unscaled, the pool samples a shrinking FRACTION of the library as it
// grows — 17% of 1,000 tracks, 1.7% of 10,000, 0.17% of 100,000 — so the
// ceiling on how much of a collection can ever surface stays flat while the
// collection does not, and RandomFill, whose job is exploration, becomes a
// thinner slice of a more diverse corpus.
const (
// libraryScaleReference is the library size at which the base limits
// apply unchanged. Below it nothing scales, so small libraries keep
// today's behaviour exactly.
//
// ASSUMED, NOT MEASURED. The size the v1 constants were actually tuned
// against is unrecorded; 5,000 is a plausible mid-size library and a
// deliberately conservative place to start growing. This constant is the
// single thing to change.
//
// Measured on the operator's library, 26,005 tracks (scale 2.28), for the
// 15 most-played seeds and two Moe Shop tracks (#3880, 2026-10-21):
// lb_similar 0–6 edges (3 of 17 seeds have any): never reaches
// even the base limit, so scaling it costs nothing
// similar_artists 860–6,307 tracks over 7–33 artists: always full
// tag_overlap 4,658–19,658 tracks: always full
// likes_overlap 193–497 liked tracks sharing a tag: always full
// taste_overlap 20,045 tracks over 174 artists: always full
// coplay 0 rows: a single-user server
// So the arms the scale grows all have far more rows than it asks for, and
// the tuned-against size cannot be recovered from data; the number stays
// until a library shows a scaled arm running dry or a pool too slow.
// An assumption, not a measurement: 5,000 is a plausible mid-size library
// and a deliberately conservative place to start growing. On a large
// library the arms the scale grows (similar_artists, tag_overlap,
// likes_overlap, taste_overlap) hold far more rows than it asks for, while
// lb_similar rarely reaches even its base limit. Change this if a library
// shows a scaled arm running dry or a pool too slow.
libraryScaleReference = 5000
// maxLibraryScale bounds growth so a very large library does not turn
@@ -63,9 +45,9 @@ const (
// Square-root growth keeps the pool meaningfully proportional while staying
// bounded: 1x at the reference, 2x at four times it, 4x at sixteen times.
//
// Never returns below 1: the base limits are a floor, not a midpoint. That
// also keeps this safe against #3889 — growing an arm ordered by unseeded
// random() is fine, shrinking one is what breaks same-day determinism.
// Never returns below 1: the base limits are a floor, not a midpoint. Growing
// an arm ordered by unseeded random() is harmless; shrinking one would break
// same-day determinism.
func libraryScale(libraryTracks int64) float64 {
if libraryTracks <= libraryScaleReference {
return 1.0
@@ -113,8 +95,7 @@ func ScaleForLibrary(base CandidateSourceLimits, libraryTracks int64) CandidateS
// CountLibraryTracks returns the whole library's track count.
//
// Reuses CountTracksMatching with an empty pattern — `title ILIKE '%%'`
// matches every row — rather than adding a query, because a new one would
// need sqlc regeneration (rule 28, and see #3889 for where that blocks).
// matches every row — rather than adding a query.
// The user id is left invalid so the quarantine anti-join is skipped: this
// number sizes a pool, and a per-user view of it would be false precision.
func CountLibraryTracks(ctx context.Context, q *dbq.Queries) (int64, error) {
@@ -155,11 +136,11 @@ func NewLibrarySize(now func() time.Time) *LibrarySize {
// Get returns the cached size, refreshing through load when stale.
//
// A NIL RECEIVER IS VALID and means "no cache": the count still runs, it is
// just not memoised. That is deliberate rather than defensive habit. The api
// handlers struct is built directly by a dozen tests that cannot know about
// every field, and a nil here previously panicked inside a radio request —
// turning a missing pool-sizing HINT into a 500. Uncached-but-correct is the
// right failure for something whose whole contract is that it degrades.
// just not memoised. The api handlers struct is built directly by a dozen
// tests that cannot know about every field, and a panic here would turn a
// missing pool-sizing HINT into a 500 inside a radio request.
// Uncached-but-correct is the right failure for something whose whole
// contract is that it degrades.
func (l *LibrarySize) Get(ctx context.Context, load func(context.Context) (int64, error)) int64 {
if l == nil {
n, err := loadWithDeadline(ctx, load)
@@ -191,8 +172,8 @@ func (l *LibrarySize) Get(ctx context.Context, load func(context.Context) (int64
return l.size
}
// loadWithDeadline bounds the count. Rule 156: the caller is a user waiting
// on a radio, and a pool-sizing hint is never worth hanging for.
// loadWithDeadline bounds the count: the caller is a user waiting on a radio,
// and a pool-sizing hint is never worth hanging for.
func loadWithDeadline(ctx context.Context, load func(context.Context) (int64, error)) (int64, error) {
cctx, cancel := context.WithTimeout(ctx, librarySizeTimeout)
defer cancel()