← 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
M skills/last30/SKILL.mdM skills/last30/references/sources.mdM skills/last30/scripts/last30.py
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 →