Skip to main content

chant/score/
traffic_light.rs

1//! Traffic light status determination and suggestion generation.
2//!
3//! Combines dimension grades into an overall traffic light status and generates
4//! actionable suggestions for improving spec quality.
5
6use crate::scoring::{
7    ACQualityGrade, ComplexityGrade, ConfidenceGrade, IsolationGrade, SpecScore,
8    SplittabilityGrade, TrafficLight,
9};
10
11/// Determine overall traffic light status based on dimension grades
12///
13/// Traffic light logic:
14/// - Ready (green): Complexity ≤ B AND Confidence ≥ B AND AC Quality ≥ B
15/// - Refine (red): Complexity is D OR Confidence is D OR AC Quality is D
16/// - Review (yellow): All other cases (Complexity/Confidence/AC Quality is C)
17///
18/// Note: Splittability and Isolation do not affect traffic light status.
19/// Splittability C is good (focused spec), not a problem.
20/// Isolation is only for group analysis, not gating work.
21pub fn determine_status(score: &SpecScore) -> TrafficLight {
22    // Check for Refine conditions: core dimensions are D
23    // Splittability and Isolation do NOT contribute to traffic light
24    if matches!(score.complexity, ComplexityGrade::D)
25        || matches!(score.confidence, ConfidenceGrade::D)
26        || matches!(score.ac_quality, ACQualityGrade::D)
27    {
28        return TrafficLight::Refine;
29    }
30
31    // Check for Ready conditions: Complexity ≤ B AND Confidence ≥ B AND AC Quality ≥ B
32    let complexity_ok = matches!(score.complexity, ComplexityGrade::A | ComplexityGrade::B);
33    let confidence_ok = matches!(score.confidence, ConfidenceGrade::A | ConfidenceGrade::B);
34    let ac_quality_ok = matches!(score.ac_quality, ACQualityGrade::A | ACQualityGrade::B);
35
36    if complexity_ok && confidence_ok && ac_quality_ok {
37        return TrafficLight::Ready;
38    }
39
40    // All other cases: Review (any core dimension is C)
41    TrafficLight::Review
42}
43
44/// Generate actionable suggestions based on failing dimensions
45///
46/// Suggestions are specific to each dimension that needs improvement.
47/// Multiple failing dimensions will generate multiple suggestions.
48/// Suggestions are deduplicated to avoid repetition.
49pub fn generate_suggestions(score: &SpecScore) -> Vec<String> {
50    let mut suggestions = Vec::new();
51
52    // Complexity suggestions
53    match score.complexity {
54        ComplexityGrade::D => {
55            suggestions.push("Reduce criteria count or split spec into smaller pieces".to_string());
56        }
57        ComplexityGrade::C => {
58            suggestions.push("Consider reducing scope or splitting into subtasks".to_string());
59        }
60        _ => {}
61    }
62
63    // Confidence suggestions - match what the scorer actually checks
64    match score.confidence {
65        ConfidenceGrade::D => {
66            suggestions.push("Use bullet points instead of prose paragraphs; avoid vague words like 'improve', 'as needed', 'etc', 'similar'".to_string());
67        }
68        ConfidenceGrade::C => {
69            suggestions.push("Increase bullet-to-prose ratio (>50%); start bullets with imperative verbs; reduce vague language".to_string());
70        }
71        _ => {}
72    }
73
74    // AC Quality suggestions - match what the scorer actually checks (count-based)
75    match score.ac_quality {
76        ACQualityGrade::D => {
77            suggestions.push("Add at least 1 acceptance criteria checkbox".to_string());
78        }
79        ACQualityGrade::C => {
80            suggestions.push("Add at least 2 acceptance criteria checkboxes".to_string());
81        }
82        ACQualityGrade::B => {
83            suggestions
84                .push("Add at least 4 acceptance criteria checkboxes for Grade A".to_string());
85        }
86        _ => {}
87    }
88
89    // Splittability suggestions - match what the scorer actually checks
90    match score.splittability {
91        SplittabilityGrade::D => {
92            suggestions.push(
93                "Remove coupling keywords ('tightly coupled', 'depends on each other')".to_string(),
94            );
95        }
96        SplittabilityGrade::C => {
97            suggestions.push(
98                "Add target_files and organize with ## section headers to improve splittability"
99                    .to_string(),
100            );
101        }
102        _ => {}
103    }
104
105    // Isolation suggestions (optional field)
106    if let Some(isolation) = score.isolation {
107        match isolation {
108            IsolationGrade::D => {
109                suggestions.push(
110                    "Reduce cross-references between group members to improve isolation"
111                        .to_string(),
112                );
113            }
114            IsolationGrade::C => {
115                suggestions.push("Consider reducing coupling between group members".to_string());
116            }
117            _ => {}
118        }
119    }
120
121    // Deduplicate suggestions (though our specific suggestions shouldn't duplicate)
122    suggestions.sort();
123    suggestions.dedup();
124
125    suggestions
126}
127
128/// Generate detailed actionable guidance with examples for failing dimensions
129///
130/// Provides comprehensive, example-driven guidance on how to fix quality issues.
131/// Returns a multi-line string with "Why This Matters" and "How to Fix" sections.
132pub fn generate_detailed_guidance(score: &SpecScore) -> String {
133    let mut output = String::new();
134
135    // Only generate guidance if there are issues
136    if matches!(score.traffic_light, TrafficLight::Ready) {
137        return output;
138    }
139
140    output.push_str("\nWhy This Matters:\n");
141    output.push_str(
142        "  Agents perform best with ISOLATED tasks that have TESTABLE acceptance criteria.\n",
143    );
144    output.push_str("  Vague specs lead to scope creep, wrong assumptions, and wasted tokens.\n");
145    output.push_str("\nHow to Fix:\n");
146
147    // Confidence guidance
148    if matches!(score.confidence, ConfidenceGrade::C | ConfidenceGrade::D) {
149        let grade_letter = match score.confidence {
150            ConfidenceGrade::D => "D",
151            ConfidenceGrade::C => "C",
152            _ => "",
153        };
154        output.push_str(&format!("\n  Confidence ({} → A):\n", grade_letter));
155        output.push_str("    ✗ \"Update the API\"\n");
156        output.push_str("    ✓ \"In src/api/users.rs, add `get_user_by_email()` method\"\n");
157        output.push_str("    → Add specific file paths, function names, or line numbers\n");
158    }
159
160    // Splittability guidance
161    if matches!(
162        score.splittability,
163        SplittabilityGrade::C | SplittabilityGrade::D
164    ) {
165        let grade_letter = match score.splittability {
166            SplittabilityGrade::D => "D",
167            SplittabilityGrade::C => "C",
168            _ => "",
169        };
170        output.push_str(&format!("\n  Splittability ({} → A):\n", grade_letter));
171        output.push_str("    ✗ \"Add auth and update docs and fix tests\"\n");
172        output.push_str(
173            "    ✓ Split into 3 specs: auth, docs, tests (use depends_on for ordering)\n",
174        );
175        output.push_str("    → Each spec should do ONE thing\n");
176    }
177
178    // AC Quality guidance
179    if matches!(score.ac_quality, ACQualityGrade::C | ACQualityGrade::D) {
180        let grade_letter = match score.ac_quality {
181            ACQualityGrade::D => "D",
182            ACQualityGrade::C => "C",
183            _ => "",
184        };
185        output.push_str(&format!("\n  AC Quality ({} → A):\n", grade_letter));
186        output.push_str("    ✗ \"- [ ] Code works correctly\"\n");
187        output.push_str("    ✗ \"- [ ] Tests pass\"\n");
188        output
189            .push_str("    ✓ \"- [ ] Add `validate_email()` fn in src/utils.rs returning bool\"\n");
190        output.push_str("    ✓ \"- [ ] `cargo test test_validate_email` passes\"\n");
191        output.push_str(
192            "    → Criteria must be: imperative verb + specific location + verifiable outcome\n",
193        );
194    }
195
196    // Complexity guidance
197    if matches!(score.complexity, ComplexityGrade::C | ComplexityGrade::D) {
198        let grade_letter = match score.complexity {
199            ComplexityGrade::D => "D",
200            ComplexityGrade::C => "C",
201            _ => "",
202        };
203        output.push_str(&format!("\n  Complexity ({} → B):\n", grade_letter));
204        output.push_str("    → Split large specs into smaller, focused tasks\n");
205        output.push_str("    → Aim for 1-5 acceptance criteria per spec\n");
206        output.push_str("    → Use `chant split <spec-id>` to break into subtasks\n");
207    }
208
209    output
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_determine_status_all_a_ready() {
218        let score = SpecScore {
219            complexity: ComplexityGrade::A,
220            confidence: ConfidenceGrade::A,
221            splittability: SplittabilityGrade::A,
222            isolation: Some(IsolationGrade::A),
223            ac_quality: ACQualityGrade::A,
224            traffic_light: TrafficLight::Ready,
225        };
226
227        assert_eq!(determine_status(&score), TrafficLight::Ready);
228    }
229
230    #[test]
231    fn test_determine_status_b_grades_ready() {
232        let score = SpecScore {
233            complexity: ComplexityGrade::B,
234            confidence: ConfidenceGrade::B,
235            splittability: SplittabilityGrade::A,
236            isolation: None,
237            ac_quality: ACQualityGrade::B,
238            traffic_light: TrafficLight::Ready,
239        };
240
241        assert_eq!(determine_status(&score), TrafficLight::Ready);
242    }
243
244    #[test]
245    fn test_determine_status_complexity_c_review() {
246        let score = SpecScore {
247            complexity: ComplexityGrade::C,
248            confidence: ConfidenceGrade::A,
249            splittability: SplittabilityGrade::A,
250            isolation: None,
251            ac_quality: ACQualityGrade::A,
252            traffic_light: TrafficLight::Review,
253        };
254
255        assert_eq!(determine_status(&score), TrafficLight::Review);
256    }
257
258    #[test]
259    fn test_determine_status_confidence_c_review() {
260        let score = SpecScore {
261            complexity: ComplexityGrade::B,
262            confidence: ConfidenceGrade::C,
263            splittability: SplittabilityGrade::A,
264            isolation: None,
265            ac_quality: ACQualityGrade::A,
266            traffic_light: TrafficLight::Review,
267        };
268
269        assert_eq!(determine_status(&score), TrafficLight::Review);
270    }
271
272    #[test]
273    fn test_determine_status_ac_quality_c_review() {
274        let score = SpecScore {
275            complexity: ComplexityGrade::A,
276            confidence: ConfidenceGrade::B,
277            splittability: SplittabilityGrade::A,
278            isolation: None,
279            ac_quality: ACQualityGrade::C,
280            traffic_light: TrafficLight::Review,
281        };
282
283        assert_eq!(determine_status(&score), TrafficLight::Review);
284    }
285
286    #[test]
287    fn test_determine_status_complexity_d_refine() {
288        let score = SpecScore {
289            complexity: ComplexityGrade::D,
290            confidence: ConfidenceGrade::A,
291            splittability: SplittabilityGrade::A,
292            isolation: None,
293            ac_quality: ACQualityGrade::A,
294            traffic_light: TrafficLight::Refine,
295        };
296
297        assert_eq!(determine_status(&score), TrafficLight::Refine);
298    }
299
300    #[test]
301    fn test_determine_status_confidence_d_refine() {
302        let score = SpecScore {
303            complexity: ComplexityGrade::A,
304            confidence: ConfidenceGrade::D,
305            splittability: SplittabilityGrade::A,
306            isolation: None,
307            ac_quality: ACQualityGrade::A,
308            traffic_light: TrafficLight::Refine,
309        };
310
311        assert_eq!(determine_status(&score), TrafficLight::Refine);
312    }
313
314    #[test]
315    fn test_determine_status_isolation_d_still_ready() {
316        // Isolation D does not affect traffic light anymore
317        let score = SpecScore {
318            complexity: ComplexityGrade::A,
319            confidence: ConfidenceGrade::A,
320            splittability: SplittabilityGrade::A,
321            isolation: Some(IsolationGrade::D),
322            ac_quality: ACQualityGrade::A,
323            traffic_light: TrafficLight::Ready,
324        };
325
326        assert_eq!(determine_status(&score), TrafficLight::Ready);
327    }
328
329    #[test]
330    fn test_determine_status_splittability_d_still_ready() {
331        // Splittability D does not affect traffic light anymore
332        let score = SpecScore {
333            complexity: ComplexityGrade::A,
334            confidence: ConfidenceGrade::A,
335            splittability: SplittabilityGrade::D,
336            isolation: None,
337            ac_quality: ACQualityGrade::A,
338            traffic_light: TrafficLight::Ready,
339        };
340
341        assert_eq!(determine_status(&score), TrafficLight::Ready);
342    }
343
344    #[test]
345    fn test_determine_status_splittability_c_still_ready() {
346        // Splittability C is good (focused spec), should not prevent Ready
347        let score = SpecScore {
348            complexity: ComplexityGrade::B,
349            confidence: ConfidenceGrade::B,
350            splittability: SplittabilityGrade::C,
351            isolation: None,
352            ac_quality: ACQualityGrade::B,
353            traffic_light: TrafficLight::Ready,
354        };
355
356        assert_eq!(determine_status(&score), TrafficLight::Ready);
357    }
358
359    #[test]
360    fn test_generate_suggestions_all_a_no_suggestions() {
361        let score = SpecScore {
362            complexity: ComplexityGrade::A,
363            confidence: ConfidenceGrade::A,
364            splittability: SplittabilityGrade::A,
365            isolation: Some(IsolationGrade::A),
366            ac_quality: ACQualityGrade::A,
367            traffic_light: TrafficLight::Ready,
368        };
369
370        let suggestions = generate_suggestions(&score);
371        assert!(suggestions.is_empty());
372    }
373
374    #[test]
375    fn test_generate_suggestions_complexity_d() {
376        let score = SpecScore {
377            complexity: ComplexityGrade::D,
378            confidence: ConfidenceGrade::A,
379            splittability: SplittabilityGrade::A,
380            isolation: None,
381            ac_quality: ACQualityGrade::A,
382            traffic_light: TrafficLight::Refine,
383        };
384
385        let suggestions = generate_suggestions(&score);
386        assert_eq!(suggestions.len(), 1);
387        assert!(suggestions[0].contains("Reduce criteria count"));
388    }
389
390    #[test]
391    fn test_generate_suggestions_confidence_c() {
392        let score = SpecScore {
393            complexity: ComplexityGrade::B,
394            confidence: ConfidenceGrade::C,
395            splittability: SplittabilityGrade::A,
396            isolation: None,
397            ac_quality: ACQualityGrade::A,
398            traffic_light: TrafficLight::Review,
399        };
400
401        let suggestions = generate_suggestions(&score);
402        assert_eq!(suggestions.len(), 1);
403        assert!(suggestions[0].contains("bullet-to-prose ratio"));
404    }
405
406    #[test]
407    fn test_generate_suggestions_multiple_dimensions() {
408        let score = SpecScore {
409            complexity: ComplexityGrade::D,
410            confidence: ConfidenceGrade::C,
411            splittability: SplittabilityGrade::C,
412            isolation: Some(IsolationGrade::C),
413            ac_quality: ACQualityGrade::D,
414            traffic_light: TrafficLight::Refine,
415        };
416
417        let suggestions = generate_suggestions(&score);
418        assert_eq!(suggestions.len(), 5);
419        // Verify each dimension has a suggestion
420        assert!(suggestions
421            .iter()
422            .any(|s| s.contains("Reduce criteria count")));
423        assert!(suggestions
424            .iter()
425            .any(|s| s.contains("bullet-to-prose ratio")));
426        assert!(suggestions.iter().any(|s| s.contains("target_files")));
427        assert!(suggestions
428            .iter()
429            .any(|s| s.contains("reducing coupling between group")));
430        assert!(suggestions.iter().any(|s| s.contains("checkbox")));
431    }
432
433    #[test]
434    fn test_generate_suggestions_no_duplicates() {
435        let score = SpecScore {
436            complexity: ComplexityGrade::C,
437            confidence: ConfidenceGrade::C,
438            splittability: SplittabilityGrade::A,
439            isolation: None,
440            ac_quality: ACQualityGrade::A,
441            traffic_light: TrafficLight::Review,
442        };
443
444        let suggestions = generate_suggestions(&score);
445        // Check for uniqueness
446        let unique_count = suggestions.len();
447        let mut sorted = suggestions.clone();
448        sorted.sort();
449        sorted.dedup();
450        assert_eq!(unique_count, sorted.len());
451    }
452
453    #[test]
454    fn test_generate_suggestions_isolation_none() {
455        let score = SpecScore {
456            complexity: ComplexityGrade::D,
457            confidence: ConfidenceGrade::A,
458            splittability: SplittabilityGrade::A,
459            isolation: None,
460            ac_quality: ACQualityGrade::A,
461            traffic_light: TrafficLight::Refine,
462        };
463
464        let suggestions = generate_suggestions(&score);
465        // Should only have complexity suggestion, no isolation suggestion
466        assert_eq!(suggestions.len(), 1);
467        assert!(suggestions[0].contains("Reduce criteria count"));
468    }
469}