use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub mod commands;
pub mod generator;
pub mod parser;
pub mod quality;
pub mod tracker;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskStatus {
Planned,
InProgress,
Completed,
Blocked,
Deferred,
}
impl TaskStatus {
#[must_use]
pub fn to_emoji(&self) -> &'static str {
match self {
Self::Planned => "π",
Self::InProgress => "π§",
Self::Completed => "β
",
Self::Blocked => "π«",
Self::Deferred => "βΈοΈ",
}
}
#[must_use]
pub fn from_emoji(emoji: &str) -> Option<Self> {
match emoji {
"π" => Some(Self::Planned),
"π§" => Some(Self::InProgress),
"β
" => Some(Self::Completed),
"π«" => Some(Self::Blocked),
"βΈοΈ" => Some(Self::Deferred),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Complexity {
Low,
Medium,
High,
}
impl std::str::FromStr for Complexity {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"low" => Ok(Self::Low),
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
_ => Err(()),
}
}
}
impl Complexity {
#[must_use]
pub fn to_string(&self) -> &str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Priority {
P0, P1, P2, }
impl std::str::FromStr for Priority {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"P0" => Ok(Self::P0),
"P1" => Ok(Self::P1),
"P2" => Ok(Self::P2),
_ => Err(()),
}
}
}
impl Priority {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: String, pub description: String,
pub status: TaskStatus,
pub complexity: Complexity,
pub priority: Priority,
pub assignee: Option<String>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
}
impl Task {
#[must_use]
pub fn seed(&self) -> u64 {
if let Some(captures) = Regex::new(r"PMAT-(\d+)").unwrap().captures(&self.id) {
if let Some(num) = captures.get(1) {
return num.as_str().parse().unwrap_or(42);
}
}
42 }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sprint {
pub version: String,
pub title: String,
pub start_date: DateTime<Utc>,
pub end_date: DateTime<Utc>,
pub priority: Priority,
pub tasks: Vec<Task>,
pub definition_of_done: Vec<String>,
pub quality_gates: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Roadmap {
pub current_sprint: Option<String>,
pub sprints: HashMap<String, Sprint>,
pub backlog: Vec<Task>,
pub completed_sprints: Vec<String>,
}
impl Roadmap {
pub fn from_file(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read roadmap from {}", path.display()))?;
parser::parse_roadmap(&content)
}
pub fn to_file(&self, path: &Path) -> Result<()> {
let content = parser::roadmap_to_markdown(self)?;
std::fs::write(path, content)
.with_context(|| format!("Failed to write roadmap to {}", path.display()))?;
Ok(())
}
#[must_use]
pub fn get_sprint(&self, sprint_id: &str) -> Option<&Sprint> {
self.sprints.get(sprint_id)
}
#[must_use]
pub fn get_task(&self, task_id: &str) -> Option<&Task> {
for sprint in self.sprints.values() {
if let Some(task) = sprint.tasks.iter().find(|t| t.id == task_id) {
return Some(task);
}
}
self.backlog.iter().find(|t| t.id == task_id)
}
pub fn update_task_status(&mut self, task_id: &str, status: TaskStatus) -> Result<()> {
for sprint in self.sprints.values_mut() {
if let Some(task) = sprint.tasks.iter_mut().find(|t| t.id == task_id) {
task.status = status;
match status {
TaskStatus::InProgress if task.started_at.is_none() => {
task.started_at = Some(Utc::now());
}
TaskStatus::Completed => {
task.completed_at = Some(Utc::now());
}
_ => {}
}
return Ok(());
}
}
if let Some(task) = self.backlog.iter_mut().find(|t| t.id == task_id) {
task.status = status;
return Ok(());
}
anyhow::bail!("Task {task_id} not found in roadmap")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoadmapConfig {
pub enabled: bool,
pub path: PathBuf,
pub auto_generate_todos: bool,
pub enforce_quality_gates: bool,
pub require_task_ids: bool,
pub task_id_pattern: String,
pub quality_gates: QualityGateConfig,
pub git: GitConfig,
pub tracking: TrackingConfig,
}
impl Default for RoadmapConfig {
fn default() -> Self {
Self {
enabled: true,
path: PathBuf::from("docs/execution/roadmap.md"),
auto_generate_todos: true,
enforce_quality_gates: true,
require_task_ids: true,
task_id_pattern: "PMAT-[0-9]{4}".to_string(),
quality_gates: QualityGateConfig::default(),
git: GitConfig::default(),
tracking: TrackingConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGateConfig {
pub complexity_max: u32,
pub coverage_min: u8,
pub documentation_required: bool,
pub satd_tolerance: u32,
pub lint_compliance: bool,
}
impl Default for QualityGateConfig {
fn default() -> Self {
Self {
complexity_max: 20,
coverage_min: 80,
documentation_required: true,
satd_tolerance: 0,
lint_compliance: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitConfig {
pub create_branches: bool,
pub branch_pattern: String,
pub commit_pattern: String,
pub require_quality_check: bool,
}
impl Default for GitConfig {
fn default() -> Self {
Self {
create_branches: true,
branch_pattern: "feature/{task_id}".to_string(),
commit_pattern: "{task_id}: {message}".to_string(),
require_quality_check: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackingConfig {
pub velocity_tracking: bool,
pub burndown_charts: bool,
pub quality_metrics: bool,
pub export_format: String,
}
impl Default for TrackingConfig {
fn default() -> Self {
Self {
velocity_tracking: true,
burndown_charts: true,
quality_metrics: true,
export_format: "markdown".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
use tempfile::tempdir;
#[test]
fn test_task_status_emoji_conversion() {
assert_eq!(TaskStatus::Planned.to_emoji(), "π");
assert_eq!(TaskStatus::InProgress.to_emoji(), "π§");
assert_eq!(TaskStatus::Completed.to_emoji(), "β
");
assert_eq!(TaskStatus::Blocked.to_emoji(), "π«");
assert_eq!(TaskStatus::Deferred.to_emoji(), "βΈοΈ");
}
#[test]
fn test_task_status_from_emoji() {
assert_eq!(TaskStatus::from_emoji("π"), Some(TaskStatus::Planned));
assert_eq!(TaskStatus::from_emoji("π§"), Some(TaskStatus::InProgress));
assert_eq!(TaskStatus::from_emoji("β
"), Some(TaskStatus::Completed));
assert_eq!(TaskStatus::from_emoji("π«"), Some(TaskStatus::Blocked));
assert_eq!(TaskStatus::from_emoji("βΈοΈ"), Some(TaskStatus::Deferred));
assert_eq!(TaskStatus::from_emoji("β"), None);
}
#[test]
fn test_complexity_from_str() {
use std::str::FromStr;
assert_eq!(Complexity::from_str("low").unwrap(), Complexity::Low);
assert_eq!(Complexity::from_str("LOW").unwrap(), Complexity::Low);
assert_eq!(Complexity::from_str("medium").unwrap(), Complexity::Medium);
assert_eq!(Complexity::from_str("high").unwrap(), Complexity::High);
assert!(Complexity::from_str("invalid").is_err());
}
#[test]
fn test_complexity_to_string() {
assert_eq!(Complexity::Low.to_string(), "low");
assert_eq!(Complexity::Medium.to_string(), "medium");
assert_eq!(Complexity::High.to_string(), "high");
}
#[test]
fn test_priority_from_str() {
use std::str::FromStr;
assert_eq!(Priority::from_str("P0").unwrap(), Priority::P0);
assert_eq!(Priority::from_str("p0").unwrap(), Priority::P0);
assert_eq!(Priority::from_str("P1").unwrap(), Priority::P1);
assert_eq!(Priority::from_str("P2").unwrap(), Priority::P2);
assert!(Priority::from_str("P3").is_err());
}
#[test]
fn test_task_seed_generation() {
let task = Task {
id: "PMAT-1234".to_string(),
description: "Test task".to_string(),
status: TaskStatus::Planned,
complexity: Complexity::Medium,
priority: Priority::P1,
assignee: None,
started_at: None,
completed_at: None,
};
assert_eq!(task.seed(), 1234);
let task_invalid = Task {
id: "INVALID-ID".to_string(),
description: "Test task".to_string(),
status: TaskStatus::Planned,
complexity: Complexity::Medium,
priority: Priority::P1,
assignee: None,
started_at: None,
completed_at: None,
};
assert_eq!(task_invalid.seed(), 42); }
#[test]
fn test_roadmap_save_and_load() {
let dir = tempdir().unwrap();
let roadmap_path = dir.path().join("test_roadmap.md");
let mut roadmap = Roadmap {
current_sprint: Some("v2.46.0".to_string()),
sprints: HashMap::new(),
backlog: vec![],
completed_sprints: vec![],
};
let sprint = Sprint {
version: "v2.46.0".to_string(),
title: "Test Sprint".to_string(),
start_date: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
end_date: Utc.with_ymd_and_hms(2025, 1, 7, 0, 0, 0).unwrap(),
priority: Priority::P0,
tasks: vec![],
definition_of_done: vec!["All tests pass".to_string()],
quality_gates: vec!["Coverage > 80%".to_string()],
};
roadmap.sprints.insert("v2.46.0".to_string(), sprint);
roadmap.to_file(&roadmap_path).unwrap();
let loaded = Roadmap::from_file(&roadmap_path).unwrap();
assert_eq!(loaded.current_sprint, Some("v2.46.0".to_string()));
assert!(loaded.sprints.contains_key("v2.46.0"));
}
#[test]
fn test_roadmap_get_sprint() {
let mut roadmap = Roadmap {
current_sprint: None,
sprints: HashMap::new(),
backlog: vec![],
completed_sprints: vec![],
};
let sprint = Sprint {
version: "v2.46.0".to_string(),
title: "Test Sprint".to_string(),
start_date: Utc::now(),
end_date: Utc::now(),
priority: Priority::P0,
tasks: vec![],
definition_of_done: vec![],
quality_gates: vec![],
};
roadmap.sprints.insert("v2.46.0".to_string(), sprint);
assert!(roadmap.get_sprint("v2.46.0").is_some());
assert!(roadmap.get_sprint("v2.47.0").is_none());
}
#[test]
fn test_roadmap_get_task() {
let mut roadmap = Roadmap {
current_sprint: None,
sprints: HashMap::new(),
backlog: vec![],
completed_sprints: vec![],
};
let task = Task {
id: "PMAT-0001".to_string(),
description: "Test task".to_string(),
status: TaskStatus::Planned,
complexity: Complexity::Medium,
priority: Priority::P1,
assignee: None,
started_at: None,
completed_at: None,
};
let sprint = Sprint {
version: "v2.46.0".to_string(),
title: "Test Sprint".to_string(),
start_date: Utc::now(),
end_date: Utc::now(),
priority: Priority::P0,
tasks: vec![task.clone()],
definition_of_done: vec![],
quality_gates: vec![],
};
roadmap.sprints.insert("v2.46.0".to_string(), sprint);
assert!(roadmap.get_task("PMAT-0001").is_some());
assert!(roadmap.get_task("PMAT-9999").is_none());
let backlog_task = Task {
id: "PMAT-0002".to_string(),
description: "Backlog task".to_string(),
status: TaskStatus::Planned,
complexity: Complexity::Low,
priority: Priority::P2,
assignee: None,
started_at: None,
completed_at: None,
};
roadmap.backlog.push(backlog_task);
assert!(roadmap.get_task("PMAT-0002").is_some());
}
#[test]
fn test_roadmap_update_task_status() {
let mut roadmap = Roadmap {
current_sprint: None,
sprints: HashMap::new(),
backlog: vec![],
completed_sprints: vec![],
};
let task = Task {
id: "PMAT-0001".to_string(),
description: "Test task".to_string(),
status: TaskStatus::Planned,
complexity: Complexity::Medium,
priority: Priority::P1,
assignee: None,
started_at: None,
completed_at: None,
};
let sprint = Sprint {
version: "v2.46.0".to_string(),
title: "Test Sprint".to_string(),
start_date: Utc::now(),
end_date: Utc::now(),
priority: Priority::P0,
tasks: vec![task],
definition_of_done: vec![],
quality_gates: vec![],
};
roadmap.sprints.insert("v2.46.0".to_string(), sprint);
roadmap
.update_task_status("PMAT-0001", TaskStatus::InProgress)
.unwrap();
let task = roadmap.get_task("PMAT-0001").unwrap();
assert_eq!(task.status, TaskStatus::InProgress);
roadmap
.update_task_status("PMAT-0001", TaskStatus::Completed)
.unwrap();
let task = roadmap.get_task("PMAT-0001").unwrap();
assert_eq!(task.status, TaskStatus::Completed);
assert!(roadmap
.update_task_status("PMAT-9999", TaskStatus::Completed)
.is_err());
}
#[test]
fn test_roadmap_config_defaults() {
let config = RoadmapConfig::default();
assert!(config.enabled);
assert_eq!(config.path, PathBuf::from("docs/execution/roadmap.md"));
assert!(config.auto_generate_todos);
assert!(config.enforce_quality_gates);
assert!(config.require_task_ids);
assert_eq!(config.task_id_pattern, "PMAT-[0-9]{4}");
}
#[test]
fn test_quality_gate_config_defaults() {
let config = QualityGateConfig::default();
assert_eq!(config.complexity_max, 20);
assert_eq!(config.coverage_min, 80);
assert!(config.documentation_required);
assert_eq!(config.satd_tolerance, 0);
assert!(config.lint_compliance);
}
#[test]
fn test_git_config_defaults() {
let config = GitConfig::default();
assert!(config.create_branches);
assert_eq!(config.branch_pattern, "feature/{task_id}");
assert_eq!(config.commit_pattern, "{task_id}: {message}");
assert!(config.require_quality_check);
}
#[test]
fn test_tracking_config_defaults() {
let config = TrackingConfig::default();
assert!(config.velocity_tracking);
assert!(config.burndown_charts);
assert!(config.quality_metrics);
assert_eq!(config.export_format, "markdown");
}
#[test]
fn test_update_task_status_timestamps() {
let mut roadmap = Roadmap {
current_sprint: None,
sprints: HashMap::new(),
backlog: vec![],
completed_sprints: vec![],
};
let task = Task {
id: "PMAT-0001".to_string(),
description: "Test task".to_string(),
status: TaskStatus::Planned,
complexity: Complexity::Medium,
priority: Priority::P1,
assignee: None,
started_at: None,
completed_at: None,
};
let sprint = Sprint {
version: "v2.46.0".to_string(),
title: "Test Sprint".to_string(),
start_date: Utc::now(),
end_date: Utc::now(),
priority: Priority::P0,
tasks: vec![task],
definition_of_done: vec![],
quality_gates: vec![],
};
roadmap.sprints.insert("v2.46.0".to_string(), sprint);
roadmap
.update_task_status("PMAT-0001", TaskStatus::InProgress)
.unwrap();
if let Some(sprint) = roadmap.sprints.get("v2.46.0") {
if let Some(task) = sprint.tasks.iter().find(|t| t.id == "PMAT-0001") {
assert!(task.started_at.is_some());
assert!(task.completed_at.is_none());
}
}
roadmap
.update_task_status("PMAT-0001", TaskStatus::Completed)
.unwrap();
if let Some(sprint) = roadmap.sprints.get("v2.46.0") {
if let Some(task) = sprint.tasks.iter().find(|t| t.id == "PMAT-0001") {
assert!(task.started_at.is_some());
assert!(task.completed_at.is_some());
}
}
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}