1use crate::config::Config;
9use crate::scoring::ConfidenceGrade;
10use crate::spec::Spec;
11
12const IMPERATIVE_VERBS: &[&str] = &[
14 "implement",
15 "add",
16 "create",
17 "update",
18 "fix",
19 "remove",
20 "delete",
21 "refactor",
22 "test",
23 "verify",
24 "ensure",
25 "validate",
26 "configure",
27 "setup",
28 "install",
29 "deploy",
30 "build",
31 "run",
32 "execute",
33 "check",
34 "document",
35 "write",
36 "read",
37 "parse",
38 "handle",
39 "process",
40 "calculate",
41 "compute",
42 "convert",
43 "transform",
44 "migrate",
45 "upgrade",
46 "downgrade",
47];
48
49pub fn calculate_confidence(spec: &Spec, _config: &Config) -> ConfidenceGrade {
70 if spec.body.trim().is_empty() {
72 return ConfidenceGrade::D;
73 }
74
75 let (bullet_lines, paragraph_lines) = count_bullets_and_paragraphs(&spec.body);
77
78 let bullet_ratio = if bullet_lines + paragraph_lines == 0 {
80 0.0
81 } else {
82 bullet_lines as f64 / (bullet_lines + paragraph_lines) as f64
83 };
84
85 let bullets_with_verbs = count_bullets_with_imperative_verbs(&spec.body);
87 let verb_ratio = if bullet_lines == 0 {
88 0.0
89 } else {
90 bullets_with_verbs as f64 / bullet_lines as f64
91 };
92
93 let vague_count = count_all_vague_instances(&spec.body);
95
96 if bullet_ratio < 0.10 || vague_count > 8 {
99 return ConfidenceGrade::D;
100 }
101
102 if bullet_ratio > 0.60 && verb_ratio > 0.80 && vague_count == 0 {
104 return ConfidenceGrade::A;
105 }
106
107 if bullet_ratio > 0.30 && verb_ratio > 0.50 && vague_count < 3 {
109 return ConfidenceGrade::B;
110 }
111
112 ConfidenceGrade::C
114}
115
116fn count_bullets_and_paragraphs(body: &str) -> (usize, usize) {
123 let mut bullet_count = 0;
124 let mut paragraph_count = 0;
125 let mut in_code_fence = false;
126
127 for line in body.lines() {
128 let trimmed = line.trim();
129
130 if trimmed.starts_with("```") {
132 in_code_fence = !in_code_fence;
133 continue;
134 }
135
136 if trimmed.is_empty() || in_code_fence || trimmed.starts_with('#') {
138 continue;
139 }
140
141 if trimmed == "-" || trimmed == "*" {
143 continue;
144 }
145
146 if let Some(content) = trimmed
148 .strip_prefix("- ")
149 .or_else(|| trimmed.strip_prefix("* "))
150 {
151 if !content.trim().is_empty() {
153 bullet_count += 1;
154 }
155 } else {
156 paragraph_count += 1;
158 }
159 }
160
161 (bullet_count, paragraph_count)
162}
163
164fn count_all_vague_instances(body: &str) -> usize {
170 let body_lower = body.to_lowercase();
171 let mut count = 0;
172
173 for pattern in super::vague::DEFAULT_VAGUE_PATTERNS {
174 let pattern_lower = pattern.to_lowercase();
175
176 let mut start = 0;
178 while let Some(pos) = body_lower[start..].find(&pattern_lower) {
179 count += 1;
180 start += pos + pattern_lower.len();
181 }
182 }
183
184 count
185}
186
187fn count_bullets_with_imperative_verbs(body: &str) -> usize {
193 let mut count = 0;
194 let mut in_code_fence = false;
195
196 for line in body.lines() {
197 let trimmed = line.trim();
198
199 if trimmed.starts_with("```") {
201 in_code_fence = !in_code_fence;
202 continue;
203 }
204
205 if in_code_fence {
207 continue;
208 }
209
210 let content = if let Some(c) = trimmed
212 .strip_prefix("- ")
213 .or_else(|| trimmed.strip_prefix("* "))
214 {
215 c
216 } else {
217 continue;
218 };
219
220 let content = if content.trim_start().starts_with("[") {
222 if let Some(pos) = content.find(']') {
223 &content[pos + 1..]
224 } else {
225 content
226 }
227 } else {
228 content
229 };
230
231 let first_word = content.split_whitespace().next();
233
234 if let Some(word) = first_word {
236 let word_lower = word.to_lowercase();
237 if IMPERATIVE_VERBS.contains(&word_lower.as_str()) {
238 count += 1;
239 }
240 }
241 }
242
243 count
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::spec::{Spec, SpecFrontmatter};
250
251 fn make_config() -> Config {
252 Config {
254 project: crate::config::ProjectConfig {
255 name: "test".to_string(),
256 prefix: None,
257 silent: false,
258 },
259 defaults: crate::config::DefaultsConfig::default(),
260 providers: crate::provider::ProviderConfig::default(),
261 parallel: crate::config::ParallelConfig::default(),
262 repos: vec![],
263 enterprise: crate::config::EnterpriseConfig::default(),
264 approval: crate::config::ApprovalConfig::default(),
265 validation: crate::config::OutputValidationConfig::default(),
266 site: crate::config::SiteConfig::default(),
267 lint: crate::config::LintConfig::default(),
268 watch: crate::config::WatchConfig::default(),
269 }
270 }
271
272 #[test]
273 fn test_empty_body_returns_grade_d() {
274 let spec = Spec {
275 id: "test".to_string(),
276 frontmatter: SpecFrontmatter::default(),
277 title: Some("Test".to_string()),
278 body: String::new(),
279 };
280
281 let config = make_config();
282 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::D);
283 }
284
285 #[test]
286 fn test_grade_a_high_bullet_ratio_no_vague() {
287 let spec = Spec {
291 id: "test".to_string(),
292 frontmatter: SpecFrontmatter::default(),
293 title: Some("Test".to_string()),
294 body: r#"
295## Acceptance Criteria
296
297- [ ] Implement feature A
298- [ ] Add functionality B
299- [ ] Create component C
300- [ ] Update module D
301- [ ] Fix bug E
302- [ ] Remove deprecated code
303- [ ] Test the implementation
304- [ ] Verify the results
305- [ ] Document the changes
306- [ ] Deploy to production
307
308Some paragraph here.
309Another paragraph here.
310"#
311 .to_string(),
312 };
313
314 let config = make_config();
315 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::A);
316 }
317
318 #[test]
319 fn test_grade_b_medium_bullet_ratio_few_vague() {
320 let spec = Spec {
324 id: "test".to_string(),
325 frontmatter: SpecFrontmatter::default(),
326 title: Some("Test".to_string()),
327 body: r#"
328## Acceptance Criteria
329
330- [ ] Implement feature A
331- [ ] Add functionality B
332- [ ] Create component C
333- [ ] Update module D
334- [ ] Fix bug E
335- [ ] Deploy to production
336
337Some paragraph here.
338Another paragraph here.
339Third paragraph.
340Fourth paragraph with improve here.
341"#
342 .to_string(),
343 };
344
345 let config = make_config();
346 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::B);
347 }
348
349 #[test]
350 fn test_grade_d_low_bullet_ratio() {
351 let spec = Spec {
353 id: "test".to_string(),
354 frontmatter: SpecFrontmatter::default(),
355 title: Some("Test".to_string()),
356 body: r#"
357This is a wall of prose.
358It has many paragraphs.
359But very few bullets.
360This makes it hard to understand.
361Requirements should be clear.
362Bullets help with clarity.
363Paragraphs can be ambiguous.
364We need more structure.
365This spec is poorly written.
366It will get a low grade.
367
368- [ ] Implement something
369"#
370 .to_string(),
371 };
372
373 let config = make_config();
374 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::D);
375 }
376
377 #[test]
378 fn test_grade_d_many_vague_patterns() {
379 let spec = Spec {
382 id: "test".to_string(),
383 frontmatter: SpecFrontmatter::default(),
384 title: Some("Test".to_string()),
385 body: r#"
386## Acceptance Criteria
387
388- [ ] Improve performance as needed
389- [ ] Add features and related functionality, etc
390- [ ] Create tests etc as needed
391- [ ] Update components as needed
392- [ ] Fix bugs and related issues
393- [ ] Similar improvements needed
394"#
395 .to_string(),
396 };
397
398 let config = make_config();
399 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::D);
400 }
401
402 #[test]
403 fn test_count_bullets_and_paragraphs() {
404 let body = r#"
405This is a paragraph.
406
407- [ ] This is a bullet
408- [ ] Another bullet
409
410Another paragraph here.
411
412- This is also a bullet
413
414# This is a heading (not counted)
415
416Final paragraph.
417"#;
418
419 let (bullets, paragraphs) = count_bullets_and_paragraphs(body);
420 assert_eq!(bullets, 3);
421 assert_eq!(paragraphs, 3); }
423
424 #[test]
425 fn test_empty_bullets_not_counted() {
426 let body = r#"
427- [ ] Valid bullet
428-
429- [ ] Another valid bullet
430"#;
431
432 let (bullets, paragraphs) = count_bullets_and_paragraphs(body);
433 assert_eq!(bullets, 2); assert_eq!(paragraphs, 0);
435 }
436
437 #[test]
438 fn test_count_bullets_with_imperative_verbs() {
439 let body = r#"
440- [ ] Implement feature A
441- [ ] Add functionality B
442- [ ] This does not start with a verb
443- [ ] Create component C
444- Update something without checkbox
445"#;
446
447 let count = count_bullets_with_imperative_verbs(body);
448 assert_eq!(count, 4); }
450
451 #[test]
452 fn test_code_blocks_ignored() {
453 let body = r#"
454- [ ] Implement feature
455
456```rust
457// This is code, not a bullet
458- This looks like a bullet but it's in a code block
459```
460
461- [ ] Add another feature
462"#;
463
464 let (bullets, _) = count_bullets_and_paragraphs(body);
465 assert_eq!(bullets, 2); }
467
468 #[test]
469 fn test_case_insensitive_verb_matching() {
470 let body = r#"
471- [ ] IMPLEMENT feature
472- [ ] Add functionality
473- [ ] CrEaTe component
474"#;
475
476 let count = count_bullets_with_imperative_verbs(body);
477 assert_eq!(count, 3); }
479
480 #[test]
481 fn test_grade_c_fallthrough() {
482 let spec = Spec {
486 id: "test".to_string(),
487 frontmatter: SpecFrontmatter::default(),
488 title: Some("Test".to_string()),
489 body: r#"
490## Acceptance Criteria
491
492- [ ] Implement feature A
493- [ ] Add functionality B as needed
494- [ ] Create tests etc
495
496Some paragraph here.
497Another paragraph with improve mentioned.
498Third paragraph and related stuff.
499"#
500 .to_string(),
501 };
502
503 let config = make_config();
504 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::C);
505 }
506}