[object Object]

← back to Last30 Skill

last30: add relevance gate (topic-token coverage) to kill keyword collisions

248dc83b26cf14d3d1989404c8ce9399f779b648 · 2026-07-13 15:20:04 -0700 · Steve Abrams

Filters items that don't cover enough topic tokens (default --min-relevance
0.4), e.g. F1 'Kimi Antonelli' noise on a 'Kimi K2' query. Reports drop
counts (JSON filtered_out + markdown footer); --no-filter disables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 248dc83b26cf14d3d1989404c8ce9399f779b648
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 13 15:20:04 2026 -0700

    last30: add relevance gate (topic-token coverage) to kill keyword collisions
    
    Filters items that don't cover enough topic tokens (default --min-relevance
    0.4), e.g. F1 'Kimi Antonelli' noise on a 'Kimi K2' query. Reports drop
    counts (JSON filtered_out + markdown footer); --no-filter disables.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 skills/last30/SKILL.md              |  4 ++
 skills/last30/references/sources.md | 16 +++++++
 skills/last30/scripts/last30.py     | 85 ++++++++++++++++++++++++++++++++++++-
 3 files changed, 103 insertions(+), 2 deletions(-)

diff --git a/skills/last30/SKILL.md b/skills/last30/SKILL.md
index cdf1033..14e1e3e 100644
--- a/skills/last30/SKILL.md
+++ b/skills/last30/SKILL.md
@@ -74,6 +74,10 @@ Common flags:
 - `--sources hn,arxiv,github,polymarket` — default set (all free, no keys)
 - `--limit N` — items per source (default 12)
 - `--emit json|markdown`
+- `--min-relevance 0.4` — topic-token coverage gate (default 0.4) that kills
+  keyword collisions (e.g. "Kimi" the F1 driver vs "Kimi K2" the model). Raise
+  toward 0.6 to tighten, `--no-filter` (or `0`) to disable. Dropped counts are
+  reported, never silent.
 - add `reddit` to `--sources` only on networks where Reddit isn't IP-blocked
 
 The engine is fault-isolated: a source that 429s/403s returns an `error` marker,
diff --git a/skills/last30/references/sources.md b/skills/last30/references/sources.md
index 1350b0b..54ef7fc 100644
--- a/skills/last30/references/sources.md
+++ b/skills/last30/references/sources.md
@@ -70,6 +70,22 @@ Each item gets a normalized `score` in **0–100**:
 `top_across()` flattens every source and returns the global top-N by `score` —
 that's the "Top across all sources" block the brief leads with.
 
+## Relevance gate (`apply_relevance`)
+
+Runs **before** scoring to kill keyword collisions (the classic case: a query for
+"Kimi K2" pulling Polymarket's F1 driver "Kimi Antonelli"). It tokenizes the topic
+(lowercasing, dropping stopwords and separators like "vs"), then keeps an item only
+if it matches at least `ceil(len(tokens) * threshold)` distinct topic tokens
+(minimum 1). Defaults to `--min-relevance 0.4`:
+
+- 3-token topic ("kimi k2 deepseek") → needs ≥2 hits → the F1 "Kimi" item (1 hit) drops.
+- 2-token topic ("nvidia earnings") → needs 1 hit → lenient, keeps either term.
+- 1-token topic ("openclaw") → keeps anything containing it.
+
+Every kept item carries a `relevance` fraction; each source reports `filtered_out`
+so drops are visible (JSON field + a markdown footer note), never silent. Disable
+with `--no-filter` or `--min-relevance 0`; tighten toward `0.6` for noisy topics.
+
 ## Extending
 
 - **New free source** → add a `fetch_<name>()` returning the standard shape, add it
diff --git a/skills/last30/scripts/last30.py b/skills/last30/scripts/last30.py
index 5e94a87..87ccab8 100755
--- a/skills/last30/scripts/last30.py
+++ b/skills/last30/scripts/last30.py
@@ -34,6 +34,7 @@ from __future__ import annotations
 import argparse
 import concurrent.futures
 import json
+import math
 import re
 import sys
 import time
@@ -341,6 +342,73 @@ FETCHERS = {
 }
 
 
+# --------------------------------------------------------------------------- #
+# Relevance gate — kill keyword collisions (e.g. Kimi-the-model vs Kimi-the-   #
+# F1-driver) before scoring, using topic-token coverage. Dependency-free.      #
+# --------------------------------------------------------------------------- #
+_STOPWORDS = {
+    "the", "a", "an", "of", "and", "or", "for", "in", "on", "to", "is", "are",
+    "vs", "v", "versus", "with", "about", "what", "whats", "people", "saying",
+    "say", "latest", "recent", "buzz", "current", "state", "new", "news", "this",
+    "that", "how", "why", "best", "top", "review", "reviews", "reaction",
+}
+
+
+def _topic_tokens(topic: str) -> list[str]:
+    """Distinctive lowercase tokens from the topic (stopwords/separators dropped)."""
+    raw = re.split(r"[^a-z0-9]+", topic.lower())
+    seen, out = set(), []
+    for t in raw:
+        if len(t) >= 2 and t not in _STOPWORDS and t not in seen:
+            seen.add(t)
+            out.append(t)
+    return out
+
+
+def _item_haystack(it: dict) -> str:
+    parts = [
+        str(it.get("title", "")), str(it.get("snippet", "")),
+        str(it.get("subreddit", "")), str(it.get("language", "")),
+        " ".join(it.get("authors", []) or []),
+        " ".join(it.get("odds", []) or []),
+    ]
+    return " ".join(parts).lower()
+
+
+def apply_relevance(results: list[dict], topic: str, threshold: float) -> None:
+    """Filter each source's items by topic-token coverage, in-place.
+
+    An item is kept if it matches at least `ceil(len(tokens) * threshold)`
+    distinct topic tokens (min 1). Every kept item gets a `relevance` fraction;
+    each source result records `filtered_out` for transparency. A single-token
+    topic keeps anything containing that token. `threshold <= 0` disables.
+    """
+    tokens = _topic_tokens(topic)
+    if not tokens or threshold <= 0:
+        for r in results:
+            for it in r.get("items", []):
+                it["relevance"] = 1.0
+        return
+    need = max(1, math.ceil(len(tokens) * threshold))
+    for r in results:
+        items = r.get("items")
+        if not items:
+            continue
+        kept = []
+        dropped = 0
+        for it in items:
+            hay = _item_haystack(it)
+            matched = sum(1 for tk in tokens if tk in hay)
+            it["relevance"] = round(matched / len(tokens), 2)
+            if matched >= need:
+                kept.append(it)
+            else:
+                dropped += 1
+        r["items"] = kept
+        if dropped:
+            r["filtered_out"] = dropped
+
+
 # --------------------------------------------------------------------------- #
 # Cross-source scoring                                                         #
 # --------------------------------------------------------------------------- #
@@ -387,7 +455,11 @@ def emit_json(topic, days, sources, results, elapsed):
         if "error" in r:
             scored_sources[r["source"]] = {"error": r["error"], "items": []}
         else:
-            scored_sources[r["source"]] = {"count": len(r["items"]), "items": r["items"]}
+            scored_sources[r["source"]] = {
+                "count": len(r["items"]),
+                "filtered_out": r.get("filtered_out", 0),
+                "items": r["items"],
+            }
     payload = {
         "topic": topic,
         "window_days": days,
@@ -402,8 +474,10 @@ def emit_json(topic, days, sources, results, elapsed):
 
 def emit_markdown(topic, days, sources, results, elapsed):
     out = [f"# /last30 — “{topic}” (last {days} days)", ""]
+    total_filtered = sum(r.get("filtered_out", 0) for r in results)
+    filt = f" · {total_filtered} off-topic filtered" if total_filtered else ""
     out.append(f"_generated {_now().strftime('%Y-%m-%d %H:%M UTC')} · {elapsed:.1f}s · "
-               f"sources: {', '.join(sources)}_")
+               f"sources: {', '.join(sources)}{filt}_")
     out.append("")
     top = top_across(results, 12)
     if top:
@@ -492,6 +566,11 @@ def main() -> int:
     ap.add_argument("--emit", choices=["json", "markdown"], default="json")
     ap.add_argument("--no-comments", action="store_true",
                     help="skip Reddit comment enrichment (faster)")
+    ap.add_argument("--min-relevance", type=float, default=0.4,
+                    help="topic-token coverage gate 0..1 (default 0.4); "
+                         "kills keyword collisions. 0 disables.")
+    ap.add_argument("--no-filter", action="store_true",
+                    help="disable the relevance gate (same as --min-relevance 0)")
     args = ap.parse_args()
 
     if not args.topic or args.topic == "doctor":
@@ -517,6 +596,8 @@ def main() -> int:
             by_source[r["source"]] = r
         results = [by_source[s] for s in sources if s in by_source]
 
+    threshold = 0.0 if args.no_filter else args.min_relevance
+    apply_relevance(results, args.topic, threshold)
     score_items(results, args.days)
     elapsed = time.time() - t0
 

← ea5183d last30: multi-source last-30-days research skill (own-stack,  ·  back to Last30 Skill  ·  last30: add TikTok + Instagram to the web-search social laye a554306 →