feedsignal/DEFINITION_OF_DONE.md
Austin Schaefer e70909af47
All checks were successful
CI / check (pull_request) Successful in 2m2s
CI / test (pull_request) Successful in 3m15s
CI / audit (pull_request) Successful in 10s
Add and apply a models.rs convention for data-model structs
Document in the DoD that data-model structs (domain models, query row
types, DTOs/views) belong in a crate's models.rs rather than the file
that produces/consumes them, once used outside that function -
matching the existing crates/core/src/models.rs pattern. Component-
local structs (Props, Styles, context) are exempt.

Apply it to db: move RankedArticleRow out of articles.rs into a new
crates/db/src/models.rs.
2026-09-03 21:23:34 +02:00

4.4 KiB

Definition of Done — feedsignal

A change is done when all of the following hold, not just when it compiles.

Single Responsibility

  • Each module/function does one thing. If a PR description needs "and" to describe what a function does, split it.
  • Crate boundaries stay honest: core has no I/O (see crates/core — domain models + scoring/affinity math only); db only talks to SQLite; feeds only fetches/parses; llm only talks to Ollama. A change that makes core reach for a DB handle or db parse RSS is a sign the code landed in the wrong crate.
  • Within db, one file per domain concern (feeds.rs, articles.rs, reading_events.rs, affinities.rs) — a new table gets its own file, not a growing queries.rs.
  • Data-model structs (domain models, query row types, DTOs/views) live in a crate's models.rs rather than the file that produces or consumes them, whenever a struct is used outside the function that builds it — matching the existing crates/core/src/models.rs pattern. This doesn't apply to structs that are inherently local to one file/component (e.g. a Dioxus component's Props/Styles/context struct, or a helper struct scoped to a single function's internals).

DRY, but not premature

  • No copy-pasted logic across call sites — extract a shared function once a second real call site exists, not in anticipation of one.
  • No new abstraction (trait, generic helper, config knob) unless at least two concrete cases need it today.

Testing

  • All non-trivial logic ships with unit tests wherever practical — not just core's pure functions (scoring, affinity math — the existing pattern in crates/core/src/affinity.rs), but query/filter logic in db and non-passthrough logic in web's server::services. "Non-trivial" excludes thin wrappers (a #[server] fn or service function that's just db.some_query(args).await? with no branching of its own) and glue code whose only failure mode is a compiler error.
  • When a DB/network dependency makes a case genuinely impractical to unit test (e.g. a Diesel query's SQL correctness), say so in the PR rather than silently skipping it — that's a real gap to track, not a pass.
  • Test rationale goes in a /// doc comment above #[test] fn, not inline in the body.
  • A bug fix includes a regression test that fails on the old code.
  • cargo test --workspace --exclude feedsignal-web passes locally before pushing (CI runs this; feedsignal-web itself currently has no test target).

Static checks (all of CI, not a subset)

  • cargo fmt --check clean.
  • cargo clippy --workspace --exclude feedsignal-web --all-targets -- -D warnings clean.
  • cargo clippy -p feedsignal-web --no-default-features --features server --all-targets -- -D warnings clean.
  • cargo clippy -p feedsignal-web --no-default-features --features web --target wasm32-unknown-unknown -- -D warnings clean.
  • cargo audit clean, or a documented reason if it can't be (e.g. an unfixed transitive advisory).

No half-finished work

  • No TODOs standing in for a decision the PR itself was supposed to make (a TODO marking genuinely deferred, ticketed follow-up work is fine — see LlmConfig's config-story TODO).
  • No dead code kept "just in case" — if it's unused, delete it. Library- surface exceptions (e.g. dioxus-primitives-derived components) get an explicit #[allow(dead_code)] with a one-line reason, matching the existing pattern in components/button/component.rs.
  • No commented-out code left in.

Correctness at the boundary

  • New #[server] fns validate/trim input the way services::feeds::subscribe does (anyhow::ensure! on empty URL) — don't trust the browser.
  • DB writes that must be atomic with respect to a failure path (e.g. "fetch before writing so a bad URL leaves no half-registered row") get a comment explaining why, like subscribe's doc comment does.
  • Migrations are additive/reversible — no editing a shipped migration file.

Docs

  • A non-obvious "why" (a workaround, a chosen threshold, a design tradeoff) gets a comment; "what" is left to the code.
  • README.md's architecture section gets updated if the change adds a crate, a pipeline stage, or a scoring input.

PR hygiene

  • Diff is scoped to the stated task — no drive-by refactors bundled in.
  • Cargo.lock changes match what the dependency change actually requires.