# Quality gates — RSS provider Criteria an implementation of `architecture/rss-provider.md` must satisfy. Automated coverage lives in `rssdy/src/tests.rs` (unit, over a fake `Feeds`), plus the settings/toggle tests in `crabidy-server`. ## Secrets (hard rule — highest priority) - [ ] **G1 — A feed URL never reaches a log, an error, or a path.** `FeedEntry`, `Settings`, `Client`, and `FeedFetcher` `Debug` output redact urls (manual impls, not derived). `FetchError` variants carry status/parse context only. No `debug!`/`warn!`/`error!` call takes a feed or enclosure URL. *(tests: `settings_debug_redacts_urls`, `feed_entry_debug_redacts_the_url`.)* - [ ] **G2 — No path contains a URL.** Subscription paths are `/rss/`, episode paths `/rss//<16 hex>`. A tokened URL therefore cannot leak through the UI, the queue, `current`, or a bookmark toml. *(tests: `episode_paths_carry_only_slug_and_key`.)* ## Freshness — the point of the feature - [ ] **G3 — Listing a subscription always fetches.** Two listings of the same node cause two fetches, and the second reflects a feed that gained an episode in between. *(test: `every_listing_refetches_the_feed`.)* - [ ] **G4 — The memo is written by listings and read only by track lookups.** After one listing, resolving every episode of that feed costs **no** further fetches; a track lookup with a cold memo fetches once. *(tests: `track_lookups_reuse_the_listing_fetch`, `a_cold_memo_fetches_once`.)* - [ ] **G5 — The memo is bounded** to `MEMO_CAPACITY` feeds, evicting the oldest, so subscribing to 100 feeds cannot grow it without limit. *(test: `the_memo_evicts_beyond_its_capacity`.)* - [ ] **G6 — `/rss` is in `MUTABLE_ROOTS` in both clients** (`cbd-tui/src/rpc.rs`, `cbd-web/src/state.rs`), or the client cache defeats G3. *(tests: the existing cacheability tests, extended.)* ## Identity and paths - [ ] **G7 — An episode key is stable** across restarts and builds: `blake3(guid)[..16]`, and the guid falls back to the enclosure URL only when the feed omits one. *(tests: `episode_keys_are_stable_and_short`, `a_feed_without_guids_keys_on_the_enclosure_url`.)* - [ ] **G8 — Slugs are derived, bounded, and de-duplicated.** Lowercased, non-alphanumerics folded to single dashes, trimmed, length-capped; an empty or all-punctuation name yields `feed`; two subscriptions that slug alike get `-2`, `-3`. *(tests: `slugify_folds_and_bounds`, `duplicate_slugs_get_a_suffix`.)* - [ ] **G9 — `provider_item_id` is the guid**, so the content store de-duplicates captures of the same episode across visits and feeds. *(test: `tracks_carry_the_guid_as_provider_item_id`.)* - [ ] **G10 — Foreign and malformed paths are typed errors**, never panics: an unknown slug, a wrong-provider path, a bad episode key, and a path with too many segments all yield `MalformedPath` (or `NotSupported` for edits). *(test: `foreign_and_malformed_paths_reject`.)* ## Subscriptions (`%`, `e`, `d`) - [ ] **G11 — `%` on `/rss` takes a URL, fetches it once, and names the subscription from the feed's own title**, de-duplicating the slug. The new node is returned. *(test: `subscribing_names_from_the_feed_title`.)* - [ ] **G12 — `%` with a non-URL or an unfetchable URL fails with a typed error and adds nothing.** *(test: `subscribing_rejects_bad_input`.)* - [ ] **G13 — `e` renames and the path follows the new slug**; `d` removes the subscription and no audio. Both return what a client should display next. *(tests: `renaming_moves_the_slug`, `unsubscribing_removes_only_the_entry`.)* - [ ] **G14 — Every subscription change round-trips through `rss.toml`** — `settings()` reserializes the live list so the server's write-back persists it, and reloading yields the same subscriptions. *(test: `subscription_changes_round_trip_through_toml`.)* ## Robustness (hard rules) - [ ] **G15 — No panics on feed data.** A missing enclosure, empty title, unparseable date, absent duration, or an entry with no guid is skipped or degraded — never a panic and never a failed listing. Only an unfetchable/unparseable *feed* errors, and it fails that node only. *(tests: `malformed_entries_are_skipped`, `backend_failures_are_typed`.)* - [ ] **G16 — Bounded network use.** Every request carries `call_timeout_secs`; a body over `max_feed_bytes` is rejected without being buffered whole; listings are capped at `episodes_per_feed`. - [ ] **G17 — Newest first**, by publication date descending where dates parse, feed order preserved otherwise (stable sort). *(test: `episodes_are_newest_first`.)* - [ ] **G18 — A provider failure never takes the server down.** A feed that cannot be fetched fails its own node; `init` with no feeds still mounts an empty, creatable `/rss`. *(test: `no_feeds_still_mounts`.)* ## Wiring - [ ] **G19 — Behind the `rss` cargo feature**, default on: `dep:rssdy`, a `BUILT_IN_PROVIDERS` entry, an `ALL_PROVIDERS` name, a `ProviderToggles` field, a mount registration, and a `check-features` row. The provider-less and each-provider-alone builds stay clean. - [ ] **G20 — Documented.** A `docs/src/providers/rss.md` page (tree, subscribing, premium feeds and what "not cached" means, every config option), an `rssdy/README.md`, links from the providers index, `SUMMARY.md`, `config.md`, and the root README's tree and config table. - [ ] **G21 — Clippy clean under `-D warnings`** for the workspace, fmt clean, and no `todo!()` from the stub stage left.