tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

/// The `tix time` subcommands (mission `per-issue-time-tracking-01M3C9A4`, WP04).
///
/// A thin adapter: the entry model and its validation, the mutation service and its
/// append-only History, and every filter, total and finding live under
/// `crate::time_tracking`. This enum owns only the spelling of the surface — the
/// duration is whole minutes, a date is a calendar `YYYY-MM-DD`, and there is no flag
/// here that could express a timer, a timezone, an overlap rule or a billing unit
/// (C-002/C-009).
#[derive(Debug, Clone, Subcommand)]
pub enum TimeCommand {
    /// Record effort against an issue, in whole minutes
    ///
    /// The date is optional and is a calendar date, never an instant: an entry without
    /// one is real, and a period-bounded read reports it as undated rather than as zero.
    Add {
        /// Issue number, filename fragment or unique title fragment
        #[arg(value_name = "ISSUE")]
        issue: String,
        /// The effort, in whole minutes at or above zero
        ///
        /// A negative value is accepted by the parser and refused by the shared entry
        /// validator with its field name, which is a better message than clap's
        /// "unexpected argument".
        #[arg(long, value_name = "N", allow_hyphen_values = true)]
        minutes: i64,
        /// The calendar date the effort belongs to, `YYYY-MM-DD`
        #[arg(long, value_name = "YYYY-MM-DD")]
        date: Option<String>,
        /// Who did the work: a handle (`ada`, `@ada`) or a `Name <email>` identity
        #[arg(long, value_name = "IDENTITY")]
        author: String,
        /// Free-text context, preserved verbatim
        #[arg(long, value_name = "TEXT")]
        note: Option<String>,
    },
    /// Correct one entry, naming only the fields that change
    ///
    /// A field the command does not name is left exactly as it was, including the entry's
    /// date, note and any metadata another build attached. `--clear-date` and
    /// `--clear-note` are how an optional value is removed (an empty `--note ""` would be
    /// a value, not an absence). A correction requires `--reason`: the prior value stays
    /// in History, and the reason is what makes the change attributable.
    Edit {
        /// Issue number, filename fragment or unique title fragment
        #[arg(value_name = "ISSUE")]
        issue: String,
        /// The stable id of the entry to correct (`tix time show <ISSUE>` lists them)
        #[arg(long, value_name = "ID")]
        entry: String,
        #[command(flatten)]
        change: TimeEditArgs,
    },
    /// Remove one entry from the issue's current entries
    ///
    /// The entry leaves the frontmatter; its value and its event stay in History, so a
    /// deletion is auditable rather than a rewrite. Requires `--reason` and `--confirm`.
    Delete {
        /// Issue number, filename fragment or unique title fragment
        #[arg(value_name = "ISSUE")]
        issue: String,
        /// The stable id of the entry to remove
        #[arg(long, value_name = "ID")]
        entry: String,
        /// Why the entry is being removed; recorded in History
        #[arg(long, value_name = "TEXT")]
        reason: Option<String>,
        /// Confirm the removal without prompting
        #[arg(long)]
        confirm: bool,
    },
    /// Show an issue's current entries and its total
    Show {
        /// Issue number, filename fragment or unique title fragment
        #[arg(value_name = "ISSUE")]
        issue: String,
    },
    /// Show what History retains for an issue: current entries beside its audit
    History {
        /// Issue number, filename fragment or unique title fragment
        #[arg(value_name = "ISSUE")]
        issue: String,
    },
    /// Total recorded effort, by issue, assignee and calendar period
    ///
    /// Every value comes from the shared calculation; a scope with nothing to total and a
    /// scope whose entries carry no usable duration print `-` with their state, never a
    /// zero. Deleted entries are excluded unless `--audit` asks for them.
    Total {
        /// Issue number, filename fragment or unique title fragment (alternative to `--issue`)
        #[arg(value_name = "ISSUE")]
        selector: Option<String>,
        #[command(flatten)]
        filter: TimeTotalArgs,
    },
}

/// The fields an entry correction can name (T020).
///
/// A patch, not a replacement: a flag that is absent leaves its field alone, which is why
/// removal is spelled `--clear-date`/`--clear-note` rather than an empty value. The reason
/// is required — by the service, which refuses a missing one by name — and is what the
/// History event keeps once the prior value is replaced.
#[derive(Debug, Clone, Args)]
pub struct TimeEditArgs {
    /// Replacement duration, in whole minutes
    #[arg(long, value_name = "N", allow_hyphen_values = true)]
    pub minutes: Option<i64>,
    /// Replacement calendar date, `YYYY-MM-DD`
    #[arg(long, value_name = "YYYY-MM-DD", conflicts_with = "clear_date")]
    pub date: Option<String>,
    /// Remove the entry's date
    #[arg(long)]
    pub clear_date: bool,
    /// Replacement note
    #[arg(long, value_name = "TEXT", conflicts_with = "clear_note")]
    pub note: Option<String>,
    /// Remove the entry's note
    #[arg(long)]
    pub clear_note: bool,
    /// Replacement author identity
    #[arg(long, value_name = "IDENTITY")]
    pub author: Option<String>,
    /// Why the entry is being corrected; recorded in History, and required
    #[arg(long, value_name = "TEXT")]
    pub reason: Option<String>,
}

/// The read-only scope a `tix time total` is evaluated under (T021).
///
/// Every criterion is optional and every named one must match, and the period is
/// inclusive. The dates are the caller's: this surface never resolves a `today`, so a
/// total means exactly the calendar window it was asked for.
#[derive(Debug, Clone, Args)]
pub struct TimeTotalArgs {
    /// Stable issue number to scope the total to
    #[arg(long, value_name = "N")]
    pub issue: Option<u32>,
    /// The identity that did the work (an entry's author)
    #[arg(long, value_name = "IDENTITY")]
    pub assignee: Option<String>,
    /// Inclusive period start, `YYYY-MM-DD`
    #[arg(long, value_name = "YYYY-MM-DD")]
    pub from: Option<String>,
    /// Inclusive period end, `YYYY-MM-DD`
    #[arg(long, value_name = "YYYY-MM-DD")]
    pub to: Option<String>,
    /// Also report the entries deletions left in History (never added to the total)
    #[arg(long)]
    pub audit: bool,
    /// Break the total down: `issue`, `assignee` or `date`
    #[arg(long, value_name = "DIMENSION")]
    pub by: Option<String>,
}