From 8f4d25b1413f51d0825fe7dd5318609400705448 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Mon, 23 Feb 2026 13:11:54 -0800 Subject: [PATCH] feat(nbd): render tickets as TOML frontmatter markdown [4036aa] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the key-value table format with TOML frontmatter + body when printing tickets to stdout (nbd read, nbd create, nbd archive, nbd next — all non-JSON paths). The --json output is unchanged. New format: +++ id = "a3f9c2" title = "Fix login bug" priority = 8 status = "in_progress" ticket_type = "bug" dependencies = ["b7d41e"] +++ Body text here. Changes: - display.rs: add DisplayFrontmatter struct, rewrite format_ticket using toml::to_string with id prepended as first frontmatter key - tests.rs: update format_ticket_joins_dependencies and format_ticket_empty_dependencies for the new format - integration.rs: update TestEnv::create to use --json for reliable ID extraction instead of parsing the key-value text format Co-Authored-By: Claude Sonnet 4.6 --- nbd/src/display.rs | 69 ++++++++++++++++++++++++---------------- nbd/src/tests.rs | 17 ++++++---- nbd/tests/integration.rs | 15 +++++---- 3 files changed, 60 insertions(+), 41 deletions(-) diff --git a/nbd/src/display.rs b/nbd/src/display.rs index 7d52fae..4521598 100644 --- a/nbd/src/display.rs +++ b/nbd/src/display.rs @@ -9,6 +9,8 @@ use std::collections::HashSet; +use serde::Serialize; + use crate::graph::TicketGraph; use crate::store::MigrateReport; use crate::ticket::{Status, Ticket, TicketType}; @@ -23,15 +25,29 @@ const COL_PRI: usize = 5; const COL_TYPE: usize = 9; /// Width of the status column ("in_progress" = 11 chars, +2 padding). const COL_STATUS: usize = 13; -/// Width of each label in the full ticket view ("Dependencies:" = 13 + 1 space). +/// Width of each label in the diff view ("dependencies:" = 13 + 1 space). const LABEL_WIDTH: usize = 14; // ── Internal helpers ────────────────────────────────────────────────────────── +/// Ticket metadata serialised into the TOML frontmatter block for display. +/// +/// Mirrors the on-disk `MarkdownFrontmatter` in `store.rs` but adds `id` as +/// the first field so that human-readable output is self-contained. +#[derive(Serialize)] +struct DisplayFrontmatter<'a> { + id: &'a str, + title: &'a str, + priority: u8, + status: &'a Status, + ticket_type: &'a TicketType, + dependencies: &'a [String], +} + /// Return the canonical display string for a [`Status`] variant. /// /// The strings match the serde serialisation: `"todo"`, `"in_progress"`, -/// `"done"`. +/// `"done"`, etc. fn status_str(status: &Status) -> &'static str { match status { Status::Todo => "todo", @@ -58,37 +74,34 @@ fn ticket_type_str(ticket_type: &TicketType) -> &'static str { // ── Public formatting functions ─────────────────────────────────────────────── -/// Format a single ticket as a human-readable key–value table. +/// Format a single ticket as a TOML-frontmatter markdown document. /// -/// Each field is displayed on its own line with a label padded to -/// [`LABEL_WIDTH`] characters. An empty `Dependencies` field is rendered as -/// an empty string rather than being omitted. +/// The output mirrors the `.md` file format used on disk, with `id` added as +/// the first frontmatter key so the output is self-contained. The ticket body +/// follows the closing `+++` delimiter. /// /// ```text -/// ID: a3f9c2 -/// Title: Fix login bug -/// Body: Users cannot log in with email addresses containing + -/// Priority: 8 -/// Status: in_progress -/// Type: bug -/// Dependencies: b7d41e, c9e823 +/// +++ +/// id = "a3f9c2" +/// title = "Fix login bug" +/// priority = 8 +/// status = "in_progress" +/// ticket_type = "bug" +/// dependencies = ["b7d41e", "c9e823"] +/// +++ +/// Users cannot log in with email addresses containing + /// ``` pub fn format_ticket(ticket: &Ticket) -> String { - let deps = ticket.dependencies.join(", "); - [ - format!("{: String { let mut args = vec!["create"]; args.extend_from_slice(extra_args); + args.push("--json"); let output = self.run(&args); assert!( output.status.success(), @@ -54,11 +55,11 @@ impl TestEnv { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).unwrap(); - stdout - .lines() - .find(|l| l.trim_start().starts_with("ID:")) - .and_then(|l| l.split_whitespace().last()) - .expect("could not extract ID from create output") + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("create --json output should be valid JSON"); + parsed["id"] + .as_str() + .expect("create --json output should have 'id' field") .to_string() } }