From a9fe5dded53077a7c2b339a68d5c300d9d38c4bd Mon Sep 17 00:00:00 2001 From: Nubenetes Bot Date: Sat, 20 Jun 2026 22:51:54 +0200 Subject: [PATCH] feat(v2): momentum-weighted, category-diverse Trending Now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/static/v2_elite.css | 11 ++++++ src/v2_optimizer.py | 81 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/docs/static/v2_elite.css b/docs/static/v2_elite.css index bc32730b..013fd2b5 100644 --- a/docs/static/v2_elite.css +++ b/docs/static/v2_elite.css @@ -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; diff --git a/src/v2_optimizer.py b/src/v2_optimizer.py index 05184699..4dcc0605 100644 --- a/src/v2_optimizer.py +++ b/src/v2_optimizer.py @@ -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'