packages feed

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