Files
news/.omo/evidence/f4-news-triage.txt
T
connor 20a6c26f7c
Check / check (push) Failing after 1m27s
Check / guardrails (push) Failing after 39s
Check / bundle (push) Successful in 1m4s
Initial commit -- did OMO do a good job?
2026-09-01 18:29:46 -04:00

360 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
F4 Scope-Fidelity Review — news-triage
Reviewer: Sisyphus-Junior (F4)
Date: 2026-09-01
Project: /home/connor/docs/projects/news
Standard: Every direct dependency, route, and config key must trace to a specific plan todo; every Scope-OUT bullet must be confirmed unviolated.
================================================================================
FINAL VERDICT: APPROVE
================================================================================
All required traces succeed and all eight Scope-OUT bullets are unviolated.
No unexplained direct dependency, no extra route, and no extra config key was
found. Minor accepted deviations (NEWS_DATABASE_URL env support, root
package.json, --once dry-run helper) are already recorded in the project
notepads/ledger and are not flagged.
================================================================================
CHECK 1 — DIRECT-DEPENDENCY TRACE
================================================================================
Command:
$ cargo tree --workspace -e normal --depth 1
Output (direct dependencies only):
news-cli v0.1.0
├── chrono v0.4.45
├── chrono-tz v0.10.4
├── clap v4.6.6
├── news-core v0.1.0
├── news-ingest v0.1.0
├── news-server v0.1.0
├── news-store v0.1.0
├── serde v1.0.229
├── sqlx v0.8.6
├── thiserror v2.0.20
├── tokio v1.53.1
├── toml v0.8.23
└── uuid v1.26.0
news-core v0.1.0
(no extra direct deps beyond workspace deps)
news-ingest v0.1.0
(no extra direct deps beyond workspace/internal deps)
[dev-dependencies: wiremock]
news-server v0.1.0
(no extra direct deps beyond workspace/internal deps)
[dev-dependencies: tower, tracing-test, tempfile, wiremock]
news-store v0.1.0
(no extra direct deps beyond workspace/internal deps)
news-web v0.1.0
├── console_error_panic_hook v0.1.7
├── gloo-net v0.6.0
├── leptos v0.8.20
├── serde v1.0.229
└── serde_json v1.0.151
Workspace / root direct dependencies:
Internal: news-core, news-store, news-ingest, news-server
serde, thiserror, uuid, chrono, chrono-tz, serde_json, pretty_assertions,
sqlx, tokio, reqwest, url, quick-xml, toml, tracing, tracing-subscriber,
clap, tracing-test, tempfile, axum, tower, prometheus
Root package.json devDependencies:
@playwright/test, @tailwindcss/cli, tailwindcss
--------------------------------------------------------------------------------
Traceability table — every direct dependency maps to a todo
--------------------------------------------------------------------------------
Dependency Justifying todo / evidence
-------------------------- ----------------------------------------------------
serde Todo 1 (workspace skeleton, serde derive), Todo 2
(model structs), Todo 14 (NewsConfig). Used in
every crate.
thiserror Todo 1 (workspace skeleton). Error types in all
crates.
uuid Todo 2 (domain ids). Used in news-core, news-store,
news-ingest, news-server, news-cli.
chrono Todo 2 (timestamps). Used throughout.
chrono-tz Todo 14/16 (quiet hours America/Louisville, digest
07:00 Louisville). Also news-cli replay uses the
gate (todo 17). Recorded as accepted deviation.
serde_json Todo 2 (round-trip tests), Todo 13 (feedback JSON),
Todo 19 (frontend JSON). Also workspace tests.
pretty_assertions Todo 1 ("JSON round-trip assertions + readable
diffs").
sqlx Todo 5 (SQLite schema/migrations). Used in
news-store, news-server, news-cli.
tokio Todo 5 (async runtime), Todo 18 (scheduler/signal).
reqwest Todo 7 (feed polling HTTP client), Todo 15 (ntfy
publisher HTTP POST).
url Todo 7 (reqwest), Todo 15 (NtfyPublisher base_url
is a Url).
quick-xml Todo 7 (RSS/Atom + news-sitemap parsing).
toml Todo 14 (TOML config loader), Todo 17 (replay CLI
may use config). Also news-cli.
tracing Todo 14 (SIGHUP reload logging), Todo 18
(scheduler logging).
tracing-subscriber Todo 18 (binary logging init).
clap Todo 17 (--dry-run server flag, news-cli replay
subcommand), Todo 18 (server CLI).
tracing-test Todo 14 (capture tracing logs in tests). Dev-only.
tempfile Todo 14 (temp config fixtures). Dev-only.
axum Todo 13 (POST /api/feedback), Todo 18 (/healthz,
/metrics), Todo 19a (/api/stories, /api/config).
tower Todo 13 (tower::ServiceExt::oneshot in-process
tests). Dev-only in news-server.
prometheus Todo 18 (Prometheus text exposition for /metrics).
console_error_panic_hook Todo 19 (Leptos 0.8 CSR app, matching runway-web
toolchain; runway-web Cargo.toml also lists this
dep).
gloo-net Todo 19b (frontend fetch layer). Recorded as
accepted deviation.
leptos Todo 19 (Leptos 0.8 CSR web UI).
@playwright/test Todo 19b (browser QA). Recorded as accepted
deviation (root package.json).
@tailwindcss/cli Todo 19b (Tailwind v4 build). Accepted deviation.
tailwindcss Todo 19b (Tailwind v4). Accepted deviation.
wiremock Todo 7 (feed polling fixtures), Todo 15 (ntfy
publisher mock). Dev-only.
Observed, not present:
rusqlite, feed-rs, scraper, regex — these are NOT direct dependencies.
(regex appears only transitively via leptos.)
Check 1 result: PASS — every direct dependency traces to a todo; no unexplained
direct dependency found.
================================================================================
CHECK 2 — ENDPOINT TRACE
================================================================================
All routes are registered in crates/news-server/src/main.rs:
let app = axum::Router::new()
.merge(news_server::api::feedback::router(rt.pool.clone()))
.merge(news_server::api::stories::router(
rt.pool.clone(),
Arc::clone(&rt.gate),
Arc::clone(&config),
))
.merge(news_server::api::config::router(Arc::clone(&config)))
.merge(news_server::health::router(rt.pool, rt.metrics));
Routers and their paths:
feedback.rs: POST /api/feedback
stories.rs: GET /api/stories
config.rs: GET /api/config
health.rs: GET /healthz
health.rs: GET /metrics
Trace to todos:
/healthz → Todo 18 ("GET /healthz returning 200 {status:ok,db:ok}")
/metrics → Todo 18 ("GET /metrics returning Prometheus text format")
POST /api/feedback → Todo 13 ("POST /api/feedback ... persists feedback")
GET /api/stories → Todo 19a ("add this read-only endpoint alongside todo 13's
feedback endpoint")
GET /api/config → Todo 19a ("add this endpoint too")
No other .route(...) calls exist in news-server/src. No fallback or static-file
route is mounted.
Check 2 result: PASS — exactly the five routes implied by the plan are present;
no extra route found.
================================================================================
CHECK 3 — CONFIG-KEY TRACE
================================================================================
NewsConfig fields (crates/news-server/src/config.rs):
pub struct NewsConfig {
pub sources: Vec<SourceConfig>,
pub notify: NotifySection,
pub topics: TopicsSection,
}
pub struct SourceConfig {
pub url: String,
pub kind: String,
pub weight: f64,
pub enabled: bool,
pub poll_interval_secs: Option<u64>,
}
pub struct NotifySection {
pub notify_opinions: bool,
pub quiet_hours_start: String,
pub quiet_hours_end: String,
pub budget_refill_per_day: f64,
pub budget_burst: f64,
pub bypass_ceiling_per_day: u32,
}
pub struct TopicsSection {
pub interests: Vec<String>,
pub blocklist: Vec<String>,
}
Trace to plan/draft:
sources[] → Todo 14/18 ("[[sources]] url/kind/weight/enabled/
poll_interval_secs override")
SourceConfig.url → Todo 14
SourceConfig.kind → Todo 14
SourceConfig.weight → Todo 14 (source trust weight)
SourceConfig.enabled → Todo 14
SourceConfig.poll_interval_secs → Todo 14 (optional override)
notify.notify_opinions → Todo 14/16 (digest opinion filtering)
notify.quiet_hours_start → Todo 14
notify.quiet_hours_end → Todo 14
notify.budget_refill_per_day → Todo 12/14 (token bucket 2/day)
notify.budget_burst → Todo 12/14 (burst 3)
notify.bypass_ceiling_per_day → Todo 12/14 (bypass cap 4/24h)
topics.interests → Todo 14
topics.blocklist → Todo 10/14 (blocklist veto)
Digest fields: there is NO dedicated [digest] config section. Digest time
(07:00 America/Louisville) and percentile band (75th90th) are hardcoded in
crates/news-server/src/notify/digest.rs and crates/news-server/src/gate.rs,
matching Todo 16.
Dry-run: this is a CLI flag (--dry-run), not a NewsConfig key. Implemented in
Todo 17 (crates/news-server/src/main.rs Cli struct).
Check 3 result: PASS — every NewsConfig field traces to the plan; no extra
config key found.
================================================================================
CHECK 4 — SCOPE-OUT RE-CHECK (8 bullets)
================================================================================
Bullet 1 — "No LLM, LiteLLM, llama.cpp, or GPU dependency anywhere in the pipeline."
Evidence: grep -riE across crates/ for llm|litellm|llama\.cpp|gpt-oss|gemma3|
openai|anthropic|claude|gemini|moonshot|kimi|deepseek|huggingface|transformers|
ollama|vllm|tensorrt|cuda|gpu|nvidia returned ZERO matches.
Result: UNVIOLATED.
Bullet 2 — "No full-article scraping, paywall circumvention, or republishing
article text; headline + feed summary + link only."
Evidence: Ingest parsers only extract title, summary, link, published date,
and category:
- crates/news-ingest/src/rss_atom.rs fields: Title, Link, PublishedAt,
Summary, Category.
- crates/news-ingest/src/news_sitemap.rs fields: Loc, Title,
PublicationDate; summary is explicitly empty string.
grep for scraper|scrape|full.?article|paywall|bypass.*pay|article.?body|
readability in crates/ returned only metric names ("scrape_duration_seconds",
"poll" in scheduler/cycle.rs) — no article-scraping code.
Result: UNVIOLATED.
Bullet 3 — "No paid API, no hosted AI service, no new Postgres/Redis/Kafka/
Kubernetes."
Evidence: grep for postgres|redis|kafka|kubernetes|k8s|mysql|mariadb|mongodb|
dynamodb|bigquery|gcs|s3\.amazon|azure|openai.*api|paid.*api returned ZERO
matches. Persistence is SQLite via sqlx (Todo 5). Notifications use self-hosted
ntfy configured via env vars (Todo 15).
Result: UNVIOLATED.
Bullet 4 — "No public consumer-facing product, mobile app, new public DNS record,
or Authelia gate — LAN/VPN-only posture, matching grafana/llama-swap."
Evidence: deploy/README.md states:
"TLS and auth | The root Caddy, on news.rcjohnstone.com, LAN/VPN-only behind
the internal snippet"
Caddyfile snippet uses `tls internal`, `import internal`, `import
secure_headers`, `import accesslog`, and explicitly comments:
"Internal CA and no # ddns: public marker. Publishing this name would put
it in Certificate Transparency permanently and make a personal dashboard
reachable from the internet."
No mobile app source exists; the only UI is the Leptos CSR app served by
Caddy, not by the backend.
Result: UNVIOLATED.
Bullet 5 — "No implementer edits to ~/compose.yml, ~/Caddyfile,
~/data/ntfy/etc/server.yml, or ~/config/prometheus/prometheus.yml
— snippets only."
Evidence:
- grep for news-triage|news-backend|news.rcjohnstone.com in
/home/connor/compose.yml returned ZERO matches.
- grep for news-triage|news.rcjohnstone.com|news-backend|/srv/news in
/home/connor/Caddyfile returned ZERO matches (the lone `path /api /api/*`
match is inside runway.rcjohnstone.com, not news-triage).
- grep for news-triage|news-backend in
/home/connor/config/prometheus/prometheus.yml returned ZERO matches.
- /home/connor/data/ntfy/etc/server.yml was not touched for news-triage
configuration; the deploy README only instructs the operator to run an
`ntfy access` command.
- Note: /home/connor/compose.yml mtime is 2026-09-01, but its content
contains no news-triage snippets, so no implementer edit for this project
is present.
Result: UNVIOLATED.
Bullet 6 — "No unbounded breaking-news notifications — the bypass lane is capped
independently of the normal budget."
Evidence: crates/news-server/src/gate.rs:
- NotifyConfig::default sets bypass_daily_ceiling: 4.
- decide() checks bypass_count_last_24h() < bypass_daily_ceiling before
emitting a bypass Notify.
- bypass_count_last_24h queries `lane = 'bypass' AND dry_run = 0`.
- TokenBucket is only decremented in the normal lane (`if lane == "normal"`
then budget.tokens -= 1.0); the bypass lane never touches it.
- Test `bypass_ceiling_is_independent_of_normal_budget` verifies exactly 4
bypass notifications with an exhausted normal bucket.
Result: UNVIOLATED.
Bullet 7 — "No serving the frontend SPA from the backend process — the image
only lifts dist out for Caddy."
Evidence:
- crates/news-server/src/main.rs mounts only API/health routers; no static
file server, no ServeDir, no fallback to index.html.
- Dockerfile:
"COPY --from=web /app/crates/news-web/dist /srv/dist"
with comment: "Where the updater looks for the frontend. Nothing serves
it from inside this container."
- deploy/news-update copies /srv/dist out of the image onto the host at
~/data/news/web/dist for Caddy to serve.
Result: UNVIOLATED.
Bullet 8 — "No invented MVP/phase-1 reduction of any of the above."
Evidence: All major Scope-IN items are present:
- 6-crate workspace, rust-toolchain.toml, CI (Todo 1)
- Domain model, scoring, clustering (Todos 2-4, 9-11)
- SQLite + FTS5, source registry, feed polling (Todos 5-8)
- Relevance/importance scorers, percentile tracker (Todos 10-11)
- Notification gate with budget/quiet-hours/bypass/suppression (Todo 12)
- Feedback endpoint + Bayes retrain (Todo 13)
- SIGHUP config reload (Todo 14)
- ntfy publisher + ACL snippet (Todo 15)
- Daily digest (Todo 16)
- Replay harness + --dry-run (Todo 17)
- /healthz, /metrics, scheduler, pruning (Todo 18)
- Leptos web UI + API read endpoints (Todo 19)
- Dockerfile + CI bundle gate (Todo 20)
- Operator snippets (Todo 21)
No Scope-IN item was dropped or reduced.
Result: UNVIOLATED.
Check 4 result: PASS — all 8 Scope-OUT bullets confirmed unviolated.
================================================================================
SUMMARY
================================================================================
Check 1 (Dependency trace): PASS
Check 2 (Endpoint trace): PASS
Check 3 (Config-key trace): PASS
Check 4 (Scope-OUT re-check): PASS
No unexplained direct dependencies, no extra routes, no extra config keys, and
no Scope-OUT violations were found. Accepted deviations recorded in
.omo/notepads/news-triage/learnings.md and .omo/start-work/ledger.jsonl were
not flagged.
FINAL VERDICT: APPROVE