F1 Plan Compliance Audit — news-triage ====================================== Auditor: F1 (plan compliance) Repo: /home/connor/docs/projects/news Date: 2026-09-01 Verdict: APPROVE All required checks were executed and recorded below. Every check passed. ================================================================================ CHECK 1 — CHECKBOX COUNT (todos 1-21, F1-F4 excluded) ================================================================================ Command: grep -E '^- \[x\] [0-9]+\.|^- \[ \] [0-9]+\.' /home/connor/docs/projects/news/.omo/plans/news-triage.md | wc -l grep -E '^- \[x\] [0-9]+\.' /home/connor/docs/projects/news/.omo/plans/news-triage.md | wc -l grep -E '^- \[ \] [0-9]+\.' /home/connor/docs/projects/news/.omo/plans/news-triage.md | wc -l Output: total matching todo lines: 21 checked ([x]): 21 unchecked ([ ]): 0 Result: PASS — all 21 implementation todos are checked. ================================================================================ CHECK 2 — FORBIDDEN STRINGS ================================================================================ Command (crates): grep -riE "liteLLM|llama.cpp|gpt-oss|gemma3" /home/connor/docs/projects/news/crates Output: (no output) Command (Dockerfile / CI / deploy): grep -riE "liteLLM|llama.cpp|gpt-oss|gemma3" /home/connor/docs/projects/news/Dockerfile /home/connor/docs/projects/news/.gitea/workflows/ci.yml /home/connor/docs/projects/news/deploy/ Output: (no output) Result: PASS — zero forbidden-string matches in code, build, CI, or deploy artifacts. ================================================================================ CHECK 3 — PROTECTED FILES (mtime baseline audit) ================================================================================ Implementation began ~2026-09-01 13:40 -0400. Any mtime later than the listed baseline is a REJECT. Measured with `stat --format="%n %y"`. Command: stat --format="%n %y" /home/connor/compose.yml /home/connor/Caddyfile \ /home/connor/data/ntfy/etc/server.yml /home/connor/config/prometheus/prometheus.yml Output: /home/connor/compose.yml 2026-09-01 13:31:00.123329809 -0400 (baseline <= 2026-09-01 13:31:00) /home/connor/Caddyfile 2026-08-28 15:51:59.475726235 -0400 (baseline <= 2026-08-28 15:51:59) /home/connor/data/ntfy/etc/server.yml 2026-08-20 17:25:40.623346035 -0400 (baseline <= 2026-08-20 17:25:40) /home/connor/config/prometheus/prometheus.yml 2026-08-25 22:21:00.811095356 -0400 (baseline <= 2026-08-25 22:21:00) Result: PASS — all four protected files have mtimes at or before their baselines. No implementer edits detected. ================================================================================ CHECK 4 — MUST-NOT SWEEP ================================================================================ 4a. Article-body scraping / full-text extraction / paywall circumvention ------------------------------------------------------------------------ Commands: grep -riE "scraper\b|full.?article|article.?body|paywall|readability|html2text|trafilatura|goose|newspaper" \ /home/connor/docs/projects/news/crates /home/connor/docs/projects/news/Cargo.toml /home/connor/docs/projects/news/Dockerfile grep -riE "scraper crate|scraper::|full.?text|readability" /home/connor/docs/projects/news/crates Output: Only benign matches in generated CSS/JS/dist and a "newspaper" ntfy tag: /home/connor/docs/projects/news/crates/news-server/src/notify/ntfy.rs: /// Uses `Priority: 2`, `Markdown: yes`, and `Tags: newspaper`. /home/connor/docs/projects/news/crates/news-server/src/notify/ntfy.rs: .header("Tags", "newspaper") No scraper crate, no readability crate, no full-article fetching code. Inspection: Feed ingestion is limited to parsing RSS/Atom (`crates/news-ingest/src/rss_atom.rs`) and the AP news-sitemap (`crates/news-ingest/src/news_sitemap.rs`). The only text indexed for search is title+summary via FTS5 (`crates/news-store/src/items.rs`); the schema stores `title`, `summary`, and `link` only (`crates/news-store/migrations/0001_init.sql`). Result: PASS — no article-body scraping code beyond feed-provided lead data. 4b. Backend serving the frontend dist / static SPA files -------------------------------------------------------- Command: grep -riE "ServeDir|ServeFile|include_bytes.*index\.html|/srv/dist|actix.files|tower_http.*fs|warp.*fs" \ /home/connor/docs/projects/news/crates/news-server Output: (no output) Inspection: `crates/news-server/src/health.rs` exposes only `/healthz` and `/metrics`. `crates/news-server/src/api/mod.rs` mounts `/api/stories`, `/api/config`, and `/api/feedback`. No static-file handler is registered anywhere in news-server. `Dockerfile` line 81 copies the built frontend to `/srv/dist` with the explicit comment: "Where the updater looks for the frontend. Nothing serves it from inside this container." Result: PASS — backend does not serve dist or news-web assets. 4c. LLM / embedding / GPU / paid-AI API dependencies ---------------------------------------------------- Commands: grep -riE "\bcandle\b|\bort\b|\bllama\b|\bopenai\b|\banthropic\b|\btiktoken\b|\btokenizers\b|\bembedding\b|\bembeddings\b|\bbert\b|\bsentence.?transformers\b|\bfastembed\b|\bollama\b|\bgpt-4\b|\bgpt4\b|\bclaude\b" \ /home/connor/docs/projects/news/crates /home/connor/docs/projects/news/Cargo.toml \ /home/connor/docs/projects/news/Dockerfile /home/connor/docs/projects/news/.gitea/workflows/ci.yml \ /home/connor/docs/projects/news/deploy Output: No matches in source, Cargo.toml, Dockerfile, CI, or deploy files. (The earlier broad regex caught only false positives: the word "importance", "internal", "newspaper", and references in plan documents / node_modules Playwright READMEs, none of which are code dependencies.) Inspection of workspace dependencies (`Cargo.toml` lines 26-73): Internal: news-core, news-store, news-ingest, news-server General: serde, thiserror, uuid, chrono, chrono-tz, serde_json, pretty_assertions Persistence: sqlx, tokio Feed polling: reqwest, url, quick-xml Config/logging: toml, tracing, clap, tracing-subscriber HTTP API: axum, tower Metrics: prometheus news-web: leptos, gloo-net, console_error_panic_hook, serde, serde_json Result: PASS — no LLM, embedding, GPU, or paid-AI crates anywhere in the build. ================================================================================ CHECK 5 — SCOPE-IN COVERAGE ================================================================================ The plan's "Must have" list maps to implemented components as follows: 1. Cargo workspace with 6 crates, edition 2024, pinned toolchain → /home/connor/docs/projects/news/Cargo.toml (members lines 3-10, workspace.package, lints) → /home/connor/docs/projects/news/rust-toolchain.toml (channel = "1.89.0", wasm32 target) → crates/news-core, crates/news-ingest, crates/news-store, crates/news-server, crates/news-web, crates/news-cli 2. Source registry: Al Jazeera, BBC World, AP sitemap; per-source weight/enable/poll; conditional GET; honest UA; backoff → crates/news-store/src/sources.rs (seed_default_sources, SourceRepo CRUD, fixed v1 seed IDs) → crates/news-ingest/src/lib.rs (FeedPoller, PollResult, ETag/If-Modified-Since, Retry-After, HONEST_USER_AGENT) → crates/news-ingest/src/rss_atom.rs, crates/news-ingest/src/news_sitemap.rs 3. Story normalization, dedupe, clustering, cross-source corroboration → crates/news-store/src/items.rs (insert_if_new dedupes on source_id+link) → crates/news-core/src/clustering.rs (Jaccard title similarity) → crates/news-store/src/clusters.rs (assign_or_create, distinct-source source_count) 4. Two-axis non-LLM scoring + trailing-7-day percentile distributions → crates/news-core/src/scoring/importance.rs (importance_score: prominence + trust + corroboration) → crates/news-core/src/scoring/relevance.rs (BM25 + NaiveBayes, cold-start at 20 samples) → crates/news-server/src/scoring.rs (ImportanceScorer, RelevanceScorer wiring) → crates/news-store/src/percentile.rs (PercentileTracker with MetricKind::Importance/Relevance, 7-day window) 5. Notification gate: token bucket, quiet hours, adaptive p90 relevance threshold, p99/importance-or-2-source bypass, suppression → crates/news-server/src/gate.rs (NotificationGate, TokenBucket, GateDecision, SuppressReason) → bypass ceiling independent of normal budget is configured and tested 6. ntfy publishing with title/markdown/priority/click/actions + ACL snippet → crates/news-server/src/notify/ntfy.rs (NtfyPublisher, NotifyLane, action buttons) → deploy/ntfy-access-snippet.md (exact `ntfy access news-bot news-triage rw` command) 7. Feedback capture feeding Naive Bayes → crates/news-server/src/api/feedback.rs (POST /api/feedback) → crates/news-store/src/feedback.rs (FeedbackRepo persistence) → crates/news-core/src/scoring/relevance.rs (incremental train) 8. Daily digest at 07:00 America/Louisville for 75th-90th relevance band → crates/news-server/src/notify/digest.rs (build_and_send_digest, DIGEST_HOUR=7, BAND_FLOOR=75, BAND_CEILING=90) 9. Replay harness and --dry-run flag → crates/news-cli/src/replay.rs (run replay over fixture XML) → crates/news-cli/src/main.rs (clap subcommand) → crates/news-server/src/main.rs (--dry-run CLI flag, dry_run tagging in notification_log) 10. Operator control surface: config.toml + SIGHUP hot-reload + Leptos web UI → crates/news-server/src/config.rs (NewsConfig, load, reload_once, watch_sighup) → deploy/README.md instructs operator to create ./config/news/config.toml → crates/news-web/src/main.rs + app.rs + story_list.rs + config_view.rs + api.rs → crates/news-server/src/api/stories.rs (GET /api/stories) → crates/news-server/src/api/config.rs (GET /api/config) → crates/news-server/src/api/feedback.rs (POST /api/feedback) 11. Containerized deployment: one image, dist lifted out, /healthz, /metrics, 90-day pruning → /home/connor/docs/projects/news/Dockerfile (multi-stage web/server/runtime, COPY dist to /srv/dist) → crates/news-server/src/health.rs (/healthz, /metrics) → crates/news-server/src/scheduler/loops.rs (retention loop prunes raw_items > 90 days) 12. CI: fmt, clippy, tests, cargo-machete, cargo-deny, WASM bundle-size budget → /home/connor/docs/projects/news/.gitea/workflows/ci.yml - check job: cargo fmt --all --check, cargo clippy -D warnings, cargo test --workspace, wasm check - guardrails job: cargo machete, cargo deny check - bundle job: trunk build --release + 1.8MB WASM size budget 13. Operator-applied snippets for compose/Caddy/ntfy/Prometheus → deploy/README.md contains compose service block, Caddy LAN-only vhost block, ntfy ACL grant command, Prometheus scrape-config snippet → deploy/news-update.service + deploy/news-update.timer (systemd system timer) 14. TDD throughout → Every scope component listed above has corresponding tests in the same crate (e.g. model tests, scoring property tests, gate tests, API oneshot tests, healthz oneshot tests, digest mock tests). Evidence files task-1 through task-21 are present in .omo/evidence/. Result: PASS — every Scope-IN item has a corresponding implemented component. ================================================================================ ACCEPTED DEVIATIONS (NOT FLAGGED) ================================================================================ - test names flexed to real module paths (per task instructions) - root package.json (runway layout) - percentile_rank helper duplicated in two modules (F2's concern) - NEWS_DATABASE_URL added as env source (required by todo 20's Dockerfile contract) - The runtime `config/news/config.toml` file is operator-applied per deploy/README.md; the loader/hot-reload machinery is fully implemented and tested. ================================================================================ FINAL VERDICT ================================================================================ All checks pass: [✓] Checkbox count: 21/21 checked [✓] Forbidden strings: zero matches [✓] Protected files: mtimes unchanged [✓] No article-body scraping [✓] No backend static-file serving of dist [✓] No LLM/embedding/GPU/paid-AI dependencies [✓] Scope-IN fully covered F1 verdict: APPROVE