---
title: Implement the news-week relevance floor for trending news
type: task
status: completed
description: >-
  Give trending-news surfaces a calendar 'news week' published_after floor (floor = most recent
  Saturday before the current Tue-Mon week, rolling each Tuesday) so stale corroborated stories can
  no longer pin to the top of Around the League, /news Top, or the wire.
base_uri: user:task/league/implement-news-week-relevance-floor.md
created_at: '2026-10-01T03:01:34.503Z'
entity_id: d23d4bec-6aec-43fd-8021-a85b5d8d9bc2
finished_at: '2026-10-01T03:23:48.340Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[█████████████████████-███████-████████-███████████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-01T03:23:48.340Z'
visibility_analyzed_at: '2026-11-05T23:49:36.739Z'
---

## Context

League home's "Around the League" section ranked an 8-day-old Jaxson Dart article first on 2026-09-30 (league 1). Investigation (thread `█████████████████████████████`) established the root cause: `/api/news` trending credits any article carried by at least two other publishers (`corroborating_publishers`) with a fixed, age-independent boost (CORROBORATION_RATIO 0.156 in `content-feed/lib/score-feed-item.mjs`; the decay term was retired upstream on 2026-09-25, so the window is the only age control), and the Around the League blocks request `trending_descending` with the default `decay_seconds` (90000), which resolves to NO `published_after` floor. Sort is strength DESC, coverage (corroborating_publishers) DESC, recency. The Dart story (item_id `f19030ee...`, corroborating_publishers = 14, the current max) therefore pins at #1 indefinitely until a more-covered story arrives.

Operator ruling (2026-09-30): implement a "week" relevance rule — any news from prior to Saturday is no longer relevant starting the following Tuesday.

## Objective

Add a calendar "news week" `published_after` floor that applies to every trending-news request on the site, so stale corroborated stories cannot pin to the top of Around the League, /news Top, or the wire.

## The rule

A trending-news item stays relevant for the week whose games it belongs to and drops the following Tuesday. The floor is **00:00 UTC of the most recent Saturday before the current Tue–Mon week** = `(most recent Tuesday ≤ now) − 3 days`, at start of UTC day; it advances each Tuesday and is otherwise fixed for the whole week (half-open boundary, floor inclusive).

Worked against the calendar (today 2026-09-30, Wed): current week is Tue 09-29 → Mon 10-05, floor 09-26 — the Dart story (09-22) is cut, the J.J. McCarthy trade (09-28/09-29) stays. On Tue 10-06 the floor advances to 10-03, dropping everything from 09-26 → 10-02.

Properties: cache-stable (one value per week vs the per-hour-rolling 168h/24h floors today); UTC, matching the route's other date floors; composes under `resolve_news_published_after`'s "tightest floor wins" (Live's 24h and any reader range still tighten from it; a looser request cannot widen past the week); year-round, no dependency on season constants.

## Surface sweep

- Around the League reading + wire (league home) — trending, default decay → no floor today → gets the week floor with zero caller change.
- /news Top band — previously `decay_seconds: 259200` (rolling 168h); now sends no horizon and takes the default week floor.
- /news Live/Trending band (`decay_seconds: 21600`) — 24h, tighter → unchanged.
- /news recency stream, league-home Team Wire, the player news tab, and text search — not `trending_descending` → untouched (personal/recency surfaces stay deliberately floor-less so a quiet roster's wire does not empty and the player tab can page past a week).

Consequence to note in the exit summary: a trending request can no longer be made floor-less; the closed `decay_seconds` set stops offering "no floor" for top news, and anyone wanting history uses a recency order or an explicit `start_date` on one.

## Plan

- [x] Add a pure, exported `resolve_news_week_floor(now)` to `api/routes/news.mjs`, returning `YYYY-MM-DDT00:00:00.000Z`, computed as `(most recent Tuesday ≤ now) − 3 days` in UTC (getUTCDay arithmetic; Tuesday = 2).
- [x] In `resolve_news_published_after`, resolve trending with the default `decay_seconds` (90000) — and the Top band's 259200 — to the week floor instead of no floor / 168h. Keep 21600 (Live) as 24h. Update `DECAY_SECONDS_WINDOW_HOURS`, the swagger `decay_seconds` description, and the surrounding comments to say the default now applies the news-week floor. Shipped, then 259200 was retired from the closed set outright once Top stopped sending it (league 53bb8cbc6), so the set is now 21600 and 90000.
- [x] Make the /news Top unification call (recommended: yes — stop sending `decay_seconds: 259200` from `NEWS_BANDS.top` in `app/core/news/build-news-request-params.mjs` so "the news week" has ONE spelling) or land the league-home fix first and unify in a second commit. Decided: unified in the same commit.
- [x] Extend the news-route spec to assert: the helper's boundary (rolls on Tuesday, stable all week); default + trending → week floor; Live 24h and a reader 24h/3d still win; nothing can widen past the week.
- [x] Follow league CLAUDE.md (`docs/guides/test.md`, `docs/guides/ship.md`): run `yarn test:isolated <spec>` and `yarn lint`, commit coherently (DDL/schema not involved).
- [x] After deploy, verify live: `curl 'https://xo.football/api/news?sort_order=trending_descending&source_types=rss,news-sitemap,hackernews,github&limit=40'` returns no item older than the current week floor, and the Dart story is absent.

## Key locations

- `repository/active/league/api/routes/news.mjs` — the route, `resolve_news_published_after`, floor derivation, cache keys.
- `repository/active/league/app/core/news/build-news-request-params.mjs` — `NEWS_BANDS` (Top/Live `decay_seconds`).
- `repository/active/league/app/core/team-beat/build-team-beat-request-params.mjs` — around-the-league request (expected: no caller change, but update the comment).
- Upstream corroboration scoring — `content-feed/lib/score-feed-item.mjs`, `extension/content-feed-query/lib/build-feed-query.mjs` — DO NOT modify; the relevance floor is league-side policy.
- Root-cause reproduction and ranking evidence live in the authoring thread (`█████████████████████████████`), 2026-09-30.
