What to build
Two domain rules currently live in crates/commands/src/focus.rs:
title.trim().is_empty() check in Commands::create_focus
text.trim().is_empty() check in Commands::append_task
These are domain invariants that belong in crates/domain/. Move them there as typed constructors. Commands drops its own validation guards.
Domain changes (crates/domain/)
impl NewFocus {
pub fn new(title: impl Into<String>, description: impl Into<String>) -> Result<Self, DomainError>
// Err if title.trim().is_empty()
}
pub struct TaskText(String);
impl TaskText {
pub fn new(text: impl Into<String>) -> Result<Self, DomainError>
// Err if text.trim().is_empty()
pub fn as_str(&self) -> &str
}
#[derive(Debug, thiserror::Error)]
pub enum DomainError {
#[error("title must not be empty")]
EmptyTitle,
#[error("task text must not be empty")]
EmptyTaskText,
}
Commands changes
Remove the trim().is_empty() guards in Commands::create_focus and Commands::append_task. Use NewFocus::new(...) and TaskText::new(...). Map DomainError to CommandError::BadRequest.
Domain tests own the invariant assertions. Commands tests drop the validation assertions.
Completion promise
Focus title and task text invariants are enforced once in crates/domain/ via typed constructors; no validation logic remains in the commands layer.
Acceptance criteria
Blocked by
None — can start immediately.
What to build
Two domain rules currently live in
crates/commands/src/focus.rs:title.trim().is_empty()check inCommands::create_focustext.trim().is_empty()check inCommands::append_taskThese are domain invariants that belong in
crates/domain/. Move them there as typed constructors. Commands drops its own validation guards.Domain changes (
crates/domain/)Commands changes
Remove the
trim().is_empty()guards inCommands::create_focusandCommands::append_task. UseNewFocus::new(...)andTaskText::new(...). MapDomainErrortoCommandError::BadRequest.Domain tests own the invariant assertions. Commands tests drop the validation assertions.
Completion promise
Focus title and task text invariants are enforced once in
crates/domain/via typed constructors; no validation logic remains in the commands layer.Acceptance criteria
NewFocus::newreturnsErr(DomainError::EmptyTitle)for blank titleTaskText::newreturnsErr(DomainError::EmptyTaskText)for blank textCommands::create_focusandCommands::append_taskcontain notrim().is_empty()guardstask checkgreenBlocked by
None — can start immediately.