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.
4.4 KiB
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:
corehas no I/O (seecrates/core— domain models + scoring/affinity math only);dbonly talks to SQLite;feedsonly fetches/parses;llmonly talks to Ollama. A change that makescorereach for a DB handle ordbparse 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 growingqueries.rs. - Data-model structs (domain models, query row types, DTOs/views) live in
a crate's
models.rsrather than the file that produces or consumes them, whenever a struct is used outside the function that builds it — matching the existingcrates/core/src/models.rspattern. This doesn't apply to structs that are inherently local to one file/component (e.g. a Dioxus component'sProps/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 incrates/core/src/affinity.rs), but query/filter logic indband non-passthrough logic inweb'sserver::services. "Non-trivial" excludes thin wrappers (a#[server]fn or service function that's justdb.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-webpasses locally before pushing (CI runs this;feedsignal-webitself currently has no test target).
Static checks (all of CI, not a subset)
cargo fmt --checkclean.cargo clippy --workspace --exclude feedsignal-web --all-targets -- -D warningsclean.cargo clippy -p feedsignal-web --no-default-features --features server --all-targets -- -D warningsclean.cargo clippy -p feedsignal-web --no-default-features --features web --target wasm32-unknown-unknown -- -D warningsclean.cargo auditclean, 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 (aTODOmarking genuinely deferred, ticketed follow-up work is fine — seeLlmConfig'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 incomponents/button/component.rs. - No commented-out code left in.
Correctness at the boundary
- New
#[server]fns validate/trim input the wayservices::feeds::subscribedoes (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.lockchanges match what the dependency change actually requires.