Initial commit -- did OMO do a good job?
Check / check (push) Failing after 1m27s
Check / guardrails (push) Failing after 39s
Check / bundle (push) Successful in 1m4s

This commit is contained in:
2026-09-01 18:29:46 -04:00
commit 20a6c26f7c
179 changed files with 21704 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
Task 16 — daily digest (news-server/src/notify/digest.rs)
==========================================================
What was built
--------------
- NEW crates/news-server/src/notify/digest.rs
- pub async fn build_and_send_digest(pool, scorer, ntfy, config, now)
- Due-time gate: today's 07:00 in now.timezone(); before that -> Ok(()) debug log.
- Idempotency gate: SELECT COUNT(*) FROM notification_log WHERE lane = 'digest' AND created_at >= ?1 (today's 07:00 UTC RFC3339).
- Queries story_clusters created in the last 24h joined to canonical raw_items.
- Scores canonical items, skips blocklisted (NEG_INFINITY) and opinion pieces when notify_opinions = false.
- Band check: score in [p75, p90) of the trailing-7-day relevance distribution via PercentileTracker.
- Sends one ntfy request via NtfyPublisher::publish_digest with Title, Priority: 2, Markdown: yes, Tags: newspaper, bullet-list body.
- Records one notification_log row with Uuid::nil(), lane='digest', source_count_at_notify=<story count>, dry_run=0.
- No token-bucket interaction.
- MODIFIED crates/news-server/src/notify/ntfy.rs
- Added NotifyError::{Store, Sql, Server} #[from] variants.
- Added pub async fn publish_digest(title, body_markdown).
- Factored retry-once logic into private send_with_retry(Fn() -> RequestBuilder) so publish() behavior/tests are unchanged.
- MODIFIED crates/news-server/src/notify/mod.rs
- Added pub mod digest.
Test names added
----------------
notify::digest::tests::
- skips_when_no_eligible_stories
- sends_digest_with_in_band_stories
- second_call_same_day_is_no_op
- before_seven_am_is_no_op
- excludes_opinion_when_notify_opinions_false
- includes_opinion_when_notify_opinions_true
Opinion flagging representation
-------------------------------
- news-core/src/model.rs: RawItem.kind is ItemKind::{News, Opinion}.
- news-store/migrations/0001_init.sql: raw_items.kind TEXT NOT NULL stores "news" or "opinion".
- The digest filters on the joined canonical raw_items.kind column; no new migration or column was needed.
Verification
============
$ cargo test -p news-server
...
running 26 tests
...
test notify::digest::tests::skips_when_no_eligible_stories ... ok
test notify::digest::tests::excludes_opinion_when_notify_opinions_false ... ok
test notify::digest::tests::includes_opinion_when_notify_opinions_true ... ok
test notify::digest::tests::sends_digest_with_in_band_stories ... ok
test notify::digest::tests::before_seven_am_is_no_op ... ok
test notify::digest::tests::second_call_same_day_is_no_op ... ok
...
test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo fmt --all --check
(no output, exit 0)
$ cargo clippy --workspace --all-targets -- -D warnings
Checking news-server v0.1.0 (/home/connor/docs/projects/news/crates/news-server)
Finished dev profile [unoptimized + debuginfo] target(s) in 5.29s
Captured happy-test ntfy request body
-------------------------------------
(temporarily instrumented sends_digest_with_in_band_stories to eprintln the wiremock request body, then removed the instrumentation)
CAPTURED_DIGEST_BODY:
- [Story 2](https://example.test/2)
- [Story 1](https://example.test/1)
- [Story 0](https://example.test/0)
The single POST to /news-triage carried:
Title: Daily digest: 3 stories
Priority: 2
Markdown: yes
Tags: newspaper
body as shown above.