feat(discover): Surprise me leans on what you watch, and stays a surprise

The Home hero's surprise tier and the Plex Discover rail were uniform draws over the unseen. They
are now weighted draws (`app.draw`, Efraimidis-Spirakis over the same md5 shuffle): a title's
weight is 1 + its affinity. Plex: the genres of the latest watched titles (half-life 180 days, a
show counts once) and the TMDB recommendations stored on them (`plex.affinity`). YouTube: the
channels the user watches (half-life 90 days). One slot of three stays a blind draw, and with no
history the order is exactly the old one. No visible change.

The YouTube draw builds its pool once (a materialized CTE read twice): the scope is ~200k videos on
a prod-sized catalog, and a second ordered pass doubled the cost. Measured on localdev: a reroll
210 -> 292 ms.
This commit is contained in:
2026-11-07 02:41:30 +01:00
parent 5b846136ad
commit 9310ff85bf
7 changed files with 598 additions and 31 deletions
+2 -2
View File
@@ -25,8 +25,8 @@ Three layers, by *who* runs the code — not by feature:
background run's durable row, its words in `runstatus`),
`notifications`, `userscope`, `ratelimit`, `progress`, `logs` (the one logging
configuration), `logsink` (its `log_events` table), `diagnostics` (a user's consented log
level), `debugsession` (a recording that tags its user's lines, E16) and `collation` (A→Z in the
reader's alphabet, E30), … are single-file primitives many layers import. They depend on nothing above them (a core
level), `debugsession` (a recording that tags its user's lines, E16), `collation` (A→Z in the
reader's alphabet, E30) and `draw` (the weighted, seeded draw "Surprise me" deals from), … are single-file primitives many layers import. They depend on nothing above them (a core
module must never import a `routes/…` module).
**Extraction trigger: the SECOND consumer.** Keep new logic inline in its first caller. The moment a