mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 11:22:25 +00:00
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]>
This commit is contained in:
1 parent
44bf88cf88
commit
c0a8636678
8 files changed
+322
-7
No files matched your search
@@ -3,6 +3,54 @@
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { markAsNormalized } from "./utils.js";
|
||||
|
||||
// Ranks by the For You score (set by createForYouScorerStage): score desc,
|
||||
// trusted-muted last, recency as the tiebreak. Reuses the same muted-last rule
|
||||
// as the chronological sorter so behavior is consistent across feeds.
|
||||
export function createForYouScoreSorter() {
|
||||
return function forYouScoreSorter(items = [], context = {}) {
|
||||
if (!Array.isArray(items)) {
|
||||
return [];
|
||||
}
|
||||
void context;
|
||||
const copy = [...items];
|
||||
|
||||
const isMuted = (entry) =>
|
||||
entry?.metadata?.moderation?.trustedMuted === true ||
|
||||
entry?.video?.moderation?.trustedMuted === true;
|
||||
|
||||
const scoreOf = (entry) => {
|
||||
const score = Number(entry?.metadata?.forYouScore);
|
||||
return Number.isFinite(score) ? score : 0;
|
||||
};
|
||||
|
||||
const timestampOf = (entry) => {
|
||||
const video = entry?.video;
|
||||
if (!video || typeof video !== "object") {
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
const root = Number(video.rootCreatedAt);
|
||||
if (Number.isFinite(root)) return Math.floor(root);
|
||||
const created = Number(video.created_at);
|
||||
return Number.isFinite(created) ? Math.floor(created) : Number.NEGATIVE_INFINITY;
|
||||
};
|
||||
|
||||
copy.sort((a, b) => {
|
||||
const aMuted = isMuted(a);
|
||||
const bMuted = isMuted(b);
|
||||
if (aMuted !== bMuted) {
|
||||
return aMuted ? 1 : -1;
|
||||
}
|
||||
const scoreDiff = scoreOf(b) - scoreOf(a);
|
||||
if (scoreDiff !== 0) {
|
||||
return scoreDiff;
|
||||
}
|
||||
return timestampOf(b) - timestampOf(a);
|
||||
});
|
||||
|
||||
return copy;
|
||||
};
|
||||
}
|
||||
|
||||
export function createChronologicalSorter({
|
||||
direction = "desc",
|
||||
postedAtResolver,
|
||||
|
||||
Reference in new issue
Block a user