Skip to main content

atomcode_core/tool/
auto_fix.rs

1//! Pre-write validation and auto-fix for edited content.
2//!
3//! All checks operate on in-memory content — nothing is written to disk.
4//! The caller is responsible for writing the validated/fixed content.
5//!
6//! Post-write syntax check (`post_edit_syntax_check`) needs the file on disk
7//! (runs external commands like `node --check`) and is called separately after write.
8
9/// Result of `validate_and_fix`: the (possibly fixed) content, any warnings,
10/// and whether any auto-fix was applied.
11pub struct ValidateResult {
12    pub fixed_content: String,
13    pub warnings: Vec<String>,
14    pub was_fixed: bool,
15    /// If true, the edit should be REJECTED — don't write to disk.
16    /// The model must fix its edit and retry.
17    pub rejected: bool,
18}
19
20/// Run all pre-write validations on the content in memory:
21/// 1. Duplicate block detection
22/// 2. Brace auto-fix
23/// 3. HTML tag auto-fix
24///
25/// Returns a `ValidateResult` with the (possibly fixed) content.
26/// The caller should write `fixed_content` to disk, then optionally call
27/// `post_edit_syntax_check` for on-disk syntax validation.
28/// `content` is the post-edit content (what would be written to disk).
29/// `original_content` is the pre-edit content (for delta validation).
30pub async fn validate_and_fix(
31    content: &str,
32    _file_path: &str,
33    _new_string: &str,
34    _original_content: &str,
35) -> ValidateResult {
36    let warnings: Vec<String> = Vec::new();
37    let current = content.to_string();
38
39    // Duplicate detection REMOVED.
40    // Zero proven positive value across all sessions.
41    // False positives on HTML (repeated <div>/<button>) caused edit rejections.
42    // auto-compile catches real structural errors with better diagnostics.
43
44    // Structural checks (brace balance, HTML balance, Vue SFC) REMOVED.
45    // auto-compile after edit catches all structural errors with better
46    // diagnostics (line number + error type from compiler).
47    // Delta validation was net negative: 7 false rejections today, 0 true catches.
48    // Principle: "don't block the model, enhance the tool."
49
50    if !warnings.is_empty() {
51        return ValidateResult {
52            fixed_content: current,
53            warnings,
54            was_fixed: false,
55            rejected: true,
56        };
57    }
58
59    ValidateResult {
60        fixed_content: current,
61        warnings: vec![],
62        was_fixed: false,
63        rejected: false,
64    }
65}
66
67/// Post-edit syntax check for common file types.
68/// Runs a fast, non-destructive check and returns a warning if syntax is broken.
69/// This needs the file to be on disk — call AFTER writing.
70pub async fn post_edit_syntax_check(file_path: &str) -> String {
71    let ext = file_path.rsplit('.').next().unwrap_or("");
72    let cmd = match ext {
73        "js" | "mjs" | "cjs" => Some(("node", vec!["--check".to_string(), file_path.to_string()])),
74        "json" => {
75            // Validate JSON by attempting parse
76            return match tokio::fs::read_to_string(file_path).await {
77                Ok(content) => {
78                    if serde_json::from_str::<serde_json::Value>(&content).is_err() {
79                        format!(
80                            "\n\u{26a0} SYNTAX ERROR: {} is not valid JSON. Fix before proceeding.",
81                            file_path
82                        )
83                    } else {
84                        String::new()
85                    }
86                }
87                Err(_) => String::new(),
88            };
89        }
90        "ts" | "tsx" => {
91            return String::new();
92        }
93        "vue" | "svelte" => {
94            // Quick checks for common Vue SFC errors:
95            // 1. Nested backticks in <script> (template strings containing `)
96            // 2. Fast build check if no dev server is running
97            let mut warnings = Vec::new();
98
99            if let Ok(content) = tokio::fs::read_to_string(file_path).await {
100                // Check for nested backticks in <script> section
101                if let Some(script_start) = content.find("<script") {
102                    let script_end = content.find("</script>").unwrap_or(content.len());
103                    let script = &content[script_start..script_end];
104                    // Count backticks — odd number means unclosed template string
105                    let backtick_count = script.chars().filter(|c| *c == '`').count();
106                    if backtick_count % 2 != 0 {
107                        warnings.push(format!(
108                            "Unclosed template string (`) in <script> — {} backticks found (odd). \
109                             Use regular strings ('') for data containing backticks.",
110                            backtick_count
111                        ));
112                    }
113                }
114            }
115
116            // NOTE: auto-build removed — violates "do NOT deploy" principle.
117            // Model should run build manually via bash when it wants to verify.
118
119            if warnings.is_empty() {
120                return String::new();
121            }
122            return format!("\n⚠ VUE SYNTAX: {}", warnings.join("; "));
123        }
124        "py" => Some((
125            "python3",
126            vec![
127                "-m".to_string(),
128                "py_compile".to_string(),
129                file_path.to_string(),
130            ],
131        )),
132        _ => None,
133    };
134
135    if let Some((program, args)) = cmd {
136        let mut command = tokio::process::Command::new(program);
137        command.args(&args);
138        crate::process_utils::suppress_console_window(&mut command);
139        match command.output().await {
140            Ok(output) if !output.status.success() => {
141                let stderr = String::from_utf8_lossy(&output.stderr);
142                let first_lines: String = stderr.lines().take(3).collect::<Vec<_>>().join("\n");
143                format!("\n\u{26a0} SYNTAX ERROR in {}:\n{}", file_path, first_lines)
144            }
145            _ => String::new(),
146        }
147    } else {
148        String::new()
149    }
150}