"""Documentation-oriented validation for the testing contract.""" from __future__ import annotations import ast from pathlib import Path from typing import Any def _module_has_docstring(path: Path) -> bool: source = path.read_text(encoding="utf-8") return ast.get_docstring(ast.parse(source)) is not None def _iter_contract_paths(contract: dict[str, Any]) -> list[str]: paths: list[str] = [] for item in contract.get("required_docs", []): paths.append(item["path"]) paths.extend(contract.get("managed_modules", [])) paths.extend(contract.get("lint_paths", [])) for suite in contract.get("pytest_suites", {}).values(): paths.extend(suite.get("paths", [])) for item in contract.get("manual_scripts", []): paths.append(item["path"]) return paths def run_check(contract: dict[str, Any], root: Path) -> list[str]: """Return human-readable issues for contract/documentation violations.""" issues: list[str] = [] for item in contract.get("required_docs", []): path = root / item["path"] if not path.exists(): issues.append(f"required doc missing: {item['path']}") continue if path.is_file() and not path.read_text(encoding="utf-8").strip(): issues.append(f"required doc empty: {item['path']}") if not item.get("description", "").strip(): issues.append(f"required doc missing description: {item['path']}") for relative_path in sorted(set(_iter_contract_paths(contract))): if not (root / relative_path).exists(): issues.append(f"contract path missing: {relative_path}") for suite_name, suite in contract.get("pytest_suites", {}).items(): if not suite.get("description", "").strip(): issues.append(f"pytest suite missing description: {suite_name}") for item in contract.get("manual_scripts", []): if not item.get("description", "").strip(): issues.append(f"manual script missing description: {item['path']}") for relative_path in contract.get("managed_modules", []): path = root / relative_path if path.exists() and path.suffix == ".py" and not _module_has_docstring(path): issues.append(f"module docstring missing: {relative_path}") return issues