Task 12 — NotificationGate implementation Date: 2026-09-01 What was built: - crates/news-server/src/gate.rs - NotificationGate, TokenBucket, NotifyConfig - GateDecision / SuppressReason enums - evaluate() with refill → already-notified → bypass → normal → bucket logic - crates/news-server/src/lib.rs: pub mod gate + re-exports - crates/news-server/src/error.rs: ServerError::DateTime(String) variant - root Cargo.toml [workspace.dependencies]: chrono-tz = "0.10" - crates/news-server/Cargo.toml: chrono-tz = { workspace = true } Verification commands: --- cargo fmt --all --check --- (no output, exit 0) --- cargo clippy --workspace --all-targets -- -D warnings --- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s (exit 0) --- cargo test -p news-server --- running 11 tests test gate::tests::material_update_notifies_again_when_delta_reached ... ok test gate::tests::bypass_ceiling_is_independent_of_normal_budget ... ok test gate::tests::happy_path_notifies_when_relevance_and_budget_allow ... ok test scoring::trained_model_persists_and_reloads_with_same_total_samples ... ok test gate::tests::below_band_suppresses_below_threshold ... ok test gate::tests::already_notified_suppresses_when_delta_not_reached ... ok test scoring::matching_item_scores_higher_than_non_matching ... ok test gate::tests::digest_band_returns_digest ... ok test gate::tests::quiet_hours_suppresses_high_relevance ... ok test scoring::importance_scorer_matches_direct_formula ... ok test scoring::blocklist_veto_returns_neg_infinity ... ok test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s Gate test names: - gate::tests::happy_path_notifies_when_relevance_and_budget_allow - gate::tests::digest_band_returns_digest - gate::tests::below_band_suppresses_below_threshold - gate::tests::material_update_notifies_again_when_delta_reached - gate::tests::already_notified_suppresses_when_delta_not_reached - gate::tests::quiet_hours_suppresses_high_relevance - gate::tests::bypass_ceiling_is_independent_of_normal_budget