fix: stale-news control — repair clientRank's age source, add isFossil backstop
clientRank already existed specifically to stop stale high-scorers from leading the feed, but computed age from updatedAt — the same timestamp that keeps resetting whenever the backend attaches any article to a cluster (BACKLOG #15). A fossil's age always read as ~0, silently defeating the whole mechanism. Fixed to use firstSeenAt. clientRank's decay is soft (max 20% penalty) and isn't always enough against a high signalScore alone, so added StorySummary.isFossil (>5 days old by firstSeenAt) as a hard backstop: mainStories and makeDigest now sink fossils below current stories, so one can never take the hero/lead slot regardless of score. Still shown in the feed, just not in the most prominent position. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,12 +45,25 @@ Status: 🔴 open · 🟡 mitigated client-side (real fix still pending) · 🟢
|
||||
unrelated articles keep a stale cluster's clock alive. Also confirmed the API's own
|
||||
`isStale` field is unusable as a stopgap — it reads `false` on every story checked
|
||||
regardless of actual age, so it isn't computed from a real staleness threshold.
|
||||
*Client mitigation shipped:* age display (row meta line, hero card) now reads from
|
||||
`firstSeenAt` instead of `updatedAt`, so the UI no longer shows "1 hr ago" on stories
|
||||
that are actually weeks old. *Ranking is still wrong* — old stories still surface at
|
||||
the top of the feed, because `signalScore`/`freshnessScore` (used for ordering) are
|
||||
computed server-side and the client must not re-rank (architecture rule). The
|
||||
real fix is still backend-side.
|
||||
*Client mitigations shipped:*
|
||||
1. Age display (row meta line, hero card) now reads from `firstSeenAt` instead of
|
||||
`updatedAt`, so the UI no longer shows "1 hr ago" on stories that are actually
|
||||
weeks old.
|
||||
2. Found that `StorySummary.clientRank` — an *existing* time-decay mechanism, already
|
||||
in the codebase specifically to stop stale high-scorers from leading the feed — was
|
||||
itself silently neutralized by this same bug: it computed age from `updatedAt`, so a
|
||||
fossil's age always read as ~0. Fixed to use `firstSeenAt`. This isn't new
|
||||
client-side ranking logic; it's un-breaking a pre-existing, intentional one.
|
||||
3. Added `StorySummary.isFossil` (`firstSeenAt` > 5 days old) as a hard backstop —
|
||||
`clientRank`'s decay is soft (max 20% score penalty) and isn't always enough to
|
||||
overcome a high `signalScore` alone. Fossils are sunk below current stories in
|
||||
`mainStories` and `makeDigest`, so a stale story can never occupy the hero/lead slot
|
||||
even if the server still ranks it #1. They still appear in the feed, just not in
|
||||
the most prominent, most misleading position.
|
||||
*Still wrong at the source* — the server's own `signalScore`/`freshnessScore` ordering
|
||||
(what page 2+, pagination cutoffs, and `min_signal` filtering all rely on) is
|
||||
unaffected; these are display/ordering patches downstream of it. The real fix is
|
||||
still backend-side.
|
||||
|
||||
## Backend — features
|
||||
|
||||
|
||||
Reference in New Issue
Block a user