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
« 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
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"""
13import ast
14import json
15from pathlib import Path
16from typing import Any
18from moai_adk.core.quality.validators.base_validator import ValidationResult
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
29# File encoding
30DEFAULT_FILE_ENCODING = "utf-8"
32# Constants for validation
35class TrustChecker:
36 """Integrated TRUST principle validator"""
38 def __init__(self):
39 """Initialize TrustChecker"""
40 self.results: dict[str, ValidationResult] = {}
42 # ========================================
43 # T: Test First - Coverage Validation
44 # ========================================
46 def validate_coverage(
47 self, project_path: Path, coverage_data: dict[str, Any]
48 ) -> ValidationResult:
49 """
50 Validate test coverage (≥85%)
52 Args:
53 project_path: Project path
54 coverage_data: Coverage data (total_coverage, low_coverage_files)
56 Returns:
57 ValidationResult: Validation result
58 """
59 total_coverage = coverage_data.get("total_coverage", 0)
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 )
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."
75 return ValidationResult(
76 passed=False,
77 message=f"Test coverage: {total_coverage}% (Target: {MIN_TEST_COVERAGE_PERCENT}%)",
78 details=details,
79 )
81 # ========================================
82 # R: Readable - Code Constraints
83 # ========================================
85 def validate_file_size(self, src_path: Path) -> ValidationResult:
86 """
87 Validate file size (≤300 LOC)
89 Args:
90 src_path: Source code directory path
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 )
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 )
110 violations = []
112 for py_file in src_path.rglob("*.py"):
113 # Apply guard clause (improves readability)
114 if py_file.name.startswith("test_"):
115 continue
117 try:
118 lines = py_file.read_text(encoding="utf-8").splitlines()
119 loc = len(lines)
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
129 if not violations:
130 return ValidationResult(passed=True, message="All files within 300 LOC")
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."
137 return ValidationResult(
138 passed=False,
139 message=f"{len(violations)} files exceed 300 LOC",
140 details=details,
141 )
143 def validate_function_size(self, src_path: Path) -> ValidationResult:
144 """
145 Validate function size (≤50 LOC)
147 Args:
148 src_path: Source code directory path
150 Returns:
151 ValidationResult: Validation result
152 """
153 violations = []
155 for py_file in src_path.rglob("*.py"):
156 if py_file.name.startswith("test_"):
157 continue
159 try:
160 content = py_file.read_text()
161 tree = ast.parse(content)
162 lines = content.splitlines()
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
170 # Compute actual function lines of code (decorators excluded)
171 func_lines = lines[start_line - 1 : end_line]
172 func_loc = len(func_lines)
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
181 if not violations:
182 return ValidationResult(passed=True, message="All functions within 50 LOC")
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."
189 return ValidationResult(
190 passed=False,
191 message=f"{len(violations)} functions exceed 50 LOC",
192 details=details,
193 )
195 def validate_param_count(self, src_path: Path) -> ValidationResult:
196 """
197 Validate parameter count (≤5)
199 Args:
200 src_path: Source code directory path
202 Returns:
203 ValidationResult: Validation result
204 """
205 violations = []
207 for py_file in src_path.rglob("*.py"):
208 if py_file.name.startswith("test_"):
209 continue
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
224 if not violations:
225 return ValidationResult(
226 passed=True, message="All functions within 5 parameters"
227 )
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."
234 return ValidationResult(
235 passed=False,
236 message=f"{len(violations)} functions exceed 5 parameters",
237 details=details,
238 )
240 def validate_complexity(self, src_path: Path) -> ValidationResult:
241 """
242 Validate cyclomatic complexity (≤10)
244 Args:
245 src_path: Source code directory path
247 Returns:
248 ValidationResult: Validation result
249 """
250 violations = []
252 for py_file in src_path.rglob("*.py"):
253 if py_file.name.startswith("test_"):
254 continue
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
269 if not violations:
270 return ValidationResult(
271 passed=True, message="All functions within complexity 10"
272 )
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."
279 return ValidationResult(
280 passed=False,
281 message=f"{len(violations)} functions exceed complexity 10",
282 details=details,
283 )
285 def _calculate_complexity(self, node: ast.FunctionDef) -> int:
286 """
287 Calculate cyclomatic complexity (McCabe complexity)
289 Args:
290 node: Function AST node
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
308 # ========================================
309 # T: Trackable - Code Traceability
310 # ========================================
311 # Tracking now handled through SPEC references
313 # ========================================
314 # Report Generation
315 # ========================================
317 def generate_report(self, results: dict[str, Any], format: str = "markdown") -> str:
318 """
319 Generate validation report
321 Args:
322 results: Validation result dictionary
323 format: Report format ("markdown" or "json")
325 Returns:
326 str: Report string
327 """
328 if format == "json":
329 return json.dumps(results, indent=2)
331 # Markdown format
332 report = "# TRUST Validation Report\n\n"
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)
343 report += f"## {category.upper()}\n"
344 report += f"**Status**: {status}\n"
345 report += f"**Value**: {value_str}\n\n"
347 return report
349 # ========================================
350 # Tool Selection
351 # ========================================
353 def select_tools(self, project_path: Path) -> dict[str, str]:
354 """
355 Automatically select tools by language
357 Args:
358 project_path: Project path
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 }
372 config = json.loads(config_path.read_text())
373 language = config.get("project", {}).get("language", "python")
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 }
389 # Default (Python)
390 return {
391 "test_framework": "pytest",
392 "coverage_tool": "coverage.py",
393 "linter": "ruff",
394 "type_checker": "mypy",
395 }