360 lines
16 KiB
Plaintext
360 lines
16 KiB
Plaintext
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 (75th–90th) 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
|