1use serde::{Deserialize, Serialize};
6
7use crate::scoring::{ACQualityGrade, ComplexityGrade, ConfidenceGrade, SplittabilityGrade};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct QualityAssessment {
12 pub complexity: ComplexityGrade,
14 pub confidence: ConfidenceGrade,
16 pub splittability: SplittabilityGrade,
18 pub ac_quality: ACQualityGrade,
20}
21
22pub fn assess_quality(spec: &crate::spec::Spec) -> QualityAssessment {
26 use crate::score::{ac_quality, confidence, splittability};
27 use crate::scoring::calculate_complexity;
28
29 let config = make_minimal_config();
31
32 let complexity = calculate_complexity(spec);
34 let confidence_grade = confidence::calculate_confidence(spec, &config);
35 let splittability_grade = splittability::calculate_splittability(spec);
36
37 let criteria = extract_acceptance_criteria(spec);
39 let ac_quality_grade = ac_quality::calculate_ac_quality(&criteria);
40
41 QualityAssessment {
42 complexity,
43 confidence: confidence_grade,
44 splittability: splittability_grade,
45 ac_quality: ac_quality_grade,
46 }
47}
48
49fn make_minimal_config() -> crate::config::Config {
50 crate::config::Config {
51 project: crate::config::ProjectConfig {
52 name: "test".to_string(),
53 prefix: None,
54 silent: false,
55 },
56 defaults: crate::config::DefaultsConfig::default(),
57 providers: crate::provider::ProviderConfig::default(),
58 parallel: crate::config::ParallelConfig::default(),
59 repos: vec![],
60 enterprise: crate::config::EnterpriseConfig::default(),
61 approval: crate::config::ApprovalConfig::default(),
62 validation: crate::config::OutputValidationConfig::default(),
63 site: crate::config::SiteConfig::default(),
64 lint: crate::config::LintConfig::default(),
65 watch: crate::config::WatchConfig::default(),
66 }
67}
68
69fn extract_acceptance_criteria(spec: &crate::spec::Spec) -> Vec<String> {
71 let acceptance_criteria_marker = "## Acceptance Criteria";
72 let mut criteria = Vec::new();
73 let mut in_code_fence = false;
74 let mut in_ac_section = false;
75
76 for line in spec.body.lines() {
77 let trimmed = line.trim_start();
78
79 if trimmed.starts_with("```") {
80 in_code_fence = !in_code_fence;
81 continue;
82 }
83
84 if !in_code_fence && trimmed.starts_with(acceptance_criteria_marker) {
85 in_ac_section = true;
86 continue;
87 }
88
89 if in_ac_section && !in_code_fence && trimmed.starts_with("## ") {
91 break;
92 }
93
94 if in_ac_section
96 && !in_code_fence
97 && (trimmed.starts_with("- [ ]") || trimmed.starts_with("- [x]"))
98 {
99 let text = trimmed
101 .trim_start_matches("- [ ]")
102 .trim_start_matches("- [x]")
103 .trim()
104 .to_string();
105 criteria.push(text);
106 }
107 }
108
109 criteria
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::spec::{Spec, SpecFrontmatter};
116
117 #[test]
118 fn test_assess_quality_simple_spec() {
119 let spec = Spec {
120 id: "test".to_string(),
121 frontmatter: SpecFrontmatter {
122 target_files: Some(vec!["file1.rs".to_string()]),
123 ..Default::default()
124 },
125 title: Some("Simple test spec".to_string()),
126 body: r#"## Problem
127
128This is a simple test spec.
129
130## Solution
131
132Do something simple.
133
134## Acceptance Criteria
135
136- [ ] Create a new file
137- [ ] Add a function
138- [ ] Write a test
139
140Simple implementation."#
141 .to_string(),
142 };
143
144 let assessment = assess_quality(&spec);
145
146 assert_eq!(assessment.complexity, ComplexityGrade::A);
148
149 assert!(matches!(
151 assessment.ac_quality,
152 ACQualityGrade::A | ACQualityGrade::B | ACQualityGrade::C
153 ));
154 }
155
156 #[test]
157 fn test_assess_quality_empty_body() {
158 let spec = Spec {
159 id: "test".to_string(),
160 frontmatter: SpecFrontmatter::default(),
161 title: Some("Empty spec".to_string()),
162 body: String::new(),
163 };
164
165 let assessment = assess_quality(&spec);
166
167 assert_eq!(assessment.confidence, ConfidenceGrade::D);
169 }
170
171 #[test]
172 fn test_assess_quality_detailed_ac() {
173 let spec = Spec {
174 id: "test".to_string(),
175 frontmatter: SpecFrontmatter {
176 target_files: Some(vec!["file1.rs".to_string()]),
177 ..Default::default()
178 },
179 title: Some("Detailed spec".to_string()),
180 body: r#"## Problem
181
182Need comprehensive acceptance criteria.
183
184## Acceptance Criteria
185
186- [ ] Implement function calculate_total with proper error handling
187- [ ] Add unit tests covering edge cases for empty inputs
188- [ ] Create integration test validating end-to-end workflow
189- [ ] Update API documentation with new endpoint details
190- [ ] Verify performance meets sub-100ms response time requirement
191- [ ] Validate input sanitization prevents SQL injection
192
193Well-structured requirements."#
194 .to_string(),
195 };
196
197 let assessment = assess_quality(&spec);
198
199 assert!(matches!(
201 assessment.ac_quality,
202 ACQualityGrade::A | ACQualityGrade::B
203 ));
204 }
205
206 #[test]
207 fn test_assess_quality_vague_ac() {
208 let spec = Spec {
209 id: "test".to_string(),
210 frontmatter: SpecFrontmatter {
211 target_files: Some(vec!["file1.rs".to_string()]),
212 ..Default::default()
213 },
214 title: Some("Vague spec".to_string()),
215 body: r#"## Problem
216
217Poorly defined criteria.
218
219## Acceptance Criteria
220
221- [ ] The code works
222- [ ] Everything is good
223- [ ] Make sure it's okay
224
225That should do it."#
226 .to_string(),
227 };
228
229 let assessment = assess_quality(&spec);
230
231 assert_eq!(assessment.ac_quality, ACQualityGrade::B);
234 }
235
236 #[test]
237 fn test_assess_quality_long_body() {
238 let long_body = format!(
240 r#"## Problem
241
242{}
243
244## Acceptance Criteria
245
246- [ ] Implement feature
247- [ ] Add tests
248- [ ] Document changes"#,
249 "word ".repeat(210)
250 );
251
252 let spec = Spec {
253 id: "test".to_string(),
254 frontmatter: SpecFrontmatter {
255 target_files: Some(vec!["file1.rs".to_string()]),
256 ..Default::default()
257 },
258 title: Some("Long spec".to_string()),
259 body: long_body,
260 };
261
262 let assessment = assess_quality(&spec);
263
264 assert!(matches!(
266 assessment.complexity,
267 ComplexityGrade::B | ComplexityGrade::C | ComplexityGrade::D
268 ));
269 }
270}