Coverage for src / moai_adk / core / quality / trust_checker.py: 0.00%

141 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-20 20:52 +0900

1# # REMOVED_ORPHAN_CODE:TRUST-002 | SPEC: SPEC-TRUST-001/spec.md | TEST: tests/unit/core/quality/test_trust_checker.py 

2# type: ignore 

3""" 

4Integrated TRUST principle validation system 

5 

6TRUST 4 principles: 

7- T: Test First (test coverage ≥85%) 

8- R: Readable (file ≤300 LOC, function ≤50 LOC, parameters ≤5) 

9- U: Unified (type safety) 

10- S: Secured (vulnerability scanning) 

11""" 

12 

13import ast 

14import json 

15from pathlib import Path 

16from typing import Any 

17 

18from moai_adk.core.quality.validators.base_validator import ValidationResult 

19 

20# ======================================== 

21# Constants (descriptive names) 

22# ======================================== 

23MIN_TEST_COVERAGE_PERCENT = 85 

24MAX_FILE_LINES_OF_CODE = 300 

25MAX_FUNCTION_LINES_OF_CODE = 50 

26MAX_FUNCTION_PARAMETERS = 5 

27MAX_CYCLOMATIC_COMPLEXITY = 10 

28 

29# File encoding 

30DEFAULT_FILE_ENCODING = "utf-8" 

31 

32# Constants for validation 

33 

34 

35class TrustChecker: 

36 """Integrated TRUST principle validator""" 

37 

38 def __init__(self): 

39 """Initialize TrustChecker""" 

40 self.results: dict[str, ValidationResult] = {} 

41 

42 # ======================================== 

43 # T: Test First - Coverage Validation 

44 # ======================================== 

45 

46 def validate_coverage( 

47 self, project_path: Path, coverage_data: dict[str, Any] 

48 ) -> ValidationResult: 

49 """ 

50 Validate test coverage (≥85%) 

51 

52 Args: 

53 project_path: Project path 

54 coverage_data: Coverage data (total_coverage, low_coverage_files) 

55 

56 Returns: 

57 ValidationResult: Validation result 

58 """ 

59 total_coverage = coverage_data.get("total_coverage", 0) 

60 

61 if total_coverage >= MIN_TEST_COVERAGE_PERCENT: 

62 return ValidationResult( 

63 passed=True, 

64 message=f"Test coverage: {total_coverage}% (Target: {MIN_TEST_COVERAGE_PERCENT}%)", 

65 ) 

66 

67 # Generate detailed information on failure 

68 low_files = coverage_data.get("low_coverage_files", []) 

69 details = f"Current coverage: {total_coverage}% (Target: {MIN_TEST_COVERAGE_PERCENT}%)\n" 

70 details += "Low coverage files:\n" 

71 for file_info in low_files: 

72 details += f" - {file_info['file']}: {file_info['coverage']}%\n" 

73 details += "\nRecommended: Add more test cases to increase coverage." 

74 

75 return ValidationResult( 

76 passed=False, 

77 message=f"Test coverage: {total_coverage}% (Target: {MIN_TEST_COVERAGE_PERCENT}%)", 

78 details=details, 

79 ) 

80 

81 # ======================================== 

82 # R: Readable - Code Constraints 

83 # ======================================== 

84 

85 def validate_file_size(self, src_path: Path) -> ValidationResult: 

86 """ 

87 Validate file size (≤300 LOC) 

88 

89 Args: 

90 src_path: Source code directory path 

91 

92 Returns: 

93 ValidationResult: Validation result 

94 """ 

95 # Input validation (security) 

96 if not src_path.exists(): 

97 return ValidationResult( 

98 passed=False, 

99 message=f"Source path does not exist: {src_path}", 

100 details="", 

101 ) 

102 

103 if not src_path.is_dir(): 

104 return ValidationResult( 

105 passed=False, 

106 message=f"Source path is not a directory: {src_path}", 

107 details="", 

108 ) 

109 

110 violations = [] 

111 

112 for py_file in src_path.rglob("*.py"): 

113 # Apply guard clause (improves readability) 

114 if py_file.name.startswith("test_"): 

115 continue 

116 

117 try: 

118 lines = py_file.read_text(encoding="utf-8").splitlines() 

119 loc = len(lines) 

120 

121 if loc > MAX_FILE_LINES_OF_CODE: 

122 violations.append( 

123 f"{py_file.name}: {loc} LOC (Limit: {MAX_FILE_LINES_OF_CODE})" 

124 ) 

125 except (UnicodeDecodeError, PermissionError): 

126 # Security: handle file access errors 

127 continue 

128 

129 if not violations: 

130 return ValidationResult(passed=True, message="All files within 300 LOC") 

131 

132 details = "Files exceeding 300 LOC:\n" + "\n".join( 

133 f" - {v}" for v in violations 

134 ) 

135 details += "\n\nRecommended: Refactor large files into smaller modules." 

136 

137 return ValidationResult( 

138 passed=False, 

139 message=f"{len(violations)} files exceed 300 LOC", 

140 details=details, 

141 ) 

142 

143 def validate_function_size(self, src_path: Path) -> ValidationResult: 

144 """ 

145 Validate function size (≤50 LOC) 

146 

147 Args: 

148 src_path: Source code directory path 

149 

150 Returns: 

151 ValidationResult: Validation result 

152 """ 

153 violations = [] 

154 

155 for py_file in src_path.rglob("*.py"): 

156 if py_file.name.startswith("test_"): 

157 continue 

158 

159 try: 

160 content = py_file.read_text() 

161 tree = ast.parse(content) 

162 lines = content.splitlines() 

163 

164 for node in ast.walk(tree): 

165 if isinstance(node, ast.FunctionDef): 

166 # AST line numbers are 1-based 

167 start_line = node.lineno 

168 end_line = node.end_lineno if node.end_lineno else start_line # type: ignore 

169 

170 # Compute actual function lines of code (decorators excluded) 

171 func_lines = lines[start_line - 1 : end_line] 

172 func_loc = len(func_lines) 

173 

174 if func_loc > MAX_FUNCTION_LINES_OF_CODE: 

175 violations.append( 

176 f"{py_file.name}::{node.name}(): {func_loc} LOC (Limit: {MAX_FUNCTION_LINES_OF_CODE})" 

177 ) 

178 except SyntaxError: 

179 continue 

180 

181 if not violations: 

182 return ValidationResult(passed=True, message="All functions within 50 LOC") 

183 

184 details = "Functions exceeding 50 LOC:\n" + "\n".join( 

185 f" - {v}" for v in violations 

186 ) 

187 details += "\n\nRecommended: Extract complex functions into smaller ones." 

188 

189 return ValidationResult( 

190 passed=False, 

191 message=f"{len(violations)} functions exceed 50 LOC", 

192 details=details, 

193 ) 

194 

195 def validate_param_count(self, src_path: Path) -> ValidationResult: 

196 """ 

197 Validate parameter count (≤5) 

198 

199 Args: 

200 src_path: Source code directory path 

201 

202 Returns: 

203 ValidationResult: Validation result 

204 """ 

205 violations = [] 

206 

207 for py_file in src_path.rglob("*.py"): 

208 if py_file.name.startswith("test_"): 

209 continue 

210 

211 try: 

212 tree = ast.parse(py_file.read_text()) 

213 for node in ast.walk(tree): 

214 if isinstance(node, ast.FunctionDef): 

215 param_count = len(node.args.args) 

216 if param_count > MAX_FUNCTION_PARAMETERS: 

217 violations.append( 

218 f"{py_file.name}::{node.name}(): {param_count} parameters " 

219 f"(Limit: {MAX_FUNCTION_PARAMETERS})" 

220 ) 

221 except SyntaxError: 

222 continue 

223 

224 if not violations: 

225 return ValidationResult( 

226 passed=True, message="All functions within 5 parameters" 

227 ) 

228 

229 details = "Functions exceeding 5 parameters:\n" + "\n".join( 

230 f" - {v}" for v in violations 

231 ) 

232 details += "\n\nRecommended: Use data classes or parameter objects." 

233 

234 return ValidationResult( 

235 passed=False, 

236 message=f"{len(violations)} functions exceed 5 parameters", 

237 details=details, 

238 ) 

239 

240 def validate_complexity(self, src_path: Path) -> ValidationResult: 

241 """ 

242 Validate cyclomatic complexity (≤10) 

243 

244 Args: 

245 src_path: Source code directory path 

246 

247 Returns: 

248 ValidationResult: Validation result 

249 """ 

250 violations = [] 

251 

252 for py_file in src_path.rglob("*.py"): 

253 if py_file.name.startswith("test_"): 

254 continue 

255 

256 try: 

257 tree = ast.parse(py_file.read_text()) 

258 for node in ast.walk(tree): 

259 if isinstance(node, ast.FunctionDef): 

260 complexity = self._calculate_complexity(node) 

261 if complexity > MAX_CYCLOMATIC_COMPLEXITY: 

262 violations.append( 

263 f"{py_file.name}::{node.name}(): complexity {complexity} " 

264 f"(Limit: {MAX_CYCLOMATIC_COMPLEXITY})" 

265 ) 

266 except SyntaxError: 

267 continue 

268 

269 if not violations: 

270 return ValidationResult( 

271 passed=True, message="All functions within complexity 10" 

272 ) 

273 

274 details = "Functions exceeding complexity 10:\n" + "\n".join( 

275 f" - {v}" for v in violations 

276 ) 

277 details += "\n\nRecommended: Simplify complex logic using guard clauses." 

278 

279 return ValidationResult( 

280 passed=False, 

281 message=f"{len(violations)} functions exceed complexity 10", 

282 details=details, 

283 ) 

284 

285 def _calculate_complexity(self, node: ast.FunctionDef) -> int: 

286 """ 

287 Calculate cyclomatic complexity (McCabe complexity) 

288 

289 Args: 

290 node: Function AST node 

291 

292 Returns: 

293 int: Cyclomatic complexity 

294 """ 

295 complexity = 1 

296 for child in ast.walk(node): 

297 # Add 1 for each branching statement 

298 if isinstance( 

299 child, (ast.If, ast.While, ast.For, ast.ExceptHandler, ast.With) 

300 ): 

301 complexity += 1 

302 # Add 1 for each and/or operator 

303 elif isinstance(child, ast.BoolOp): 

304 complexity += len(child.values) - 1 

305 # elif is already counted as ast.If, no extra handling needed 

306 return complexity 

307 

308 # ======================================== 

309 # T: Trackable - Code Traceability 

310 # ======================================== 

311 # Tracking now handled through SPEC references 

312 

313 # ======================================== 

314 # Report Generation 

315 # ======================================== 

316 

317 def generate_report(self, results: dict[str, Any], format: str = "markdown") -> str: 

318 """ 

319 Generate validation report 

320 

321 Args: 

322 results: Validation result dictionary 

323 format: Report format ("markdown" or "json") 

324 

325 Returns: 

326 str: Report string 

327 """ 

328 if format == "json": 

329 return json.dumps(results, indent=2) 

330 

331 # Markdown format 

332 report = "# TRUST Validation Report\n\n" 

333 

334 for category, result in results.items(): 

335 status = "✅ PASS" if result.get("passed", False) else "❌ FAIL" 

336 value = result.get("value", "N/A") 

337 # Add % suffix when the value is numeric 

338 if isinstance(value, (int, float)): 

339 value_str = f"{value}%" 

340 else: 

341 value_str = str(value) 

342 

343 report += f"## {category.upper()}\n" 

344 report += f"**Status**: {status}\n" 

345 report += f"**Value**: {value_str}\n\n" 

346 

347 return report 

348 

349 # ======================================== 

350 # Tool Selection 

351 # ======================================== 

352 

353 def select_tools(self, project_path: Path) -> dict[str, str]: 

354 """ 

355 Automatically select tools by language 

356 

357 Args: 

358 project_path: Project path 

359 

360 Returns: 

361 dict[str, str]: Selected tool dictionary 

362 """ 

363 config_path = project_path / ".moai" / "config" / "config.json" 

364 if not config_path.exists(): 

365 return { 

366 "test_framework": "pytest", 

367 "coverage_tool": "coverage.py", 

368 "linter": "ruff", 

369 "type_checker": "mypy", 

370 } 

371 

372 config = json.loads(config_path.read_text()) 

373 language = config.get("project", {}).get("language", "python") 

374 

375 if language == "python": 

376 return { 

377 "test_framework": "pytest", 

378 "coverage_tool": "coverage.py", 

379 "linter": "ruff", 

380 "type_checker": "mypy", 

381 } 

382 elif language == "typescript": 

383 return { 

384 "test_framework": "vitest", 

385 "linter": "biome", 

386 "type_checker": "tsc", 

387 } 

388 

389 # Default (Python) 

390 return { 

391 "test_framework": "pytest", 

392 "coverage_tool": "coverage.py", 

393 "linter": "ruff", 

394 "type_checker": "mypy", 

395 }