1use crate::scoring::{
7 ACQualityGrade, ComplexityGrade, ConfidenceGrade, IsolationGrade, SpecScore,
8 SplittabilityGrade, TrafficLight,
9};
10
11pub fn determine_status(score: &SpecScore) -> TrafficLight {
22 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 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 TrafficLight::Review
42}
43
44pub fn generate_suggestions(score: &SpecScore) -> Vec<String> {
50 let mut suggestions = Vec::new();
51
52 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 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 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 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 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 suggestions.sort();
123 suggestions.dedup();
124
125 suggestions
126}
127
128pub fn generate_detailed_guidance(score: &SpecScore) -> String {
133 let mut output = String::new();
134
135 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 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 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 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 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 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 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 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 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 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 assert_eq!(suggestions.len(), 1);
467 assert!(suggestions[0].contains("Reduce criteria count"));
468 }
469}