diff --git a/README.md b/README.md index b82b72a5..9f20c8d3 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ Additionally, as of May 2026, Nubenetes has reached the **Platinum Operational T | :--- | :--- | | **Total Technical Resources (Links)** | **18647+** | | **Specialized MD Pages** | **162** | -| **Total Commits** | **6431+** | +| **Total Commits** | **6435+** | | **Primary AI Engine** | **Google Gemini (Agentic)** | @@ -180,7 +180,7 @@ The growth of Nubenetes reflects the acceleration of the Cloud Native ecosystem. | 6 | 2023 | 30 | 123 | Maintenance & Refinement | | 7 | 2024 | 53 | 218 | Curation Strategy Pivot | | 8 | 2025 | 5 | 20 | Stability & Research Phase | -| 9 | 2026 | 2872 | 11,861 | **Agentic AI Surge** (May 2026 Inception) | +| 9 | 2026 | 2876 | 11,877 | **Agentic AI Surge** (May 2026 Inception) | @@ -196,8 +196,8 @@ xychart-beta title "Nubenetes Annual Growth Metrics (2018โ€“2026)" x-axis ["2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025", "2026"] y-axis "Volume (Commits / Estimated New Refs)" 0 --> 12000 - bar [1445, 586, 8449, 2193, 1660, 123, 218, 20, 11861] - bar [350, 142, 2046, 531, 402, 30, 53, 5, 2872] + bar [1445, 586, 8449, 2193, 1660, 123, 218, 20, 11877] + bar [350, 142, 2046, 531, 402, 30, 53, 5, 2876] ``` @@ -207,7 +207,7 @@ xychart-beta | :--- | :---: | :---: | :--- | | 2026-04 | 25 | 103 | Active Curation | | 2026-05 | 2101 | 8,677 | **Agentic Inception (Gemini Era)** | -| 2026-06 | 746 | 3,080 | Active Curation | +| 2026-06 | 750 | 3,097 | Active Curation | ### 2.4. Content Distribution and Semantic Clustering diff --git a/data/news_digest.json b/data/news_digest.json index 4e9bd3d0..5e7598bd 100644 --- a/data/news_digest.json +++ b/data/news_digest.json @@ -7440,6 +7440,6 @@ "method": "fallback_small" } }, - "last_updated": "2026-06-20T22:59:59.133383+02:00" + "last_updated": "2026-06-20T23:19:51.802335+02:00" } } \ No newline at end of file diff --git a/src/v2_optimizer.py b/src/v2_optimizer.py index 5f86df50..939aee2f 100644 --- a/src/v2_optimizer.py +++ b/src/v2_optimizer.py @@ -1074,7 +1074,16 @@ class V2VisionEngine: # --- 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. + # Anchor "now" to the digest's analysis timestamp (not wall-clock) so + # identical digest input always renders identical cards โ€” re-renders on + # later days must not shuffle ages/NEW pills and churn the committed HTML. now = datetime.now(MADRID_TZ) + try: + raw_now = digest_data.get("_meta", {}).get("last_updated", "") + if raw_now: + now = datetime.fromisoformat(raw_now) + except Exception as e: + log_event(f"[WARN] trending now-anchor: {str(e)[:100]}") impact_weight = {"critical": 1.0, "high": 0.66, "medium": 0.4} impact_icons = {"critical": "๐Ÿ”ด", "high": "๐ŸŸก", "medium": "๐Ÿ”ต"} @@ -1104,20 +1113,18 @@ class V2VisionEngine: return f"{n / 1000:.1f}kโ˜…".replace(".0k", "k") return f"{n}โ˜…" - 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"). + def _select_lane(window_key, count, exclude_urls, score_fn): + # Build a scored pool from the window's top-2 items per category, + # then apply a per-category diversity quota. The scoring policy is + # supplied by score_fn so each lane surfaces a different signal. pool = [] for cat_name, items in digest_data.get(window_key, {}).items(): - for item in items[:2]: - if item.get("url") in exclude_urls: + for item in (items or [])[:2]: + if not isinstance(item, dict) or 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.append({**item, "digest_category": cat_name, "_age_days": age_days, "_score": score_fn(item, age_days)}) 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) @@ -1139,7 +1146,7 @@ class V2VisionEngine: break return sel - def _render_cards(items): + def _render_cards(items, show_new=True): html = "" for item in items: impact = item.get("impact", "medium") @@ -1154,7 +1161,7 @@ class V2VisionEngine: meta = item.get("date", "") if metric: meta += f" ยท {metric}" - new_pill = ' ๐Ÿ†• NEW' if item.get("_age_days", 999) <= 7 else "" + new_pill = ' ๐Ÿ†• NEW' if show_new and item.get("_age_days", 999) <= 7 else "" html += ( f'\n' if rising_items: cards_html += '\n\n' cards_html += '