Task: todo 19a — notification-gate peek + read-only HTTP API Date: 2026-09-01 Files created/modified: - crates/news-server/src/gate.rs (refactored: shared decide(), new peek(), tests) - crates/news-server/src/api/mod.rs (register config + stories modules) - crates/news-server/src/api/config.rs (new GET /api/config endpoint + tests) - crates/news-server/src/api/stories.rs (new GET /api/stories endpoint) - crates/news-server/src/api/stories/tests.rs (new tests) - crates/news-server/src/main.rs (wire /api/config and /api/stories routers) Verification commands --------------------- 1. Formatting $ cargo fmt --all --check Result: clean 2. Clippy $ cargo clippy --workspace --all-targets -- -D warnings Result: clean 3. Workspace tests $ cargo test --workspace Result: passed (83 tests total) - news-cli: 2 passed - news-core: 18 passed - news-ingest: 4 passed - news-server: 41 passed (up from 33 in task 18) - news-store: 22 passed 4. Build $ cargo build --workspace --bins Result: finished successfully Implementation notes -------------------- - NotificationGate::peek is side-effect-free: clones the token bucket, refills in memory only, calls the shared decide() logic, and never writes notification_log or decrements budget.tokens. - Shared decision logic lives in a private decide() method used by both evaluate_with_mode and peek, guaranteeing identical Notify/Digest/Suppress outcomes. - GET /api/stories queries story_clusters joined to raw_items for the canonical item, recomputes relevance (RelevanceScorer) and importance (ImportanceScorer) per request, derives percentile ranks from percentile_stats over a 7-day window, calls gate.peek, and returns a JSON array of story rows with decision/reason/last_notification_lane. - GET /api/config returns the current in-memory NewsConfig as JSON. - Both endpoints are read-only: no notification_log rows are written and the gate token budget is unchanged after a request. Known divergences from plan wording ----------------------------------- - Percentile ranks are computed with a direct SQL query on percentile_stats rather than through PercentileTracker, because PercentileTracker exposes no rank API and news-store is frozen for this task.