feat(v2): momentum-weighted, category-diverse Trending Now

Rework the V2 'Trending Now' section so it reflects real momentum instead
of pinning evergreen/foundational tools:

- Hybrid ranking: impact_weight x recency_decay (21d half-life) so fresh
  items surface and the section rotates between releases (date was shown
  but never used to rank before).
- Diversity quota: at most one card per digest category, then backfill —
  replaces the old impact-only sort that filled all slots with the 2-3
  highest-volume categories.
- Real momentum signal: join each item with the inventory by URL and show
  the live GitHub star count (e.g. 38.2k★); fall back to the 1-5 Gemini
  score only when no gh_stars. Adds a 🆕 NEW pill for items <= 7 days old.
- Clean titles: strip github.com/owner/ paths and 'domain.tld:' prefixes
  (no more 'github.com: Istio' or raw repo slugs).
- Fix star-render bug ('🌟'*stars or 0 printed a literal 0 when stars=0).

Generator-only change; CI publisher (04.1) regenerates v2-docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nubenetes Bot
2026-06-20 22:51:54 +02:00
co-authored by Claude Opus 4.8
parent 7a98574000
commit a9fe5dded5
2 changed files with 85 additions and 7 deletions
+11
View File
@@ -1210,6 +1210,17 @@ input[type="text"] {
color: #0ea5e9;
}
.trending-card__new {
margin-left: 6px;
padding: 1px 6px;
border-radius: 6px;
font-size: 0.85em;
font-weight: 800;
letter-spacing: 0.03em;
background: rgba(16, 185, 129, 0.18);
color: #10b981;
}
/* Digest link cards (CTA to full digest pages) */
.digest-links {
display: flex;
+74 -7
View File
@@ -1071,12 +1071,67 @@ class V2VisionEngine:
pass
if digest_data and "3_months" in digest_data:
top_items = []
# --- Trending v2: momentum-weighted, category-diverse selection ---
# Score = impact_weight * recency_decay so the section actually rotates
# with fresh items instead of pinning evergreen/foundational tools.
now = datetime.now(MADRID_TZ)
HALF_LIFE_DAYS = 21.0
impact_weight = {"critical": 1.0, "high": 0.66, "medium": 0.4}
def _parse_day(s):
try:
return datetime.fromisoformat(
str(s).replace("Z", "+00:00").split("T")[0]
).date()
except Exception:
return None
pool = []
for cat_name, items in digest_data.get("3_months", {}).items():
for item in items[:2]:
top_items.append({**item, "digest_category": cat_name})
top_items.sort(key=lambda x: {"critical": 3, "high": 2, "medium": 1}.get(x.get("impact", "medium"), 0), reverse=True)
top_items = top_items[:6]
d = _parse_day(item.get("date"))
age_days = (now.date() - d).days if d else 999
recency = 0.5 ** (max(age_days, 0) / HALF_LIFE_DAYS)
score = impact_weight.get(item.get("impact", "medium"), 0.4) * (0.35 + 0.65 * recency)
pool.append({**item, "digest_category": cat_name, "_age_days": age_days, "_score": score})
pool.sort(key=lambda x: x["_score"], reverse=True)
# Diversity quota: at most one card per category, then backfill.
top_items, used_cats, used_urls = [], set(), set()
for it in pool:
if it["digest_category"] in used_cats:
continue
top_items.append(it)
used_cats.add(it["digest_category"])
used_urls.add(it.get("url"))
if len(top_items) >= 6:
break
if len(top_items) < 6:
for it in pool:
if it.get("url") in used_urls:
continue
top_items.append(it)
used_urls.add(it.get("url"))
if len(top_items) >= 6:
break
def _clean_title(t):
t = nuclear_strip(t)
# "github.com/owner/repo" -> "repo"
if "github.com/" in t.lower():
t = t.rstrip("/").split("/")[-1]
# "domain.tld: Real Name" -> "Real Name" (Gemini host-prefixed titles)
m = re.match(r'^[\w.-]+\.[a-z]{2,}:\s*(.+)$', t, re.I)
if m:
t = m.group(1)
return t
def _fmt_stars(n):
if not n:
return ""
if n >= 1000:
return f"{n / 1000:.1f}k★".replace(".0k", "k")
return f"{n}"
impact_icons = {"critical": "🔴", "high": "🟡", "medium": "🔵"}
try:
@@ -1096,12 +1151,24 @@ class V2VisionEngine:
cards_html = f'<div class="trending-section">\n<div class="trending-section__title">🔥 Trending Now — Cloud Native Intelligence {updated_badge}</div>\n<div class="trending-grid">\n'
for item in top_items:
impact = item.get("impact", "medium")
# Momentum: prefer real GitHub star count from the inventory join;
# fall back to the 1-5 Gemini impact score rendered as 🌟.
inv_meta = self.inventory.get(item.get("url")) if isinstance(self.inventory, dict) else None
gh_stars = inv_meta.get("gh_stars") if isinstance(inv_meta, dict) else None
metric = _fmt_stars(gh_stars)
if not metric:
score = item.get("stars") or 0
metric = "🌟" * score if score else ""
meta = item.get("date", "")
if metric:
meta += f" · {metric}"
new_pill = ' <span class="trending-card__new">🆕 NEW</span>' if item.get("_age_days", 999) <= 7 else ""
cards_html += (
f'<div class="trending-card">\n'
f' <div class="trending-card__impact trending-card__impact--{impact}">{impact_icons.get(impact, "🔵")} {impact.upper()}</div>\n'
f' <div class="trending-card__impact trending-card__impact--{impact}">{impact_icons.get(impact, "🔵")} {impact.upper()}{new_pill}</div>\n'
f' <div class="trending-card__category">{item.get("digest_category", "")}</div>\n'
f' <div class="trending-card__title"><a href="{item.get("url", "#")}">{nuclear_strip(item.get("title", "Unknown"))}</a></div>\n'
f' <div class="trending-card__meta">{item.get("date", "")} · {"🌟" * item.get("stars") or 0}</div>\n'
f' <div class="trending-card__title"><a href="{item.get("url", "#")}">{_clean_title(item.get("title", "Unknown"))}</a></div>\n'
f' <div class="trending-card__meta">{meta}</div>\n'
f' <div class="trending-card__why">{item.get("why", "")}</div>\n'
f'</div>\n'
)