# CLAUDE.md — newsfeed Agent-facing notes for working in this repo. Read the workspace-wide conventions in `~/git/architecture/*.md` first; this file records only what's specific to newsfeed, especially the deliberate deviations. ## What this is A self-hosted, user-controlled news feed. The core idea: the user owns the ranking. Never introduce an implicit/opaque signal that overrides the user's explicit weights. Signals (view/click/save/dismiss) may *inform* future ranking but must remain subordinate to the per-source and per-interest weights the user sets. ## Layout & boundaries (strict) - `newsfeed-entities` — types/DTOs only, no I/O. Wire DTOs derive `ts_rs::TS` with `#[ts(export)]`; `cargo test -p newsfeed-entities` regenerates `web/src/api/bindings/` (path set by `.cargo/config.toml`'s `TS_RS_EXPORT_DIR`). Don't hand-edit bindings. - `newsfeed-core` — business logic + data-access **ports** (traits). Pure where possible (ranking, auth hashing). No DB or network calls. - `newsfeed-data` — SQLite **adapters** implementing the core ports. - `newsfeed-fetch` — the outbound-feed adapter (RSS fetch, feed discovery, OPML parse); implements the `FeedProbe` port. The one crate that talks HTTP to the wider web for feeds — used by the api (discovery on add) and the worker (polling). Kept separate from `data` (SQLite-only) on purpose. - `newsfeed-api` / `newsfeed-worker` — thin binaries; wire config/logging/signals and the concrete store. No business logic that could live in a library crate. - `newsfeed-modelwatch` — a **push producer** (lib + one-shot bin): watches the Hugging Face Hub for open-weight releases, applies an admission predicate, and POSTs candidates to `/v1/ingest/candidates` under a token. A *client* of the API, not an internal component — it holds no DB/core deps; its pure logic (predicate, mapping) is in the lib. New types → entities. New logic → core. New feed I/O → `fetch`. New DB I/O → `data`. Add a port to `core::ports`, implement it in the matching adapter crate. ## Deliberate deviations from `generic.md` 1. **SQLite instead of Postgres (§5).** Explicit project choice. Because SQLite is single-file/single-host, **api and worker co-locate** and share `/var/lib/newsfeed/newsfeed.db`. There is no central DB cluster, no mTLS DB auth, no `pg_ident` mapping. The worker does in-process scheduling — the `FOR UPDATE SKIP LOCKED` guidance does not apply. 2. **Runtime sqlx queries, not `query!` macros (§5).** SQLite's dynamic typing makes compile-time checking low-value and forces a live DB or fiddly offline cache into CI. We use `sqlx::query`/`query_as` with `FromRow` row structs (see `data/src/rows.rs`) and map to entities explicitly. No `.sqlx/` cache; CI builds need no database. Both are documented at their site (`data/src/lib.rs` header, `readme.md`) — keep them in sync if you change the approach. ## Datastore conventions TEXT UUIDs (hyphenated), RFC3339 timestamps, INTEGER booleans, JSON-in-TEXT for small collections (`tags`, `media`). Migrations are immutable once committed (§5): add a new `crates/newsfeed-data/migrations/NNNN_*.sql`, never edit a landed one. `RETURNING` and `ON CONFLICT` are used, so the target needs SQLite ≥ 3.35. ## Auth model - Humans: Argon2id password hash + opaque session cookie (`nf_session`), hash stored. - Producers: per-user bearer API tokens (`nf__`), SHA-256 hashed at rest, shown once. Ingest is attributed to the token's owner. Crypto/token logic lives in `core::auth`; the flow orchestration in `core::service`. ## Deploy topology api + worker (+ the `newsfeed-modelwatch` systemd timer, 3-hourly, whose ingest token lives in `/etc/newsfeed/modelwatch.env` — a secret kept out of the deployable config so redeploys never clobber it) → `slartibartfast.kosherinata.internal`; SPA → `oolon.kosherinata.internal` (nginx serves + proxies `/v1` to the API over the mesh; TLS terminates at oolon). API port 22672 (registered in architecture `port-allocations.md`; deliberately not the crowded `8081`), plain HTTP behind firewalld. See `.gitea/workflows/deploy.yml` (infra truth) and `script/infra-setup.sh` (one-time host prep). `Type=exec`, not `notify`, because axum doesn't `sd_notify`. ## Before you commit - `cargo fmt --all` · `cargo clippy --workspace --all-targets -- -D warnings` · `cargo test --workspace` - `pnpm --dir web build` (tsc typecheck + vite) · `pnpm --dir web lint` - If you touched an entity DTO, regenerate bindings and include them in the commit. - Conventional Commits; scope = crate/area (`feat(api):`, `fix(core):`, …).