27 releases (6 breaking)
| new 0.7.1 | Jun 8, 2026 |
|---|---|
| 0.6.0 | May 15, 2026 |
| 0.3.5 | Mar 22, 2026 |
| 0.1.16 | Dec 21, 2025 |
| 0.1.14 | Nov 17, 2025 |
#1 in #traits
84 downloads per month
Used in 8 crates
130KB
3K
SLoC
kanban-core
Foundation crate for the kanban workspace. Provides shared types, error handling, configuration, state primitives, and the dependency graph used by all other crates.
Modules
Error Types
CoreError
pub enum CoreError {
Validation(String), // Input validation failure
Config(String), // Configuration error
}
pub type CoreResult<T> = Result<T, CoreError>;
AppConfig
Application configuration loaded from a TOML or JSON config file.
| Field | Type | Default | Description |
|---|---|---|---|
configuration_format |
Option<String> |
"toml" |
Config file format ("json" or "toml") |
configuration_location |
Option<String> |
system default | Path to the config file |
default_card_prefix |
Option<String> |
"task" |
Default card identifier prefix (e.g. "KAN" → KAN-1) |
default_sprint_prefix |
Option<String> |
"sprint" |
Default sprint identifier prefix |
editing_format |
Option<String> |
"json" |
Format used for external editor ("json" or "toml") |
storage_backend |
Option<String> |
"json" |
Storage backend ("json" or "sqlite") |
storage_location |
Option<String> |
"boards.json" / "boards.sqlite" |
Path to the data file |
Effective-value getters (return the value or its default):
config.effective_default_card_prefix() // → "task"
config.effective_default_sprint_prefix() // → "sprint"
config.effective_storage_backend() // → "json"
config.effective_editing_format() // → "json"
config.effective_configuration_format() // → "toml"
config.effective_storage_location() // → "boards.json"
Validation: config.validate_values() returns CoreError::Validation if any field is out of range.
Branch prefix validation: validate_branch_prefix(prefix: &str) -> bool — non-empty, alphanumeric + hyphens/underscores, must start and end with an alphanumeric character.
PaginatedList<T>
Serialized pagination envelope used by CLI and MCP list responses.
pub struct PaginatedList<T> {
pub items: Vec<T>,
pub total: usize,
pub page: usize,
pub page_size: usize,
pub total_pages: usize,
}
PaginatedList::paginate(items, page, page_size)— slicesitemsand returns the envelope. ReturnsCoreError::Validationifpage_size > MAX_PAGE_SIZE(500) orpage_size == 0.resolve_page_params(page: Option<u32>, page_size: Option<u32>) -> CoreResult<(usize, usize)>— applies defaults (page=1,page_size=50) and validates.- Constants:
DEFAULT_PAGE = 1,DEFAULT_PAGE_SIZE = 50,MAX_PAGE_SIZE = 500.
Page / PageInfo
TUI viewport pagination — manages which items are visible in a terminal viewport given a scroll offset. Pure in-memory state — lives only in the TUI process.
pub struct PageInfo {
pub visible_indices: Vec<usize>,
pub first_visible: usize,
pub last_visible: usize,
pub items_per_page: usize,
pub show_above_indicator: bool,
pub items_above: usize,
pub show_below_indicator: bool,
pub items_below: usize,
pub current_page: usize,
pub total_pages: usize,
}
Page computes a PageInfo for a given item count, viewport height, and scroll offset.
InputState
UTF-8–aware text input with cursor management. Used for all text entry dialogs in the TUI.
pub struct InputState {
pub value: String, // Current text
pub cursor_pos: usize, // Byte offset of cursor
}
Methods: insert_char, delete_char_before, delete_char_after, move_left, move_right, jump_to_start, jump_to_end, clear.
The cursor tracks byte offsets that always fall on UTF-8 character boundaries.
SelectionState
Cursor navigation for a fixed-size list.
pub struct SelectionState {
pub selected: usize,
pub total: usize,
}
Methods: next(), prev(), clamp() — wraps around at boundaries.
Editable<T> trait
pub trait Editable<T> {
fn to_editable(&self) -> T;
fn from_editable(value: T) -> Self;
}
Implemented by domain types to support round-trip serialization through an external editor.
Graph machinery
Reusable graph primitives for relating domain entities (used by kanban-domain for card dependency tracking). Direction is a property of the container, not the edge — DagGraph<E> carries directed edges, UndirectedGraph<E> carries undirected ones, and the type system prevents calling directed vocabulary on an undirected graph.
The machinery is fully generic over Edge::NodeId. The kanban domain keys on Uuid today, but the algorithms only require Copy + Eq + Hash, so external callers can pick any node identity (e.g. u32, or a discriminated (EntityKind, Uuid) newtype for heterogeneous-entity graphs).
Trait taxonomy
| Trait | Purpose |
|---|---|
GraphNode |
Implemented by domain entities (Card, Sprint, ...) — provides node_id() -> Uuid. |
Graph |
Direction-agnostic core: add_edge, remove_edge, contains_edge. Object-safe with an explicit NodeId binding (e.g. &dyn Graph<NodeId = Uuid>). |
Directed: Graph |
Adds outgoing(node) / incoming(node) — distinct successor / predecessor sets. Only directed containers implement this. |
Undirected: Graph |
Adds neighbors(node) — a single direction-less neighbour set. Only undirected containers implement this. |
Cascadable: Graph |
Mutating node-keyed cascade: archive_node, unarchive_node, remove_node. |
EdgeSet: Graph |
Read-only set vocabulary aligned with HashSet / BTreeSet: len, active_len, is_empty, contains, contains_archived. contains returns the active view; contains_archived is the explicit escape hatch for history. |
Splitting Cascadable (mutating) from EdgeSet (read-only) keeps each trait's name accurate to its single purpose and lets a generic consumer ask for only the surface it actually uses.
The Edge trait and EdgeBase<N>
Concrete edge kinds (e.g. SpawnsEdge, BlocksEdge, RelatesEdge in kanban-domain) embed EdgeBase<N> for the common fields and implement the Edge trait so the graph containers can operate on them uniformly:
pub struct EdgeBase<N = Uuid> {
pub source: N,
pub target: N,
pub created_at: DateTime<Utc>,
pub archived_at: Option<DateTime<Utc>>,
}
pub trait Edge {
type NodeId: Copy + Eq + std::hash::Hash;
fn source(&self) -> Self::NodeId;
fn target(&self) -> Self::NodeId;
fn created_at(&self) -> DateTime<Utc>;
fn archived_at(&self) -> Option<DateTime<Utc>>;
fn archive(&mut self);
fn unarchive(&mut self);
fn from_endpoints(source: Self::NodeId, target: Self::NodeId) -> Self where Self: Sized;
// Provided: is_active, is_archived, involves.
}
EdgeBase<N> itself implements Edge for N: Copy + Eq + Hash, so it works as a standalone edge when no per-kind metadata is needed.
Concrete containers
DagGraph<E: Edge>— directed acyclic graph. Rejects self-references, active duplicates, and any edge whose insertion would create a cycle in the active subgraph. ImplementsGraph + Directed + Cascadable + EdgeSet. Providesdescendants/ancestorsfor transitive traversal.Deserializere-runs the DAG invariants so a corrupted file fails to load up front.UndirectedGraph<E: Edge>— undirected graph. Rejects self-references and active duplicates (either ordering of endpoints counts as the same edge). Cycles are permitted. ImplementsGraph + Undirected + Cascadable + EdgeSet.
Both are backed by a crate-internal EdgeStore<E> that holds active + archived edges in a flat list. Archived edges survive remove operations as history; the active view ignores them.
GraphError
pub enum GraphError {
Cycle, // DAG only: insertion would close a directed cycle.
SelfReference, // Both: source == target.
EdgeNotFound, // Both: remove_edge target is absent.
Duplicate, // Both: an active edge with the same endpoints exists.
}
Plugging in a custom edge struct
use chrono::{DateTime, Utc};
use kanban_core::graph::{DagGraph, Edge, EdgeBase};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct BlocksEdge {
pub base: EdgeBase<Uuid>,
pub severity: Severity,
}
#[derive(Debug, Clone, Copy)]
pub enum Severity { Low, High }
impl Edge for BlocksEdge {
type NodeId = Uuid;
fn source(&self) -> Uuid { self.base.source }
fn target(&self) -> Uuid { self.base.target }
fn created_at(&self) -> DateTime<Utc> { self.base.created_at }
fn archived_at(&self) -> Option<DateTime<Utc>> { self.base.archived_at }
fn archive(&mut self) { self.base.archive(); }
fn unarchive(&mut self) { self.base.unarchive(); }
fn from_endpoints(source: Uuid, target: Uuid) -> Self {
Self { base: EdgeBase::new(source, target), severity: Severity::Low }
}
}
let mut graph: DagGraph<BlocksEdge> = DagGraph::new();
graph.add_edge_with_metadata(BlocksEdge {
base: EdgeBase::new(blocker, blocked),
severity: Severity::High,
})?;
Use Graph::add_edge(from, to) when default metadata is fine (it calls Edge::from_endpoints under the hood); use add_edge_with_metadata(edge) when the caller needs to set kind-specific fields explicitly.
Algorithms
The cycle, path, and reachability primitives live in kanban_core::graph::algorithms (would_create_cycle, has_cycle, reachable_from). They operate on a generic HashMap<N, Vec<N>> adjacency list and are reused both by the containers and by EdgeStore::adjacency_list for ad-hoc analysis.
LogEntry / Loggable trait
Structured logging support for domain events.
pub trait Loggable {
fn log_entries(&self) -> Vec<LogEntry>;
}
Dependencies
| Crate | Purpose |
|---|---|
serde + serde_json |
Serialization |
uuid |
Uuid type |
thiserror |
Error derivation |
chrono |
Timestamps |
Dependencies
~1.3–2.3MB
~41K SLoC