Task 2 — RSS parse moves out of the collector refactor/rss-parse-to-consumer

PR #252 deferred task · The collector stops parsing feeds; RSSConsumer takes over parse + per-entry build + dedup.

Why (ADR 0003): the collector's end-state is zero-sync — async fetch + async publish, nothing else. feedparser is CPU work hiding in an I/O tier that can't scale horizontally. The consumer tier can. Gate PR #279 merged this morning ✓ — path is clear.

The pipeline, before → after

BEFORE — collector does the thinking

🛰 AsyncRssCollector (per feed)

rss-feeds
N msgs — one per entry

⚙️ RSSConsumer

AFTER — collector is a dumb pump

🛰 AsyncRssCollector

rss-feeds
1 msg — whole raw feed
same rss_feed type

⚙️ RSSConsumer

Key decisions

DECIDED

Same name rss_feed, new shape

No rename (Uri's call) — the message still means "an RSS feed". Stale per-entry messages still fail loudly: they lack raw_body_b64 → non-recoverable, dropped + error log.

NEW

Raw body ships base64

feedparser does its own encoding detection on the original bytes. Hebrew feeds (windows-1255 etc.) make byte fidelity load-bearing — decoding server-side would corrupt it.

REVISED 06-11

Batched DB lookup, not a cache

Per message: ONE feed_url = ANY(...) indexed SELECT pre-filters the whole window; misses go through find_or_create_feed. Stateless, exact, replica/restart-proof (~1–3 cheap queries/sec total). Replaced the in-memory SeenUrlCache — Uri's call after review.

KPI SPLIT

Mark seen at link-found

A link counts as seen the moment its feeds row is created (Uri's call). Downstream failures are NOT silently retried — they stay visible in DB state as "link found, no article", keeping the link-found vs article-found KPIs divergent.

MOVED

RssEntryParser → consumer/utils/

Title/content/date/tags parsing relocates wholesale (git mv). Entry dicts the consumer builds are field-identical to today's messages — downstream (hits, COLLECT_MORE_INFO) untouched.

DELETED

Dead code goes with it

execute() override (base publishes), _seen deque + constant, _create_feed_message. (Legacy RSSFeedMessage dead code: untouched — separate cleanup.)

Deploy — queue contract change

⚠️ Drain required (same playbook as PR #252's COLLECT_MORE_INFO migration):
  1. Stop collector container
  2. Wait until rss-feeds depth = 0 (old consumer drains old per-entry messages)
  3. Deploy collector + consumer together, start

📊 Dashboard note: rss-feeds depth now counts feeds pending parse, not entries. Sustained growth = the parse tier lagging = add RSSConsumer capacity (the ADR's accepted cost — the consumer is the scalable tier).