Attention strip: richer source-health items (stale/reject/dup/thin/rate-limit)

Per Codex — make the Overview strip diagnostic without making the operator hunt
through tables. Aggregated (one calm line per condition with a count), volume-
gated, conservative thresholds:

* Stale: active+visible source, last success > 10 days ago (warn).
* High rejection: >=20 ingested, acceptance < 25% (info).
* High duplicate: >=10 accepted, accepted-dup > 50% (info).
* Thin images: >=10 served, per-source image coverage < 25% (info).
* Long rate-limit: retry_after_at more than 12h out (info).

source_health gains a per-source images count + image_coverage. _attention takes
an optional now (for tests). Existing site-wide items (global image coverage,
thin brief, unread feedback) unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-09 11:50:17 -04:00
parent 01de5a3ef0
commit d2e2b303ac
2 changed files with 93 additions and 2 deletions
+40
View File
@@ -74,3 +74,43 @@ def test_attention_clear_when_healthy():
content = {"served": 100, "with_image": 95, "latest_brief_size": 7}
sources = [{"failures": 0, "review_flag": 0}]
assert queries._attention(content, sources, feedback_unread=0) == []
def test_attention_richer_items_fire_and_are_quiet_below_threshold():
from datetime import UTC, datetime
from goodnews import queries
now = datetime(2026, 6, 9, 12, 0, 0, tzinfo=UTC)
content = {"served": 100, "with_image": 95, "latest_brief_size": 7} # no site-wide items
sources = [
# stale: active, visible, last success 20 days ago
{"status": "active", "content_visible": 1, "last_success_at": "2026-05-20 00:00:00"},
# high rejection: 40 ingested, 10% acceptance
{"status": "active", "content_visible": 1, "total_articles": 40, "acceptance_rate": 10},
# high duplicate: 30 accepted, 70% accepted-dup
{"status": "active", "content_visible": 1, "accepted_total": 30, "accepted_dup_rate": 70},
# thin images: 40 served, 5% coverage
{"status": "active", "content_visible": 1, "served": 40, "image_coverage": 5},
# long rate-limit rest: 2 days out
{"status": "active", "content_visible": 1, "retry_after_at": "2026-06-11 12:00:00"},
]
texts = " | ".join(i["text"] for i in queries._attention(content, sources, 0, now=now))
assert "haven't updated in over 10 days" in texts
assert "accepting under 25%" in texts
assert "mostly duplicating" in texts
assert "thin image coverage" in texts
assert "rate-limited for 12h+" in texts
def test_attention_quiet_below_thresholds():
from datetime import UTC, datetime
from goodnews import queries
now = datetime(2026, 6, 9, 12, 0, 0, tzinfo=UTC)
content = {"served": 100, "with_image": 95, "latest_brief_size": 7}
sources = [
{"status": "active", "content_visible": 1, "last_success_at": "2026-06-08 00:00:00"}, # 1 day — fresh
{"status": "active", "content_visible": 1, "total_articles": 5, "acceptance_rate": 10}, # too little volume
{"status": "active", "content_visible": 1, "accepted_total": 3, "accepted_dup_rate": 90}, # too little volume
{"status": "active", "content_visible": 1, "served": 5, "image_coverage": 0}, # too little volume
{"status": "paused", "content_visible": 1, "last_success_at": "2020-01-01 00:00:00"}, # paused → ignored
]
assert queries._attention(content, sources, 0, now=now) == []