Coverage for src / moai_adk / core / config / auto_spec_config.py: 27.22%
158 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""Auto-Spec Completion Configuration Reader."""
3import json
4import logging
5from pathlib import Path
6from typing import Any, Dict, List
8# Configure logging
9logger = logging.getLogger(__name__)
12class AutoSpecConfig:
13 """
14 Configuration reader for Auto-Spec Completion system.
16 This class reads and validates the auto-spec completion configuration
17 from the main MoAI configuration file.
18 """
20 def __init__(self, config_path: str = None):
21 """Initialize the configuration reader."""
22 self.config_path = config_path or self._get_default_config_path()
23 self.config = {}
24 self.load_config()
26 def _get_default_config_path(self) -> str:
27 """Get the default configuration file path."""
28 # Try to find config in multiple locations
29 possible_paths = [
30 Path.cwd() / ".moai" / "config.json",
31 Path.cwd() / "config.json",
32 Path.home() / ".moai" / "config.json",
33 ]
35 for path in possible_paths:
36 if path.exists():
37 return str(path)
39 # Default to current directory
40 return str(Path.cwd() / ".moai" / "config.json")
42 def load_config(self) -> None:
43 """Load configuration from file."""
44 try:
45 with open(self.config_path, "r", encoding="utf-8") as f:
46 self.config = json.load(f)
48 # Extract auto-spec completion config
49 self.config = self.config.get("auto_spec_completion", {})
51 logger.info(
52 f"Loaded auto-spec completion configuration from {self.config_path}"
53 )
55 except FileNotFoundError:
56 logger.warning(f"Configuration file not found: {self.config_path}")
57 self._load_default_config()
58 except json.JSONDecodeError as e:
59 logger.error(f"Invalid JSON in configuration file: {e}")
60 self._load_default_config()
61 except Exception as e:
62 logger.error(f"Error loading configuration: {e}")
63 self._load_default_config()
65 def _load_default_config(self) -> None:
66 """Load default configuration."""
67 logger.info("Loading default auto-spec completion configuration")
68 self.config = self._get_default_config()
70 def _get_default_config(self) -> Dict[str, Any]:
71 """Get default configuration."""
72 return {
73 "enabled": True,
74 "trigger_tools": ["Write", "Edit", "MultiEdit"],
75 "confidence_threshold": 0.7,
76 "execution_timeout_ms": 1500,
77 "quality_threshold": {
78 "ears_compliance": 0.85,
79 "min_content_length": 500,
80 "max_review_suggestions": 10,
81 },
82 "excluded_patterns": [
83 "test_*.py",
84 "*_test.py",
85 "*/tests/*",
86 "*/__pycache__/*",
87 "*/node_modules/*",
88 "*/dist/*",
89 "*/build/*",
90 ],
91 "domain_templates": {
92 "enabled": True,
93 "auto_detect": True,
94 "supported_domains": ["auth", "api", "data", "ui", "business"],
95 "fallback_domain": "general",
96 },
97 "spec_structure": {
98 "include_meta": True,
99 "include_traceability": True,
100 "include_edit_guide": True,
101 "required_sections": [
102 "Overview",
103 "Environment",
104 "Assumptions",
105 "Requirements",
106 "Specifications",
107 "Traceability",
108 ],
109 },
110 "validation": {
111 "enabled": True,
112 "quality_grades": ["A", "B", "C", "D", "F"],
113 "passing_grades": ["A", "B", "C"],
114 "auto_improve": True,
115 "max_iterations": 3,
116 },
117 "output": {
118 "auto_create_files": True,
119 "open_in_editor": True,
120 "file_format": "markdown",
121 "encoding": "utf-8",
122 },
123 }
125 def is_enabled(self) -> bool:
126 """Check if auto-spec completion is enabled."""
127 return self.config.get("enabled", False)
129 def get_trigger_tools(self) -> List[str]:
130 """Get list of tools that trigger auto-spec completion."""
131 return self.config.get("trigger_tools", [])
133 def get_confidence_threshold(self) -> float:
134 """Get confidence threshold for triggering auto-spec completion."""
135 return self.config.get("confidence_threshold", 0.7)
137 def get_execution_timeout_ms(self) -> int:
138 """Get execution timeout in milliseconds."""
139 return self.config.get("execution_timeout_ms", 1500)
141 def get_quality_threshold(self) -> Dict[str, Any]:
142 """Get quality threshold configuration."""
143 return self.config.get("quality_threshold", {})
145 def get_excluded_patterns(self) -> List[str]:
146 """Get list of file patterns to exclude from auto-spec completion."""
147 return self.config.get("excluded_patterns", [])
149 def get_domain_templates_config(self) -> Dict[str, Any]:
150 """Get domain templates configuration."""
151 return self.config.get("domain_templates", {})
153 def get_spec_structure_config(self) -> Dict[str, Any]:
154 """Get spec structure configuration."""
155 return self.config.get("spec_structure", {})
157 def get_validation_config(self) -> Dict[str, Any]:
158 """Get validation configuration."""
159 return self.config.get("validation", {})
161 def get_output_config(self) -> Dict[str, Any]:
162 """Get output configuration."""
163 return self.config.get("output", {})
165 def should_exclude_file(self, file_path: str) -> bool:
166 """Check if a file should be excluded from auto-spec completion."""
167 excluded_patterns = self.get_excluded_patterns()
169 if not excluded_patterns:
170 return False
172 # Convert file path to normalized string
173 normalized_path = str(Path(file_path)).lower()
175 for pattern in excluded_patterns:
176 # Convert pattern to lowercase for case-insensitive matching
177 pattern.lower()
179 # Handle directory patterns
180 if pattern.startswith("*/") and pattern.endswith("/*"):
181 dir_pattern = pattern[2:-2] # Remove */ and /*
182 if dir_pattern in normalized_path:
183 return True
184 # Handle file patterns
185 elif "*" in pattern:
186 # Simple wildcard matching
187 regex_pattern = pattern.replace("*", ".*")
188 import re
190 if re.search(regex_pattern, normalized_path):
191 return True
192 # Exact match
193 elif pattern in normalized_path:
194 return True
196 return False
198 def get_required_sections(self) -> List[str]:
199 """Get list of required SPEC sections."""
200 return self.config.get("spec_structure", {}).get("required_sections", [])
202 def get_supported_domains(self) -> List[str]:
203 """Get list of supported domains."""
204 return self.config.get("domain_templates", {}).get("supported_domains", [])
206 def get_fallback_domain(self) -> str:
207 """Get fallback domain for unsupported domains."""
208 return self.config.get("domain_templates", {}).get("fallback_domain", "general")
210 def should_include_meta(self) -> bool:
211 """Check if meta information should be included."""
212 return self.config.get("spec_structure", {}).get("include_meta", True)
214 def should_include_traceability(self) -> bool:
215 """Check if traceability information should be included."""
216 return self.config.get("spec_structure", {}).get("include_traceability", True)
218 def should_include_edit_guide(self) -> bool:
219 """Check if edit guide should be included."""
220 return self.config.get("spec_structure", {}).get("include_edit_guide", True)
222 def get_passing_quality_grades(self) -> List[str]:
223 """Get list of passing quality grades."""
224 return self.config.get("validation", {}).get("passing_grades", ["A", "B", "C"])
226 def should_auto_improve(self) -> bool:
227 """Check if auto-improvement is enabled."""
228 return self.config.get("validation", {}).get("auto_improve", True)
230 def get_max_improvement_iterations(self) -> int:
231 """Get maximum improvement iterations."""
232 return self.config.get("validation", {}).get("max_iterations", 3)
234 def should_auto_create_files(self) -> bool:
235 """Check if auto-creation of files is enabled."""
236 return self.config.get("output", {}).get("auto_create_files", True)
238 def should_open_in_editor(self) -> bool:
239 """Check if files should be opened in editor."""
240 return self.config.get("output", {}).get("open_in_editor", True)
242 def get_file_format(self) -> str:
243 """Get output file format."""
244 return self.config.get("output", {}).get("file_format", "markdown")
246 def get_encoding(self) -> str:
247 """Get output encoding."""
248 return self.config.get("output", {}).get("encoding", "utf-8")
250 def is_validation_enabled(self) -> bool:
251 """Check if validation is enabled."""
252 return self.config.get("validation", {}).get("enabled", True)
254 def is_domain_detection_enabled(self) -> bool:
255 """Check if domain detection is enabled."""
256 return self.config.get("domain_templates", {}).get("auto_detect", True)
258 def validate_config(self) -> List[str]:
259 """Validate configuration and return list of errors."""
260 errors = []
262 # Check required fields
263 if not isinstance(self.config.get("enabled"), bool):
264 errors.append("enabled must be a boolean")
266 if not isinstance(self.config.get("confidence_threshold"), (int, float)):
267 errors.append("confidence_threshold must be a number")
268 elif not 0 <= self.config.get("confidence_threshold") <= 1:
269 errors.append("confidence_threshold must be between 0 and 1")
271 if not isinstance(self.config.get("execution_timeout_ms"), int):
272 errors.append("execution_timeout_ms must be an integer")
273 elif self.config.get("execution_timeout_ms") <= 0:
274 errors.append("execution_timeout_ms must be positive")
276 # Check trigger tools
277 trigger_tools = self.config.get("trigger_tools", [])
278 if not isinstance(trigger_tools, list):
279 errors.append("trigger_tools must be a list")
280 else:
281 for tool in trigger_tools:
282 if not isinstance(tool, str):
283 errors.append("All trigger_tools must be strings")
285 # Check excluded patterns
286 excluded_patterns = self.config.get("excluded_patterns", [])
287 if not isinstance(excluded_patterns, list):
288 errors.append("excluded_patterns must be a list")
289 else:
290 for pattern in excluded_patterns:
291 if not isinstance(pattern, str):
292 errors.append("All excluded_patterns must be strings")
294 # Check quality threshold
295 quality_threshold = self.config.get("quality_threshold", {})
296 if not isinstance(quality_threshold, dict):
297 errors.append("quality_threshold must be a dictionary")
298 else:
299 if "ears_compliance" in quality_threshold:
300 if not isinstance(quality_threshold["ears_compliance"], (int, float)):
301 errors.append("quality_threshold.ears_compliance must be a number")
302 elif not 0 <= quality_threshold["ears_compliance"] <= 1:
303 errors.append(
304 "quality_threshold.ears_compliance must be between 0 and 1"
305 )
307 return errors
309 def to_dict(self) -> Dict[str, Any]:
310 """Convert configuration to dictionary."""
311 return self.config.copy()
313 def update_config(self, updates: Dict[str, Any]) -> None:
314 """Update configuration with new values."""
315 self.config.update(updates)
316 logger.info(f"Updated auto-spec completion configuration: {updates}")
318 def save_config(self) -> None:
319 """Save configuration to file."""
320 try:
321 # Load the full config file
322 with open(self.config_path, "r", encoding="utf-8") as f:
323 full_config = json.load(f)
325 # Update the auto-spec completion config
326 full_config["auto_spec_completion"] = self.config
328 # Save back to file
329 with open(self.config_path, "w", encoding="utf-8") as f:
330 json.dump(full_config, f, indent=2, ensure_ascii=False)
332 logger.info(
333 f"Saved auto-spec completion configuration to {self.config_path}"
334 )
336 except Exception as e:
337 logger.error(f"Error saving configuration: {e}")
338 raise
340 def __str__(self) -> str:
341 """String representation of configuration."""
342 return json.dumps(self.config, indent=2, ensure_ascii=False)
344 def __repr__(self) -> str:
345 """String representation for debugging."""
346 return f"AutoSpecConfig(enabled={self.is_enabled()}, config_path='{self.config_path}')"