You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vibed/nbd/docs/ARCHITECTURE.md

4.2 KiB

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 010 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<T> is aliased to Result<T, Box<dyn Error + Send + Sync>>, allowing ? to propagate both io::Error and serde_json::Error without explicit wrapping.

display.rs — Output formatting

Two output modes:

  • Tabular — human-readable keyvalue 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<Ticket>
      ├── store::write_ticket()      ──►  ()
      │
      └── display::print_ticket()   ──►  stdout
          display::print_list()     ──►  stdout

Storage Layout

<project-root>/
└── .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.