agnix_core/config/schema.rs
1use super::*;
2
3impl LintConfig {
4 /// Validate the configuration and return any warnings.
5 ///
6 /// This performs semantic validation beyond what TOML parsing can check:
7 /// - Validates that disabled_rules match known rule ID patterns
8 /// - Validates that tools array contains known tool names
9 /// - Warns on deprecated fields
10 pub fn validate(&self) -> Vec<ConfigWarning> {
11 let mut warnings = Vec::new();
12
13 // Validate disabled_rules match known patterns
14 // Note: imports:: is a legacy prefix used in some internal diagnostics
15 let known_prefixes = [
16 "AS-",
17 "CC-SK-",
18 "CC-HK-",
19 "CC-AG-",
20 "CC-MEM-",
21 "CC-PL-",
22 "CDX-",
23 "XML-",
24 "MCP-",
25 "REF-",
26 "XP-",
27 "AGM-",
28 "COP-",
29 "CUR-",
30 "CLN-",
31 "OC-",
32 "GM-",
33 "PE-",
34 "VER-",
35 "ROO-",
36 "AMP-",
37 "WS-",
38 "WS-SK-",
39 "KIRO-",
40 "KR-SK-",
41 "imports::",
42 ];
43 for rule_id in &self.data.rules.disabled_rules {
44 let matches_known = known_prefixes
45 .iter()
46 .any(|prefix| rule_id.starts_with(prefix));
47 if !matches_known {
48 warnings.push(ConfigWarning {
49 field: "rules.disabled_rules".to_string(),
50 message: t!(
51 "core.config.unknown_rule",
52 rule = rule_id.as_str(),
53 prefixes = known_prefixes.join(", ")
54 )
55 .to_string(),
56 suggestion: Some(t!("core.config.unknown_rule_suggestion").to_string()),
57 });
58 }
59 }
60
61 // Validate tools array contains known tools
62 let known_tools = [
63 "claude-code",
64 "cursor",
65 "codex",
66 "kiro",
67 "copilot",
68 "github-copilot",
69 "cline",
70 "opencode",
71 "gemini-cli",
72 "amp",
73 "roo-code",
74 "windsurf",
75 "generic",
76 ];
77 for tool in &self.data.tools {
78 let tool_lower = tool.to_lowercase();
79 if !known_tools
80 .iter()
81 .any(|k| k.eq_ignore_ascii_case(&tool_lower))
82 {
83 warnings.push(ConfigWarning {
84 field: "tools".to_string(),
85 message: t!(
86 "core.config.unknown_tool",
87 tool = tool.as_str(),
88 valid = known_tools.join(", ")
89 )
90 .to_string(),
91 suggestion: Some(t!("core.config.unknown_tool_suggestion").to_string()),
92 });
93 }
94 }
95
96 // Warn on deprecated fields
97 if self.data.target != TargetTool::Generic && self.data.tools.is_empty() {
98 // Only warn if target is non-default and tools is empty
99 // (if both are set, tools takes precedence silently)
100 warnings.push(ConfigWarning {
101 field: "target".to_string(),
102 message: t!("core.config.deprecated_target").to_string(),
103 suggestion: Some(t!("core.config.deprecated_target_suggestion").to_string()),
104 });
105 }
106 if self.data.mcp_protocol_version.is_some() {
107 warnings.push(ConfigWarning {
108 field: "mcp_protocol_version".to_string(),
109 message: t!("core.config.deprecated_mcp_version").to_string(),
110 suggestion: Some(t!("core.config.deprecated_mcp_version_suggestion").to_string()),
111 });
112 }
113
114 // Validate files config glob patterns
115 let pattern_lists = [
116 (
117 "files.include_as_memory",
118 &self.data.files.include_as_memory,
119 ),
120 (
121 "files.include_as_generic",
122 &self.data.files.include_as_generic,
123 ),
124 ("files.exclude", &self.data.files.exclude),
125 ];
126 for (field, patterns) in &pattern_lists {
127 // Warn if pattern count exceeds recommended limit
128 if patterns.len() > MAX_FILE_PATTERNS {
129 warnings.push(ConfigWarning {
130 field: field.to_string(),
131 message: t!(
132 "core.config.files_pattern_count_limit",
133 field = *field,
134 count = patterns.len(),
135 limit = MAX_FILE_PATTERNS
136 )
137 .to_string(),
138 suggestion: Some(
139 t!("core.config.files_pattern_count_limit_suggestion").to_string(),
140 ),
141 });
142 }
143 for pattern in *patterns {
144 let normalized = pattern.replace('\\', "/");
145 if let Err(e) = glob::Pattern::new(&normalized) {
146 warnings.push(ConfigWarning {
147 field: field.to_string(),
148 message: t!(
149 "core.config.invalid_files_pattern",
150 pattern = pattern.as_str(),
151 message = e.to_string()
152 )
153 .to_string(),
154 suggestion: Some(
155 t!("core.config.invalid_files_pattern_suggestion").to_string(),
156 ),
157 });
158 }
159 // Reject path traversal patterns
160 if has_path_traversal(&normalized) {
161 warnings.push(ConfigWarning {
162 field: field.to_string(),
163 message: t!(
164 "core.config.files_path_traversal",
165 pattern = pattern.as_str()
166 )
167 .to_string(),
168 suggestion: Some(
169 t!("core.config.files_path_traversal_suggestion").to_string(),
170 ),
171 });
172 }
173 // Reject absolute paths (Unix-style leading slash or Windows drive letter)
174 if normalized.starts_with('/')
175 || (normalized.len() >= 3
176 && normalized.as_bytes()[0].is_ascii_alphabetic()
177 && normalized.as_bytes().get(1..3) == Some(b":/"))
178 {
179 warnings.push(ConfigWarning {
180 field: field.to_string(),
181 message: t!(
182 "core.config.files_absolute_path",
183 pattern = pattern.as_str()
184 )
185 .to_string(),
186 suggestion: Some(
187 t!("core.config.files_absolute_path_suggestion").to_string(),
188 ),
189 });
190 }
191 }
192 }
193
194 warnings
195 }
196}
197
198/// Warning from configuration validation.
199///
200/// These warnings indicate potential issues with the configuration that
201/// don't prevent validation from running but may indicate user mistakes.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct ConfigWarning {
204 /// The field path that has the issue (e.g., "rules.disabled_rules")
205 pub field: String,
206 /// Description of the issue
207 pub message: String,
208 /// Optional suggestion for how to fix the issue
209 pub suggestion: Option<String>,
210}
211
212/// Generate a JSON Schema for the LintConfig type.
213///
214/// This can be used to provide editor autocompletion and validation
215/// for `.agnix.toml` configuration files.
216///
217/// # Example
218///
219/// ```rust
220/// use agnix_core::config::generate_schema;
221///
222/// let schema = generate_schema();
223/// let json = serde_json::to_string_pretty(&schema).unwrap();
224/// println!("{}", json);
225/// ```
226pub fn generate_schema() -> schemars::Schema {
227 schemars::schema_for!(LintConfig)
228}