fn extract_version(content: &str) -> String {
content
.lines()
.find(|line| line.contains("v2."))
.and_then(|line| {
line.split("v2.")
.nth(1)
.and_then(|s| s.split_whitespace().next())
})
.map(|s| format!("v2.{}", s))
.unwrap_or_else(|| "unknown".to_string())
}
fn parse_sprint_header(line: &str) -> Option<Sprint> {
if !line.starts_with("### Sprint ") {
return None;
}
let parts: Vec<&str> = line.split(':').collect();
if parts.len() < 2 {
return None;
}
let number = parts[0].split_whitespace().nth(2)?.parse::<u32>().ok()?;
let rest = parts[1].trim();
let name = rest.split(" (").next()?.trim().to_string();
let status = parse_sprint_status(rest);
let duration = extract_duration(rest);
Some(Sprint {
number,
name,
focus: String::new(),
status,
duration,
tickets: Vec::new(),
quality_gates: Vec::new(),
})
}
fn parse_sprint_status(text: &str) -> SprintStatus {
if text.contains(" - COMPLETE") {
SprintStatus::Complete
} else if text.contains(" - IN PROGRESS") {
SprintStatus::InProgress
} else {
SprintStatus::Planned
}
}
fn extract_duration(text: &str) -> String {
if let Some(start) = text.find('(') {
if let Some(end) = text.find(')') {
return text.get(start + 1..end).unwrap_or_default().to_string();
}
}
"unknown".to_string()
}
fn parse_ticket_line(line: &str) -> Option<Ticket> {
let line = line.trim();
if !line.starts_with("- [") {
return None;
}
if !line.contains("TICKET-") {
return None;
}
let completed = line.contains("[x]");
let content = if completed {
line.strip_prefix("- [x]")?.trim()
} else {
line.strip_prefix("- [ ]")?.trim()
};
let parts: Vec<&str> = content.split(':').collect();
if parts.len() < 2 {
return None;
}
let id = parts[0].trim().to_string();
let description = parts[1].split('(').next()?.trim().to_string();
let commit = if let Some(commit_start) = content.find("(commit: ") {
let commit_end = content.get(commit_start..)?.find(')')?;
Some(
content
.get(commit_start + 9..commit_start + commit_end)
.unwrap_or_default()
.to_string(),
)
} else {
None
};
Some(Ticket {
id,
description,
completed,
commit,
})
}
fn is_quality_gate_section(line: &str) -> bool {
line.trim() == "**Quality Gates:**"
}
fn parse_quality_gate(line: &str) -> Option<String> {
let line = line.trim();
if line.starts_with("- ") {
Some(line.strip_prefix("- ")?.to_string())
} else {
None
}
}
fn validate_ticket_id(id: &str) -> Result<()> {
if !id.starts_with("TICKET-PMAT-") {
return Err(RoadmapError::InvalidTicketId(id.to_string()));
}
let number_part = id
.strip_prefix("TICKET-PMAT-")
.ok_or_else(|| RoadmapError::InvalidTicketId(id.to_string()))?;
if number_part.len() != 4 || !number_part.chars().all(|c| c.is_ascii_digit()) {
return Err(RoadmapError::InvalidTicketId(id.to_string()));
}
Ok(())
}