use super::{RoadmapConfig, Path, Roadmap, HashMap, Sprint, Utc, Priority, generator, TaskStatus, Task};
use crate::cli::OutputFormat;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Parser)]
#[command(about = "Roadmap management with PDMT todos and quality gates")]
pub struct RoadmapCommand {
#[command(subcommand)]
pub command: RoadmapSubcommand,
}
#[derive(Debug, Subcommand)]
pub enum RoadmapSubcommand {
Init {
#[arg(long)]
version: String,
#[arg(long)]
title: String,
#[arg(long, default_value = "14")]
duration_days: u32,
#[arg(long, default_value = "P0")]
priority: String,
},
Todos {
#[arg(long)]
sprint: Option<String>,
#[arg(long, default_value = "todos.md")]
output: PathBuf,
#[arg(long)]
include_quality_gates: bool,
},
Start {
task_id: String,
#[arg(long)]
create_branch: bool,
},
Complete {
task_id: String,
#[arg(long)]
skip_quality_check: bool,
},
Status {
#[arg(long)]
sprint: Option<String>,
#[arg(long)]
task: Option<String>,
#[arg(long, default_value = "human")]
format: OutputFormat,
},
Validate {
#[arg(long)]
sprint: String,
#[arg(long)]
strict: bool,
},
QualityCheck {
#[arg(long)]
task_id: String,
},
}
pub async fn execute(cmd: RoadmapCommand, config: RoadmapConfig) -> Result<()> {
let roadmap_path = config.path.clone();
match cmd.command {
RoadmapSubcommand::Init {
version,
title,
duration_days,
priority,
} => init_sprint(&roadmap_path, &version, &title, duration_days, &priority).await,
RoadmapSubcommand::Todos {
sprint,
output,
include_quality_gates,
} => {
generate_todos(
&roadmap_path,
sprint.as_deref(),
&output,
include_quality_gates,
&config,
)
.await
}
RoadmapSubcommand::Start {
task_id,
create_branch,
} => start_task(&roadmap_path, &task_id, create_branch, &config).await,
RoadmapSubcommand::Complete {
task_id,
skip_quality_check,
} => complete_task(&roadmap_path, &task_id, skip_quality_check, &config).await,
RoadmapSubcommand::Status {
sprint,
task,
format,
} => show_status(&roadmap_path, sprint.as_deref(), task.as_deref(), format).await,
RoadmapSubcommand::Validate { sprint, strict } => {
validate_sprint(&roadmap_path, &sprint, strict, &config).await
}
RoadmapSubcommand::QualityCheck { task_id } => quality_check(&task_id, &config).await,
}
}
async fn init_sprint(
roadmap_path: &Path,
version: &str,
title: &str,
duration_days: u32,
priority: &str,
) -> Result<()> {
println!("📋 Initializing sprint {version} - {title}");
let mut roadmap = if roadmap_path.exists() {
Roadmap::from_file(roadmap_path)?
} else {
Roadmap {
current_sprint: None,
sprints: HashMap::new(),
backlog: Vec::new(),
completed_sprints: Vec::new(),
}
};
let sprint = Sprint {
version: version.to_string(),
title: title.to_string(),
start_date: Utc::now(),
end_date: Utc::now() + chrono::Duration::days(i64::from(duration_days)),
priority: Priority::from_str(priority).unwrap_or(Priority::P0),
tasks: Vec::new(),
definition_of_done: vec![
"All tasks completed".to_string(),
"Quality gates passed".to_string(),
"Documentation updated".to_string(),
"Tests passing".to_string(),
"Changelog updated".to_string(),
],
quality_gates: vec![
format!("Complexity ≤ 20"),
format!("SATD = 0"),
format!("Coverage ≥ 80%"),
],
};
roadmap.sprints.insert(version.to_string(), sprint);
if roadmap.current_sprint.is_none() {
roadmap.current_sprint = Some(version.to_string());
}
roadmap.to_file(roadmap_path)?;
println!("✅ Sprint {version} initialized successfully");
println!("📝 Roadmap updated at: {}", roadmap_path.display());
Ok(())
}
async fn generate_todos(
roadmap_path: &Path,
sprint_id: Option<&str>,
output_path: &Path,
include_quality_gates: bool,
config: &RoadmapConfig,
) -> Result<()> {
println!("🔄 Generating PDMT todos from roadmap...");
let roadmap = Roadmap::from_file(roadmap_path)?;
let sprint_id = sprint_id
.or(roadmap.current_sprint.as_deref())
.context("No sprint specified and no current sprint found")?;
let sprint = roadmap
.get_sprint(sprint_id)
.context(format!("Sprint {sprint_id} not found"))?;
let generator = generator::RoadmapTodoGenerator::new(config.quality_gates.clone());
let todos = generator.generate_sprint_todos(sprint).await?;
println!(
"📝 Generated {} todos for {} tasks",
todos.len(),
sprint.tasks.len()
);
let output = if include_quality_gates {
generator.export_todos_markdown(&todos)
} else {
let mut simple = String::new();
for todo in &todos {
simple.push_str(&format!("- [ ] {}: {}\n", todo.id, todo.description));
}
simple
};
std::fs::write(output_path, output)?;
println!("✅ Todos written to: {}", output_path.display());
Ok(())
}
async fn start_task(
roadmap_path: &Path,
task_id: &str,
create_branch: bool,
config: &RoadmapConfig,
) -> Result<()> {
println!("🚀 Starting task {task_id}");
let mut roadmap = Roadmap::from_file(roadmap_path)?;
roadmap.update_task_status(task_id, TaskStatus::InProgress)?;
roadmap.to_file(roadmap_path)?;
println!("✅ Task {task_id} status updated to: 🚧 In Progress");
if create_branch && config.git.create_branches {
let branch_name = config
.git
.branch_pattern
.replace("{task_id}", &task_id.to_lowercase());
println!("🌿 Creating branch: {branch_name}");
std::process::Command::new("git")
.args(["checkout", "-b", &branch_name])
.output()
.context("Failed to create git branch")?;
println!("✅ Branch created and checked out: {branch_name}");
}
if let Some(task) = roadmap.get_task(task_id) {
println!("\n📋 Task Details:");
println!(" ID: {}", task.id);
println!(" Description: {}", task.description);
println!(" Complexity: {:?}", task.complexity);
println!(" Priority: {:?}", task.priority);
}
Ok(())
}
async fn complete_task(
roadmap_path: &Path,
task_id: &str,
skip_quality_check: bool,
config: &RoadmapConfig,
) -> Result<()> {
println!("🏁 Completing task {task_id}");
if !skip_quality_check && config.enforce_quality_gates {
println!("🔍 Running quality checks...");
quality_check(task_id, config).await?;
}
let mut roadmap = Roadmap::from_file(roadmap_path)?;
roadmap.update_task_status(task_id, TaskStatus::Completed)?;
roadmap.to_file(roadmap_path)?;
println!("✅ Task {task_id} completed successfully");
if config.git.require_quality_check {
let message = config
.git
.commit_pattern
.replace("{task_id}", task_id)
.replace("{message}", "Complete implementation");
println!("📝 Creating commit: {message}");
std::process::Command::new("git")
.args(["add", "-A"])
.output()?;
std::process::Command::new("git")
.args(["commit", "-m", &message])
.output()?;
println!("✅ Changes committed");
}
Ok(())
}
async fn show_status(
roadmap_path: &Path,
sprint_id: Option<&str>,
task_id: Option<&str>,
format: OutputFormat,
) -> Result<()> {
let roadmap = Roadmap::from_file(roadmap_path)?;
if let Some(task_id) = task_id {
show_task_status(&roadmap, task_id, format)?;
} else {
show_sprint_status(&roadmap, sprint_id, format).await?;
}
Ok(())
}
fn show_task_status(roadmap: &Roadmap, task_id: &str, format: OutputFormat) -> Result<()> {
let task = roadmap
.get_task(task_id)
.context(format!("Task {task_id} not found"))?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(task)?);
}
_ => {
display_task_details(task);
}
}
Ok(())
}
fn display_task_details(task: &Task) {
println!("Task {}: {}", task.id, task.status.to_emoji());
println!(" Description: {}", task.description);
println!(" Complexity: {:?}", task.complexity);
println!(" Priority: {:?}", task.priority);
if let Some(started) = task.started_at {
println!(" Started: {}", started.format("%Y-%m-%d %H:%M"));
}
if let Some(completed) = task.completed_at {
println!(" Completed: {}", completed.format("%Y-%m-%d %H:%M"));
}
}
async fn show_sprint_status(
roadmap: &Roadmap,
sprint_id: Option<&str>,
format: OutputFormat,
) -> Result<()> {
let sprint_id = sprint_id
.or(roadmap.current_sprint.as_deref())
.context("No sprint specified and no current sprint found")?;
let sprint = roadmap
.get_sprint(sprint_id)
.context(format!("Sprint {sprint_id} not found"))?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(sprint)?);
}
_ => {
display_sprint_details(sprint);
}
}
Ok(())
}
fn display_sprint_details(sprint: &Sprint) {
let (completed, in_progress, total) = calculate_sprint_progress(sprint);
println!("Sprint {}: {}", sprint.version, sprint.title);
println!(
" Duration: {} to {}",
sprint.start_date.format("%Y-%m-%d"),
sprint.end_date.format("%Y-%m-%d")
);
println!(
" Progress: {completed}/{total} completed, {in_progress} in progress"
);
display_sprint_tasks(sprint);
}
fn calculate_sprint_progress(sprint: &Sprint) -> (usize, usize, usize) {
let completed = sprint
.tasks
.iter()
.filter(|t| t.status == TaskStatus::Completed)
.count();
let in_progress = sprint
.tasks
.iter()
.filter(|t| t.status == TaskStatus::InProgress)
.count();
let total = sprint.tasks.len();
(completed, in_progress, total)
}
fn display_sprint_tasks(sprint: &Sprint) {
println!("\n Tasks:");
for task in &sprint.tasks {
println!(
" {} {} - {}",
task.status.to_emoji(),
task.id,
task.description
);
}
}
async fn validate_sprint(
roadmap_path: &Path,
sprint_id: &str,
strict: bool,
config: &RoadmapConfig,
) -> Result<()> {
println!("🔍 Validating sprint {sprint_id} for release...");
let roadmap = Roadmap::from_file(roadmap_path)?;
let sprint = roadmap
.get_sprint(sprint_id)
.context(format!("Sprint {sprint_id} not found"))?;
let mut all_passed = true;
let incomplete_tasks: Vec<_> = sprint
.tasks
.iter()
.filter(|t| t.status != TaskStatus::Completed)
.collect();
if incomplete_tasks.is_empty() {
println!("✅ All tasks completed");
} else {
println!("❌ Incomplete tasks:");
for task in incomplete_tasks {
println!(" {} - {}", task.id, task.description);
}
all_passed = false;
}
println!("\n📋 Definition of Done:");
for item in &sprint.definition_of_done {
println!(" - [ ] {item}");
}
if config.enforce_quality_gates {
println!("\n🔍 Quality Gates:");
for gate in &sprint.quality_gates {
println!(" - [ ] {gate}");
}
}
if all_passed {
println!("\n✅ Sprint {sprint_id} is ready for release!");
} else {
println!("\n❌ Sprint {sprint_id} is NOT ready for release");
if strict {
anyhow::bail!("Sprint validation failed");
}
}
Ok(())
}
async fn quality_check(task_id: &str, config: &RoadmapConfig) -> Result<()> {
println!("🔍 Running quality checks for task {task_id}...");
let complexity_result = std::process::Command::new("pmat")
.args([
"analyze",
"complexity",
"--max-cyclomatic",
&config.quality_gates.complexity_max.to_string(),
])
.output()?;
if !complexity_result.status.success() {
println!("❌ Complexity check failed");
anyhow::bail!("Complexity exceeds limit");
}
println!("✅ Complexity check passed");
let satd_result = std::process::Command::new("pmat")
.args(["analyze", "satd", "--strict"])
.output()?;
if !satd_result.status.success() && config.quality_gates.satd_tolerance == 0 {
println!("❌ SATD check failed");
anyhow::bail!("SATD violations found");
}
println!("✅ SATD check passed");
if config.quality_gates.lint_compliance {
let lint_result = std::process::Command::new("make").args(["lint"]).output()?;
if !lint_result.status.success() {
println!("❌ Lint check failed");
anyhow::bail!("Lint violations found");
}
println!("✅ Lint check passed");
}
println!("✅ All quality checks passed for task {task_id}");
Ok(())
}
#[allow(dead_code)]
fn handle_init(
version: String,
title: String,
duration_days: u32,
priority: String,
roadmap_path: PathBuf,
) -> Result<()> {
Priority::from_str(&priority)
.map_err(|()| anyhow::anyhow!("Invalid priority format. Use P0, P1, or P2"))?;
let content = format!(
r"# Roadmap
## Current Sprint: {version} {title}
- **Duration**: {duration_days} days
- **Priority**: {priority}
- **Status**: Active
### Tasks
- [ ] Initial task placeholder
### Quality Gates
- [ ] All tests pass
- [ ] Code coverage maintained
- [ ] Zero SATD violations
"
);
std::fs::write(&roadmap_path, content)
.with_context(|| format!("Failed to write roadmap to {roadmap_path:?}"))?;
println!("✅ Initialized roadmap at {roadmap_path:?}");
Ok(())
}
#[allow(dead_code)]
fn handle_start(task_id: String, create_branch: bool) -> Result<()> {
if !task_id.starts_with("PMAT-") {
anyhow::bail!("Invalid task ID format. Expected PMAT-XXXX");
}
println!("🚀 Starting work on task: {task_id}");
if create_branch {
let branch_name = format!("feature/{}", task_id.to_lowercase());
println!("🌿 Creating branch: {branch_name}");
let result = std::process::Command::new("git")
.args(["checkout", "-b", &branch_name])
.output();
match result {
Ok(output) if output.status.success() => {
println!("✅ Branch created successfully");
}
Ok(_) => {
println!("⚠️ Branch creation attempted but may have failed");
}
Err(_) => {
println!("⚠️ Git not available or branch creation failed");
}
}
}
println!("✅ Task {task_id} is now active");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_roadmap_command_parsing() {
let cmd = RoadmapCommand::try_parse_from(&[
"roadmap",
"init",
"--version",
"v1.0.0",
"--title",
"Test Sprint",
"--duration-days",
"7",
"--priority",
"P0",
]);
assert!(cmd.is_ok());
if let Ok(parsed) = cmd {
match parsed.command {
RoadmapSubcommand::Init {
version,
title,
duration_days,
priority,
} => {
assert_eq!(version, "v1.0.0");
assert_eq!(title, "Test Sprint");
assert_eq!(duration_days, 7);
assert_eq!(priority, "P0");
}
_ => panic!("Expected Init subcommand"),
}
}
}
#[test]
fn test_handle_init_command() {
let temp_dir = TempDir::new().unwrap();
let roadmap_path = temp_dir.path().join("roadmap.md");
let result = handle_init(
"v2.0.0".to_string(),
"Test Initiative".to_string(),
14,
"P1".to_string(),
roadmap_path.clone(),
);
assert!(result.is_ok());
assert!(roadmap_path.exists());
let content = fs::read_to_string(&roadmap_path).unwrap();
assert!(content.contains("v2.0.0"));
assert!(content.contains("Test Initiative"));
assert!(content.contains("P1"));
}
#[test]
fn test_handle_init_invalid_priority() {
let temp_dir = TempDir::new().unwrap();
let roadmap_path = temp_dir.path().join("roadmap.md");
let result = handle_init(
"v1.0.0".to_string(),
"Test".to_string(),
14,
"INVALID_PRIORITY".to_string(),
roadmap_path,
);
assert!(result.is_err());
}
#[test]
fn test_todos_subcommand_parsing() {
let cmd = RoadmapCommand::try_parse_from(&[
"roadmap",
"todos",
"--sprint",
"v1.0.0",
"--output",
"custom_todos.md",
"--include-quality-gates",
]);
assert!(cmd.is_ok());
if let Ok(parsed) = cmd {
match parsed.command {
RoadmapSubcommand::Todos {
sprint,
output,
include_quality_gates,
} => {
assert_eq!(sprint, Some("v1.0.0".to_string()));
assert_eq!(output, PathBuf::from("custom_todos.md"));
assert!(include_quality_gates);
}
_ => panic!("Expected Todos subcommand"),
}
}
}
#[test]
fn test_start_subcommand_parsing() {
let cmd =
RoadmapCommand::try_parse_from(&["roadmap", "start", "PMAT-1001", "--create-branch"]);
assert!(cmd.is_ok());
if let Ok(parsed) = cmd {
match parsed.command {
RoadmapSubcommand::Start {
task_id,
create_branch,
} => {
assert_eq!(task_id, "PMAT-1001");
assert!(create_branch);
}
_ => panic!("Expected Start subcommand"),
}
}
}
#[test]
fn test_complete_subcommand_parsing() {
let cmd = RoadmapCommand::try_parse_from(&[
"roadmap",
"complete",
"PMAT-1001",
"--format",
"json",
"--skip-quality-checks",
]);
assert!(cmd.is_ok());
if let Ok(parsed) = cmd {
match parsed.command {
RoadmapSubcommand::Complete {
task_id,
skip_quality_check,
} => {
assert_eq!(task_id, "PMAT-1001");
assert!(skip_quality_check);
}
_ => panic!("Expected Complete subcommand"),
}
}
}
#[test]
fn test_priority_from_str() {
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("INVALID").is_err());
}
#[test]
fn test_handle_start_task() {
let result = handle_start("PMAT-1001".to_string(), false);
assert!(result.is_ok());
}
#[test]
fn test_handle_start_task_with_branch() {
let result = handle_start("PMAT-2001".to_string(), true);
assert!(result.is_ok() || result.is_err()); }
}
#[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);
}
}
}