gachi-cli-0.1.0.0: src/Gachi/CLI/Doctor.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Module: Gachi.CLI.Doctor
-- Description: Diagnostic health checks and cellular sheaf coherence auditing
--
-- This module implements the operational dispatch for the @gachi doctor@ command,
-- including ambient toolchain verification and 1-dimensional cellular sheaf
-- cohomology auditing across ecosystem checkouts (~/src/PLAN.md §1.A, Milestone 3.5).
--
-- = Architectural Context & Homological Invariants
--
-- Per Note [Cellular Sheaf Coherence and Homological Obstruction Localization]
-- [ref:note-cellular-sheaf-coherence], @gachi doctor@ evaluates the structural
-- health of single repositories or the entire multi-repository ecosystem:
--
-- 1. __Toolchain Health Verification:__
-- Audits ambient system binaries required by the physical verification harnesses:
-- * GHC compiler (pinned to 9.14.1)
-- * Cabal build tool (pinned to 3.18)
-- * HLint static analyzer (zero-hint policy)
-- * Tagref cross-reference validator (zero dangling tags)
--
-- 2. __Cellular Sheaf Coherence Audit ('doctorSheaf'):__
-- Constructs the 1-D cellular sheaf \(\mathcal{F}\) over cell complex \(G = (V, E)\)
-- where vertices \(V\) represent repository checkouts and edges \(E\) represent
-- ten standardization contract dimensions.
-- * Evaluates the sheaf coboundary operator \(\delta^0\).
-- * Confirms 0-th cohomology \(H^0(G; \mathcal{F}) = \ker \delta^0\) harmonic equilibrium (0 drift).
-- * Localizes 1-st cohomology \(H^1(G; \mathcal{F}) = \operatorname{coker} \delta^0\)
-- boundary obstructions, reporting exact drifting repositories, breached contracts,
-- and actionable remediation instructions.
--
-- = Worked Example
--
-- >>> import Gachi.CLI.Options (DoctorOptions(..))
-- >>> -- Invoked via CLI:
-- >>> -- code <- runDoctor (DoctorOptions Nothing True False)
-- >>> -- code == ExitSuccess
module Gachi.CLI.Doctor
( -- * High-Level Doctor Command Runners
runDoctor
, runSheafAudit
, checkToolchainHealth
-- * Helper Binary Inspection
, checkBinaryVersion
, formatToolchainStatus
) where
import Control.Exception (SomeException, catch)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import System.Directory (doesDirectoryExist, doesFileExist, findExecutable, getHomeDirectory)
import System.Exit (ExitCode(..))
import System.FilePath ((</>))
import System.Process (readProcessWithExitCode)
import Gachi.CLI.Options (DoctorOptions(..))
import Gachi.Sync.Sheaf
( SheafReport(..)
, auditEcosystemSheaf
, auditSheafTarget
, formatSheafReport
)
--------------------------------------------------------------------------------
-- 1. Doctor Command Dispatcher
--------------------------------------------------------------------------------
-- | Main execution entry point for 'gachi doctor'.
--
-- Dispatches based on 'DoctorOptions':
--
-- * When @--all@ / @-a@ is specified: Executes whole-ecosystem cellular sheaf
-- coherence auditing across all discovered checkouts in the parent directory.
-- * When @--sheaf@ / @-s@ is specified: Executes targeted cellular sheaf
-- coherence auditing on the designated or current repository.
-- * Default mode: Evaluates ambient toolchain binaries, checks repository
-- governance pinning, and performs the cellular sheaf coherence audit.
--
-- Produces 'ExitSuccess' if all boundary restrictions are in harmonic equilibrium,
-- or 'ExitFailure 1' if fatal cohomological obstructions are detected.
runDoctor :: DoctorOptions -> IO ExitCode
runDoctor opts = do
if doctorAll opts
then do
let parentDir = fromMaybe ".." (doctorTargetDir opts)
runSheafAudit parentDir True
else do
let targetDir = fromMaybe "." (doctorTargetDir opts)
if doctorSheaf opts
then runSheafAudit targetDir False
else do
toolchainOk <- checkToolchainHealth
sheafExit <- runSheafAudit targetDir False
if toolchainOk && sheafExit == ExitSuccess
then pure ExitSuccess
else pure (ExitFailure 1)
--------------------------------------------------------------------------------
-- 2. Cellular Sheaf Coherence Audit Dispatcher
--------------------------------------------------------------------------------
-- | Executes 1-D cellular sheaf cohomology auditing and formats results.
--
-- When 'isEcosystem' is 'True', iterates over all checkouts discovered via
-- 'discoverEcosystemRepositories'. Otherwise, inspects a single target repository.
runSheafAudit :: FilePath -> Bool -> IO ExitCode
runSheafAudit targetPath isEcosystem = do
isDir <- doesDirectoryExist targetPath
if not isDir
then do
TIO.putStrLn ("[-] Error: Target directory does not exist: " <> T.pack targetPath)
pure (ExitFailure 1)
else do
report <- if isEcosystem
then auditEcosystemSheaf targetPath
else auditSheafTarget targetPath
TIO.putStr (formatSheafReport report)
if reportIsHarmonic report
then pure ExitSuccess
else pure (ExitFailure 1)
--------------------------------------------------------------------------------
-- 3. Ambient Toolchain Health Checks
--------------------------------------------------------------------------------
-- | Status descriptor for an ambient system binary.
data ToolStatus = ToolStatus
{ toolName :: !Text
-- ^ Command binary name (e.g. @"ghc"@).
, toolExpected :: !Text
-- ^ Pinned target version or requirement.
, toolDetected :: !(Maybe Text)
-- ^ Detected version string on PATH.
, toolIsCompliant :: !Bool
-- ^ Whether detected version satisfies ecosystem pinning.
} deriving stock (Eq, Show)
-- | Audits ambient system toolchains required by the 5 physical verification gates.
--
-- Verifies availability and version compliance of GHC (9.14.1), Cabal (3.18),
-- HLint, and Tagref. Outputs an aligned status table.
checkToolchainHealth :: IO Bool
checkToolchainHealth = do
ghcVer <- checkBinaryVersion "ghc" ["--numeric-version"]
cabalVer <- checkBinaryVersion "cabal" ["--numeric-version"]
hlintVer <- checkBinaryVersion "hlint" ["--numeric-version"]
tagrefVer <- checkBinaryVersion "tagref" ["--version"]
let tools =
[ ToolStatus "GHC Compiler" "9.14.1" ghcVer (ghcVer == Just "9.14.1")
, ToolStatus "Cabal Tool" "3.18" cabalVer (checkPrefix "3." cabalVer)
, ToolStatus "HLint Linter" "Clean (>=3.5)" hlintVer (checkPresent hlintVer)
, ToolStatus "Tagref Engine" "Active (>=1.4)" tagrefVer (checkPresent tagrefVer)
]
TIO.putStr (formatToolchainStatus tools)
pure (all toolIsCompliant tools)
where
checkPrefix pfx (Just v) = pfx `T.isPrefixOf` v
checkPrefix _ Nothing = False
checkPresent (Just _) = True
checkPresent Nothing = False
-- | Inspects an external binary on @PATH@ or fallback paths, executing arguments and returning stdout.
checkBinaryVersion :: String -> [String] -> IO (Maybe Text)
checkBinaryVersion bin args = do
mbExec <- findExecutable bin >>= \case
Just p -> pure (Just p)
Nothing -> do
home <- getHomeDirectory
let candidates =
[ home </> ".local" </> "bin" </> bin
, home </> ".ghcup" </> "bin" </> bin
]
findExisting candidates
case mbExec of
Nothing -> pure Nothing
Just execPath -> do
(code, out, _) <- readProcessWithExitCode execPath args "" `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))
case code of
ExitSuccess ->
let firstLine = case T.lines (T.strip (T.pack out)) of
(l:_) -> l
[] -> ""
in pure (if T.null firstLine then Nothing else Just firstLine)
ExitFailure _ -> pure Nothing
where
findExisting [] = pure Nothing
findExisting (c:cs) = do
exists <- doesFileExist c
if exists then pure (Just c) else findExisting cs
-- | Formats toolchain audit results as an aligned ASCII diagnostic table.
formatToolchainStatus :: [ToolStatus] -> Text
formatToolchainStatus tools = T.unlines $
[ "================================================================================"
, " Gachi Ambient Toolchain Health Diagnostics"
, "================================================================================"
, " Tool | Expected Version | Detected Version | Status"
, "---------------------+------------------+--------------------------+------------"
]
++ map formatRow tools
++
[ "================================================================================" ]
where
formatRow t =
let nameCol = T.justifyLeft 20 ' ' (toolName t)
expCol = T.justifyLeft 17 ' ' (toolExpected t)
detCol = T.justifyLeft 25 ' ' (fromMaybe "Not Found on PATH" (toolDetected t))
statCol = if toolIsCompliant t then "[+] OK" else "[-] DRIFT"
in nameCol <> "| " <> expCol <> "| " <> detCol <> "| " <> statCol