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> {
197 let acceptance_criteria_marker = "## Acceptance Criteria";
198 let mut criteria = Vec::new();
199 let mut in_code_fence = false;
200 let mut in_ac_section = false;
201
202 for line in spec.body.lines() {
203 let trimmed = line.trim_start();
204
205 if trimmed.starts_with("```") {
206 in_code_fence = !in_code_fence;
207 continue;
208 }
209
210 if !in_code_fence && trimmed.starts_with(acceptance_criteria_marker) {
211 in_ac_section = true;
212 continue;
213 }
214
215 if in_ac_section && !in_code_fence && trimmed.starts_with("## ") {
217 break;
218 }
219
220 if in_ac_section
222 && !in_code_fence
223 && (trimmed.starts_with("- [ ]") || trimmed.starts_with("- [x]"))
224 {
225 let text = trimmed
227 .trim_start_matches("- [ ]")
228 .trim_start_matches("- [x]")
229 .trim()
230 .to_string();
231 criteria.push(text);
232 }
233 }
234
235 criteria
236}
237
238pub fn calculate_spec_score(
242 spec: &crate::spec::Spec,
243 all_specs: &[crate::spec::Spec],
244 config: &crate::config::Config,
245) -> SpecScore {
246 use crate::score::{ac_quality, confidence, isolation, splittability, traffic_light};
247
248 let complexity = calculate_complexity(spec);
250 let confidence_grade = confidence::calculate_confidence(spec, config);
251 let splittability_grade = splittability::calculate_splittability(spec);
252 let isolation_grade = isolation::calculate_isolation(spec, all_specs);
253
254 let criteria = extract_acceptance_criteria(spec);
256 let ac_quality_grade = ac_quality::calculate_ac_quality(&criteria);
257
258 let mut score = SpecScore {
260 complexity,
261 confidence: confidence_grade,
262 splittability: splittability_grade,
263 isolation: isolation_grade,
264 ac_quality: ac_quality_grade,
265 traffic_light: TrafficLight::Ready, };
267
268 score.traffic_light = traffic_light::determine_status(&score);
270
271 score
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_traffic_light_display() {
280 assert_eq!(TrafficLight::Ready.to_string(), "🟢 Ready");
281 assert_eq!(TrafficLight::Review.to_string(), "🟡 Review");
282 assert_eq!(TrafficLight::Refine.to_string(), "🔴 Refine");
283 }
284
285 #[test]
286 fn test_calculate_complexity_grade_a() {
287 use crate::spec::{Spec, SpecFrontmatter};
288
289 let spec = Spec {
290 id: "test".to_string(),
291 frontmatter: SpecFrontmatter {
292 target_files: Some(vec!["file1.rs".to_string()]),
293 ..Default::default()
294 },
295 title: Some("Test".to_string()),
296 body: format!(
297 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n\n{}",
298 "word ".repeat(150)
299 ),
300 };
301
302 assert_eq!(calculate_complexity(&spec), ComplexityGrade::A);
303 }
304
305 #[test]
306 fn test_calculate_complexity_grade_b() {
307 use crate::spec::{Spec, SpecFrontmatter};
308
309 let spec = Spec {
310 id: "test".to_string(),
311 frontmatter: SpecFrontmatter {
312 target_files: Some(vec![
313 "file1.rs".to_string(),
314 "file2.rs".to_string(),
315 "file3.rs".to_string(),
316 ]),
317 ..Default::default()
318 },
319 title: Some("Test".to_string()),
320 body: format!(
321 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n- [ ] Third\n- [ ] Fourth\n- [ ] Fifth\n\n{}",
322 "word ".repeat(300)
323 ),
324 };
325
326 assert_eq!(calculate_complexity(&spec), ComplexityGrade::B);
327 }
328
329 #[test]
330 fn test_calculate_complexity_grade_d_criteria() {
331 use crate::spec::{Spec, SpecFrontmatter};
332
333 let spec = Spec {
334 id: "test".to_string(),
335 frontmatter: SpecFrontmatter {
336 target_files: Some(vec!["file1.rs".to_string(), "file2.rs".to_string()]),
337 ..Default::default()
338 },
339 title: Some("Test".to_string()),
340 body: format!(
341 "## Acceptance Criteria\n{}\n\n{}",
342 (1..=10)
343 .map(|i| format!("- [ ] Item {}", i))
344 .collect::<Vec<_>>()
345 .join("\n"),
346 "word ".repeat(100)
347 ),
348 };
349
350 assert_eq!(calculate_complexity(&spec), ComplexityGrade::D);
351 }
352
353 #[test]
354 fn test_calculate_complexity_no_target_files() {
355 use crate::spec::{Spec, SpecFrontmatter};
356
357 let spec = Spec {
358 id: "test".to_string(),
359 frontmatter: SpecFrontmatter {
360 target_files: None,
361 ..Default::default()
362 },
363 title: Some("Test".to_string()),
364 body:
365 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n\nSome content here with words."
366 .to_string(),
367 };
368
369 assert_eq!(calculate_complexity(&spec), ComplexityGrade::A);
370 }
371}