tix-tracker 0.11.0

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

/// A single issue comment, parsed from the `## Comments` body section.
///
/// Mirrors `contracts/comments-format.md`: each comment is a level-3 heading
/// `### <author> (<YYYY-MM-DD>)` followed by free text up to the next `###`
/// head or end of file. Blank lines inside `text` are preserved verbatim
/// (NFR-005); the model never normalizes hand-authored content.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Comment {
    pub author: String,
    pub date: String,
    pub text: String,
}

/// The recurrence metadata an issue carries (`series:`), mission
/// `recurring-issues-01M3C7G7`.
///
/// The current-health half of the `health:` frontmatter key (mission
/// `project-issue-health-status-01M3C8HX`, WP02 T007).
///
/// Every field is the file's own spelling and every field is optional, so a partial
/// or hand-written block round-trips as written; the health layer interprets it.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct HealthRef {
    /// The current state, e.g. `on_track`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state: Option<String>,
    /// How it was obtained: `current`, `derived`, or `override`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
    /// The policy version the value was derived under, when it was derived.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub policy_version: Option<String>,
    /// When the value was last updated, as the event recorded it.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub updated_at: Option<String>,
}

/// One table, two roles, told apart by which fields are set โ€” the definition
/// carries `rule`, an occurrence carries `key`/`date`. A `role:` flag would be a
/// second source of truth a writer could disagree with, so there is none:
/// [`crate::recurrence::series::SeriesRef::role`] derives it.
///
/// Identity is (`id`, `date`) and never the issue number, the title, or the
/// filename (C-002): those are display values that move, and an occurrence that
/// moved with them would be generated twice.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SeriesRef {
    /// The stable series id. Required in both roles.
    pub id: String,
    /// The series' display name. Mutable label, never identity; on the
    /// definition, and copied onto an occurrence for readability.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// The canonical local date key (`YYYY-MM-DD`) an occurrence was generated
    /// for. Set on occurrences only.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub date: Option<String>,
    /// The occurrence's idempotency key (`<series-id>@<YYYY-MM-DD>`). Stored
    /// rather than re-derived, so a file whose key the current rule would no
    /// longer produce is still readable โ€” and still a no-op (C-006).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub key: Option<String>,
    /// The schedule, as the file spells it. Set on the definition only.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rule: Option<SeriesRule>,
    /// The lifecycle state. Set on the definition only.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub state: Option<crate::recurrence::series::SeriesLifecycle>,
    /// The generation cursor: the last date the generator considered. Set on the
    /// definition only.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next_cursor: Option<String>,
    /// Occurrence dates the user explicitly skipped, so generation never
    /// recreates them (C-006). Set on the definition only.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub skipped: Vec<String>,
}

/// The rule half of `series:`, as the file spells it (`data-model.md` ยง2).
///
/// Typed for the fields the contract names and **flat strings** for everything
/// else, so a cadence key this build does not know round-trips verbatim in
/// `extensions` instead of being dropped on the next edit. The typed rule with its
/// validation and its arithmetic is
/// [`crate::recurrence::rule::RecurrenceRule`], built from this by
/// [`crate::recurrence::series::Series::from_ref`] โ€” the split exists so the issue
/// model stays `Eq` (the typed rule's preserved extensions hold floats, which have
/// no `Eq`) while the file keeps every key it was given.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeriesRule {
    /// The family: `daily`, `weekly`, `monthly`, `interval`, `cron-like`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub kind: Option<String>,
    /// The cadence, where the family takes one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub interval: Option<u32>,
    /// The schedule origin, a canonical `YYYY-MM-DD` date.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub anchor: Option<String>,
    /// The timezone policy, e.g. `+02:00` or a named zone (refused by validation).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timezone: Option<String>,
    /// The cron-like expression, behind the reviewed parser gate.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expression: Option<String>,
    /// The unit an `interval` rule counts in: `days`, `weeks`, `months`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub unit: Option<String>,
    /// Preserved unknown keys, exactly as they were written.
    #[serde(default, flatten)]
    pub extensions: BTreeMap<String, String>,
}