Thank you for considering contributing to Kanban! This document provides guidelines and instructions for contributing.
- Rust 1.74+ with cargo
- Nix (recommended for reproducible environment)
# Clone the repository
git clone <repo-url>
cd kanban
# Using Nix (recommended)
nix develop
# Or install dependencies manually
rustup update stable# Run the application
cargo run
# Run with import
cargo run -- test-board.json
# Auto-reload on changes
cargo watch -x run
# Fast compile check
cargo check
# Run tests
cargo test
# Linting (with warnings as errors)
cargo clippy --all-targets --all-features -- -D warnings
# Format code
cargo fmt --all- Follow standard Rust conventions and idioms
- Use
rustfmtfor formatting (enforced in CI) - Address all
clippywarnings before submitting PR - Choose string parameter types by what the function does with the value:
- Read-only inspection / parsing:
&str - Validate-then-maybe-store (rejection possible):
&str - Always store (constructors, unconditional setters):
impl Into<String>(orOption<impl Into<String>>) - In-place mutation of existing string:
&mut String - Avoid
impl AsRef<str>-- signature clutter without ergonomic gain
- Read-only inspection / parsing:
- Use
impl Traitfor return types when appropriate - Keep functions focused and under 50 lines when possible
NO COMMENTS unless:
- Documenting public APIs
- Explaining complex algorithms
- Required for safety/correctness
Module Organization:
- Each file should be < 300 lines
- Extract reusable patterns into separate modules
- Follow existing module structure in
crates/kanban-tui/src/:app.rs- Application state and event handlingui.rs- Rendering logicevents.rs- Event loop and input handlinginput.rs- Input state managementdialog.rs- Dialog interaction patternseditor.rs- External editor integration
Type Safety:
- Leverage newtype pattern (
BoardId,CardId,ColumnId) - Use enums for state machines (
AppMode,Focus,CardFocus) - Prefer compile-time guarantees over runtime checks
Error Handling:
- All public APIs return
KanbanResult<T> - Use
thiserrorfor error definitions - Provide context in error messages
- Log errors with
tracing::error!
Immutability:
- Prefer immutable data structures
- Use
&mutonly when necessary - Update timestamps on mutation methods
The codebase follows SOLID principles:
- Single Responsibility: Each crate and module has one clear purpose
- Open/Closed: Domain models are extensible through methods
- Liskov Substitution: Types are consistent and predictable
- Interface Segregation: Focused, minimal abstractions
- Dependency Inversion: Layers depend on abstractions
crates/
├── kanban-core/ # Core traits, errors, result types
├── kanban-domain/ # Domain models (Board, Card, Column, Sprint)
├── kanban-persistence/ # Persistence traits, registry, and shared types
├── kanban-persistence-json/ # JSON file storage backend
├── kanban-persistence-sqlite/ # SQLite storage backend
├── kanban-service/ # Service layer: KanbanContext, persistence orchestration
├── kanban-tui/ # Terminal UI (ratatui + crossterm)
├── kanban-cli/ # CLI entry point (clap)
└── kanban-mcp/ # Model Context Protocol server for LLM integration
Dependency Flow:
graph LR
CLI[kanban-cli] --> TUI[kanban-tui]
CLI --> SVC[kanban-service]
MCP[kanban-mcp] --> SVC
TUI --> SVC
SVC --> PER[kanban-persistence]
SVC -.-> JSON[kanban-persistence-json]
SVC -.-> SQL[kanban-persistence-sqlite]
JSON --> PER
SQL --> PER
PER --> DOM[kanban-domain]
DOM --> CORE[kanban-core]
When adding a new field to any struct in kanban-domain (e.g., Card, Board, Column, Sprint):
-
Add the field to the struct in
kanban-domain. -
If the field is non-optional (no
#[serde(default)]):row_to_*()inkanban-persistence-sqlite/src/sqlite_store.rswill fail to compile because the struct literal is exhaustive — add the column toschema.sql, write a migration if the database already exists, and update bothrow_to_*()and the correspondingupsert_*()binds. -
If the field is optional (
Option<T>with#[serde(default)]): the SQLite code compiles but silently returnsNoneon load — manually updaterow_to_*()andupsert_*(), then set the new field to a non-Nonevalue infully_populated_snapshot()inside both:crates/kanban-persistence-sqlite/tests/roundtrip.rscrates/kanban-persistence-json/tests/roundtrip.rs
The roundtrip test (
full_roundtrip_preserves_all_fields) will fail until all three are updated.
The card-relation graph is designed to be extensible. To add a fourth relation kind (e.g. "duplicates", a board-scoped variant, etc.), the moving parts are:
-
Define the edge struct in
kanban-domain/src/dependencies/edges.rs. EmbedEdgeBasevia#[serde(flatten)]and add any per-kind metadata (severity-like enum, weight, label, …):pub struct MyEdge { #[serde(flatten)] pub base: EdgeBase, pub my_metadata: MyMeta, }
Implement
Edge for MyEdge(the trait surface inkanban-core::graph::edge).from_endpointsmust construct a default- metadata instance so the genericGraph::add_edgepath works. -
Add a sub-graph to
DependencyGraphinkanban-domain/src/dependencies/dependency_graph.rs— pickDagGraph<MyEdge>(directed, cycle-rejecting) orUndirectedGraph<MyEdge>(no direction, cycles permitted). Register it incascadable_parts_mut()andedge_sets()— every cross-cutting cascade (archive_node,remove_node,len,contains) then picks it up automatically. Add per-kind convenience methods (my_action,un_my_action, listing accessors) and amy_edges()raw accessor for the persistence layer. -
Add per-kind commands in
kanban-domain/src/commands/dependency_commands.rs:AddMyKind { source, target, my_metadata: MyMeta }RemoveMyKind { source, target, #[serde(default)] tolerate_missing: bool }- Wire them through the
DependencyCommandenum'sexecute/description/capture_inversedispatchers. AddMyKind::capture_inversereturns a tolerantRemoveMyKind;RemoveMyKind::capture_inversereads pre-remove metadata.
-
Add
GraphOperationstrait methods inkanban-domain/src/graph_operations.rsfor the new kind. Mirror the pattern ofblock/unblock(single-edge directed) orrelate/dissociate(undirected) depending on direction. -
Implement the new trait methods on
KanbanContextinkanban-service/src/context.rs,CliContext,McpContext,TuiContext. -
Persistence:
- JSON — add a
my_kind: { edges: [...] }key to the V6 envelope (no migration needed if a field is added cleanly with#[serde(default)]); otherwise bump to V7 with a transform step inkanban-persistence-json/src/migration/. - SQLite — add a
my_kind_edgestable inkanban-persistence-sqlite/src/schema.sqlwith appropriate columns and CHECK constraints; add read/write paths insqlite_store.rs.
- JSON — add a
-
App surfaces — expose via
kanban relationsubcommands (CLI),tool_*handlers (MCP), and TUI popup hooks as needed. -
Tests — parameterise existing graph tests over the new kind in
kanban-service/tests/card_graph.rs::card_graph_tests!, and add inverse round-trip tests ininverse_commands.rs.
Domain First Approach:
-
Define Domain Model in
kanban-domain- Add fields to structs
- Implement behavior methods
- Update
updated_attimestamps
-
Update Application State in
kanban-tui/src/app.rs- Add new
AppModevariants if needed - Implement event handlers
- Add business logic methods
- Add new
-
Implement UI in
kanban-tui/src/ui.rs- Add rendering functions
- Use existing helpers (
render_input_popup,centered_rect) - Follow existing panel/dialog patterns
-
Wire Up Events in event handlers
- Add keyboard shortcuts
- Update help text in footer
- Handle dialog interactions
The application uses a command pattern for all state mutations, enabling progressive auto-save:
Command Pattern Flow:
- Event Handler (kanban-tui): Processes keyboard input, collects data
- Command (kanban-domain): Encapsulates the mutation with parameters
- KanbanContext (kanban-service): Executes command via CommandContext
- CommandContext: Applies mutation to data vectors
- Save: KanbanContext writes state to disk via PersistenceStore
Example Handler Pattern:
pub fn handle_create_card_key(&mut self) {
// Collect immutable data before command execution
let (board_id, column_id) = { /* ... */ };
// Create command
let cmd = Box::new(CreateCard {
board_id,
column_id,
title: self.input.as_str().to_string(),
// ... other fields
});
// Execute (sets dirty flag automatically)
if let Err(e) = self.execute_command(cmd) {
tracing::error!("Failed to create card: {}", e);
return;
}
}Persistence Features:
- Progressive Auto-Save: Changes saved immediately after each operation (not just on exit)
- Async Processing: Commands queued immediately via bounded channel, processed by background worker
- Conflict Detection: Multi-instance changes detected via file metadata (timestamp + size + content hash)
- Format Versioning: JSON envelope versioned V1..V6 (current shipped is V6); reader auto-migrates older files on load via the V1→V2→V3→…→V6 chain, writing
.v{N}.backupfor V3/V4/V5 starting points before the split-graph step. SQLite usesmetadata.schema_version(currently1) plus one-shot legacy-table drops on open. - Multi-Instance Support: Last-write-wins resolution for concurrent edits (see CONFLICT_RESOLUTION.md for data loss scenarios and limitations)
- Atomic Writes: Crash-safe write pattern (temp file → atomic rename) prevents corruption
- Own-Write Detection: Metadata-based filtering prevents false positives from our own saves
When Adding Features:
- Define domain command in
kanban-domain/src/commands/ - Implement Command trait with execute() and description()
- Update handler in kanban-tui to use
self.execute_command() - KanbanContext in kanban-service handles persistence automatically
# All tests
cargo test
# Specific crate
cargo test --package kanban-domain
# With output
cargo test -- --nocapture- Unit tests go in the same file as implementation
- Test domain logic independently
- Use descriptive test names:
test_card_completion_toggle - Test edge cases and error conditions
Example:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_card_completion_toggle() {
let mut card = Card::new(column_id, "Test".to_string(), 0);
assert_eq!(card.status, CardStatus::Todo);
card.update_status(CardStatus::Done);
assert_eq!(card.status, CardStatus::Done);
}
}develop → master release workflow:
- Feature branches → merge to
develop - develop → accumulates features for next release
- master → production releases only
-
Create feature branch from
develop:git checkout develop git pull origin develop git checkout -b MVP-123/my-feature
-
Make changes and commit regularly (atomic commits)
-
Create changeset before submitting PR:
# Auto-generate from commits (default: patch) ./scripts/create-changeset.sh # Or specify bump type and description ./scripts/create-changeset.sh minor "Add sprint support"
-
Submit PR to develop:
- PR will check for changeset presence
- Changesets accumulate in
develop(not consumed yet)
-
Periodic releases from
develop→master:- All accumulated changesets consumed
- Single version bump (highest precedence wins: patch < minor < major)
- Automatic publish to crates.io
- GitHub release created
- Features merge to
developcontinuously develop→masterreleases at the end of the sprint- One version bump per release, not per feature
All crates in this workspace maintain synchronized versions:
- Root
Cargo.tomldefines workspace version via[workspace.package] version = "X.Y.Z" - All crates reference this via
version.workspace = true - Cross-crate dependencies use path-only references:
{ path = "../kanban-core" }(no version) - This prevents version skew between interdependent crates during publishing
Why this matters:
When publishing:
kanban-corepublishes first (no internal dependencies)kanban-domainpublishes second (depends on kanban-core)kanban-persistencepublishes third (depends on kanban-domain)kanban-servicepublishes fourth (depends on kanban-persistence)kanban-tuiandkanban-mcppublish fifth (depend on kanban-service)kanban-clipublishes last (depends on all others)
If versions diverge between crates, the published versions on crates.io won't resolve dependencies correctly, causing build failures for users.
Before publishing, the validate-release.sh script automatically:
- Checks all crates use workspace versioning
- Verifies no hardcoded versions in path dependencies
- Validates entire workspace builds correctly
- Runs dry-run publish for each crate
- Confirms dependency resolution will work when published
Run locally before release:
# Using Nix
nix run .#validate-release
# Or directly
bash scripts/validate-release.shAutomated in CI:
- Runs on every PR to
developandmaster - Blocks merge if validation fails
- Ensures no broken releases reach crates.io
- Run
cargo fmt --allto format code - Run
cargo clippy --all-targets --all-features -- -D warningsand address all warnings - Run
cargo testand ensure all tests pass - Test manually with
cargo run - Create changeset with
./scripts/create-changeset.sh - Update README.md if adding user-facing features
- Update CLAUDE.md if changing architecture/conventions
Use format: <branch-name>
Include concise list of changes:
Example:
Fixes task filtering behavior:
- Add sprint filter toggle to task view
- Update UI to show active sprint indicator
- Fix filter persistence across sessions
And include concisely:
- What: Brief description of changes
- Why: Motivation and context
- How: Implementation approach
- Testing: How you tested the changes
Use semantic commit format:
<type>: <description>
[optional body]
Types:
feat: New featurefix: Bug fixdocs: Documentation changesrefactor: Code refactoringtest: Adding/updating testschore: Maintenance tasksci: CI/CD changes
Examples:
feat: add sprint filtering to task viewfix: handle empty board state correctlydocs: update keyboard shortcuts in READMErefactor: extract dialog rendering logic
Commit Strategy:
Make small, atomic commits that contain one functionally related change:
✅ Good - Refactoring:
refactor: add handlers module
refactor: extract navigation handlers
refactor: extract board handlers
refactor: simplify handle_key_event to use handlers
✅ Good - Features:
feat: add sprint domain model
feat: add sprint UI rendering
feat: wire up sprint keyboard shortcuts
✅ Good - Fixes:
fix: validate card title before creation
fix: handle empty board state in renderer
fix: prevent duplicate card IDs on import
❌ Bad:
refactor: extract all handlers and simplify app.rs (giant commit)
feat: add complete sprint feature with UI and tests (too large)
fix: fix bugs (vague, multiple unrelated fixes)
Guidelines:
- One logical change per commit
- Each commit should compile and pass tests
- Keep commits focused and reviewable
- Group related file additions together
- Separate creation from refactoring
Quality Criteria - Each commit should be:
- Independent: Can be understood on its own
- Atomic: Contains one logical change
- Descriptive: Clear commit message following conventional commits format
- Buildable: Each commit compiles successfully
The commits should tell a clear story of the feature or refactoring from start to finish.
When submitting a PR, add a changeset file to describe your changes:
- Create
.changeset/<descriptive-name>.md:
---
bump: patch
---
Description of changes
- List of changes-
Bump types:
patch- Bug fixes, small changes (0.1.0 → 0.1.1)minor- New features, backwards compatible (0.1.0 → 0.2.0)major- Breaking changes (0.1.0 → 1.0.0)
-
On merge to master:
- Version automatically bumps based on changeset
- CHANGELOG.md updates with your description
- New version publishes to crates.io
- GitHub release created with tag
- Automated checks run on all PRs (format, clippy, tests)
- Maintainer reviews code and provides feedback
- Address feedback and update PR
- Once approved, maintainer will merge
- Bug Fixes: Crashes, regressions, and cross-platform fixes (Windows, macOS, Linux) are especially welcome. Small, targeted fixes are easy to review and ship quickly.
- UI Improvements: Enhance TUI rendering, add color themes
- Features: New metadata fields, filtering, searching
- Testing: Increase test coverage, integration tests
- Documentation: Improve docs, add examples
- Performance: Optimize rendering, reduce allocations
- Refactoring: Extract common patterns, improve modularity
To enable automated publishing and releases, configure these secrets in GitHub repository settings:
CARGO_REGISTRY_TOKEN
- Required for: Publishing to crates.io
- How to obtain:
- Login to crates.io with GitHub account
- Go to Account Settings → API Tokens
- Create new token with "publish-update" scope
- Add to GitHub: Settings → Secrets → Actions → New repository secret
DEPLOY_KEY
- Required for: Automated git commits and tag pushes
- How to generate:
ssh-keygen -t ed25519 -C "github-actions@kanban" -f deploy_key -N ""
- Add public key (deploy_key.pub) to GitHub: Settings → Deploy keys → Add (with write access)
- Add private key (deploy_key) to GitHub: Settings → Secrets → Actions → New repository secret
ci.yml - Runs on all pushes and PRs
- Format check (cargo fmt)
- Linter (cargo clippy)
- Tests (cargo test)
- Build validation
- Changeset validation (only on PRs to develop)
release.yml - Runs on push to master
- Checks for changesets (skips if none found)
- Bumps version based on changesets
- Updates CHANGELOG.md
- Publishes to crates.io
- Creates GitHub release with tag
Feature Branch → develop (via PR + changeset)
↓
(accumulate features)
↓
develop → master (weekly release PR)
↓
[CI checks] → [Release workflow]
- Open an issue for bugs or feature requests
- Start a discussion for design questions
- Check existing issues for similar topics
By contributing, you agree that your contributions will be licensed under the Apache 2.0 License.