Durability pass: tests, clearer diversity/classify behavior, Calm Filters foundation

- Add pytest suite (34 tests) covering scoring thresholds, dedup clustering +
  representative selection + time window, brief source/category diversity,
  avoid-term phrase matching, and text canonicalization/truncation.
- Rewrite _select_diverse with an explicit, tested contract (best-first, one
  per source, backfill, then inject a second category by evicting the
  lowest-ranked pick).
- classify_articles now returns attempted/succeeded/skipped (ClassifyReport) so
  silent model failures are visible in both the cycle and classify output.
- Fix clean_text truncation to stay within max_len (ellipsis no longer
  overshoots).
- New filters.py: canonical FilterPrefs shape (include/mute topics+flavors,
  avoid_terms, pauses) and pure word/phrase-boundary matching engine seeding
  Calm Filters. Not yet wired into the API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-05-30 19:07:31 +00:00
parent 470e9ecbf8
commit 9cdcda5e02
12 changed files with 479 additions and 18 deletions
+22 -11
View File
@@ -141,21 +141,30 @@ def _candidate_articles(
def _select_diverse(rows: list[sqlite3.Row], limit: int) -> list[sqlite3.Row]:
selected = []
seen_sources = set()
seen_categories = set()
"""Pick up to `limit` items from `rows` (already ranked best-first).
Contract:
1. Prefer higher-ranked items.
2. Source diversity: take at most one item per source while other sources
remain; only repeat a source once distinct sources are exhausted.
3. Category diversity: if the result ended up single-category and a different
category is available in the pool, swap in the highest-ranked off-category
candidate by evicting the lowest-ranked currently-selected item (so we
gain breadth without dropping a higher-ranked pick).
"""
selected: list[sqlite3.Row] = []
seen_sources: set = set()
# Pass 1: best-first, one per source.
for row in rows:
if len(selected) >= limit:
break
source = row["source_name"]
category = row["default_category"]
if source in seen_sources and len(rows) > limit:
if row["source_name"] in seen_sources:
continue
selected.append(row)
seen_sources.add(source)
seen_categories.add(category)
seen_sources.add(row["source_name"])
# Pass 2: if short on distinct sources, backfill best-first regardless.
if len(selected) < limit:
selected_ids = {row["id"] for row in selected}
for row in rows:
@@ -166,13 +175,15 @@ def _select_diverse(rows: list[sqlite3.Row], limit: int) -> list[sqlite3.Row]:
selected.append(row)
selected_ids.add(row["id"])
if len(seen_categories) < 2 and len(rows) > limit:
# Pass 3: ensure >= 2 categories when the pool allows it.
categories = {row["default_category"] for row in selected}
if len(categories) < 2:
selected_ids = {row["id"] for row in selected}
for row in rows:
if row["id"] in selected_ids:
continue
if row["default_category"] not in seen_categories:
selected[-1] = row
if row["default_category"] not in categories:
selected[-1] = row # evict the lowest-ranked selected item
break
return selected