1use crate::utils::validation::{
4 conditional_error, conditional_suggestion, conditional_suggestions,
5};
6
7#[derive(Debug, Clone, Copy)]
9pub enum ContentType {
10 Vision,
11 TechStack,
12 Summary,
13 Spec,
14 Notes,
15 Tasks,
16}
17
18pub struct ValidationResult {
20 pub is_valid: bool,
21 pub errors: Vec<String>,
22 pub suggestions: Vec<String>,
23}
24
25pub fn validate_content(content_type: ContentType, content: &str) -> ValidationResult {
27 match content_type {
28 ContentType::Vision => validate_vision_content(content),
29 ContentType::TechStack => validate_tech_stack_content(content),
30 ContentType::Summary => validate_summary_content(content),
31 ContentType::Spec => validate_spec_content(content),
32 ContentType::Notes => validate_notes_content(content),
33 ContentType::Tasks => validate_tasks_content(content),
34 }
35}
36
37fn validate_vision_content(content: &str) -> ValidationResult {
39 let errors = conditional_error(
40 content.len() < 200,
41 "Vision content must be at least 200 characters",
42 );
43
44 let paragraphs_count = content
45 .split("\n\n")
46 .filter(|p| !p.trim().is_empty())
47 .count();
48
49 let lower_content = content.to_lowercase();
50
51 let context_keywords = [
53 "architecture",
54 "integration",
55 "dependencies",
56 "business",
57 "rationale",
58 "context",
59 "implementation",
60 "approach",
61 ];
62 let context_score = context_keywords
63 .iter()
64 .filter(|&k| lower_content.contains(k))
65 .count();
66
67 let mut suggestions = conditional_suggestions(&[
68 (
69 paragraphs_count < 2,
70 "Consider adding more paragraphs to provide comprehensive vision coverage",
71 ),
72 (
73 !lower_content.contains("problem") && !lower_content.contains("solve"),
74 "Consider including what problem this solves",
75 ),
76 (
77 !lower_content.contains("target") && !lower_content.contains("user"),
78 "Consider specifying target users or audience",
79 ),
80 ]);
81
82 if context_score < 3 {
84 suggestions.push("Consider adding more context for future implementers (architectural decisions, business rationale, implementation approach)".to_string());
85 }
86
87 ValidationResult {
88 is_valid: errors.is_empty(),
89 errors,
90 suggestions,
91 }
92}
93
94fn validate_tech_stack_content(content: &str) -> ValidationResult {
96 let errors = conditional_error(
97 content.len() < 150,
98 "Tech stack content must be at least 150 characters",
99 );
100
101 let lower_content = content.to_lowercase();
102 let tech_keywords = [
103 "language",
104 "framework",
105 "database",
106 "deployment",
107 "infrastructure",
108 ];
109 let has_tech = tech_keywords
110 .iter()
111 .any(|&keyword| lower_content.contains(keyword));
112
113 let suggestions = conditional_suggestion(
114 !has_tech,
115 "Consider including specific technologies, frameworks, or deployment platforms",
116 );
117
118 ValidationResult {
119 is_valid: errors.is_empty(),
120 errors,
121 suggestions,
122 }
123}
124
125fn validate_summary_content(content: &str) -> ValidationResult {
127 let errors = conditional_error(
128 content.len() < 100,
129 "Summary content must be at least 100 characters",
130 );
131
132 let suggestions = conditional_suggestion(
133 content.len() > 500,
134 "Consider making the summary more concise (under 500 characters)",
135 );
136
137 ValidationResult {
138 is_valid: errors.is_empty(),
139 errors,
140 suggestions,
141 }
142}
143
144fn validate_spec_content(content: &str) -> ValidationResult {
146 let errors = conditional_error(
147 content.len() < 100,
148 "Spec content must be at least 100 characters",
149 );
150
151 let lower_content = content.to_lowercase();
152 let structure_keywords = ["requirements", "functionality", "behavior", "interface"];
153 let has_structure = structure_keywords
154 .iter()
155 .any(|&keyword| lower_content.contains(keyword));
156
157 let context_keywords = [
159 "architecture",
160 "integration",
161 "dependencies",
162 "business",
163 "rationale",
164 "implementation",
165 "approach",
166 "constraints",
167 "edge",
168 "validation",
169 ];
170 let context_score = context_keywords
171 .iter()
172 .filter(|&k| lower_content.contains(k))
173 .count();
174
175 let mut suggestions = conditional_suggestion(
176 !has_structure,
177 "Consider adding requirements, functionality, or behavioral specifications",
178 );
179
180 if context_score < 4 {
182 suggestions.push("Consider adding more implementation context (architecture, dependencies, business rationale, constraints, edge cases)".to_string());
183 }
184
185 ValidationResult {
186 is_valid: errors.is_empty(),
187 errors,
188 suggestions,
189 }
190}
191
192fn validate_notes_content(content: &str) -> ValidationResult {
194 let errors = conditional_error(
195 content.len() < 50,
196 "Notes content must be at least 50 characters",
197 );
198
199 let lower_content = content.to_lowercase();
200
201 let context_keywords = [
203 "rationale",
204 "decision",
205 "tradeoff",
206 "constraint",
207 "dependency",
208 "business",
209 "architecture",
210 "integration",
211 "why",
212 "because",
213 ];
214 let context_score = context_keywords
215 .iter()
216 .filter(|&k| lower_content.contains(k))
217 .count();
218
219 let mut suggestions = Vec::new();
220
221 if context_score < 3 {
223 suggestions.push("Consider adding more context for future implementers (decision rationale, tradeoffs, constraints, dependencies)".to_string());
224 }
225
226 ValidationResult {
227 is_valid: errors.is_empty(),
228 errors,
229 suggestions,
230 }
231}
232
233fn validate_tasks_content(content: &str) -> ValidationResult {
235 let errors = conditional_error(
236 content.len() < 30,
237 "Tasks content must be at least 30 characters",
238 );
239
240 let lower_content = content.to_lowercase();
241 let task_keywords = ["todo", "task", "implement", "create", "add", "-", "*"];
242 let has_task_format = task_keywords
243 .iter()
244 .any(|&keyword| lower_content.contains(keyword));
245
246 let suggestions = conditional_suggestion(
247 !has_task_format,
248 "Consider using task list format with - or * bullets or TODO items",
249 );
250
251 ValidationResult {
252 is_valid: errors.is_empty(),
253 errors,
254 suggestions,
255 }
256}
257
258pub fn parse_content_type(content_type: &str) -> anyhow::Result<ContentType> {
260 match content_type.to_lowercase().as_str() {
261 "vision" => Ok(ContentType::Vision),
262 "tech-stack" => Ok(ContentType::TechStack),
263 "summary" => Ok(ContentType::Summary),
264 "spec" => Ok(ContentType::Spec),
265 "notes" => Ok(ContentType::Notes),
266 "tasks" => Ok(ContentType::Tasks),
267 _ => Err(anyhow::anyhow!("Unknown content type: {}", content_type)),
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_validate_vision_content_too_short() {
277 let content = "Too short";
278 let result = validate_vision_content(content);
279
280 assert!(!result.is_valid);
281 assert_eq!(result.errors.len(), 1);
282 assert!(result.errors[0].contains("200 characters"));
283 }
284
285 #[test]
286 fn test_validate_tech_stack_content_too_short() {
287 let content = "Too short";
288 let result = validate_tech_stack_content(content);
289
290 assert!(!result.is_valid);
291 assert_eq!(result.errors.len(), 1);
292 assert!(result.errors[0].contains("150 characters"));
293 }
294
295 #[test]
296 fn test_validate_summary_content_too_short() {
297 let content = "Too short";
298 let result = validate_summary_content(content);
299
300 assert!(!result.is_valid);
301 assert_eq!(result.errors.len(), 1);
302 assert!(result.errors[0].contains("100 characters"));
303 }
304
305 #[test]
306 fn test_validate_spec_content_too_short() {
307 let content = "Too short";
308 let result = validate_spec_content(content);
309
310 assert!(!result.is_valid);
311 assert_eq!(result.errors.len(), 1);
312 assert!(result.errors[0].contains("100 characters"));
313 }
314
315 #[test]
316 fn test_validate_notes_content_too_short() {
317 let content = "Too short";
318 let result = validate_notes_content(content);
319
320 assert!(!result.is_valid);
321 assert_eq!(result.errors.len(), 1);
322 assert!(result.errors[0].contains("50 characters"));
323 }
324
325 #[test]
326 fn test_parse_content_type_valid() {
327 assert!(matches!(
328 parse_content_type("vision"),
329 Ok(ContentType::Vision)
330 ));
331 assert!(matches!(
332 parse_content_type("tech-stack"),
333 Ok(ContentType::TechStack)
334 ));
335 assert!(matches!(
336 parse_content_type("summary"),
337 Ok(ContentType::Summary)
338 ));
339 assert!(matches!(parse_content_type("spec"), Ok(ContentType::Spec)));
340 assert!(matches!(
341 parse_content_type("notes"),
342 Ok(ContentType::Notes)
343 ));
344 }
345
346 #[test]
347 fn test_parse_content_type_case_insensitive() {
348 assert!(matches!(
349 parse_content_type("VISION"),
350 Ok(ContentType::Vision)
351 ));
352 assert!(matches!(
353 parse_content_type("Tech-Stack"),
354 Ok(ContentType::TechStack)
355 ));
356 assert!(matches!(
357 parse_content_type("SUMMARY"),
358 Ok(ContentType::Summary)
359 ));
360 }
361
362 #[test]
363 fn test_parse_content_type_invalid() {
364 assert!(parse_content_type("invalid").is_err());
365 assert!(parse_content_type("").is_err());
366 assert!(parse_content_type("random").is_err());
367 }
368}