1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9macro_rules! impl_letter_grade_display {
11 ($type:ty) => {
12 impl fmt::Display for $type {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 match self {
15 Self::A => write!(f, "A"),
16 Self::B => write!(f, "B"),
17 Self::C => write!(f, "C"),
18 Self::D => write!(f, "D"),
19 }
20 }
21 }
22 };
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SpecScore {
28 pub complexity: ComplexityGrade,
30 pub confidence: ConfidenceGrade,
32 pub splittability: SplittabilityGrade,
34 pub isolation: Option<IsolationGrade>,
36 pub ac_quality: ACQualityGrade,
38 pub traffic_light: TrafficLight,
40}
41
42impl Default for SpecScore {
43 fn default() -> Self {
44 Self {
45 complexity: ComplexityGrade::A,
46 confidence: ConfidenceGrade::A,
47 splittability: SplittabilityGrade::A,
48 isolation: None,
49 ac_quality: ACQualityGrade::A,
50 traffic_light: TrafficLight::Ready,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum ComplexityGrade {
58 A,
60 B,
62 C,
64 D,
66}
67
68impl_letter_grade_display!(ComplexityGrade);
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72pub enum ConfidenceGrade {
73 A,
75 B,
77 C,
79 D,
81}
82
83impl_letter_grade_display!(ConfidenceGrade);
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87pub enum SplittabilityGrade {
88 A,
90 B,
92 C,
94 D,
96}
97
98impl_letter_grade_display!(SplittabilityGrade);
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102pub enum IsolationGrade {
103 A,
105 B,
107 C,
109 D,
111}
112
113impl_letter_grade_display!(IsolationGrade);
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117pub enum ACQualityGrade {
118 A,
120 B,
122 C,
124 D,
126}
127
128impl_letter_grade_display!(ACQualityGrade);
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132pub enum TrafficLight {
133 Ready,
135 Review,
137 Refine,
139}
140
141impl fmt::Display for TrafficLight {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 Self::Ready => write!(f, "🟢 Ready"),
145 Self::Review => write!(f, "🟡 Review"),
146 Self::Refine => write!(f, "🔴 Refine"),
147 }
148 }
149}
150
151pub fn calculate_complexity(spec: &crate::spec::Spec) -> ComplexityGrade {
161 let criteria_count = spec.count_total_checkboxes();
163
164 let file_count = spec
166 .frontmatter
167 .target_files
168 .as_ref()
169 .map(|files| files.len())
170 .unwrap_or(0);
171
172 let word_count = spec.body.split_whitespace().count();
174
175 if criteria_count >= 8 || file_count >= 5 || word_count >= 600 {
178 return ComplexityGrade::D;
179 }
180
181 if criteria_count >= 6 || file_count >= 4 || word_count >= 400 {
183 return ComplexityGrade::C;
184 }
185
186 if criteria_count >= 4 || file_count >= 3 || word_count >= 200 {
188 return ComplexityGrade::B;
189 }
190
191 ComplexityGrade::A
193}
194
195fn extract_acceptance_criteria(spec: &crate::spec::Spec) -> Vec<String> {
200 let mut criteria = Vec::new();
201 let mut in_code_fence = false;
202
203 for line in spec.body.lines() {
204 let trimmed = line.trim_start();
205
206 if trimmed.starts_with("```") {
207 in_code_fence = !in_code_fence;
208 continue;
209 }
210
211 if in_code_fence {
213 continue;
214 }
215
216 if trimmed.starts_with("- [ ]")
218 || trimmed.starts_with("- [x]")
219 || trimmed.starts_with("- [X]")
220 {
221 let text = trimmed
223 .trim_start_matches("- [ ]")
224 .trim_start_matches("- [x]")
225 .trim_start_matches("- [X]")
226 .trim()
227 .to_string();
228 if !text.is_empty() {
229 criteria.push(text);
230 }
231 }
232 }
233
234 criteria
235}
236
237pub fn calculate_spec_score(
241 spec: &crate::spec::Spec,
242 all_specs: &[crate::spec::Spec],
243 config: &crate::config::Config,
244) -> SpecScore {
245 use crate::score::{ac_quality, confidence, isolation, splittability, traffic_light};
246
247 let complexity = calculate_complexity(spec);
249 let confidence_grade = confidence::calculate_confidence(spec, config);
250 let splittability_grade = splittability::calculate_splittability(spec);
251 let isolation_grade = isolation::calculate_isolation(spec, all_specs);
252
253 let criteria = extract_acceptance_criteria(spec);
255 let ac_quality_grade = ac_quality::calculate_ac_quality(&criteria);
256
257 let mut score = SpecScore {
259 complexity,
260 confidence: confidence_grade,
261 splittability: splittability_grade,
262 isolation: isolation_grade,
263 ac_quality: ac_quality_grade,
264 traffic_light: TrafficLight::Ready, };
266
267 score.traffic_light = traffic_light::determine_status(&score);
269
270 score
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_traffic_light_display() {
279 assert_eq!(TrafficLight::Ready.to_string(), "🟢 Ready");
280 assert_eq!(TrafficLight::Review.to_string(), "🟡 Review");
281 assert_eq!(TrafficLight::Refine.to_string(), "🔴 Refine");
282 }
283
284 #[test]
285 fn test_calculate_complexity_grade_a() {
286 use crate::spec::{Spec, SpecFrontmatter};
287
288 let spec = Spec {
289 id: "test".to_string(),
290 frontmatter: SpecFrontmatter {
291 target_files: Some(vec!["file1.rs".to_string()]),
292 ..Default::default()
293 },
294 title: Some("Test".to_string()),
295 body: format!(
296 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n\n{}",
297 "word ".repeat(150)
298 ),
299 };
300
301 assert_eq!(calculate_complexity(&spec), ComplexityGrade::A);
302 }
303
304 #[test]
305 fn test_calculate_complexity_grade_b() {
306 use crate::spec::{Spec, SpecFrontmatter};
307
308 let spec = Spec {
309 id: "test".to_string(),
310 frontmatter: SpecFrontmatter {
311 target_files: Some(vec![
312 "file1.rs".to_string(),
313 "file2.rs".to_string(),
314 "file3.rs".to_string(),
315 ]),
316 ..Default::default()
317 },
318 title: Some("Test".to_string()),
319 body: format!(
320 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n- [ ] Third\n- [ ] Fourth\n- [ ] Fifth\n\n{}",
321 "word ".repeat(300)
322 ),
323 };
324
325 assert_eq!(calculate_complexity(&spec), ComplexityGrade::B);
326 }
327
328 #[test]
329 fn test_calculate_complexity_grade_d_criteria() {
330 use crate::spec::{Spec, SpecFrontmatter};
331
332 let spec = Spec {
333 id: "test".to_string(),
334 frontmatter: SpecFrontmatter {
335 target_files: Some(vec!["file1.rs".to_string(), "file2.rs".to_string()]),
336 ..Default::default()
337 },
338 title: Some("Test".to_string()),
339 body: format!(
340 "## Acceptance Criteria\n{}\n\n{}",
341 (1..=10)
342 .map(|i| format!("- [ ] Item {}", i))
343 .collect::<Vec<_>>()
344 .join("\n"),
345 "word ".repeat(100)
346 ),
347 };
348
349 assert_eq!(calculate_complexity(&spec), ComplexityGrade::D);
350 }
351
352 #[test]
353 fn test_calculate_complexity_no_target_files() {
354 use crate::spec::{Spec, SpecFrontmatter};
355
356 let spec = Spec {
357 id: "test".to_string(),
358 frontmatter: SpecFrontmatter {
359 target_files: None,
360 ..Default::default()
361 },
362 title: Some("Test".to_string()),
363 body:
364 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n\nSome content here with words."
365 .to_string(),
366 };
367
368 assert_eq!(calculate_complexity(&spec), ComplexityGrade::A);
369 }
370}