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_spec_score_creation_with_all_a() {
280 let score = SpecScore {
281 complexity: ComplexityGrade::A,
282 confidence: ConfidenceGrade::A,
283 splittability: SplittabilityGrade::A,
284 isolation: Some(IsolationGrade::A),
285 ac_quality: ACQualityGrade::A,
286 traffic_light: TrafficLight::Ready,
287 };
288
289 assert_eq!(score.complexity, ComplexityGrade::A);
290 assert_eq!(score.confidence, ConfidenceGrade::A);
291 assert_eq!(score.splittability, SplittabilityGrade::A);
292 assert_eq!(score.isolation, Some(IsolationGrade::A));
293 assert_eq!(score.ac_quality, ACQualityGrade::A);
294 assert_eq!(score.traffic_light, TrafficLight::Ready);
295 }
296
297 #[test]
298 fn test_complexity_grade_display() {
299 assert_eq!(ComplexityGrade::B.to_string(), "B");
300 assert_eq!(ComplexityGrade::A.to_string(), "A");
301 assert_eq!(ComplexityGrade::C.to_string(), "C");
302 assert_eq!(ComplexityGrade::D.to_string(), "D");
303 }
304
305 #[test]
306 fn test_confidence_grade_display() {
307 assert_eq!(ConfidenceGrade::B.to_string(), "B");
308 }
309
310 #[test]
311 fn test_splittability_grade_display() {
312 assert_eq!(SplittabilityGrade::B.to_string(), "B");
313 }
314
315 #[test]
316 fn test_isolation_grade_display() {
317 assert_eq!(IsolationGrade::B.to_string(), "B");
318 }
319
320 #[test]
321 fn test_ac_quality_grade_display() {
322 assert_eq!(ACQualityGrade::B.to_string(), "B");
323 }
324
325 #[test]
326 fn test_traffic_light_display() {
327 assert_eq!(TrafficLight::Ready.to_string(), "🟢 Ready");
328 assert_eq!(TrafficLight::Review.to_string(), "🟡 Review");
329 assert_eq!(TrafficLight::Refine.to_string(), "🔴 Refine");
330 }
331
332 #[test]
333 fn test_spec_score_serialization() {
334 let score = SpecScore {
335 complexity: ComplexityGrade::A,
336 confidence: ConfidenceGrade::B,
337 splittability: SplittabilityGrade::A,
338 isolation: None,
339 ac_quality: ACQualityGrade::A,
340 traffic_light: TrafficLight::Ready,
341 };
342
343 let json = serde_json::to_string(&score).unwrap();
344 let deserialized: SpecScore = serde_json::from_str(&json).unwrap();
345
346 assert_eq!(deserialized.complexity, ComplexityGrade::A);
347 assert_eq!(deserialized.confidence, ConfidenceGrade::B);
348 assert_eq!(deserialized.splittability, SplittabilityGrade::A);
349 assert_eq!(deserialized.isolation, None);
350 assert_eq!(deserialized.ac_quality, ACQualityGrade::A);
351 assert_eq!(deserialized.traffic_light, TrafficLight::Ready);
352 }
353
354 #[test]
355 fn test_isolation_is_optional() {
356 let score = SpecScore {
357 complexity: ComplexityGrade::A,
358 confidence: ConfidenceGrade::A,
359 splittability: SplittabilityGrade::A,
360 isolation: None, ac_quality: ACQualityGrade::A,
362 traffic_light: TrafficLight::Ready,
363 };
364
365 assert_eq!(score.isolation, None);
366 }
367
368 #[test]
369 fn test_default_spec_score() {
370 let score = SpecScore::default();
371 assert_eq!(score.complexity, ComplexityGrade::A);
372 assert_eq!(score.confidence, ConfidenceGrade::A);
373 assert_eq!(score.splittability, SplittabilityGrade::A);
374 assert_eq!(score.isolation, None);
375 assert_eq!(score.ac_quality, ACQualityGrade::A);
376 assert_eq!(score.traffic_light, TrafficLight::Ready);
377 }
378
379 #[test]
380 fn test_calculate_complexity_grade_a() {
381 use crate::spec::{Spec, SpecFrontmatter};
382
383 let spec = Spec {
385 id: "test".to_string(),
386 frontmatter: SpecFrontmatter {
387 target_files: Some(vec!["file1.rs".to_string()]),
388 ..Default::default()
389 },
390 title: Some("Test".to_string()),
391 body: format!(
392 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n\n{}",
393 "word ".repeat(150)
394 ),
395 };
396
397 assert_eq!(calculate_complexity(&spec), ComplexityGrade::A);
398 }
399
400 #[test]
401 fn test_calculate_complexity_grade_b() {
402 use crate::spec::{Spec, SpecFrontmatter};
403
404 let spec = Spec {
406 id: "test".to_string(),
407 frontmatter: SpecFrontmatter {
408 target_files: Some(vec![
409 "file1.rs".to_string(),
410 "file2.rs".to_string(),
411 "file3.rs".to_string(),
412 ]),
413 ..Default::default()
414 },
415 title: Some("Test".to_string()),
416 body: format!(
417 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n- [ ] Third\n- [ ] Fourth\n- [ ] Fifth\n\n{}",
418 "word ".repeat(300)
419 ),
420 };
421
422 assert_eq!(calculate_complexity(&spec), ComplexityGrade::B);
423 }
424
425 #[test]
426 fn test_calculate_complexity_grade_c() {
427 use crate::spec::{Spec, SpecFrontmatter};
428
429 let spec = Spec {
431 id: "test".to_string(),
432 frontmatter: SpecFrontmatter {
433 target_files: Some(vec![
434 "file1.rs".to_string(),
435 "file2.rs".to_string(),
436 "file3.rs".to_string(),
437 "file4.rs".to_string(),
438 ]),
439 ..Default::default()
440 },
441 title: Some("Test".to_string()),
442 body: format!(
443 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n- [ ] Third\n- [ ] Fourth\n- [ ] Fifth\n- [ ] Sixth\n\n{}",
444 "word ".repeat(500)
445 ),
446 };
447
448 assert_eq!(calculate_complexity(&spec), ComplexityGrade::C);
449 }
450
451 #[test]
452 fn test_calculate_complexity_grade_d_criteria() {
453 use crate::spec::{Spec, SpecFrontmatter};
454
455 let spec = Spec {
457 id: "test".to_string(),
458 frontmatter: SpecFrontmatter {
459 target_files: Some(vec!["file1.rs".to_string(), "file2.rs".to_string()]),
460 ..Default::default()
461 },
462 title: Some("Test".to_string()),
463 body: format!(
464 "## Acceptance Criteria\n{}\n\n{}",
465 (1..=10)
466 .map(|i| format!("- [ ] Item {}", i))
467 .collect::<Vec<_>>()
468 .join("\n"),
469 "word ".repeat(100)
470 ),
471 };
472
473 assert_eq!(calculate_complexity(&spec), ComplexityGrade::D);
474 }
475
476 #[test]
477 fn test_calculate_complexity_grade_d_files() {
478 use crate::spec::{Spec, SpecFrontmatter};
479
480 let spec = Spec {
482 id: "test".to_string(),
483 frontmatter: SpecFrontmatter {
484 target_files: Some(vec![
485 "file1.rs".to_string(),
486 "file2.rs".to_string(),
487 "file3.rs".to_string(),
488 "file4.rs".to_string(),
489 "file5.rs".to_string(),
490 ]),
491 ..Default::default()
492 },
493 title: Some("Test".to_string()),
494 body: format!(
495 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n\n{}",
496 "word ".repeat(100)
497 ),
498 };
499
500 assert_eq!(calculate_complexity(&spec), ComplexityGrade::D);
501 }
502
503 #[test]
504 fn test_calculate_complexity_grade_d_words() {
505 use crate::spec::{Spec, SpecFrontmatter};
506
507 let spec = Spec {
509 id: "test".to_string(),
510 frontmatter: SpecFrontmatter {
511 target_files: Some(vec!["file1.rs".to_string()]),
512 ..Default::default()
513 },
514 title: Some("Test".to_string()),
515 body: format!(
516 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n\n{}",
517 "word ".repeat(700)
518 ),
519 };
520
521 assert_eq!(calculate_complexity(&spec), ComplexityGrade::D);
522 }
523
524 #[test]
525 fn test_calculate_complexity_no_target_files() {
526 use crate::spec::{Spec, SpecFrontmatter};
527
528 let spec = Spec {
530 id: "test".to_string(),
531 frontmatter: SpecFrontmatter {
532 target_files: None,
533 ..Default::default()
534 },
535 title: Some("Test".to_string()),
536 body:
537 "## Acceptance Criteria\n- [ ] First\n- [ ] Second\n\nSome content here with words."
538 .to_string(),
539 };
540
541 assert_eq!(calculate_complexity(&spec), ComplexityGrade::A);
542 }
543
544 #[test]
545 fn test_calculate_complexity_empty_body() {
546 use crate::spec::{Spec, SpecFrontmatter};
547
548 let spec = Spec {
550 id: "test".to_string(),
551 frontmatter: SpecFrontmatter {
552 target_files: Some(vec!["file1.rs".to_string()]),
553 ..Default::default()
554 },
555 title: Some("Test".to_string()),
556 body: String::new(),
557 };
558
559 assert_eq!(calculate_complexity(&spec), ComplexityGrade::A);
560 }
561}