cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use super::category::StatusCategory;
use super::config::{StatusConfig, StatusesConfig};

fn s(name: &str) -> String {
    name.to_string()
}

/// The built-in issue preset names, in canonical order.
///
/// Decision records carry no presets — their workflow is hardcoded
/// per DDR-018QWJVHRH35B and walked through `DrStatus`.
pub const ISSUE_PRESETS: &[&str] = &["default", "scrum", "kanban"];

impl StatusesConfig {
    /// Return a built-in issue preset `StatusesConfig` by name.
    ///
    /// Returns `None` for an unknown name. Decision records have no presets
    /// — their workflow is hardcoded per DDR-018QWJVHRH35B.
    pub fn from_preset(name: &str) -> Option<Self> {
        match name {
            "default" => Some(Self::default_issue()),
            "scrum" => Some(Self::preset_issue_scrum()),
            "kanban" => Some(Self::preset_issue_kanban()),
            _ => None,
        }
    }

    /// Issue preset: `scrum` — to-do → in-progress → code-review → testing → done.
    pub fn preset_issue_scrum() -> Self {
        StatusesConfig {
            entries: vec![
                (
                    s("to-do"),
                    StatusConfig {
                        next: vec![s("in-progress"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Queued,
                    },
                ),
                (
                    s("in-progress"),
                    StatusConfig {
                        next: vec![s("to-do"), s("code-review"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Active,
                    },
                ),
                (
                    s("code-review"),
                    StatusConfig {
                        next: vec![s("in-progress"), s("testing"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Stalled,
                    },
                ),
                (
                    s("testing"),
                    StatusConfig {
                        next: vec![s("in-progress"), s("done"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Active,
                    },
                ),
                (
                    s("done"),
                    StatusConfig {
                        next: vec![s("to-do")],
                        active: false,
                        terminal: true,
                        label: None,
                        category: StatusCategory::Resolved,
                    },
                ),
                (
                    s("cancelled"),
                    StatusConfig {
                        next: vec![s("to-do")],
                        active: false,
                        terminal: true,
                        label: None,
                        category: StatusCategory::Cancelled,
                    },
                ),
            ],
            initial: s("to-do"),
        }
    }

    /// Issue preset: `kanban` — backlog → ready → in-progress → blocked → done.
    pub fn preset_issue_kanban() -> Self {
        StatusesConfig {
            entries: vec![
                (
                    s("backlog"),
                    StatusConfig {
                        next: vec![s("ready"), s("cancelled")],
                        active: false,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Queued,
                    },
                ),
                (
                    s("ready"),
                    StatusConfig {
                        next: vec![s("in-progress"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Queued,
                    },
                ),
                (
                    s("in-progress"),
                    StatusConfig {
                        next: vec![s("blocked"), s("done"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Active,
                    },
                ),
                (
                    s("blocked"),
                    StatusConfig {
                        next: vec![s("in-progress"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Stalled,
                    },
                ),
                (
                    s("done"),
                    StatusConfig {
                        next: vec![s("backlog")],
                        active: false,
                        terminal: true,
                        label: None,
                        category: StatusCategory::Resolved,
                    },
                ),
                (
                    s("cancelled"),
                    StatusConfig {
                        next: vec![s("backlog")],
                        active: false,
                        terminal: true,
                        label: None,
                        category: StatusCategory::Cancelled,
                    },
                ),
            ],
            initial: s("backlog"),
        }
    }

    /// Built-in default statuses for issues.
    pub fn default_issue() -> Self {
        StatusesConfig {
            entries: vec![
                (
                    s("open"),
                    StatusConfig {
                        next: vec![s("in-progress"), s("closed"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Queued,
                    },
                ),
                (
                    s("in-progress"),
                    StatusConfig {
                        next: vec![s("open"), s("closed"), s("cancelled")],
                        active: true,
                        terminal: false,
                        label: None,
                        category: StatusCategory::Active,
                    },
                ),
                (
                    s("closed"),
                    StatusConfig {
                        next: vec![s("open")],
                        active: false,
                        terminal: true,
                        label: None,
                        category: StatusCategory::Resolved,
                    },
                ),
                (
                    s("cancelled"),
                    StatusConfig {
                        next: vec![s("open")],
                        active: false,
                        terminal: true,
                        label: None,
                        category: StatusCategory::Cancelled,
                    },
                ),
            ],
            initial: s("open"),
        }
    }
}

#[cfg(test)]
pub mod strategy {
    use super::super::config::StatusesConfig;
    use proptest::prelude::*;

    /// Pick a name that resolves to one of the built-in issue presets.
    /// Decision records have no presets — their workflow is hardcoded.
    pub fn known_preset() -> impl Strategy<Value = &'static str> {
        prop_oneof![Just("default"), Just("scrum"), Just("kanban"),]
    }

    pub fn preset_config() -> impl Strategy<Value = StatusesConfig> {
        known_preset().prop_map(|name| {
            StatusesConfig::from_preset(name).expect("known preset always resolves")
        })
    }
}

#[cfg(test)]
mod tests {
    use super::super::config::StatusesConfig;
    use super::strategy;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn known_presets_always_resolve(name in strategy::known_preset()) {
            prop_assert!(StatusesConfig::from_preset(name).is_some());
        }

        #[test]
        fn every_preset_has_an_initial_status(cfg in strategy::preset_config()) {
            prop_assert!(cfg.resolve(&cfg.initial).is_ok());
        }
    }

    #[test]
    fn preset_scrum_has_expected_statuses() {
        let cfg = StatusesConfig::preset_issue_scrum();
        for s in &["to-do", "in-progress", "code-review", "testing", "done"] {
            assert!(cfg.resolve(s).is_ok(), "missing: {s}");
        }
    }

    #[test]
    fn preset_scrum_active_statuses_exclude_done() {
        let cfg = StatusesConfig::preset_issue_scrum();
        assert!(cfg.resolve("to-do").unwrap().active);
        assert!(cfg.resolve("in-progress").unwrap().active);
        assert!(!cfg.resolve("done").unwrap().active);
    }

    #[test]
    fn preset_scrum_done_is_terminal() {
        let cfg = StatusesConfig::preset_issue_scrum();
        assert!(cfg.resolve("done").unwrap().terminal);
        assert!(!cfg.resolve("to-do").unwrap().terminal);
    }

    #[test]
    fn preset_kanban_has_expected_statuses() {
        let cfg = StatusesConfig::preset_issue_kanban();
        for s in &["backlog", "ready", "in-progress", "blocked", "done"] {
            assert!(cfg.resolve(s).is_ok(), "missing: {s}");
        }
    }

    #[test]
    fn preset_kanban_backlog_is_not_active() {
        let cfg = StatusesConfig::preset_issue_kanban();
        assert!(!cfg.resolve("backlog").unwrap().active);
        assert!(cfg.resolve("ready").unwrap().active);
        assert!(!cfg.resolve("done").unwrap().active);
    }

    #[test]
    fn from_preset_returns_correct_config() {
        assert!(StatusesConfig::from_preset("scrum").is_some());
        assert!(StatusesConfig::from_preset("kanban").is_some());
        assert!(StatusesConfig::from_preset("default").is_some());
    }

    #[test]
    fn from_preset_unknown_returns_none() {
        assert!(StatusesConfig::from_preset("unknown").is_none());
    }
}