Files
bitvid/docs/feed-algo-audit.md
thePR0M3TH3ANandClaude Opus 4.8 c0a8636678 feat(feed): make For You inclusive + ranked by your lists (G1-G5)
The For You feed was chronological, hashtag-interest-EXCLUSIVE, and ignored
follows entirely (docs/feed-algo-audit.md). Now it is inclusive + ranked:

- createForYouScorerStage scores each video: follows-boost (author you follow)
  + interest/watch-topic affinity (cosine vs your interests+watch-history vector)
  + freshness - disinterest penalty. Reuses Explore's vector/similarity helpers.
- createForYouScoreSorter ranks by that score (muted last, recency tiebreak).
- For You pipeline: interests now BOOST not exclude (enforceInterests:false);
  scorer added; chronological sorter replaced. Never empty (falls back to
  recency when you have no follows/interests/history).
- buildForYouFeedRuntime now feeds subscriptionAuthors + watchHistoryTagCounts.

Watched/blocked/muted are still removed (suppression/blacklist/moderation
stages unchanged). Cheat-resistant tests (older boosted items only win via the
boost). Grid renders end-to-end (~1.3s). Weight tuning is a follow-up.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 16:57:05 -04:00

5.1 KiB
Raw Permalink Blame History

Feed Algorithm ↔ Lists/Notes — Wiring Audit

Read-only audit (no code changed). Goal: confirm whether the For You feed is actually driven by the user's lists/notes (follows, hashtag prefs, watch history, blocks/moderation), now that those lists load reliably after the data-flow refactor. Source of truth: js/app/feedCoordinator.js (registerForYouFeed, buildForYouFeedRuntime) + js/feedEngine/*.

For You pipeline, as built today

source:  createActiveNostrSource         // ALL active videos (block-filtered only)
stages:
  1. createTagPreferenceFilterStage()    // hashtag interests/disinterests
  2. createBlacklistFilterStage()        // blocks + admin blacklist
  3. createWatchHistorySuppressionStage()// drop already-watched
  4. createDedupeByRootStage()
  5. createModerationStage()             // NIP-56 reports + trusted mutes
  6. createResolvePostedAtStage()
sorter:  createChronologicalSorter()     // newest-first

Runtime inputs (buildForYouFeedRuntime): blacklistedEventIds, isAuthorBlocked, tagPreferences {interests, disinterests, available}, moderationThresholds, watchHistory.shouldSuppress.

What IS correctly wired (drives the feed) ✅

  • Hashtag interests/disinterests — from the real hashtagPreferencesSnapshot. Interests are a hard filter (see gap 3); disinterested tags are dropped.
  • Blocks / admin blacklist — isAuthorBlocked + blacklistedEventIds.
  • Watch history — watched items are suppressed (removed) via runtime.watchHistory.shouldSuppress.
  • Moderation — NIP-56 trusted reports + trusted mutes, threshold-gated.

So the refactor's payoff is real: these lists now load and do shape For You.

Resolution (2026-06-14)

Product decision: For You = inclusive + ranked. Implemented:

  • New createForYouScorerStage (in exploreScoring.js) scores each video by follows-boost + interest/watch-topic affinity + freshness − disinterest (reusing Explore's vector/similarity/freshness helpers; affinity uses the combined interests+watch-history user vector).
  • New createForYouScoreSorter ranks by that score (muted last, recency tiebreak).
  • registerForYouFeed: interests now boost instead of exclude (enforceInterests:false); scorer added; chronological sorter replaced.
  • buildForYouFeedRuntime now supplies subscriptionAuthors (follows) and watchHistoryTagCounts.
  • Result: G1 follows, G2 ranking, G3 inclusivity (never empty), G4 watch-topic affinity, G5 blending — all addressed. Cheat-resistant tests in tests/for-you-scorer.test.mjs.

Tuning of the weights is a follow-up (currently follows .4 / affinity .35 / freshness .25 / disinterest .3).

Gaps (original audit)

G1 — Follows/subscriptions have ZERO effect on For You (highest impact)

For You's source is createActiveNostrSource (all videos); the runtime passes no subscriptionAuthors. Follows are used only by the separate Subscriptions feed. So "who you follow" does not include, boost, or rank anything in For You. For a feed meant to be "driven by your lists," this is the biggest miss.

G2 — For You is chronological, not relevance-ranked

The tag-preference stage records metadata.matchedInterests, but the sorter is createChronologicalSorter(), so match strength/count never affects order. There is no scoring that blends interest strength + freshness + follows. (Explore does have createExploreScorerStage; For You does not.)

G3 — Interest filter is binary/exclusive

If the user has any interests set, For You keeps only videos with a matching tag and drops the rest. Niche interests or sparsely-tagged videos → a thin or empty For You. No "boost-but-don't-exclude" mode and no fallback when matches are too few.

G4 — Watch-history topic affinity is unused in For You

For You only uses watch history to suppress watched items. The topics you watch (watchHistoryTagCounts) are not an interest signal here — though Explore's runtime does compute and use them. So For You ignores a strong implicit signal.

G5 — No weighting/blending overall

For You = a chain of hard filters + chronological. "Driven by lists" today means "filtered by hashtag prefs, newest-first" — missing follows weighting, interest ranking, and watch-topic affinity working together.

Suggested fix order (incremental, each shippable)

  1. G1: feed follows into For You — add subscriptionAuthors to the runtime and either (a) blend a follows-boost into a scorer, or (b) merge a follows-source with the active source. (Biggest UX win.)
  2. G2 + G3: replace the chronological sorter with a For You scorer that ranks by interest matches + freshness + follows instead of hard-excluding, with a graceful fallback so the feed is never empty.
  3. G4: pass watchHistoryTagCounts into the For You runtime and factor watch- topic affinity into the score (reuse Explore's machinery).

Open question for product: should For You be inclusive+ranked (show lots, order by relevance — recommended) or strict (only interest/follow matches)? This decides G2/G3.