1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
//! `tix time`: the CLI adapter over the shared time service and aggregation
//! (mission `per-issue-time-tracking-01M3C9A4`, WP04, T019–T023).
//!
//! Parse, call, print. Nothing here sums a minute, validates an entry, resolves a
//! period, or reads a `## History` grammar. The mutations are
//! [`crate::time_tracking::service::apply`]'s — the one seam that resolves the issue,
//! validates the intent, stages the projection and the single event, and writes once —
//! and everything the read verbs print is a field of
//! [`crate::time_tracking::aggregate::TimeSnapshot`]'s DTOs or of the shared rendering
//! beside them ([`TimeFilter::describe`], [`TimeGroup::label`],
//! [`TimeTotalState::as_str`], [`TimeDiagnostic::render`]). That is C-006's one-DTO
//! rule and NFR-003's parity claim: a number the CLI prints is a number the shared
//! calculation held, so the CLI, the TUI, MCP and the portable mapping cannot word one
//! total four ways (T021).
//!
//! # The commands
//!
//! * `tix time add <ISSUE> --minutes <N> --author <IDENTITY> [--date YYYY-MM-DD]
//! [--note <TEXT>]` — one create, through [`apply`].
//! * `tix time edit <ISSUE> --entry <ID> [--minutes <N>] [--date <D> | --clear-date]
//! [--note <TEXT> | --clear-note] [--author <IDENTITY>] --reason <TEXT>` — one
//! correction, naming only the fields it replaces.
//! * `tix time delete <ISSUE> --entry <ID> --reason <TEXT> --confirm` — one deletion,
//! from the current projection only.
//! * `tix time show <ISSUE>` — the issue's current entries and its total.
//! * `tix time history <ISSUE>` — the audit view: current entries beside what History
//! retains.
//! * `tix time total [ISSUE] [--issue <N>] [--assignee <IDENTITY>] [--from <D>]
//! [--to <D>] [--audit] [--by <DIMENSION>]` — the scope's total and, opt-in, its
//! breakdown by issue, identity or calendar date.
//!
//! # The caller supplies the dates
//!
//! There is no `today` here and no default period. Every date this surface knows is one
//! the caller typed, either as an entry's `--date` or as a period bound; the only other
//! instant in a mutation is the crate's own clock inside [`apply`], which the caller may
//! not move and this surface never reads (C-002, C-005).
//!
//! # `--confirm`, the reason, and what a refusal leaves behind
//!
//! `delete` is the one destructive verb — it removes an entry from the issue's current
//! projection — so it is the one that requires `--confirm`, exactly as the contract
//! spells (`contracts/cli-surface.md`) and as `tix view delete` and the cycle-schedule
//! lifecycle verbs require it. An unconfirmed delete is refused before the store is even
//! read.
//!
//! A correction and a deletion also require `--reason`: the reason is what the audit
//! trail keeps once the prior value is replaced or removed, and it is *not* re-checked
//! here — [`apply`] refuses a missing or blank reason with `missing_reason`, so the
//! requirement has one owner rather than two. Every refusal this file can raise (bad
//! arguments are `clap`'s) reaches the caller as a non-zero exit carrying a reason and a
//! remedy, and none of them writes anything: the service refuses before its write seam,
//! and this file's own refusals — the unconfirmed delete, the two ways to name one
//! issue — are raised before any write is attempted.
//!
//! # An absent value is `-`, never `0`
//!
//! `nothing` (the scope selected no entry) and `unknown` (it selected entries and none
//! carried a usable duration) are different facts, and both are visibly different from a
//! recorded `0`: the total's [`TimeTotalState`] is printed by name beside a
//! `total_minutes` of `-`. The same convention renders an entry's absent minutes, date,
//! note or author in `show`'s table **and** on a mutation's line (`values`), so a
//! malformed entry reads as absent rather than as zero everywhere this surface prints it,
//! and is explained by its own finding on stderr (C-005). That is deliberately not
//! WP02's `entry_summary`, whose `?m` marks the value slot of a `## History` detail line
//! — evidence, not user-facing text.
//!
//! # stdout is the result, stderr is the diagnostic
//!
//! Values go to stdout, one `key: value` line per field (the shape `tix capacity show`
//! uses) or one tab-separated row per entry, because that is what a script reads.
//! Findings render through [`TimeDiagnostic::render`] to stderr, exactly as
//! `tix capacity` renders its warnings: a finding interleaved into the value lines would
//! be a parse error rather than a warning. A finding about an entry means the total is
//! **partial**, so it always renders; the scope's own findings tell the whole selection's
//! story, so a breakdown does not repeat them (the groups partition one selection).
//!
//! # A refused scope is a refusal, not a total
//!
//! An invalid filter and an issue the snapshot does not hold are the two states where
//! no total exists at all, and they are answered as a refusal rather than as a `-`
//! total: "unknown" and "nothing" are answers, but "your `--from` is not a date" is a
//! request to fix the request. The refusal exits `1` — the frozen exit-code table's
//! `usage/config` bucket, which is where [`TixError::Refused`] and
//! [`TixError::InvalidInput`] land (`exit_code_for`'s catch-all) — and the mission
//! requires a non-zero exit with a reason and a remedy, not a particular code.
//!
//! One refusal is this family's own rather than the aggregation's: a selector that names
//! an **archived** issue, which `total`'s active-issue enumeration cannot answer for
//! while `show` and `history` can (see `selected_number`). The MCP's `time_total` reads
//! the same project total over the same active enumeration, so it renders that refusal
//! through the same [`archived_refusal`] rather than inventing its own answer (C-006).
//!
//! [`apply`]: crate::time_tracking::service::apply
//! [`TimeTotalState`]: crate::time_tracking::aggregate::TimeTotalState
//! [`TimeGroup::label`]: crate::time_tracking::aggregate::TimeGroup::label
//! [`TimeFilter::describe`]: crate::time_tracking::filters::TimeFilter::describe
use crate;
use crateDiagnostic;
use crate;
use crateIssue;
use crate;
use crate;
use crateTimeDiagnostic;
use crateTimeEntry;
use crateTimeFilter;
use crate;
/// Route one `tix time` subcommand.
///
/// The configuration is deliberately not taken: nothing in this family reads a
/// registry — the section, the flags and the arithmetic all live behind the service and
/// the snapshot — and a parameter a command does not use is a claim it does.
// ---------------------------------------------------------------------------
// T020 — the mutations
// ---------------------------------------------------------------------------
/// `tix time add`: one create, through the shared service.
///
/// The date, the note and the identity are carried as the caller typed them — an
/// unreadable date is refused by the one entry validator, not by a second one here —
/// and the entry's id is the service's to mint.
/// `tix time edit`: one correction, naming only the fields the caller replaced.
///
/// The patch is built from exactly the flags that were given — an edit that names no
/// field stays an empty edit and is refused by the service with `empty_edit`, rather
/// than here — and the reason rides on the request, where the service requires it.
/// The one sentence an unconfirmed deletion is refused with.
///
/// `pub(crate)` because the MCP surface refuses the same call in the same words: the
/// sentence is a refusal both surfaces owe the caller, so one owner is what keeps them
/// from wording it twice (C-006). The CLI reaches it through `delete`'s `--confirm`
/// gate and the MCP tool through its `confirm` argument.
pub const TIME_DELETE_CONFIRMATION: &str = "deleting a time entry needs --confirm: it removes the entry from the issue's \
current entries, while its value and its event stay in History (`tix time history` \
reads them back)";
/// `tix time delete`: remove one entry from the current projection.
///
/// The confirmation is checked **first**, so an unconfirmed deletion does not even read
/// the issue; the value and its event stay in History, where `tix time history` reads
/// them back.
// ---------------------------------------------------------------------------
// T020/T022 — show and history
// ---------------------------------------------------------------------------
/// `tix time show <ISSUE>`: the issue's current entries and its total.
///
/// The issue is resolved by the crate's own selector grammar and then indexed by itself,
/// so an archived or otherwise off-view issue is still readable by the selector that
/// names it. Every current entry is listed, malformed ones included — a malformed entry
/// is *reported*, never hidden, and never blocks the read (C-005) — with each absent
/// value as `-`, and the total's `state` says whether the answer is `recorded`,
/// `nothing` (no entry) or `unknown` (entries, none usable).
/// `tix time history <ISSUE>`: the explicit audit view for one issue (T017, FR-005).
///
/// Current entries beside what History retains: how many entries the issue holds now,
/// and every prior value a deletion left behind, with the entry it named and who recorded
/// it when. Corrections are counted rather than re-rendered: the prior values live in the
/// `## History` line the mutation wrote, and re-parsing that grammar here would make this
/// file a second owner of a grammar WP02 already owns — so the count says a correction is
/// auditable and the file stays the one place its text is read.
// ---------------------------------------------------------------------------
// T021 — the totals, with the filters and the breakdown
// ---------------------------------------------------------------------------
/// `tix time total`: the scope's shared total, and optionally its breakdown.
///
/// The scope is the caller's: an issue (either spelling, never both), an identity, an
/// inclusive period, or any conjunction of them — all of it a [`TimeFilter`], which is
/// also what every total's `scope` line renders. The scope's own total is printed first
/// (so an empty breakdown still reads as the answer it is: `nothing`), then one block per
/// group when `--by` names a dimension. The whole read is one [`TimeSnapshot`] over the
/// project's **active** issues — the same enumeration `tix list` and `tix capacity`
/// use — so no two blocks can count a different membership (NFR-001). An explicit
/// selector that names an archived issue is refused rather than silently dropped, since
/// the two halves of that read would otherwise disagree (see [`selected_number`]).
/// The stable number a positional selector names, refusing one the project scope cannot
/// answer for.
///
/// The selector resolves against **every** issue the store holds — an archived one
/// included — while [`total`]'s snapshot is the active enumeration. Those two halves
/// disagree for exactly one case, and it is refused here rather than reported as the
/// shared `unknown_issue` finding: the issue is neither unknown (it exists, and holds
/// the entries the caller is asking about) nor absent from the snapshot by accident, and
/// that finding's remedy — "read that issue into the scope, or drop the selector" — asks
/// for something the caller has already done and cannot do again from here.
///
/// A selector naming nothing at all is *not* refused here: the store's own `not_found`
/// (exit 2) is what a selector that resolves to no record gets, exactly as it does for
/// every other `tix` command.
/// The stable number `--issue` names, refusing an archived issue the same way.
///
/// A number no issue answers to is left alone: the snapshot will not hold it, and the
/// aggregation reports that as its own `unknown_issue` finding. Only the archived case —
/// an issue the snapshot *could* have held but deliberately does not — is this surface's
/// to name. A genuine read failure is propagated rather than being read as "not
/// archived".
/// Refuse an archived issue a project total cannot answer for, with a followable remedy.
///
/// The condition is named as it is — the issue is archived — and the remedy names the two
/// verbs that do read a single issue whatever its view: `show` indexes the one issue a
/// selector resolves, and `history` reads that issue's audit view.
///
/// `kind: archived_issue` is a **command-level** name rather than a new member of
/// [`crate::time_tracking::diagnostics::TimeDiagnosticKind`]: that vocabulary is WP03's
/// file, and none of its kinds says "archived" (`unknown_issue` is the closest and would
/// be false here). It is spelled in the same `kind: <snake_case>` note the shared findings
/// use, so nothing downstream has to learn a second shape.
///
/// `pub(crate)` because the MCP's `time_total` resolves the same selector against the
/// same enumeration and owes the caller the same refusal: the condition is one, and so
/// is the sentence (C-006).
pub
/// The dimension a breakdown groups by (`--by`).
///
/// `pub(crate)` with [`parse_breakdown`] because the MCP's `time_total` groups by the
/// same three dimensions and refuses an unknown word with the same sentence: one
/// vocabulary, one parse (C-006).
pub
/// Parse `--by`. A well-formed but unknown word is a refusal naming the vocabulary, not a
/// guess — the shape [`crate::commands::capacity::parse_scope_kind`] uses. `all` is the
/// explicit spelling of "no breakdown": the scope's one ungrouped total, which is also
/// what the flag's absence means.
///
/// `pub(crate)` because the MCP's `time_total` takes the same `by` and owes the same
/// refusal for a word outside the vocabulary (C-006).
pub
// ---------------------------------------------------------------------------
// Shared rendering
// ---------------------------------------------------------------------------
/// Print one total's own fields, or refuse the scope it could not answer.
///
/// A `state` of `invalid_filter` or `unknown_issue` is a *refusal*, not an answer: no
/// entry was evaluated, so there is no total to print, and the findings say what to fix.
/// Otherwise the block is [`scope_block`]'s, printed as the one thing this line owns.
/// The one block a total prints, whichever surface prints it.
///
/// Every line is a DTO field or a DTO's own rendering: the group's label, the filter's
/// scope, the state's stable spelling, the integer-minute sum, the counts beside it, and
/// the audit context — which is `-` unless `--audit` asked for it, because an
/// unrequested count and a zero count are different facts. `pub(crate)` because the MCP's
/// time reads print the same block: a total cannot be worded two ways (C-006).
pub
/// The scope refusals as one [`TixError::Refused`], carrying [`refusal_blocks`]' bytes.
/// The scope refusals as one rendered block, with their remedies.
///
/// Rendered at **error** severity, because the crate's own rule is that the rendered
/// level and the exit code agree (#1361, `commands::lint`): a refused scope exits
/// non-zero — no entry was evaluated, so there is no total to read — while a finding
/// beside a total is a warning that exits zero. [`TimeDiagnostic::render`] is therefore
/// deliberately *not* used here; its `warning[...]` level would print a warning and exit
/// non-zero, which is exactly the drift the rule exists to prevent. Everything else is
/// shared: the kind and the context come from the DTO's own fields, and the `help:` footer
/// is WP03's own remedy string, so a refusal still says what is wrong and what to do about
/// it in the vocabulary one place declares.
///
/// `pub(crate)` because the MCP's time reads refuse the same two states with the same
/// block: the CLI hands these bytes to `TixError::Refused` (which carries them verbatim,
/// keeping the block from being wrapped in a second, non-rustc prefix at the error
/// boundary) and the MCP tool returns them as its error text (C-006).
pub
/// One refusal as a rustc-style error block.
///
/// The reason is the headline, the `kind:` note names the condition machine-readably, the
/// caller's notes carry whatever context it has, and the remedy is the `help:` footer —
/// the shape every refuse-or-explain value in the crate renders.
/// Render findings through the shared renderer, to stderr.
/// One side of a mutation line: an entry's own values, in the surface's own rendering.
///
/// Deliberately **not** WP02's `entry_summary`, which is the `## History` detail line's
/// value slot: there an absent duration must be marked `?m` (the slot has to hold
/// something, and a zero would be a lie), a missing author is `?`, and an absent date is
/// omitted. This line is user-facing and prints beside `show`'s table and the totals,
/// which mark an absent value `-`; one surface, one mark, and every line the same shape —
/// `120m on 2026-09-25 by alice: note`, and `- on - by -` for an entry the file left
/// empty. The minute unit is the documented precision both renderings share.
///
/// `None` is the untouched side of an edit, which no mutation line reaches with values to
/// print.
/// The one sentence a time mutation prints, from the service's own outcome.
///
/// The verb is the outcome's shape rather than a parameter: a create has no prior value,
/// a correction carries both sides, a deletion only the prior one, and the one no-op — a
/// request that asked for exactly what was already there, so no event was appended and
/// `updated` was not bumped — is `changed: false` and prints `Unchanged`.
///
/// `pub(crate)` because the MCP's mutation tools answer with this sentence as their own
/// text: a mutation cannot be worded two ways (C-006). The values are [`values`]' own
/// rendering, which is the user-facing one — an absent minute, date, author or note prints
/// as `-` and never as a zero — and deliberately not WP02's `entry_summary`, which marks
/// the value slot of a `## History` detail line.
pub
/// An absent number as `-`, the crate's convention for "no value" — and the reason a
/// zero total and a scope with nothing to total do not print the same bytes.
///
/// `pub(crate)` because the MCP's time tables print the same mark in the same cell
/// (C-006).
pub
/// One table cell: `-` when the value is absent or blank, and one line whatever the file
/// holds.
///
/// The collapse is the renderer's own guarantee, not the caller's: a hand-edited note
/// that is a YAML literal block round-trips a line break, and a table row that broke
/// across two lines would misread as a field nobody wrote. The finding beside the row
/// still reports the value as malformed, so nothing is hidden by rendering it on one line.
///
/// `pub(crate)` because the MCP's entry and audit tables collapse a cell the same way
/// (C-006).
pub