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.20 || vague_count > 5 {
99 return ConfidenceGrade::D;
100 }
101
102 if bullet_ratio > 0.80 && verb_ratio > 0.80 && vague_count == 0 {
104 return ConfidenceGrade::A;
105 }
106
107 if bullet_ratio > 0.50 && verb_ratio > 0.50 && vague_count < 3 {
109 return ConfidenceGrade::B;
110 }
111
112 if bullet_ratio > 0.20 && verb_ratio > 0.30 && (3..=5).contains(&vague_count) {
114 return ConfidenceGrade::C;
115 }
116
117 ConfidenceGrade::C
119}
120
121fn count_bullets_and_paragraphs(body: &str) -> (usize, usize) {
127 let mut bullet_count = 0;
128 let mut paragraph_count = 0;
129 let mut in_code_fence = false;
130
131 for line in body.lines() {
132 let trimmed = line.trim();
133
134 if trimmed.starts_with("```") {
136 in_code_fence = !in_code_fence;
137 continue;
138 }
139
140 if trimmed.is_empty() || in_code_fence || trimmed.starts_with('#') {
142 continue;
143 }
144
145 if trimmed == "-" || trimmed == "*" {
147 continue;
148 }
149
150 if let Some(content) = trimmed
152 .strip_prefix("- ")
153 .or_else(|| trimmed.strip_prefix("* "))
154 {
155 if !content.trim().is_empty() {
157 bullet_count += 1;
158 }
159 } else {
160 paragraph_count += 1;
162 }
163 }
164
165 (bullet_count, paragraph_count)
166}
167
168fn count_all_vague_instances(body: &str) -> usize {
174 let body_lower = body.to_lowercase();
175 let mut count = 0;
176
177 for pattern in super::vague::DEFAULT_VAGUE_PATTERNS {
178 let pattern_lower = pattern.to_lowercase();
179
180 let mut start = 0;
182 while let Some(pos) = body_lower[start..].find(&pattern_lower) {
183 count += 1;
184 start += pos + pattern_lower.len();
185 }
186 }
187
188 count
189}
190
191fn count_bullets_with_imperative_verbs(body: &str) -> usize {
197 let mut count = 0;
198 let mut in_code_fence = false;
199
200 for line in body.lines() {
201 let trimmed = line.trim();
202
203 if trimmed.starts_with("```") {
205 in_code_fence = !in_code_fence;
206 continue;
207 }
208
209 if in_code_fence {
211 continue;
212 }
213
214 let content = if let Some(c) = trimmed
216 .strip_prefix("- ")
217 .or_else(|| trimmed.strip_prefix("* "))
218 {
219 c
220 } else {
221 continue;
222 };
223
224 let content = if content.trim_start().starts_with("[") {
226 if let Some(pos) = content.find(']') {
227 &content[pos + 1..]
228 } else {
229 content
230 }
231 } else {
232 content
233 };
234
235 let first_word = content.split_whitespace().next();
237
238 if let Some(word) = first_word {
240 let word_lower = word.to_lowercase();
241 if IMPERATIVE_VERBS.contains(&word_lower.as_str()) {
242 count += 1;
243 }
244 }
245 }
246
247 count
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::spec::{Spec, SpecFrontmatter};
254
255 fn make_config() -> Config {
256 Config {
258 project: crate::config::ProjectConfig {
259 name: "test".to_string(),
260 prefix: None,
261 silent: false,
262 },
263 defaults: crate::config::DefaultsConfig::default(),
264 providers: crate::provider::ProviderConfig::default(),
265 parallel: crate::config::ParallelConfig::default(),
266 repos: vec![],
267 enterprise: crate::config::EnterpriseConfig::default(),
268 approval: crate::config::ApprovalConfig::default(),
269 validation: crate::config::OutputValidationConfig::default(),
270 site: crate::config::SiteConfig::default(),
271 lint: crate::config::LintConfig::default(),
272 watch: crate::config::WatchConfig::default(),
273 }
274 }
275
276 #[test]
277 fn test_empty_body_returns_grade_d() {
278 let spec = Spec {
279 id: "test".to_string(),
280 frontmatter: SpecFrontmatter::default(),
281 title: Some("Test".to_string()),
282 body: String::new(),
283 };
284
285 let config = make_config();
286 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::D);
287 }
288
289 #[test]
290 fn test_grade_a_high_bullet_ratio_no_vague() {
291 let spec = Spec {
295 id: "test".to_string(),
296 frontmatter: SpecFrontmatter::default(),
297 title: Some("Test".to_string()),
298 body: r#"
299## Acceptance Criteria
300
301- [ ] Implement feature A
302- [ ] Add functionality B
303- [ ] Create component C
304- [ ] Update module D
305- [ ] Fix bug E
306- [ ] Remove deprecated code
307- [ ] Test the implementation
308- [ ] Verify the results
309- [ ] Document the changes
310- [ ] Deploy to production
311
312Some paragraph here.
313Another paragraph here.
314"#
315 .to_string(),
316 };
317
318 let config = make_config();
319 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::A);
320 }
321
322 #[test]
323 fn test_grade_b_medium_bullet_ratio_few_vague() {
324 let spec = Spec {
327 id: "test".to_string(),
328 frontmatter: SpecFrontmatter::default(),
329 title: Some("Test".to_string()),
330 body: r#"
331## Acceptance Criteria
332
333- [ ] Implement feature A
334- [ ] Add functionality B
335- [ ] Create component C
336- [ ] Update module D
337- [ ] Fix bug E
338- [ ] Deploy to production
339
340Some paragraph here.
341Another paragraph here.
342Third paragraph.
343Fourth paragraph with improve here.
344"#
345 .to_string(),
346 };
347
348 let config = make_config();
349 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::B);
350 }
351
352 #[test]
353 fn test_grade_d_low_bullet_ratio() {
354 let spec = Spec {
356 id: "test".to_string(),
357 frontmatter: SpecFrontmatter::default(),
358 title: Some("Test".to_string()),
359 body: r#"
360This is a wall of prose.
361It has many paragraphs.
362But very few bullets.
363This makes it hard to understand.
364Requirements should be clear.
365Bullets help with clarity.
366Paragraphs can be ambiguous.
367We need more structure.
368This spec is poorly written.
369It will get a low grade.
370
371- [ ] Implement something
372"#
373 .to_string(),
374 };
375
376 let config = make_config();
377 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::D);
378 }
379
380 #[test]
381 fn test_grade_d_many_vague_patterns() {
382 let spec = Spec {
384 id: "test".to_string(),
385 frontmatter: SpecFrontmatter::default(),
386 title: Some("Test".to_string()),
387 body: r#"
388## Acceptance Criteria
389
390- [ ] Improve performance as needed
391- [ ] Add features and related functionality
392- [ ] Create tests etc
393- [ ] Update components as needed
394- [ ] Fix bugs and related issues
395- [ ] Similar improvements needed
396"#
397 .to_string(),
398 };
399
400 let config = make_config();
401 assert_eq!(calculate_confidence(&spec, &config), ConfidenceGrade::D);
402 }
403
404 #[test]
405 fn test_count_bullets_and_paragraphs() {
406 let body = r#"
407This is a paragraph.
408
409- [ ] This is a bullet
410- [ ] Another bullet
411
412Another paragraph here.
413
414- This is also a bullet
415
416# This is a heading (not counted)
417
418Final paragraph.
419"#;
420
421 let (bullets, paragraphs) = count_bullets_and_paragraphs(body);
422 assert_eq!(bullets, 3);
423 assert_eq!(paragraphs, 3); }
425
426 #[test]
427 fn test_empty_bullets_not_counted() {
428 let body = r#"
429- [ ] Valid bullet
430-
431- [ ] Another valid bullet
432"#;
433
434 let (bullets, paragraphs) = count_bullets_and_paragraphs(body);
435 assert_eq!(bullets, 2); assert_eq!(paragraphs, 0);
437 }
438
439 #[test]
440 fn test_count_bullets_with_imperative_verbs() {
441 let body = r#"
442- [ ] Implement feature A
443- [ ] Add functionality B
444- [ ] This does not start with a verb
445- [ ] Create component C
446- Update something without checkbox
447"#;
448
449 let count = count_bullets_with_imperative_verbs(body);
450 assert_eq!(count, 4); }
452
453 #[test]
454 fn test_code_blocks_ignored() {
455 let body = r#"
456- [ ] Implement feature
457
458```rust
459// This is code, not a bullet
460- This looks like a bullet but it's in a code block
461```
462
463- [ ] Add another feature
464"#;
465
466 let (bullets, _) = count_bullets_and_paragraphs(body);
467 assert_eq!(bullets, 2); }
469
470 #[test]
471 fn test_case_insensitive_verb_matching() {
472 let body = r#"
473- [ ] IMPLEMENT feature
474- [ ] Add functionality
475- [ ] CrEaTe component
476"#;
477
478 let count = count_bullets_with_imperative_verbs(body);
479 assert_eq!(count, 3); }
481
482 #[test]
483 fn test_grade_c_with_some_vague_patterns() {
484 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}