Skip to main content

tix/cli/
cli_time.rs

1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4/// The `tix time` subcommands (mission `per-issue-time-tracking-01M3C9A4`, WP04).
5///
6/// A thin adapter: the entry model and its validation, the mutation service and its
7/// append-only History, and every filter, total and finding live under
8/// `crate::time_tracking`. This enum owns only the spelling of the surface — the
9/// duration is whole minutes, a date is a calendar `YYYY-MM-DD`, and there is no flag
10/// here that could express a timer, a timezone, an overlap rule or a billing unit
11/// (C-002/C-009).
12#[derive(Debug, Clone, Subcommand)]
13pub enum TimeCommand {
14    /// Record effort against an issue, in whole minutes
15    ///
16    /// The date is optional and is a calendar date, never an instant: an entry without
17    /// one is real, and a period-bounded read reports it as undated rather than as zero.
18    Add {
19        /// Issue number, filename fragment or unique title fragment
20        #[arg(value_name = "ISSUE")]
21        issue: String,
22        /// The effort, in whole minutes at or above zero
23        ///
24        /// A negative value is accepted by the parser and refused by the shared entry
25        /// validator with its field name, which is a better message than clap's
26        /// "unexpected argument".
27        #[arg(long, value_name = "N", allow_hyphen_values = true)]
28        minutes: i64,
29        /// The calendar date the effort belongs to, `YYYY-MM-DD`
30        #[arg(long, value_name = "YYYY-MM-DD")]
31        date: Option<String>,
32        /// Who did the work: a handle (`ada`, `@ada`) or a `Name <email>` identity
33        #[arg(long, value_name = "IDENTITY")]
34        author: String,
35        /// Free-text context, preserved verbatim
36        #[arg(long, value_name = "TEXT")]
37        note: Option<String>,
38    },
39    /// Correct one entry, naming only the fields that change
40    ///
41    /// A field the command does not name is left exactly as it was, including the entry's
42    /// date, note and any metadata another build attached. `--clear-date` and
43    /// `--clear-note` are how an optional value is removed (an empty `--note ""` would be
44    /// a value, not an absence). A correction requires `--reason`: the prior value stays
45    /// in History, and the reason is what makes the change attributable.
46    Edit {
47        /// Issue number, filename fragment or unique title fragment
48        #[arg(value_name = "ISSUE")]
49        issue: String,
50        /// The stable id of the entry to correct (`tix time show <ISSUE>` lists them)
51        #[arg(long, value_name = "ID")]
52        entry: String,
53        #[command(flatten)]
54        change: TimeEditArgs,
55    },
56    /// Remove one entry from the issue's current entries
57    ///
58    /// The entry leaves the frontmatter; its value and its event stay in History, so a
59    /// deletion is auditable rather than a rewrite. Requires `--reason` and `--confirm`.
60    Delete {
61        /// Issue number, filename fragment or unique title fragment
62        #[arg(value_name = "ISSUE")]
63        issue: String,
64        /// The stable id of the entry to remove
65        #[arg(long, value_name = "ID")]
66        entry: String,
67        /// Why the entry is being removed; recorded in History
68        #[arg(long, value_name = "TEXT")]
69        reason: Option<String>,
70        /// Confirm the removal without prompting
71        #[arg(long)]
72        confirm: bool,
73    },
74    /// Show an issue's current entries and its total
75    Show {
76        /// Issue number, filename fragment or unique title fragment
77        #[arg(value_name = "ISSUE")]
78        issue: String,
79    },
80    /// Show what History retains for an issue: current entries beside its audit
81    History {
82        /// Issue number, filename fragment or unique title fragment
83        #[arg(value_name = "ISSUE")]
84        issue: String,
85    },
86    /// Total recorded effort, by issue, assignee and calendar period
87    ///
88    /// Every value comes from the shared calculation; a scope with nothing to total and a
89    /// scope whose entries carry no usable duration print `-` with their state, never a
90    /// zero. Deleted entries are excluded unless `--audit` asks for them.
91    Total {
92        /// Issue number, filename fragment or unique title fragment (alternative to `--issue`)
93        #[arg(value_name = "ISSUE")]
94        selector: Option<String>,
95        #[command(flatten)]
96        filter: TimeTotalArgs,
97    },
98}
99
100/// The fields an entry correction can name (T020).
101///
102/// A patch, not a replacement: a flag that is absent leaves its field alone, which is why
103/// removal is spelled `--clear-date`/`--clear-note` rather than an empty value. The reason
104/// is required — by the service, which refuses a missing one by name — and is what the
105/// History event keeps once the prior value is replaced.
106#[derive(Debug, Clone, Args)]
107pub struct TimeEditArgs {
108    /// Replacement duration, in whole minutes
109    #[arg(long, value_name = "N", allow_hyphen_values = true)]
110    pub minutes: Option<i64>,
111    /// Replacement calendar date, `YYYY-MM-DD`
112    #[arg(long, value_name = "YYYY-MM-DD", conflicts_with = "clear_date")]
113    pub date: Option<String>,
114    /// Remove the entry's date
115    #[arg(long)]
116    pub clear_date: bool,
117    /// Replacement note
118    #[arg(long, value_name = "TEXT", conflicts_with = "clear_note")]
119    pub note: Option<String>,
120    /// Remove the entry's note
121    #[arg(long)]
122    pub clear_note: bool,
123    /// Replacement author identity
124    #[arg(long, value_name = "IDENTITY")]
125    pub author: Option<String>,
126    /// Why the entry is being corrected; recorded in History, and required
127    #[arg(long, value_name = "TEXT")]
128    pub reason: Option<String>,
129}
130
131/// The read-only scope a `tix time total` is evaluated under (T021).
132///
133/// Every criterion is optional and every named one must match, and the period is
134/// inclusive. The dates are the caller's: this surface never resolves a `today`, so a
135/// total means exactly the calendar window it was asked for.
136#[derive(Debug, Clone, Args)]
137pub struct TimeTotalArgs {
138    /// Stable issue number to scope the total to
139    #[arg(long, value_name = "N")]
140    pub issue: Option<u32>,
141    /// The identity that did the work (an entry's author)
142    #[arg(long, value_name = "IDENTITY")]
143    pub assignee: Option<String>,
144    /// Inclusive period start, `YYYY-MM-DD`
145    #[arg(long, value_name = "YYYY-MM-DD")]
146    pub from: Option<String>,
147    /// Inclusive period end, `YYYY-MM-DD`
148    #[arg(long, value_name = "YYYY-MM-DD")]
149    pub to: Option<String>,
150    /// Also report the entries deletions left in History (never added to the total)
151    #[arg(long)]
152    pub audit: bool,
153    /// Break the total down: `issue`, `assignee` or `date`
154    #[arg(long, value_name = "DIMENSION")]
155    pub by: Option<String>,
156}