Skip to main content

tix/model/
refs.rs

1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4/// A single issue comment, parsed from the `## Comments` body section.
5///
6/// Mirrors `contracts/comments-format.md`: each comment is a level-3 heading
7/// `### <author> (<YYYY-MM-DD>)` followed by free text up to the next `###`
8/// head or end of file. Blank lines inside `text` are preserved verbatim
9/// (NFR-005); the model never normalizes hand-authored content.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Comment {
12    pub author: String,
13    pub date: String,
14    pub text: String,
15}
16
17/// The recurrence metadata an issue carries (`series:`), mission
18/// `recurring-issues-01M3C7G7`.
19///
20/// The current-health half of the `health:` frontmatter key (mission
21/// `project-issue-health-status-01M3C8HX`, WP02 T007).
22///
23/// Every field is the file's own spelling and every field is optional, so a partial
24/// or hand-written block round-trips as written; the health layer interprets it.
25#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(default, deny_unknown_fields)]
27pub struct HealthRef {
28    /// The current state, e.g. `on_track`.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub state: Option<String>,
31    /// How it was obtained: `current`, `derived`, or `override`.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub source: Option<String>,
34    /// The policy version the value was derived under, when it was derived.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub policy_version: Option<String>,
37    /// When the value was last updated, as the event recorded it.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub updated_at: Option<String>,
40}
41
42/// One table, two roles, told apart by which fields are set โ€” the definition
43/// carries `rule`, an occurrence carries `key`/`date`. A `role:` flag would be a
44/// second source of truth a writer could disagree with, so there is none:
45/// [`crate::recurrence::series::SeriesRef::role`] derives it.
46///
47/// Identity is (`id`, `date`) and never the issue number, the title, or the
48/// filename (C-002): those are display values that move, and an occurrence that
49/// moved with them would be generated twice.
50#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct SeriesRef {
53    /// The stable series id. Required in both roles.
54    pub id: String,
55    /// The series' display name. Mutable label, never identity; on the
56    /// definition, and copied onto an occurrence for readability.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub name: Option<String>,
59    /// The canonical local date key (`YYYY-MM-DD`) an occurrence was generated
60    /// for. Set on occurrences only.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub date: Option<String>,
63    /// The occurrence's idempotency key (`<series-id>@<YYYY-MM-DD>`). Stored
64    /// rather than re-derived, so a file whose key the current rule would no
65    /// longer produce is still readable โ€” and still a no-op (C-006).
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub key: Option<String>,
68    /// The schedule, as the file spells it. Set on the definition only.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub rule: Option<SeriesRule>,
71    /// The lifecycle state. Set on the definition only.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub state: Option<crate::recurrence::series::SeriesLifecycle>,
74    /// The generation cursor: the last date the generator considered. Set on the
75    /// definition only.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub next_cursor: Option<String>,
78    /// Occurrence dates the user explicitly skipped, so generation never
79    /// recreates them (C-006). Set on the definition only.
80    #[serde(default, skip_serializing_if = "Vec::is_empty")]
81    pub skipped: Vec<String>,
82}
83
84/// The rule half of `series:`, as the file spells it (`data-model.md` ยง2).
85///
86/// Typed for the fields the contract names and **flat strings** for everything
87/// else, so a cadence key this build does not know round-trips verbatim in
88/// `extensions` instead of being dropped on the next edit. The typed rule with its
89/// validation and its arithmetic is
90/// [`crate::recurrence::rule::RecurrenceRule`], built from this by
91/// [`crate::recurrence::series::Series::from_ref`] โ€” the split exists so the issue
92/// model stays `Eq` (the typed rule's preserved extensions hold floats, which have
93/// no `Eq`) while the file keeps every key it was given.
94#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
95pub struct SeriesRule {
96    /// The family: `daily`, `weekly`, `monthly`, `interval`, `cron-like`.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub kind: Option<String>,
99    /// The cadence, where the family takes one.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub interval: Option<u32>,
102    /// The schedule origin, a canonical `YYYY-MM-DD` date.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub anchor: Option<String>,
105    /// The timezone policy, e.g. `+02:00` or a named zone (refused by validation).
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub timezone: Option<String>,
108    /// The cron-like expression, behind the reviewed parser gate.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub expression: Option<String>,
111    /// The unit an `interval` rule counts in: `days`, `weeks`, `months`.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub unit: Option<String>,
114    /// Preserved unknown keys, exactly as they were written.
115    #[serde(default, flatten)]
116    pub extensions: BTreeMap<String, String>,
117}