feat(v2): add "Rising this Quarter" second lane to Trending

Extract lane selection/rendering into reusable helpers (_select_lane,
_render_cards) and add a second momentum lane below "Trending Now":

- "📈 Rising this Quarter — Sustained Momentum": 6-month digest window
  with a soft 60d half-life (vs 21d for lane 1), so it favours resources
  with sustained relevance rather than just the freshest items.
- De-duplicated against lane 1 by URL, so the two lanes surface different
  resources (verified: FinOps / NVIDIA device-plugin / Ruff / vLLM with
  real star counts, no overlap with the Trending lane).
- New .trending-section__title--secondary style (divider + lighter weight).

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:55:25 +02:00
parent 3d13cd83a7
commit c2e5772323
2 changed files with 86 additions and 54 deletions

View File

@@ -1103,6 +1103,18 @@ input[type="text"] {
flex-wrap: wrap;
}
.trending-section__title--secondary {
font-size: 1.15em;
margin-top: 28px;
padding-top: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
color: var(--md-primary-fg-color--dark);
}
[data-md-color-scheme="slate"] .trending-section__title--secondary {
border-top-color: rgba(255, 255, 255, 0.08);
}
.trending-section__updated {
font-size: 0.55em;
font-weight: 500;

View File

@@ -1075,8 +1075,8 @@ class V2VisionEngine:
# 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}
impact_icons = {"critical": "🔴", "high": "🟡", "medium": "🔵"}
def _parse_day(s):
try:
@@ -1086,35 +1086,6 @@ class V2VisionEngine:
except Exception:
return None
pool = []
for cat_name, items in digest_data.get("3_months", {}).items():
for item in items[:2]:
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"
@@ -1133,7 +1104,74 @@ class V2VisionEngine:
return f"{n / 1000:.1f}k★".replace(".0k", "k")
return f"{n}"
impact_icons = {"critical": "🔴", "high": "🟡", "medium": "🔵"}
def _select_lane(window_key, half_life, count, exclude_urls):
# Score = impact_weight * recency_decay over the given window; an
# aggressive half-life surfaces fresh items ("Trending Now"), a soft
# one favours sustained importance ("Rising this Quarter").
pool = []
for cat_name, items in digest_data.get(window_key, {}).items():
for item in items[:2]:
if item.get("url") in exclude_urls:
continue
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)
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.
sel, used_cats, used = [], set(), set(exclude_urls)
for it in pool:
if it["digest_category"] in used_cats or it.get("url") in used:
continue
sel.append(it)
used_cats.add(it["digest_category"])
used.add(it.get("url"))
if len(sel) >= count:
break
if len(sel) < count:
for it in pool:
if it.get("url") in used:
continue
sel.append(it)
used.add(it.get("url"))
if len(sel) >= count:
break
return sel
def _render_cards(items):
html = ""
for item in 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 ""
html += (
f'<div class="trending-card">\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", "#")}">{_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'
)
return html
# Lane 1: fresh momentum (3-month window, 21d half-life).
top_items = _select_lane("3_months", 21.0, 6, set())
# Lane 2: sustained importance (6-month window, soft 60d decay),
# de-duplicated against lane 1 so it surfaces different resources.
rising_items = _select_lane("6_months", 60.0, 4, {it.get("url") for it in top_items})
try:
from datetime import datetime as _dt
# Prefer _meta.last_updated (tracks actual Gemini analysis date)
@@ -1149,30 +1187,12 @@ class V2VisionEngine:
digest_updated = ""
updated_badge = f'<span class="trending-section__updated">Updated {digest_updated}</span>' if digest_updated else ""
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()}{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", "#")}">{_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'
)
cards_html += _render_cards(top_items)
cards_html += '</div>\n'
if rising_items:
cards_html += '<div class="trending-section__title trending-section__title--secondary">📈 Rising this Quarter — Sustained Momentum</div>\n<div class="trending-grid">\n'
cards_html += _render_cards(rising_items)
cards_html += '</div>\n'
cards_html += '<div class="digest-links">\n'
cards_html += ' <a href="./tech-digest/" class="digest-link-card">📊 Full Tech & Cloud Digest →</a>\n'
cards_html += ' <a href="./industry-digest/" class="digest-link-card">🌍 Industry & Geo Digest →</a>\n'