# nbd — Architecture ## Overview `nbd` is a single-binary CLI tool with no background service. All state lives in `.nbd/tickets/` inside the project directory tree. The binary is structured as four modules, each with a single responsibility: ``` src/ ├── main.rs CLI definition and command dispatch ├── ticket.rs Data model, ID generation, priority validation ├── store.rs File I/O, directory traversal, CRUD └── display.rs Human-readable and JSON output formatting ``` ## Module Responsibilities ### `main.rs` — CLI entry point Owns the `clap` struct definitions (`Cli`, `Commands`) and the top-level `main` function. Delegates all business logic to command handlers (`cmd_create`, `cmd_read`, `cmd_list`, `cmd_update`). The handlers are thin: they call helpers from `store`, `ticket`, and `display`, then return. Parsing helpers (`parse_status`, `parse_ticket_type`, `parse_deps`) live here because they translate raw CLI strings into typed values used only by the command handlers. ### `ticket.rs` — Data model Defines the canonical in-memory representation of a ticket: - `Ticket` struct — all fields - `Status` enum — `Todo | InProgress | Done` - `TicketType` enum — `Project | Feature | Task | Bug` Provides two free functions: - `generate_id() -> String` — uses `RandomState` (seeded with OS entropy) to produce a 6-character lowercase hex string. No external randomness crate needed. - `validate_priority(u8) -> Result<(), String>` — enforces the 0–10 range. The enums derive `serde`'s `Serialize`/`Deserialize` with `#[serde(rename_all)]` so the JSON representation uses lowercase strings (`"in_progress"`, `"bug"`, …). ### `store.rs` — File I/O All filesystem operations are in this module. Uses `async-std` for async I/O throughout. **Directory discovery:** ``` find_nbd_root() └─ find_nbd_root_from(cwd) Walk parent dirs until .nbd/ is found ``` `find_nbd_root_from` accepts an explicit starting path to make it testable without changing the process's working directory. **CRUD functions:** | Function | Operation | |---|---| | `ensure_tickets_dir(root)` | Create `.nbd/tickets/` if missing | | `write_ticket(root, ticket)` | Serialise → pretty JSON → write file | | `read_ticket(root, id)` | Read file → deserialise; friendly error if not found | | `list_tickets(root)` | Read all `*.json` → sort by priority desc | **Error type:** `store::Result` is aliased to `Result>`, allowing `?` to propagate both `io::Error` and `serde_json::Error` without explicit wrapping. ### `display.rs` — Output formatting Two output modes: - **Tabular** — human-readable key–value block (single ticket) or column-aligned table (list). Column widths are compile-time constants. - **JSON** — delegates to `serde_json::to_string_pretty`. Each public surface is split into a `format_*` function (returns `String`, testable) and a `print_*` function (writes to stdout via `println!`). ## Data Flow ``` CLI args (clap) │ ▼ dispatch() [main.rs] │ ├── parse_* helpers ──► typed values │ ├── store::find_nbd_root() ──► PathBuf ├── store::read_ticket() ──► Ticket ├── store::list_tickets() ──► Vec ├── store::write_ticket() ──► () │ └── display::print_ticket() ──► stdout display::print_list() ──► stdout ``` ## Storage Layout ``` / └── .nbd/ └── tickets/ ├── a3f9c2.json ├── b7d41e.json └── ... ``` Each file is a pretty-printed JSON object with the fields of `Ticket`. File name equals the ticket's `id` field with a `.json` extension. ## Testing Strategy - **Unit tests** (`src/tests.rs`): exercise `ticket.rs`, `store.rs`, and `display.rs` in isolation using `tempfile::TempDir` for any file I/O. - **Integration tests** (`tests/integration.rs`): drive full command flows (create → read → list → update) through `cmd_*` functions against a temporary directory. Include a directory-traversal test that invokes commands from a nested subdirectory.