1use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::scoring::QualityIssue;
11
12const CLAUDE_MD_TEMPLATE: &str = include_str!("templates/CLAUDE.md.template");
14
15const README_MD_TEMPLATE: &str = include_str!("templates/README.md.template");
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum FixResult {
21 Created(PathBuf),
22 Skipped(String),
23}
24
25pub struct AutoFixer;
27
28impl AutoFixer {
29 pub fn fix(root: &Path, issues: &[QualityIssue], dry_run: bool) -> Vec<FixResult> {
32 let mut results = Vec::new();
33
34 for issue in issues {
35 if issue.fix_type != crate::scoring::FixType::Scaffold {
36 continue;
37 }
38
39 match issue.id.as_str() {
40 "config-missing-claude-md" => {
41 let path = root.join("CLAUDE.md");
42 if path.exists() {
43 results.push(FixResult::Skipped(
44 "CLAUDE.md already exists".to_string(),
45 ));
46 } else if dry_run {
47 results.push(FixResult::Created(path));
48 } else {
49 let project_name = root
50 .file_name()
51 .unwrap_or_default()
52 .to_string_lossy()
53 .to_string();
54 let content = CLAUDE_MD_TEMPLATE.replace("{{PROJECT_NAME}}", &project_name);
55 match std::fs::write(&path, content) {
56 Ok(()) => results.push(FixResult::Created(path)),
57 Err(e) => results.push(FixResult::Skipped(format!(
58 "Failed to create CLAUDE.md: {e}",
59 ))),
60 }
61 }
62 }
63 "config-missing-readme" => {
64 let path = root.join("README.md");
65 if path.exists() {
66 results.push(FixResult::Skipped(
67 "README.md already exists".to_string(),
68 ));
69 } else if dry_run {
70 results.push(FixResult::Created(path));
71 } else {
72 let project_name = root
73 .file_name()
74 .unwrap_or_default()
75 .to_string_lossy()
76 .to_string();
77 let content = README_MD_TEMPLATE.replace("{{PROJECT_NAME}}", &project_name);
78 match std::fs::write(&path, content) {
79 Ok(()) => results.push(FixResult::Created(path)),
80 Err(e) => results.push(FixResult::Skipped(format!(
81 "Failed to create README.md: {e}",
82 ))),
83 }
84 }
85 }
86 _ => {}
87 }
88 }
89
90 results
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::scoring::{FixType, IssueCategory, IssueSeverity, QualityIssue};
98 use tempfile::TempDir;
99
100 fn scaffold_issue(id: &str) -> QualityIssue {
101 let mut issue = QualityIssue::new(
102 id.to_string(),
103 None,
104 IssueCategory::Configuration,
105 IssueSeverity::Blocking,
106 format!("Missing {}", id),
107 "test".to_string(),
108 None,
109 );
110 issue.fix_type = FixType::Scaffold;
111 issue
112 }
113
114 #[test]
115 fn test_scaffold_claude_md() {
116 let dir = TempDir::new().unwrap();
117 let issues = vec![scaffold_issue("config-missing-claude-md")];
118
119 let results = AutoFixer::fix(dir.path(), &issues, false);
120 assert_eq!(results.len(), 1);
121 assert!(matches!(&results[0], FixResult::Created(p) if p.ends_with("CLAUDE.md")));
122 assert!(dir.path().join("CLAUDE.md").exists());
123
124 let content = std::fs::read_to_string(dir.path().join("CLAUDE.md")).unwrap();
125 assert!(content.contains("# "));
126 }
127
128 #[test]
129 fn test_scaffold_readme() {
130 let dir = TempDir::new().unwrap();
131 let issues = vec![scaffold_issue("config-missing-readme")];
132
133 let results = AutoFixer::fix(dir.path(), &issues, false);
134 assert_eq!(results.len(), 1);
135 assert!(matches!(&results[0], FixResult::Created(p) if p.ends_with("README.md")));
136 assert!(dir.path().join("README.md").exists());
137 }
138
139 #[test]
140 fn test_dry_run() {
141 let dir = TempDir::new().unwrap();
142 let issues = vec![scaffold_issue("config-missing-claude-md")];
143
144 let results = AutoFixer::fix(dir.path(), &issues, true);
145 assert_eq!(results.len(), 1);
146 assert!(matches!(&results[0], FixResult::Created(_)));
147 assert!(!dir.path().join("CLAUDE.md").exists());
149 }
150
151 #[test]
152 fn test_skip_existing() {
153 let dir = TempDir::new().unwrap();
154 std::fs::write(dir.path().join("CLAUDE.md"), "existing").unwrap();
155 let issues = vec![scaffold_issue("config-missing-claude-md")];
156
157 let results = AutoFixer::fix(dir.path(), &issues, false);
158 assert_eq!(results.len(), 1);
159 assert!(matches!(&results[0], FixResult::Skipped(_)));
160
161 let content = std::fs::read_to_string(dir.path().join("CLAUDE.md")).unwrap();
163 assert_eq!(content, "existing");
164 }
165}