1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
impl Roadmap {
/// Parse roadmap from file
///
/// # Complexity
/// - Time: O(n) where n is number of lines
/// - Cyclomatic: 2
pub fn from_file(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
Self::parse_content(&content)
}
/// Parse roadmap from string
///
/// # Complexity
/// - Time: O(n) where n is number of lines
/// - Cyclomatic: 9
pub fn parse_content(content: &str) -> Result<Self> {
let mut state = ParseState {
sprints: Vec::new(),
current_sprint: None,
next_line_is_focus: false,
};
let version = extract_version(content);
for line in content.lines() {
process_roadmap_line(&mut state, line);
}
// Save last sprint
if let Some(sprint) = state.current_sprint {
state.sprints.push(sprint);
}
Ok(Roadmap {
version,
sprints: state.sprints,
})
}
/// Calculate sprint completion percentage
///
/// # Complexity
/// - Time: O(n) where n is number of sprints
/// - Cyclomatic: 2
pub fn completion_percentage(&self, sprint_number: u32) -> Option<f64> {
self.sprints
.iter()
.find(|s| s.number == sprint_number)
.map(|s| s.completion_percentage())
}
/// Validate roadmap structure
///
/// # Complexity
/// - Time: O(n*m) where n=sprints, m=tickets
/// - Cyclomatic: 4
///
/// # Note
/// Modern sprints (38+) may not use ticket format - they use narrative structure.
/// This is acceptable as roadmap evolved. Only validate ticket IDs if tickets exist.
pub fn validate(&self) -> Result<()> {
for sprint in &self.sprints {
// Validate ticket IDs only if sprint uses ticket format
for ticket in &sprint.tickets {
validate_ticket_id(&ticket.id)?;
}
}
Ok(())
}
}
impl Sprint {
/// Calculate completion percentage for this sprint
///
/// # Complexity
/// - Time: O(n) where n is number of tickets
/// - Cyclomatic: 2
pub fn completion_percentage(&self) -> f64 {
if self.tickets.is_empty() {
return 0.0;
}
let completed = self.tickets.iter().filter(|t| t.completed).count();
(completed as f64 / self.tickets.len() as f64) * 100.0
}
/// Check if sprint is complete
///
/// # Complexity
/// - Time: O(n) where n is number of tickets
/// - Cyclomatic: 1
pub fn is_complete(&self) -> bool {
!self.tickets.is_empty() && self.tickets.iter().all(|t| t.completed)
}
}
/// Mutable state for roadmap parsing
struct ParseState {
sprints: Vec<Sprint>,
current_sprint: Option<Sprint>,
next_line_is_focus: bool,
}
/// Process a single line during roadmap parsing
fn process_roadmap_line(state: &mut ParseState, line: &str) {
if let Some(sprint_info) = parse_sprint_header(line) {
if let Some(sprint) = state.current_sprint.take() {
state.sprints.push(sprint);
}
state.current_sprint = Some(sprint_info);
state.next_line_is_focus = true;
return;
}
if state.next_line_is_focus && line.starts_with("**Focus:**") {
if let Some(ref mut sprint) = state.current_sprint {
sprint.focus = line
.strip_prefix("**Focus:**")
.unwrap_or("")
.trim()
.to_string();
}
state.next_line_is_focus = false;
return;
}
if let Some(ticket) = parse_ticket_line(line) {
if let Some(ref mut sprint) = state.current_sprint {
sprint.tickets.push(ticket);
}
return;
}
if is_quality_gate_section(line) {
return;
}
if state.current_sprint.is_some()
&& line.trim().starts_with("- ")
&& !line.contains("TICKET-")
{
if let Some(gate) = parse_quality_gate(line) {
if let Some(ref mut sprint) = state.current_sprint {
sprint.quality_gates.push(gate);
}
}
}
}