"""Read-only and transient preflight diagnostics for operators.""" from __future__ import annotations import importlib.util import sys import tempfile from dataclasses import asdict, dataclass from pathlib import Path from gyxx_flow.catalog import WorkflowCatalog from gyxx_flow.core.config import Settings @dataclass(frozen=True, slots=True) class DiagnosticCheck: name: str passed: bool message: str @dataclass(frozen=True, slots=True) class DoctorReport: checks: tuple[DiagnosticCheck, ...] @property def is_healthy(self) -> bool: return all(check.passed for check in self.checks) def as_dict(self) -> dict[str, object]: return { "is_healthy": self.is_healthy, "checks": [asdict(check) for check in self.checks], } def run_doctor(settings: Settings) -> DoctorReport: checks = [ DiagnosticCheck( "python", sys.version_info >= (3, 12), f"Python {sys.version_info.major}.{sys.version_info.minor}", ), DiagnosticCheck( "cli", importlib.util.find_spec("gyxx_flow.__main__") is not None, "gyxx_flow module entry point is importable", ), DiagnosticCheck( "project_root", settings.project_root.is_dir(), "project root exists" if settings.project_root.is_dir() else "project root is missing", ), ] try: catalog = WorkflowCatalog.load(settings.project_root / "config") catalog_ok = len(catalog.scheduled_workflows()) == 23 catalog_message = f"catalog has {len(catalog.scheduled_workflows())} scheduled workflows" except Exception: catalog_ok = False catalog_message = "catalog cannot be validated" checks.append(DiagnosticCheck("catalog", catalog_ok, catalog_message)) data_root_ok = settings.data_root.is_absolute() and settings.data_root != Path( settings.data_root.anchor ) checks.append( DiagnosticCheck( "data_root", data_root_ok, "data root is an explicit non-root path" if data_root_ok else "unsafe data root", ) ) writable, message = _probe_write_permission(settings.data_root) checks.append(DiagnosticCheck("data_root_write", writable, message)) return DoctorReport(tuple(checks)) def _probe_write_permission(path: Path) -> tuple[bool, str]: candidate = Path(path) while not candidate.exists() and candidate.parent != candidate: candidate = candidate.parent if not candidate.is_dir(): return False, "no existing data-root parent directory" try: with tempfile.NamedTemporaryFile( mode="wb", prefix=".gyxx-doctor-probe-", dir=candidate, delete=True ) as stream: stream.write(b"probe") stream.flush() except OSError: return False, "data-root parent is not writable" return True, "data-root parent write probe passed"