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

1"""Auto-Spec Completion Configuration Reader.""" 

2 

3import json 

4import logging 

5from pathlib import Path 

6from typing import Any, Dict, List 

7 

8# Configure logging 

9logger = logging.getLogger(__name__) 

10 

11 

12class AutoSpecConfig: 

13 """ 

14 Configuration reader for Auto-Spec Completion system. 

15 

16 This class reads and validates the auto-spec completion configuration 

17 from the main MoAI configuration file. 

18 """ 

19 

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() 

25 

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 ] 

34 

35 for path in possible_paths: 

36 if path.exists(): 

37 return str(path) 

38 

39 # Default to current directory 

40 return str(Path.cwd() / ".moai" / "config.json") 

41 

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) 

47 

48 # Extract auto-spec completion config 

49 self.config = self.config.get("auto_spec_completion", {}) 

50 

51 logger.info( 

52 f"Loaded auto-spec completion configuration from {self.config_path}" 

53 ) 

54 

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() 

64 

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() 

69 

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 } 

124 

125 def is_enabled(self) -> bool: 

126 """Check if auto-spec completion is enabled.""" 

127 return self.config.get("enabled", False) 

128 

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", []) 

132 

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) 

136 

137 def get_execution_timeout_ms(self) -> int: 

138 """Get execution timeout in milliseconds.""" 

139 return self.config.get("execution_timeout_ms", 1500) 

140 

141 def get_quality_threshold(self) -> Dict[str, Any]: 

142 """Get quality threshold configuration.""" 

143 return self.config.get("quality_threshold", {}) 

144 

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", []) 

148 

149 def get_domain_templates_config(self) -> Dict[str, Any]: 

150 """Get domain templates configuration.""" 

151 return self.config.get("domain_templates", {}) 

152 

153 def get_spec_structure_config(self) -> Dict[str, Any]: 

154 """Get spec structure configuration.""" 

155 return self.config.get("spec_structure", {}) 

156 

157 def get_validation_config(self) -> Dict[str, Any]: 

158 """Get validation configuration.""" 

159 return self.config.get("validation", {}) 

160 

161 def get_output_config(self) -> Dict[str, Any]: 

162 """Get output configuration.""" 

163 return self.config.get("output", {}) 

164 

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() 

168 

169 if not excluded_patterns: 

170 return False 

171 

172 # Convert file path to normalized string 

173 normalized_path = str(Path(file_path)).lower() 

174 

175 for pattern in excluded_patterns: 

176 # Convert pattern to lowercase for case-insensitive matching 

177 pattern.lower() 

178 

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 

189 

190 if re.search(regex_pattern, normalized_path): 

191 return True 

192 # Exact match 

193 elif pattern in normalized_path: 

194 return True 

195 

196 return False 

197 

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", []) 

201 

202 def get_supported_domains(self) -> List[str]: 

203 """Get list of supported domains.""" 

204 return self.config.get("domain_templates", {}).get("supported_domains", []) 

205 

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") 

209 

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) 

213 

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) 

217 

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) 

221 

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"]) 

225 

226 def should_auto_improve(self) -> bool: 

227 """Check if auto-improvement is enabled.""" 

228 return self.config.get("validation", {}).get("auto_improve", True) 

229 

230 def get_max_improvement_iterations(self) -> int: 

231 """Get maximum improvement iterations.""" 

232 return self.config.get("validation", {}).get("max_iterations", 3) 

233 

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) 

237 

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) 

241 

242 def get_file_format(self) -> str: 

243 """Get output file format.""" 

244 return self.config.get("output", {}).get("file_format", "markdown") 

245 

246 def get_encoding(self) -> str: 

247 """Get output encoding.""" 

248 return self.config.get("output", {}).get("encoding", "utf-8") 

249 

250 def is_validation_enabled(self) -> bool: 

251 """Check if validation is enabled.""" 

252 return self.config.get("validation", {}).get("enabled", True) 

253 

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) 

257 

258 def validate_config(self) -> List[str]: 

259 """Validate configuration and return list of errors.""" 

260 errors = [] 

261 

262 # Check required fields 

263 if not isinstance(self.config.get("enabled"), bool): 

264 errors.append("enabled must be a boolean") 

265 

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") 

270 

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") 

275 

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") 

284 

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") 

293 

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 ) 

306 

307 return errors 

308 

309 def to_dict(self) -> Dict[str, Any]: 

310 """Convert configuration to dictionary.""" 

311 return self.config.copy() 

312 

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}") 

317 

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) 

324 

325 # Update the auto-spec completion config 

326 full_config["auto_spec_completion"] = self.config 

327 

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) 

331 

332 logger.info( 

333 f"Saved auto-spec completion configuration to {self.config_path}" 

334 ) 

335 

336 except Exception as e: 

337 logger.error(f"Error saving configuration: {e}") 

338 raise 

339 

340 def __str__(self) -> str: 

341 """String representation of configuration.""" 

342 return json.dumps(self.config, indent=2, ensure_ascii=False) 

343 

344 def __repr__(self) -> str: 

345 """String representation for debugging.""" 

346 return f"AutoSpecConfig(enabled={self.is_enabled()}, config_path='{self.config_path}')"