packages feed

gachi-retrofit (empty) → 0.1.0.0

raw patch · 8 files changed

+4280/−0 lines, 8 filesdep +basedep +bytestringdep +containers

Dependencies added: base, bytestring, containers, directory, filepath, gachi-core, gachi-retrofit, gachi-scaffold, gachi-schema, gachi-sync, hspec, process, text, time

Files

+ gachi-retrofit.cabal view
@@ -0,0 +1,78 @@+cabal-version: 3.0+name: gachi-retrofit+version: 0.1.0.0+synopsis: Non-destructive codebase retrofitting engine and diagnostic repository inspector for Gachi+description:+  Static analysis of arbitrary target checkouts: detects language stacks, build systems,+  existing tests, documentation gaps relative to DOC_STRATEGY.md, Git hook attribution+  conformance, and physical verification readiness ('gachi retrofit').+license: EUPL-1.2+author: Nadia Yvette Chambers+maintainer: nadia.yvette.chambers@ik.me+category: Development, Tools+build-type: Simple+tested-with:+  GHC == 9.10.3,+  GHC == 9.12.4,+  GHC == 9.14.1++library+  exposed-modules:+    Gachi.Retrofit+    Gachi.Retrofit.Diagnostic+    Gachi.Retrofit.Overlay+    Gachi.Retrofit.QualityGate+    Gachi.Retrofit.Report+    Gachi.Retrofit.Types+  hs-source-dirs: src+  default-language: Haskell2010+  default-extensions:+    OverloadedStrings+    DerivingStrategies+    ScopedTypeVariables+  ghc-options:+    -Wall+    -Wcompat+    -Wredundant-constraints+  build-depends:+    base >= 4.14 && < 5,+    bytestring >= 0.10,+    text >= 2.0,+    containers >= 0.6,+    directory >= 1.3,+    filepath >= 1.4,+    process >= 1.6,+    gachi-core >= 0.1,+    gachi-schema >= 0.1,+    gachi-scaffold >= 0.1,+    gachi-sync >= 0.1++test-suite tests+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs: test+  default-language: Haskell2010+  default-extensions:+    OverloadedStrings+    DerivingStrategies+    ScopedTypeVariables+  ghc-options:+    -Wall+    -Wcompat+    -Wredundant-constraints+    -threaded+  build-depends:+    base >= 4.14 && < 5,+    gachi-retrofit,+    gachi-core,+    gachi-schema,+    gachi-scaffold,+    gachi-sync,+    bytestring >= 0.10,+    text >= 2.0,+    containers >= 0.6,+    directory >= 1.3,+    filepath >= 1.4,+    process >= 1.6,+    time >= 1.9,+    hspec >= 2.10
+ src/Gachi/Retrofit.hs view
@@ -0,0 +1,210 @@+-- | Module: Gachi.Retrofit+-- Description: Non-destructive codebase retrofitting engine & diagnostic inspector+--+-- This is the root library module for @gachi-retrofit@. It provides the unified+-- public API for inspecting legacy, single-package, or foreign repositories, evaluating+-- six diagnostic dimensions, and computing actionable remediation plans per+-- Phase 4 and Milestone 4.1.+--+-- = Monograph: The Theory of Non-Destructive Codebase Retrofitting+--+-- == 1. The Retrofitting Problem and the Conservation of Domain Logic+--+-- Software ecosystems face a perennial tension between standardization and legacy codebases.+-- High-assurance development environments require strict invariants: zero-warning builds under+-- @-Wall -Wcompat -Wredundant-constraints@, clean linters (zero HLint hints), bi-directional+-- Tagref cross-references, unbroken documentation hyperlinks, and measured in-code comment+-- density. However, existing repositories—whether single-package libraries, ad-hoc research+-- prototypes, or foreign language checkouts—rarely conform to these contracts initially.+--+-- Traditional migration approaches either:+-- 1. Demand exhaustive manual refactoring, creating high friction and human error.+-- 2. Force destructive scaffolding re-generation, risking clobbering, overwriting, or deleting+--    irreplaceable domain logic.+--+-- The Gachi Retrofitting Engine establishes an alternative: __Non-Destructive Codebase Retrofitting__.+-- Let \(\mathcal{P}\) denote the universe of repositories. A repository \(P \in \mathcal{P}\) is+-- modeled as a pair:+--+-- \[+--   P = (D, S)+-- \]+--+-- where \(D\) represents the __domain logic__ (the algorithmic core, modules, data types, and+-- mathematical structures inhabiting @src/@, @app/@, @lib/@) and \(S\) represents the __scaffolding__+-- (build tool manifests, documentation, git hooks, CI matrices, and verification harnesses).+--+-- === The Inviolable Conservation Law of Domain Logic+--+-- Any retrofitting transformation \(R : \mathcal{P} \to \mathcal{P}\) must strictly satisfy the+-- __Conservation of Domain Logic__:+--+-- \[+--   \pi_{\text{domain}}(R(P)) = \pi_{\text{domain}}(P)+-- \]+--+-- That is, no retrofitting operation may ever delete, truncate, overwrite, or mutate any domain+-- logic file. All standardization occurs exclusively in the scaffolding component \(S\).+--+-- == 2. Phased Stratification of the Retrofitting Pipeline+--+-- To guarantee safety and transparency, the retrofitting engine decomposes into four+-- phased operational milestones across Phase 4:+--+-- * __Milestone 4.1 (Diagnostic Repository Inspector):__+--   Strictly observational, read-only static analysis. Evaluates the target repository across+--   six orthogonal dimensions, producing a strongly typed 'DiagnosticReport' and quantitative+--   retrofit readiness score \(S(P) \in [0.0, 100.0]\).+--+-- * __Milestone 4.2 (Non-Destructive Skeleton Overlay):__+--   Synthesizes missing ecosystem infrastructure: @.githooks/@, @.gitmessage@, turnkey verification+--   harnesses (@bin/*-verify@, @bin/*-tagref@, @bin/*-linkcheck@, @bin/*-cloc@), documentation+--   skeletons (@docs/INDEX.md@, @docs/notes/*_DESIGN.md@, @docs/notes/DOC_STRATEGY.md@), and+--   agent operational layers (@PLAN.md@, @STATE.md@, @AGENTS.md@).+--+-- * __Milestone 4.3 (Progressive Quality Gate Realization):__+--   Incremental quality remediation stepping through four compliance levels:+--   - Level 0: Toolchain and verification harness installation.+--   - Level 1: Tagref and Markdown link integrity certification.+--   - Level 2: Build warnings resolved under @-Wall -Wcompat@.+--   - Level 3: HLint hints completely eliminated under zero-hint policy.+--+-- * __Milestone 4.4 (Empirical Victim Project Crucible):__+--   End-to-end validation against real-world targets, beginning with @~/src/gongju/@ ("tools/implement").+--+-- == 3. The Six Orthogonal Diagnostic Dimensions+--+-- The diagnostic analyzer ('scanRepository') evaluates target workspaces across six dimensions:+--+-- @+--  +-------------------------------------------------------------------------------++--  |                       Six Diagnostic Dimensions                               |+--  +-------------------------------------------------------------------------------++--  | [1] Language Stacks    : Haskell, Rust, C/C++, Python, Shell (File counts & %) |+--  | [2] Build Systems      : Cabal Multi/Single, Stack, Cargo, CMake, Make, Meson |+--  | [3] Test Infra         : Hspec, Tasty, QuickCheck, Cargo test, Pytest, CTest  |+--  | [4] Documentation Gaps : DOC_STRATEGY.md Checklist (Critical, Recommended)    |+--  | [5] Git Governance     : .githooks, commit-msg RFC 2822 trailers, .gitmessage |+--  | [6] Quality Gates      : Turnkey verify runner, Tagref, Linkcheck, CLOC gate  |+--  +-------------------------------------------------------------------------------++-- @+--+-- == 4. Quantitative Readiness Scoring Formula+--+-- The ecosystem retrofit readiness score \(S(P)\) is a weighted sum normalized to \([0.0, 100.0]\):+--+-- \[+--   S(P) = w_{\text{lang}} \cdot s_{\text{lang}}+--        + w_{\text{test}} \cdot s_{\text{test}}+--        + w_{\text{doc}} \cdot s_{\text{doc}}+--        + w_{\text{git}} \cdot s_{\text{git}}+--        + w_{\text{verif}} \cdot s_{\text{verif}}+-- \]+--+-- where:+-- * \(w_{\text{lang}} = 15\%\): Clear primary language and coherent build manifest.+-- * \(w_{\text{test}} = 20\%\): Active test directories (10%) and declared suites with frameworks (10%).+-- * \(w_{\text{doc}} = 30\%\): Proportion of present Critical and Recommended documents per @DOC_STRATEGY.md@.+-- * \(w_{\text{git}} = 20\%\): Git repository (5%), .githooks (5%), commit-msg with attribution (5%), template (5%).+-- * \(w_{\text{verif}} = 15\%\): Turnkey verify runner (5%), Tagref (3%), Linkcheck (3%), CLOC (2%), Linters (2%).+--+-- = Worked Example+--+-- >>> import Gachi.Retrofit+-- >>> report <- scanRepository "."+-- >>> diagProjectName report+-- "gachi"+-- >>> diagRetrofitScore report >= 90.0+-- True+-- >>> let dashboard = formatDiagnosticReport report+-- >>> T.null dashboard+-- False+module Gachi.Retrofit+  ( -- * Re-exports from Gachi.Retrofit.Diagnostic+    scanRepository+  , detectLanguages+  , detectBuildSystems+  , detectTestInfrastructure+  , detectDocumentationGaps+  , detectGitGovernance+  , detectVerificationHarnesses+  , computeRetrofitScore+  , computeRemediationPlan+  , inferProjectName+  , listRepositoryFiles+  , isIgnoredDirectory++    -- * Re-exports from Gachi.Retrofit.Report+  , formatDiagnosticReport+  , formatDiagnosticMarkdown+  , formatDiagnosticSummaryLine+  , renderScoreBar+  , scoreClassification+  , padRight++    -- * Re-exports from Gachi.Retrofit.Overlay+  , OverlayActionType(..)+  , OverlayCategory(..)+  , OverlayAction(..)+  , GitGovernanceAction(..)+  , OverlayPlan(..)+  , OverlayResult(..)+  , computeOverlayPlan+  , synthesizeOverlayCandidates+  , detectGitAuthor+  , executeOverlayPlan+  , overlayRepository+  , formatOverlayPlan+  , formatOverlayPlanMarkdown+  , formatOverlayResult+  , overlayCategoryName++    -- * Re-exports from Gachi.Retrofit.Types+  , Language(..)+  , LanguageStats(..)+  , languageName+  , BuildSystem(..)+  , WorkspaceLayout(..)+  , buildSystemName+  , layoutName+  , TestFramework(..)+  , TestSuiteInfo(..)+  , TestInfrastructure(..)+  , testFrameworkName+  , DocCategory(..)+  , DocSeverity(..)+  , DocGap(..)+  , docCategoryName+  , docSeverityName+  , GitGovernanceStatus(..)+  , VerificationHarnessStatus(..)+  , DiagnosticReport(..)++    -- * Re-exports from Gachi.Retrofit.QualityGate (Milestone 4.3)+  , QualityLevel(..)+  , qualityLevelName+  , qualityLevelOrdinal+  , GateCheck(..)+  , GateResult(..)+  , QualityGateReport(..)+  , RemediationStep(..)+  , RemediationPriority(..)+  , remediationPriorityName+  , level0Gates+  , level1Gates+  , level2Gates+  , level3Gates+  , allLevelGates+  , evaluateQualityGates+  , evaluateGateCheck+  , computeAchievedLevel+  , computeRemediationSteps+  , formatQualityGateReport+  , formatQualityGateMarkdown+  ) where++import Gachi.Retrofit.Diagnostic+import Gachi.Retrofit.Overlay+import Gachi.Retrofit.QualityGate+import Gachi.Retrofit.Report+import Gachi.Retrofit.Types
+ src/Gachi/Retrofit/Diagnostic.hs view
@@ -0,0 +1,936 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.Retrofit.Diagnostic+-- Description: Diagnostic repository inspector and static analysis engine+--+-- This module implements the static analysis engine for inspecting arbitrary+-- codebases (Milestone 4.1, Phase 4). It performs strictly read-only diagnostics+-- across languages, build systems, test suites, documentation, git hooks, and+-- verification harnesses.+--+-- = Architectural Context & Invariants+--+-- Note [Non-Destructive Skeleton Overlay]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- [tag:note-nondestructive-retrofit]+-- The Non-Destructive Codebase Retrofitting engine ('gachi retrofit') adopts legacy,+-- single-package, or foreign repositories into the canonical multi-package ecosystem+-- hierarchy (~/src/PLAN.md §1.A, Phase 4).+--+-- Core Invariants:+-- 1. __Preservation of Domain Logic:__ Retrofitting operations must never delete,+--    overwrite, or mutate existing domain source files located in @src/@, @app/@,+--    @lib/@, or domain-specific subdirectories.+-- 2. __Pure Observational Diagnosis:__ Milestone 4.1 performs strictly read-only+--    static analysis across six orthogonal dimensions (languages, build systems,+--    test infrastructure, @DOC_STRATEGY.md@ gaps, Git hooks, verification gates)+--    before any mutation or overlay is considered.+-- 3. __Progressive Realization:__ Quality gates are phased incrementally:+--    - Level 0: Scaffolding, documentation skeletons, and verification harnesses.+--    - Level 1: Tagref cross-references and Markdown link integrity.+--    - Level 2: Clean build under @-Wall -Wcompat -Wredundant-constraints@.+--    - Level 3: HLint hints completely eliminated (zero-hint policy).+--+-- Note [Arbitrary Target Codebase Language Classification and Extension Semantics]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- Automated language classification across arbitrary codebases presents unique challenges:+-- 1. __Pruning Build Artifacts:__ Repositories frequently contain thousands of generated+--    files (e.g. @dist-newstyle@, @node_modules@, @target@, @.stack-work@, @__pycache__@).+--    The crawler explicitly prunes these directories prior to extension matching to prevent+--    skewed distributions or out-of-memory traversal.+-- 2. __Polyglot Workspaces:__ Large systems combine multiple language runtimes (e.g.+--    Haskell core logic, Rust foreign function bindings, C runtime drivers, Python scripts).+--    The scanner computes absolute file counts and proportional percentages across all+--    detected languages, identifying the primary language by relative majority.+-- 3. __Header Disambiguation:__ C and C++ header files (@.h@, @.hpp@) are tracked with+--    their respective language families to provide an accurate overview of native subsystems.+--+-- Note [Test Infrastructure Discovery and Framework Introspection]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- Rather than executing untrusted test scripts during diagnostic inspection, the analyzer+-- performs static introspection:+-- 1. Discovers standard test directories (@test/@, @tests/@, @spec/@).+-- 2. Scans test source files for known testing framework modules (@Test.Hspec@, @Test.Tasty@,+--    @Test.QuickCheck@, @Hedgehog@, @pytest@, @cargo test@).+-- 3. Identifies declared test targets and reports whether the repository possesses active+--    automated quality gates.+--+-- Note [DOC_STRATEGY.md Corpus Adherence and Gap Categorization]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- The canonical documentation strategy (@DOC_STRATEGY.md@ §1) establishes that documentation+-- serves distinct audiences with strictly defined fact classes and canonicity.+-- The diagnostic inspector evaluates repositories against this checklist, categorizing+-- missing documents by severity:+-- * __Critical:__ @README.md@ (arrival orientation) and @LICENSE@ (legal terms).+-- * __Recommended:__ @PLAN.md@ / @STATE.md@ (session loop), @AGENTS.md@ (agent advisory),+--   @docs/INDEX.md@ (reading paths), @docs/notes/DOC_STRATEGY.md@ (policy), and+--   @docs/notes/COMMIT_ATTRIBUTION.md@ (RFC 2822 trailer rules).+-- * __Optional:__ @CLAUDE.md@ (symlinks), @CHANGELOG.md@, and tutorial tracks.+--+-- Note [Git Governance, Hook Executability, and RFC 2822 Trailer Verification]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- To maintain clean copyright and attribution boundaries across the ecosystem, Git hooks+-- enforce strict commit trailer policies:+-- 1. __Trailer Format:__ AI coding assistants must be attributed using RFC 2822 trailers+--    (e.g. @Assisted-by: Antigravity (Google / gemini-3.8-pro)@).+-- 2. __Legal Boundaries:__ AI assistants must NEVER be recorded as @Co-authored-by:@+--    (which implies legal joint copyright authorship) or @Signed-off-by:@ (which certifies+--    the human Developer Certificate of Origin).+-- 3. __Hook Validation:__ The inspector validates that @commit-msg@ exists, has POSIX+-- Note [Heuristic Project Name and Workspace Topography Resolution]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- Determining the identity and canonical slug of an arbitrary codebase requires a deterministic+-- resolution ladder:+-- 1. __Direct Manifest Discovery:__ If a single @*.cabal@ manifest exists at the root level+--    (e.g. @victim.cabal@), its basename defines the canonical project name.+-- 2. __Multi-Package Workspace Fallback:__ If a multi-package workspace definition (@cabal.project@)+--    is present, individual child package manifests in @packages/*/@ must not be confused with+--    the parent project. In this scenario, the normalized directory basename of the workspace+--    root (e.g. @"gachi"@) serves as the authoritative project identifier.+-- 3. __Absolute Path Canonicalization:__ The scanner resolves relative target paths (e.g. @.@)+--    via 'makeAbsolute' prior to name extraction, ensuring robust identification across subpackage+--    invocations.+--+-- Note [Comprehensive Six-Pillar Diagnostic Scoring Formula]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- The diagnostic evaluation culminates in a quantitative retrofit score S(P) in [0.0, 100.0]:+-- 1. __Languages & Toolchains (15%):__ Rewards clear language identification (Haskell, Rust, etc.)+--    and recognized declarative build manifests (@cabal.project@, @Cargo.toml@).+-- 2. __Test Infrastructure (20%):__ Evaluates test directories (@test/@, @spec/@), declared suites,+--    and recognized testing frameworks (@Test.Hspec@, @Test.Tasty@, @pytest@).+-- 3. __Documentation Integrity (30%):__ Proportionally scores presence of Critical and Recommended+--    assets per @DOC_STRATEGY.md@ §1.+-- 4. __Git Governance & Attribution (20%):__ Evaluates @.git@, @.githooks@, executable @commit-msg@+--    with RFC 2822 @Assisted-by:@ trailer validation, and @.gitmessage@ commit template pinning.+-- 5. __Verification Harnesses (15%):__ Checks presence and executability of turnkey @bin/*-verify@,+--    Tagref runner, Markdown linkcheck, and CLOC comment density audit scripts.+--+-- Note [Remediation Action Priority Ordering and Phase 4 Transition]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- The diagnostic analyzer transforms localized defects into an ordered remediation plan:+-- 1. __Sovereign Git Governance:__ Bootstrapping @.githooks@ and commit trailer enforcement must+--    precede any file mutations to guarantee attribution integrity from the first commit.+-- 2. __Physical Verification Harnesses:__ Turnkey test runners (@bin/*-verify@) are provisioned+--    next so every subsequent transformation can be verified immediately.+-- 3. __Critical Documentation Scaffolding:__ @README.md@ and @LICENSE@ establish legal and+--    operational identity.+-- 4. __Recommended Operational Governance:__ @PLAN.md@, @STATE.md@, and @AGENTS.md@ establish+--    the two-file loop and AI assistant boundaries.+-- 5. __Test Suite & Layout Modernization:__ Establishes baseline test coverage and guides+--    single-package repositories toward multi-package workspace modularity.+--+-- Note [Safe Recursive Filesystem Crawling with Zero Allocations]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- Arbitrary checkouts frequently contain deep recursive structures, broken symlinks, or+-- million-file generated directories (e.g. @node_modules@, @dist-newstyle@, @.stack-work@).+-- To maintain sub-second response times and zero memory exhaustion:+-- 1. __Pre-Descent Pruning:__ The crawler matches directory basenames against 'ignoredDirectories'+--    before calling 'listDirectory', completely preventing traversal of massive trees.+-- 2. __Exception Containment:__ File permission exceptions and dangling symlinks are swallowed+--    gracefully via 'catch', allowing partial audits of restricted file trees.+-- 3. __Relative Path Normalization:__ All discovered paths are returned relative to the root,+--    enabling deterministic matching against canonical templates.+--+-- = Worked Example+--+-- >>> report <- scanRepository "."+-- >>> diagProjectName report+-- "gachi"+-- >>> diagRetrofitScore report >= 50.0+-- True+module Gachi.Retrofit.Diagnostic+  ( -- * High-Level Scanner+    scanRepository++    -- * Dimension Analyzers+  , detectLanguages+  , detectBuildSystems+  , detectTestInfrastructure+  , detectDocumentationGaps+  , detectGitGovernance+  , detectVerificationHarnesses++    -- * Scoring & Remediation Planning+  , computeRetrofitScore+  , computeRemediationPlan+  , inferProjectName++    -- * Filesystem Traversal+  , listRepositoryFiles+  , isIgnoredDirectory+  ) where++import Control.Exception (SomeException, catch)+import Data.Char (toLower)+import Data.List (isPrefixOf, isSuffixOf, nub, partition, sortBy)+import qualified Data.Map.Strict as Map+import Data.Maybe (isJust, mapMaybe)+import Data.Ord (comparing)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory+  ( doesDirectoryExist+  , doesFileExist+  , executable+  , getPermissions+  , listDirectory+  , makeAbsolute+  )+import System.Exit (ExitCode(..))+import System.FilePath (dropTrailingPathSeparator, takeDirectory, takeExtension, takeFileName, (</>))+import System.Process (readProcessWithExitCode)++import Gachi.Retrofit.Types++--------------------------------------------------------------------------------+-- 1. Filesystem Traversal+--------------------------------------------------------------------------------++-- | Directory basenames intentionally skipped during diagnostic exploration to avoid+-- infinite loops, deep virtualenvs, or massive build artifacts.+ignoredDirectories :: [String]+ignoredDirectories =+  [ ".git"+  , "dist-newstyle"+  , ".stack-work"+  , "target"+  , "node_modules"+  , "__pycache__"+  , ".venv"+  , "venv"+  , "env"+  , ".cabal-sandbox"+  , ".direnv"+  , "dist"+  , "_build"+  , ".tox"+  ]++-- | Predicate checking whether a directory path should be ignored during traversal.+--+-- Matches known build artifact directories and temporary scratch paths starting+-- with @.tmp@.+--+-- === Examples+--+-- >>> isIgnoredDirectory "dist-newstyle"+-- True+-- >>> isIgnoredDirectory "src"+-- False+isIgnoredDirectory :: FilePath -> Bool+isIgnoredDirectory dirName =+  let base = takeFileName (dropTrailingPathSeparator dirName)+  in base `elem` ignoredDirectories || ".tmp" `isPrefixOf` base++-- | Recursively traverses a target directory collecting relative file paths.+--+-- Automatically prunes ignored directories ('isIgnoredDirectory') and gracefully+-- recovers from filesystem permission errors, broken symlinks, or transient I/O faults.+-- Returns relative paths normalized against the inspected directory root.+listRepositoryFiles :: FilePath -> IO [FilePath]+listRepositoryFiles root = go ""+  where+    go relDir = do+      let fullDir = if null relDir then root else root </> relDir+      isDir <- doesDirectoryExist fullDir `catch` (\(_ :: SomeException) -> pure False)+      if not isDir || (not (null relDir) && isIgnoredDirectory relDir)+        then pure []+        else do+          entries <- listDirectory fullDir `catch` (\(_ :: SomeException) -> pure [])+          concatMapM (processEntry relDir) entries++    processEntry relDir entry+      | isIgnoredDirectory entry = pure []+      | otherwise = do+          let relPath = if null relDir then entry else relDir </> entry+              fullPath = root </> relPath+          isDir <- doesDirectoryExist fullPath `catch` (\(_ :: SomeException) -> pure False)+          if isDir+            then go relPath+            else pure [relPath]++    concatMapM f xs = fmap concat (mapM f xs)++--------------------------------------------------------------------------------+-- 2. Programming Language Detection+--------------------------------------------------------------------------------++-- | Maps a file extension to its corresponding programming language.+--+-- Covers Haskell, Rust, C, C++, Python, Shell, OCaml, Go, Java, JavaScript, and TypeScript.+classifyExtension :: String -> Maybe (Language, Text)+classifyExtension ext = case map toLower ext of+  ".hs"    -> Just (LangHaskell, ".hs")+  ".lhs"   -> Just (LangHaskell, ".lhs")+  ".rs"    -> Just (LangRust, ".rs")+  ".c"     -> Just (LangC, ".c")+  ".h"     -> Just (LangC, ".h")+  ".cpp"   -> Just (LangCpp, ".cpp")+  ".cc"    -> Just (LangCpp, ".cc")+  ".cxx"   -> Just (LangCpp, ".cxx")+  ".hpp"   -> Just (LangCpp, ".hpp")+  ".hxx"   -> Just (LangCpp, ".hxx")+  ".py"    -> Just (LangPython, ".py")+  ".sh"    -> Just (LangShell, ".sh")+  ".bash"  -> Just (LangShell, ".bash")+  ".zsh"   -> Just (LangShell, ".zsh")+  ".ml"    -> Just (LangOCaml, ".ml")+  ".mli"   -> Just (LangOCaml, ".mli")+  ".go"    -> Just (LangGo, ".go")+  ".java"  -> Just (LangJava, ".java")+  ".js"    -> Just (LangJavaScript, ".js")+  ".mjs"   -> Just (LangJavaScript, ".mjs")+  ".cjs"   -> Just (LangJavaScript, ".cjs")+  ".ts"    -> Just (LangTypeScript, ".ts")+  ".tsx"   -> Just (LangTypeScript, ".tsx")+  _        -> Nothing++-- | Purely analyzes a collection of repository file paths to determine programming+-- language distributions, file counts, and percentage breakdowns.+--+-- Groups recognized files by 'Language', sums occurrences, computes percentage+-- share relative to total recognized source files, and sorts descending by file count.+--+-- === Examples+--+-- >>> let stats = detectLanguages ["src/A.hs", "src/B.hs", "native/c.rs"]+-- >>> length stats+-- 2+-- >>> langFiles (head stats)+-- 2+detectLanguages :: [FilePath] -> [LanguageStats]+detectLanguages paths =+  let recognized = mapMaybe (classifyExtension . takeExtension) paths+      totalCount = length recognized+      grouped = foldr insertLang Map.empty recognized+      statsList = Map.foldrWithKey (buildStat totalCount) [] grouped+  in sortBy (comparing (negate . langFiles)) statsList+  where+    insertLang (lang, ext) m =+      let (cnt, exts) = Map.findWithDefault (0, []) lang m+      in Map.insert lang (cnt + 1, if ext `elem` exts then exts else ext : exts) m++    buildStat total lang (cnt, exts) acc =+      let pct = if total > 0 then (fromIntegral cnt / fromIntegral total) * 100.0 else 0.0+          stat = LanguageStats+            { langType       = lang+            , langFiles      = cnt+            , langPercentage = fromIntegral (round (pct * 10 :: Double) :: Int) / 10.0+            , langExtensions = exts+            }+      in stat : acc++--------------------------------------------------------------------------------+-- 3. Build Systems & Layout Detection+--------------------------------------------------------------------------------++-- | Detects build tool manifests and classifies the repository workspace layout.+--+-- === Build Manifest Detection Mechanics+--+-- The analyzer inspects file paths across two topological tiers:+-- 1. __Root Manifests:__ Checks whether authoritative manifests inhabit the repository+--    root directory:+--    - Cabal Workspace: @cabal.project@ establishes multi-package boundaries.+--    - Single Cabal Package: Any @*.cabal@ residing directly in @takeDirectory p == "."@.+--    - Stack Project: @stack.yaml@ defines curated Stackage resolver pins.+--    - Hpack Specification: @package.yaml@ generates @*.cabal@ dynamically.+--    - Rust Cargo: @Cargo.toml@ specifies package or virtual workspace dependencies.+--    - CMake: @CMakeLists.txt@ defines cross-platform native builds.+--    - Make: Standard @Makefile@, @makefile@, or @GNUmakefile@.+--    - Meson: @meson.build@ provides high-speed Ninja-based builds.+--    - Python Packaging: @pyproject.toml@, @setup.py@, @setup.cfg@, @requirements.txt@.+--    - Nix Expressions: @flake.nix@ and @default.nix@ establish hermetic sandbox builds.+--+-- 2. __Subpackage Traversal:__+--    Discovers nested packages within @packages/*/*.cabal@, signaling modern multi-package+--    workspaces.+--+-- === Layout Classification Semantics+--+-- The discovered manifests determine the 'WorkspaceLayout':+-- * 'LayoutMultiPackage': Segregated packages in @packages/*/@. The ideal topology for+--   Gachi ecosystem integration.+-- * 'LayoutSinglePackage': Standalone library or tool located at root. Retrofitting+--   recommends migrating to multi-package modularity when scaling.+-- * 'LayoutPolyglot': Co-existence of multiple heterogeneous build systems (e.g.+--   Haskell + Rust FFI or C + Python bindings). Requires partitioned CI workflows.+-- * 'LayoutUnstructured': Raw source files discovered without any declarative manifest.+--   Requires baseline build system synthesis.+--+-- === Examples+--+-- >>> let (builds, layout) = detectBuildSystems "." ["cabal.project", "packages/core/core.cabal"]+-- >>> layout+-- LayoutMultiPackage+-- >>> BuildCabalProject `elem` builds+-- True+detectBuildSystems :: FilePath -> [FilePath] -> ([BuildSystem], WorkspaceLayout)+detectBuildSystems _root paths =+  let manifests = findBuildManifests paths+      layout = classifyLayout paths manifests+  in (manifests, layout)+  where+    findBuildManifests ps =+      let hasRoot name = name `elem` ps+          hasRootSuffix sfx = any (\p -> takeDirectory p == "." && sfx `isSuffixOf` p) ps+          hasSubCabal = any (\p -> "packages" `isPrefixOf` p && ".cabal" `isSuffixOf` p) ps++          cabalProj = [BuildCabalProject | hasRoot "cabal.project"]+          cabalPkg  = [BuildCabalPackage | hasRootSuffix ".cabal" || hasSubCabal]+          stack     = [BuildStack | hasRoot "stack.yaml"]+          hpack     = [BuildHpack | hasRoot "package.yaml"]+          cargo     = [BuildCargo | hasRoot "Cargo.toml"]+          cmake     = [BuildCMake | hasRoot "CMakeLists.txt"]+          make      = [BuildMake | any (`elem` ps) ["Makefile", "makefile", "GNUmakefile"]]+          meson     = [BuildMeson | hasRoot "meson.build"]+          poetry    = [BuildPoetry | hasRoot "pyproject.toml"]+          setuptools = [BuildSetuptools | hasRoot "setup.py" || hasRoot "setup.cfg"]+          pip       = [BuildPip | hasRoot "requirements.txt"]+          nix       = [BuildNix | hasRoot "flake.nix" || hasRoot "default.nix"]+      in cabalProj ++ cabalPkg ++ stack ++ hpack ++ cargo ++ cmake ++ make ++ meson ++ poetry ++ setuptools ++ pip ++ nix++    classifyLayout ps manifests+      | any (\p -> "packages/" `isPrefixOf` p) ps = LayoutMultiPackage+      | isPolyglot manifests = LayoutPolyglot+      | not (null manifests) = LayoutSinglePackage+      | otherwise            = LayoutUnstructured++    isPolyglot ms =+      let hasHaskell = any (`elem` [BuildCabalProject, BuildCabalPackage, BuildStack, BuildHpack]) ms+          hasRust    = BuildCargo `elem` ms+          hasC       = any (`elem` [BuildCMake, BuildMake, BuildMeson]) ms+          hasPy      = any (`elem` [BuildPoetry, BuildFlit, BuildSetuptools, BuildPip]) ms+          count = length (filter id [hasHaskell, hasRust, hasC, hasPy])+      in count > 1++--------------------------------------------------------------------------------+-- 4. Test Infrastructure Detection+--------------------------------------------------------------------------------++-- | Identifies test directories, declared test suites, and testing frameworks.+--+-- === Static Test Introspection Mechanics+--+-- The analyzer inspects repository file paths to detect automated test infrastructure+-- without compiling or running external scripts:+--+-- 1. __Directory Detection:__ Discovers directories dedicated to testing (@test/@, @tests/@,+--    @spec/@, or subpackage test paths such as @packages/*/test/@).+-- 2. __Test File Counting:__ Identifies test source files matching standard conventions:+--    - Haskell: Files named @*Spec.hs@, @*Test.hs@, or residing inside a @test/@ directory.+--    - Rust: Files containing @"test"@ or residing inside a @tests/@ integration test folder.+--    - Python: Files matching @test_*.py@ or @*_test.py@.+-- 3. __Framework Keyword Introspection:__ Samples discovered test files (up to 5) to inspect+--    their module imports and text content, detecting testing frameworks:+--    - 'FrameworkHspec': BDD testing via @Test.Hspec@.+--    - 'FrameworkTasty': Test runner via @Test.Tasty@.+--    - 'FrameworkQuickCheck': Property testing via @Test.QuickCheck@.+--    - 'FrameworkHedgehog': Integrated property testing via @Hedgehog@.+--    - 'FrameworkCargoTest': Built-in Rust test attributes.+--    - 'FrameworkPytest': Python pytest framework.+--+-- Returns a 'TestInfrastructure' record containing test directories, file counts, and+-- identified frameworks.+detectTestInfrastructure :: FilePath -> [FilePath] -> [BuildSystem] -> IO TestInfrastructure+detectTestInfrastructure root paths _buildSystems = do+  let testDirs = filter isTestDirectory paths+      uniqueDirs = nub (map takeDirectory testDirs)+      testFiles = filter isTestFile paths+      hasAnyTests = not (null testDirs) || not (null testFiles)++  -- Scan discovered test files for framework imports+  discoveredFrameworks <- inspectTestFiles root testFiles+  let suites =+        [ TestSuiteInfo+            { testSuiteName  = "test"+            , testSuitePath  = case uniqueDirs of+                                 (d:_) -> d+                                 []    -> "test"+            , testFrameworks = discoveredFrameworks+            }+        | hasAnyTests+        ]++  pure TestInfrastructure+    { hasTests        = hasAnyTests+    , testSuites      = suites+    , testDirectories = uniqueDirs+    , testFileCount   = length testFiles+    }+  where+    isTestDirectory p =+      let d = takeDirectory p+      in d == "test" || d == "tests" || d == "spec" || "/test" `isInfixOf` d || "/tests" `isInfixOf` d++    isTestFile p =+      let f = takeFileName p+      in (takeExtension p == ".hs" && ("Spec.hs" `isSuffixOf` f || "Test.hs" `isSuffixOf` f || takeDirectory p == "test"))+         || (takeExtension p == ".rs" && ("test" `isInfixOf` f || takeDirectory p == "tests"))+         || (takeExtension p == ".py" && ("test_" `isPrefixOf` f || "_test.py" `isSuffixOf` f))++    inspectTestFiles _ [] = pure []+    inspectTestFiles r files = do+      let sample = take 5 files+      contents <- mapM (\f -> TIO.readFile (r </> f) `catch` (\(_ :: SomeException) -> pure "")) sample+      let allText = T.unwords contents+          hspec  = [FrameworkHspec | "hspec" `T.isInfixOf` T.toLower allText || "Test.Hspec" `T.isInfixOf` allText]+          tasty  = [FrameworkTasty | "tasty" `T.isInfixOf` T.toLower allText || "Test.Tasty" `T.isInfixOf` allText]+          qc     = [FrameworkQuickCheck | "quickcheck" `T.isInfixOf` T.toLower allText]+          hedge  = [FrameworkHedgehog | "hedgehog" `T.isInfixOf` T.toLower allText]+          pytest = [FrameworkPytest | "pytest" `T.isInfixOf` T.toLower allText]+          cargo  = [FrameworkCargoTest | any (\f -> takeExtension f == ".rs") files]+      pure (nub (hspec ++ tasty ++ qc ++ hedge ++ pytest ++ cargo))++    isInfixOf needle haystack = needle `isPrefixOf` haystack || case haystack of+      (_:rest) -> needle `isInfixOf` rest+      []       -> False++--------------------------------------------------------------------------------+-- 5. Documentation Gaps Analysis+--------------------------------------------------------------------------------++-- | Specification of canonical documentation items per @DOC_STRATEGY.md@.+--+-- Establishes the authoritative inventory of twelve standard documentation assets,+-- categorized by purpose and prioritized by severity.+canonicalDocChecklist :: [(FilePath, DocCategory, DocSeverity, Text)]+canonicalDocChecklist =+  [ ( "README.md"+    , DocOrientation+    , SeverityCritical+    , "Orientation and project entry point for newcomers and users."+    )+  , ( "docs/INDEX.md"+    , DocCorpusInventory+    , SeverityRecommended+    , "Curated corpus inventory and reading paths per DOC_STRATEGY.md §1."+    )+  , ( "docs/notes/DOC_STRATEGY.md"+    , DocStrategy+    , SeverityRecommended+    , "Canonical documentation policy, genres, and precedence rules."+    )+  , ( "docs/notes/COMMIT_ATTRIBUTION.md"+    , DocAttribution+    , SeverityRecommended+    , "Commit trailer specification (RFC 2822 Assisted-by: trailers)."+    )+  , ( "PLAN.md"+    , DocGovernance+    , SeverityRecommended+    , "Strategic roadmap and phase milestone tracking (the two-file loop)."+    )+  , ( "STATE.md"+    , DocGovernance+    , SeverityRecommended+    , "Active session frontier and verification status (the two-file loop)."+    )+  , ( "AGENTS.md"+    , DocAdvisory+    , SeverityRecommended+    , "Bounded AI assistant operational context and modular rules pointer."+    )+  , ( "CLAUDE.md"+    , DocAdvisory+    , SeverityOptional+    , "Compatibility pointer symlink to AGENTS.md."+    )+  , ( "docs/TUTORIAL.md"+    , DocTutorial+    , SeverityOptional+    , "End-to-end tutorial track with worked examples."+    )+  , ( "docs/MAINTAINER_GUIDE.md"+    , DocOperational+    , SeverityRecommended+    , "Maintainer operational manual: release lifecycle, gates, and triage."+    )+  , ( "CHANGELOG.md"+    , DocChangelog+    , SeverityOptional+    , "Chronological version history and notable release changes."+    )+  , ( "LICENSE"+    , DocChangelog+    , SeverityCritical+    , "Open source licensing terms (e.g. EUPL-1.2 or Apache-2.0)."+    )+  ]++-- | Evaluates presence of canonical documentation files against @DOC_STRATEGY.md@.+--+-- === Documentation Fact Classes & Canonicity+--+-- In accordance with @DOC_STRATEGY.md@ §1, documentation is partitioned into strict fact+-- classes to prevent documentation drift:+--+-- 1. __Arrivals (@README.md@):__+--    Orientation facts only. Always defers to canonical design notes for technical claims.+-- 2. __Curated Corpus Inventory (@docs/INDEX.md@):__+--    Comprehensive catalog of all documents, their reading order, fact classes, and authority.+-- 3. __Policy & Authority (@docs/notes/DOC_STRATEGY.md@):__+--    Corpus rules, lifecycle, and the Precedence Rule ("code wins at runtime; notes corrected regardless").+-- 4. __Attribution Invariants (@docs/notes/COMMIT_ATTRIBUTION.md@):__+--    Linux kernel RFC 2822 trailer format for human authors and AI coding assistants.+-- 5. __Governance & Session Frontiers (@PLAN.md@, @STATE.md@):__+--    The Two-File Loop: tracking operational roadmap milestones and active session state.+-- 6. __Agent Advisory Layer (@AGENTS.md@):__+--    Bounded (~4 KB) pointer projection into canonical documents, preventing context degradation.+--+-- Compares repository file paths against 'canonicalDocChecklist', recognizing+-- standard variations (e.g. @LICENSE.md@ or @LICENSE.txt@ for @LICENSE@).+detectDocumentationGaps :: FilePath -> [FilePath] -> [DocGap]+detectDocumentationGaps _root paths =+  map checkItem canonicalDocChecklist+  where+    checkItem (path, cat, sev, advice) =+      let present = path `elem` paths || (path == "LICENSE" && any (\p -> p == "LICENSE.md" || p == "LICENSE.txt") paths)+      in DocGap+        { docPath     = path+        , docCategory = cat+        , docPresent  = present+        , docSeverity = sev+        , docAdvice   = advice+        }++--------------------------------------------------------------------------------+-- 6. Git Governance & Attribution Conformance+--------------------------------------------------------------------------------++-- | Inspects Git repository state, hook paths, and commit trailer validation.+--+-- === Git Governance Invariants+--+-- Audits whether the repository conforms to ecosystem source control standards:+--+-- 1. __Repository Initialization:__ Verifies presence of the @.git@ directory at the root.+-- 2. __Dedicated Hooks Directory:__ Validates existence of @.githooks/@ and queries ambient+--    Git configuration (@git config core.hooksPath@) to confirm hooks are pinned to version+--    control rather than local unstaged @.git/hooks/@.+-- 3. __Executable Commit-Msg Hook:__ Confirms presence and POSIX executable bits (+x) for+--    @commit-msg@.+-- 4. __RFC 2822 Trailer Validation:__ Scans @commit-msg@ script logic to certify that+--    Sarutahiko / RFC 2822 @Assisted-by:@ trailers are validated and enforced, while rejecting+--    illicit AI @Co-authored-by:@ and @Signed-off-by:@ tags.+-- 5. __Executable Pre-Commit Hook:__ Confirms presence and executable bits (+x) for @pre-commit@,+--    which runs Tagref, linkcheck, or formatting pre-checks.+-- 6. __Commit Template Pinning:__ Checks presence of @.gitmessage@ and queries Git+--    configuration (@git config commit.template@) to ensure contributors are guided by the+--    standard HEREDOC commit format.+detectGitGovernance :: FilePath -> IO GitGovernanceStatus+detectGitGovernance root = do+  hasGit <- doesDirectoryExist (root </> ".git") `catch` (\(_ :: SomeException) -> pure False)+  hasHooks <- doesDirectoryExist (root </> ".githooks") `catch` (\(_ :: SomeException) -> pure False)++  hooksCfg <- queryGitConfig root "core.hooksPath"+  templateCfg <- queryGitConfig root "commit.template"++  -- Inspect commit-msg hook+  let commitMsgPath = if hasHooks then root </> ".githooks" </> "commit-msg" else root </> ".git" </> "hooks" </> "commit-msg"+  hasCommitMsg <- doesFileExist commitMsgPath `catch` (\(_ :: SomeException) -> pure False)+  commitMsgExec <- if hasCommitMsg+    then (executable <$> getPermissions commitMsgPath) `catch` (\(_ :: SomeException) -> pure False)+    else pure False+  commitMsgContent <- if hasCommitMsg+    then TIO.readFile commitMsgPath `catch` (\(_ :: SomeException) -> pure "")+    else pure ""+  let hasAttributionCheck = "assisted-by" `T.isInfixOf` T.toLower commitMsgContent+                         || "sarutahiko" `T.isInfixOf` T.toLower commitMsgContent+                         || "rfc 2822" `T.isInfixOf` T.toLower commitMsgContent++  -- Inspect pre-commit hook+  let preCommitPath = if hasHooks then root </> ".githooks" </> "pre-commit" else root </> ".git" </> "hooks" </> "pre-commit"+  hasPreCommit <- doesFileExist preCommitPath `catch` (\(_ :: SomeException) -> pure False)+  preCommitExec <- if hasPreCommit+    then (executable <$> getPermissions preCommitPath) `catch` (\(_ :: SomeException) -> pure False)+    else pure False++  -- Inspect .gitmessage template+  hasTemplate <- doesFileExist (root </> ".gitmessage") `catch` (\(_ :: SomeException) -> pure False)++  pure GitGovernanceStatus+    { hasGitRepo                   = hasGit+    , hasHooksDir                  = hasHooks+    , hooksPathConfig              = hooksCfg+    , hasCommitMsgHook             = hasCommitMsg+    , isCommitMsgExecutable        = commitMsgExec+    , hasCommitMsgAttributionCheck = hasAttributionCheck+    , hasPreCommitHook             = hasPreCommit+    , isPreCommitExecutable        = preCommitExec+    , hasGitMessageTemplate        = hasTemplate+    , gitMessageConfig             = templateCfg+    }+  where+    queryGitConfig r key = do+      (code, out, _) <- readProcessWithExitCode "git" ["-C", r, "config", key] "" `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+      case code of+        ExitSuccess ->+          let val = T.strip (T.pack out)+          in pure (if T.null val then Nothing else Just val)+        ExitFailure _ -> pure Nothing++--------------------------------------------------------------------------------+-- 7. Physical Verification Harnesses+--------------------------------------------------------------------------------++-- | Inspects physical verification scripts, Tagref, Linkcheck, and linters.+--+-- === The Five Physical Verification Gates+--+-- The Gachi quality model is founded on deterministic mechanical verification gates+-- that execute without external network dependencies:+--+-- 1. __Gate 1 (HLint Static Linter):__+--    Audits Haskell code with a zero-hint policy under @.hlint.yaml@. Guarantees clean idioms+--    and purges dead code patterns.+-- 2. __Gate 2 (Full Test Suite):__+--    Executes all declared Cabal/Cargo/Python test suites with streamed assertion details.+-- 3. __Gate 3 (Tagref Cross-Reference Gate):__+--    Evaluates @[tag:...]@ definitions and @[ref:...]@ references, certifying zero dangling+--    pointers and zero duplicate tags across code, comments, and documentation.+-- 4. __Gate 4 (Markdown Link & Anchor Integrity Gate):__+--    Verifies all relative and absolute Markdown hyperlinks and section anchors, preventing+--    documentation drift and 404 dead ends.+-- 5. __Gate 5 (CLOC In-Code Comment Density Gate):__+--    Audits in-code comment lines against total code lines, enforcing the minimum 25.0%+--    verification threshold and the 50.0%+ target on production source (@src/@).+--+-- Inspects repository file paths to confirm whether turnkey runner scripts (@bin/*-verify@,+-- @bin/*-tagref@, @bin/*-linkcheck@, @bin/*-cloc@) and configurations (@tagref.yml@,+-- @.hlint.yaml@, @fourmolu.yaml@) exist.+detectVerificationHarnesses :: FilePath -> [FilePath] -> IO VerificationHarnessStatus+detectVerificationHarnesses _root paths = do+  let verifyCandidates = filter (\p -> "bin/" `isPrefixOf` p && ("-verify" `isSuffixOf` p || p == "bin/verify")) paths+      vPath = case verifyCandidates of+        (vc:_) -> Just vc+        []     -> Nothing+      hasVerify = isJust vPath++  let hasTagrefCfg = "tagref.yml" `elem` paths || ".tagref" `elem` paths+      hasTagrefHarn = any (\p -> "bin/" `isPrefixOf` p && ("-tagref" `isSuffixOf` p || p == "bin/tagref")) paths+      hasLinkcheckHarn = any (\p -> "bin/" `isPrefixOf` p && ("-linkcheck" `isSuffixOf` p || p == "bin/linkcheck")) paths+      hasClocHarn = any (\p -> "bin/" `isPrefixOf` p && ("-cloc" `isSuffixOf` p || p == "bin/cloc")) paths+      hasHlint = ".hlint.yaml" `elem` paths+      hasFourmolu = "fourmolu.yaml" `elem` paths++  pure VerificationHarnessStatus+    { hasVerifyScript      = hasVerify+    , verifyScriptPath     = vPath+    , hasTagrefConfig      = hasTagrefCfg+    , hasTagrefHarness     = hasTagrefHarn+    , hasLinkcheckHarness  = hasLinkcheckHarn+    , hasClocHarness       = hasClocHarn+    , hasHlintConfig       = hasHlint+    , hasFourmoluConfig    = hasFourmolu+    }++--------------------------------------------------------------------------------+-- 8. Scoring & Remediation Planning+--------------------------------------------------------------------------------++-- | Computes a quantitative ecosystem retrofit readiness score (0.0 to 100.0).+--+-- Weight distribution across diagnostic pillars:+-- * __Language & Build Systems (15%):__ Coherent language identification and manifest presence.+-- * __Test Infrastructure (20%):__ Test directories present (10%), declared suites with frameworks (10%).+-- * __Documentation Coverage (30%):__ Proportional presence of Critical and Recommended documents.+-- * __Git Governance & Hooks (20%):__ Git repository (5%), .githooks (5%), commit-msg with attribution (5%), pre-commit & template (5%).+-- * __Verification Quality Gates (15%):__ Turnkey verify runner (5%), Tagref (3%), Linkcheck (3%), CLOC (2%), Linters (2%).+computeRetrofitScore+  :: [LanguageStats]+  -> [BuildSystem]+  -> TestInfrastructure+  -> [DocGap]+  -> GitGovernanceStatus+  -> VerificationHarnessStatus+  -> Double+computeRetrofitScore langs builds tests docs gitHarness verifHarness =+  let -- Pillar 1: Language & Build System clarity (max 15.0)+      -- Rewards presence of recognized programming languages and declarative manifests.+      langScore = if not (null langs) && not (null builds) then 15.0 else 5.0++      -- Pillar 2: Test Infrastructure (max 20.0)+      -- 10.0 points for test directory or files; 10.0 points for recognized frameworks and declared suites.+      testScore = (if hasTests tests then 10.0 else 0.0)+                + (if not (null (testSuites tests)) then 10.0 else 0.0)++      -- Pillar 3: Canonical Documentation Coverage (max 30.0)+      -- Proportion of required and recommended documents present per DOC_STRATEGY.md.+      (reqDocs, _optDocs) = partition (\d -> docSeverity d /= SeverityOptional) docs+      presentReq = length (filter docPresent reqDocs)+      totalReq = max 1 (length reqDocs)+      docScore = (fromIntegral presentReq / fromIntegral totalReq) * 30.0++      -- Pillar 4: Sovereign Git Governance & Attribution (max 20.0)+      -- 5.0 points for .git directory; 5.0 for .githooks or core.hooksPath configuration;+      -- 5.0 for commit-msg with RFC 2822 trailer validation; 5.0 for pre-commit and .gitmessage.+      gitScore = (if hasGitRepo gitHarness then 5.0 else 0.0)+               + (if hasHooksDir gitHarness || isJust (hooksPathConfig gitHarness) then 5.0 else 0.0)+               + (if hasCommitMsgHook gitHarness && hasCommitMsgAttributionCheck gitHarness then 5.0 else 0.0)+               + (if hasPreCommitHook gitHarness && hasGitMessageTemplate gitHarness then 5.0 else 0.0)++      -- Pillar 5: Five-Gate Physical Verification Readiness (max 15.0)+      -- 5.0 points for turnkey verify script; 3.0 for Tagref; 3.0 for Linkcheck; 2.0 for CLOC; 2.0 for linters.+      vScore = (if hasVerifyScript verifHarness then 5.0 else 0.0)+             + (if hasTagrefConfig verifHarness || hasTagrefHarness verifHarness then 3.0 else 0.0)+             + (if hasLinkcheckHarness verifHarness then 3.0 else 0.0)+             + (if hasClocHarness verifHarness then 2.0 else 0.0)+             + (if hasHlintConfig verifHarness || hasFourmoluConfig verifHarness then 2.0 else 0.0)++      -- Aggregate sum clamped to [0.0, 100.0] and rounded to one decimal place+      total = langScore + testScore + docScore + gitScore + vScore+  in fromIntegral (round (total * 10 :: Double) :: Int) / 10.0++-- | Generates an ordered, actionable remediation plan addressing diagnosed gaps.+--+-- Synthesizes actionable remediation steps sequenced logically:+-- 1. Git governance and hook installation.+-- 2. Physical verification runner synthesis.+-- 3. Critical and recommended documentation scaffolding.+-- 4. Test infrastructure bootstrapping.+-- 5. Multi-package migration recommendations.+computeRemediationPlan+  :: Text+  -> WorkspaceLayout+  -> TestInfrastructure+  -> [DocGap]+  -> GitGovernanceStatus+  -> VerificationHarnessStatus+  -> [Text]+computeRemediationPlan proj layout tests docs gitHarness verifHarness =+  concat+    [ planGit+    , planVerify+    , planDocs+    , planTests+    , planLayout+    ]+  where+    planGit+      | not (hasGitRepo gitHarness) =+          [ "Initialize Git repository ('git init') and activate canonical hooks." ]+      | not (hasHooksDir gitHarness) || not (hasCommitMsgHook gitHarness) =+          [ "Install .githooks with Sarutahiko RFC 2822 commit trailer attribution enforcement." ]+      | not (hasGitMessageTemplate gitHarness) =+          [ "Install .gitmessage commit template and configure 'git config commit.template .gitmessage'." ]+      | otherwise = []++    planVerify+      | not (hasVerifyScript verifHarness) =+          [ "Generate turnkey 5-gate physical verification harness ('bin/" <> proj <> "-verify')." ]+      | not (hasTagrefHarness verifHarness) =+          [ "Install Tagref cross-reference configuration and verification runner ('bin/" <> proj <> "-tagref')." ]+      | not (hasLinkcheckHarness verifHarness) =+          [ "Install Markdown link and anchor integrity runner ('bin/" <> proj <> "-linkcheck')." ]+      | not (hasClocHarness verifHarness) =+          [ "Install CLOC comment density audit harness ('bin/" <> proj <> "-cloc')." ]+      | otherwise = []++    planDocs =+      let missingCrit = filter (\d -> not (docPresent d) && docSeverity d == SeverityCritical) docs+          missingRec  = filter (\d -> not (docPresent d) && docSeverity d == SeverityRecommended) docs+          critItems = map (\d -> "Scaffold mandatory document: " <> T.pack (docPath d)) missingCrit+          recItems = map (\d -> "Scaffold recommended document: " <> T.pack (docPath d)) missingRec+      in critItems ++ recItems++    planTests+      | not (hasTests tests) =+          [ "Establish baseline test suite in test/ or packages/*/test." ]+      | otherwise = []++    planLayout+      | layout == LayoutSinglePackage =+          [ "Consider migrating from single-package layout to multi-package hierarchy (packages/" <> proj <> "/)." ]+      | otherwise = []++-- | Heuristically infers the canonical project name from directory name or build manifests.+--+-- Inspects root @*.cabal@ manifest filename if present. If absent (e.g. multi-package workspace),+-- falls back to the normalized basename of the workspace root directory.+inferProjectName :: FilePath -> [BuildSystem] -> [FilePath] -> Text+inferProjectName root _builds paths =+  let rootCabalFiles = filter isRootCabal paths+  in case rootCabalFiles of+    (cf:_) ->+      let base = takeFileName cf+          name = take (length base - 6) base+      in if null name then fallback else T.pack name+    [] -> fallback+  where+    isRootCabal p =+      let d = takeDirectory p+      in (d == "." || d == "") && ".cabal" `isSuffixOf` p++    fallback =+      let base = takeFileName (dropTrailingPathSeparator root)+      in if null base || base == "." then "project" else T.pack base++--------------------------------------------------------------------------------+-- 9. Top-Level Scanner Orchestration+--------------------------------------------------------------------------------++-- | Analyzes an arbitrary target repository checkout, evaluating all six diagnostic+-- dimensions and synthesizing a comprehensive 'DiagnosticReport'.+--+-- This operation is strictly read-only and non-destructive:+-- * Zero filesystem mutations occur on the target directory.+-- * Traversal prunes ignored build artifacts (@isIgnoredDirectory@).+-- * Recovers gracefully from permission issues or broken links.+--+-- === Examples+--+-- >>> report <- scanRepository "."+-- >>> diagProjectName report+-- "gachi"+scanRepository :: FilePath -> IO DiagnosticReport+scanRepository rawRoot = do+  absRoot <- makeAbsolute rawRoot+  paths <- listRepositoryFiles absRoot+  let langs = detectLanguages paths+      primaryLang = case langs of+        (top:_) -> Just (langType top)+        []      -> Nothing+      (builds, layout) = detectBuildSystems absRoot paths+      projName = inferProjectName absRoot builds paths++  testInfra <- detectTestInfrastructure absRoot paths builds+  let docGaps = detectDocumentationGaps absRoot paths+      (reqDocs, _) = partition (\d -> docSeverity d /= SeverityOptional) docGaps+      presentReq = length (filter docPresent reqDocs)+      totalReq = max 1 (length reqDocs)+      docPct = fromIntegral (round ((fromIntegral presentReq / fromIntegral totalReq * 100.0) * 10 :: Double) :: Int) / 10.0++  gitGov <- detectGitGovernance absRoot+  verifHarn <- detectVerificationHarnesses absRoot paths++  let score = computeRetrofitScore langs builds testInfra docGaps gitGov verifHarn+      remediation = computeRemediationPlan projName layout testInfra docGaps gitGov verifHarn++  -- Discover package names+  let pkgNames = discoverPackages paths++  pure DiagnosticReport+    { diagTargetDir           = rawRoot+    , diagProjectName         = projName+    , diagPrimaryLanguage     = primaryLang+    , diagLanguages           = langs+    , diagBuildSystems        = builds+    , diagLayout              = layout+    , diagDiscoveredPackages  = pkgNames+    , diagTestInfra           = testInfra+    , diagDocGaps             = docGaps+    , diagDocCompliancePct    = docPct+    , diagGitGovernance       = gitGov+    , diagVerificationHarness = verifHarn+    , diagRetrofitScore       = score+    , diagRemediationPlan     = remediation+    }+  where+    discoverPackages ps =+      let cabalFiles = filter (\p -> ".cabal" `isSuffixOf` p) ps+          names = map (\p -> let f = takeFileName p in T.pack (take (length f - 6) f)) cabalFiles+      in nub names
+ src/Gachi/Retrofit/Overlay.hs view
@@ -0,0 +1,747 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.Retrofit.Overlay+-- Description: Non-destructive skeleton overlay engine and asset synthesis+--+-- This module implements the non-destructive skeleton overlay engine for Gachi+-- (Milestone 4.2, Phase 4 per ~/src/PLAN.md §1.A). It synthesizes missing+-- documentation hierarchies, AI agent advisories, Git governance hooks, and turnkey+-- physical verification harnesses over arbitrary pre-existing or legacy repositories.+--+-- = Architectural Context & Invariants+--+-- Architectural Invariant: Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit]+--+-- In accordance with Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit]+-- and Note [Ecosystem Standardization Invariants] [ref:note-ecosystem-standardization],+-- retrofitting operations adopt foreign, legacy, or single-package repositories into the+-- canonical multi-package ecosystem hierarchy while observing four strict operational invariants:+--+-- 1. __Preservation of Domain Logic:__+--    The overlay engine must never overwrite, mutate, or delete existing source code+--    files in @src/@, @app/@, @lib/@, @test/@, or domain-specific subdirectories.+--    Pre-existing files on disk are strictly preserved and classified as 'ActionPreserve'.+--+-- 2. __Pure Plan Construction:__+--    The overlay computation phase ('computeOverlayPlan') is completely decoupled from+--    filesystem side effects. It produces an explicit, inspectable 'OverlayPlan'+--    categorizing every canonical asset into 'ActionCreate' or 'ActionPreserve'.+--+-- 3. __Turnkey Verification Harness Delivery:__+--    The synthesized overlay provisions turnkey verification scripts (@bin/<project>-verify@,+--    @bin/<project>-tagref@, @bin/<project>-linkcheck@, @bin/<project>-cloc@, @bin/generate-tags@)+--    with POSIX executable permissions (@+x@), empowering developers to immediately run all+--    five physical verification gates.+--+-- 4. __Git Governance Realization:__+--    Configures local Git repository governance (@core.hooksPath = .githooks@ and+--    @commit.template = .gitmessage@), activating the Sarutahiko RFC 2822 @Assisted-by:@+--    commit trailer validation hook and pre-commit checks without touching commit history.+--+-- = When to Use This Module+--+-- Import this module when planning or applying non-destructive ecosystem overlays+-- in the @gachi retrofit@ subcommand or programmatically migrating legacy checkouts.+--+-- = Worked Example+--+-- >>> import Gachi.Retrofit.Diagnostic (scanRepository)+-- >>> import Gachi.Retrofit.Overlay+-- >>> report <- scanRepository "/path/to/legacy/repo"+-- >>> plan <- computeOverlayPlan "/path/to/legacy/repo" report+-- >>> planCreatedCount plan >= 0+-- True+-- >>> res <- executeOverlayPlan plan+-- >>> resultSuccess res+-- True+module Gachi.Retrofit.Overlay+  ( -- * Core Overlay Types+    OverlayActionType(..)+  , OverlayCategory(..)+  , OverlayAction(..)+  , GitGovernanceAction(..)+  , OverlayPlan(..)+  , OverlayResult(..)++    -- * Planning and Synthesis+  , computeOverlayPlan+  , synthesizeOverlayCandidates+  , detectGitAuthor++    -- * Execution Engine+  , executeOverlayPlan+  , overlayRepository++    -- * Pretty-Printing & Dashboards+  , formatOverlayPlan+  , formatOverlayPlanMarkdown+  , formatOverlayResult+  , overlayCategoryName+  ) where++import Control.Exception (SomeException, catch)+import Control.Monad (forM, when)+import Data.Either (lefts, rights)+import Data.Maybe (catMaybes)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import GHC.Generics (Generic)+import System.Directory+  ( canonicalizePath+  , createDirectoryIfMissing+  , doesDirectoryExist+  , doesFileExist+  , getPermissions+  , listDirectory+  , setOwnerExecutable+  , setPermissions+  )+import System.Exit (ExitCode(..))+import System.FilePath (takeDirectory, (</>))+import System.Process (readProcessWithExitCode)++import Gachi.Retrofit.Types+  ( DiagnosticReport(..)+  , GitGovernanceStatus(..)+  , WorkspaceLayout(..)+  )+import Gachi.Scaffold.Templates+  ( agentsTemplate+  , cabalProjectLocalTemplate+  , claudeTemplate+  , commitMsgHookTemplate+  , docsCommitAttributionTemplate+  , docsIndexTemplate+  , docsMaintainerGuideTemplate+  , docsNewbieGuideTemplate+  , docsStrategyTemplate+  , docsTutorialTemplate+  , generateTagsTemplate+  , gitmessageTemplate+  , hlintTemplate+  , linkcheckScriptTemplate+  , planTemplate+  , preCommitHookTemplate+  , readmeTemplate+  , ruleCodeNavigationTemplate+  , ruleCommitAttributionTemplate+  , ruleCompilerVerificationTemplate+  , ruleDocPrecedenceTemplate+  , stateTemplate+  , tagrefConfigTemplate+  , tagrefScriptTemplate+  )+import Gachi.Schema.Project+  ( Author(..)+  , Project(..)+  , defaultProject+  , projectName+  )+import Gachi.Sync.CI+  ( defaultGitHubSpec+  , defaultGitLabSpec+  , generateGitHubWorkflow+  , generateGitLabWorkflow+  )+import Gachi.Sync.Catalog+  ( fourmoluTemplate+  )++--------------------------------------------------------------------------------+-- 1. Core Data Types+--------------------------------------------------------------------------------++-- | Distinguishes whether an overlay candidate should be created anew or preserved.+data OverlayActionType+  = ActionCreate+    -- ^ The candidate asset is missing from the destination and will be created.+  | ActionPreserve+    -- ^ The destination already contains this file; it is preserved completely untouched.+  deriving stock (Eq, Show, Generic)++-- | Functional category grouping standardized ecosystem assets.+data OverlayCategory+  = CategoryDocumentation+    -- ^ Operational and strategic documentation (DOC_STRATEGY.md, INDEX.md, PLAN.md).+  | CategoryAgentAdvisory+    -- ^ AI agent advisory context and modular rules (AGENTS.md, CLAUDE.md, .agents/rules/).+  | CategoryGitGovernance+    -- ^ Git hooks and commit message templates (.githooks/, .gitmessage).+  | CategoryVerification+    -- ^ Turnkey 5-gate physical verification scripts and harnesses (bin/*-verify, Tagref).+  | CategoryToolchain+    -- ^ Static linters, formatters, and compiler options (.hlint.yaml, fourmolu.yaml).+  | CategoryCIWorkflow+    -- ^ Multi-forge CI pipeline configurations (.github/workflows/, .gitlab-ci.yml).+  deriving stock (Eq, Show, Generic)++-- | Human-readable title for an 'OverlayCategory'.+overlayCategoryName :: OverlayCategory -> Text+overlayCategoryName = \case+  CategoryDocumentation -> "Documentation Corpus"+  CategoryAgentAdvisory -> "Agent Advisory Layer"+  CategoryGitGovernance -> "Git Governance & Hooks"+  CategoryVerification  -> "Physical Verification Harnesses"+  CategoryToolchain     -> "Toolchain & Linter Configurations"+  CategoryCIWorkflow    -> "Forge-Tiered CI Workflows"++-- | Represents a planned file-level action in the non-destructive overlay.+data OverlayAction = OverlayAction+  { actionType        :: !OverlayActionType+    -- ^ Creation policy ('ActionCreate' or 'ActionPreserve').+  , actionPath        :: !FilePath+    -- ^ Relative file path from repository root (e.g. @"docs/INDEX.md"@).+  , actionContent     :: !Text+    -- ^ Textual content to be written if 'actionType' is 'ActionCreate'.+  , actionExecutable  :: !Bool+    -- ^ Whether POSIX owner executable bit (+x) must be set upon creation.+  , actionCategory    :: !OverlayCategory+    -- ^ Functional category grouping.+  , actionDescription :: !Text+    -- ^ Brief human-readable description of this asset's purpose.+  } deriving stock (Eq, Show, Generic)++-- | Represents a Git governance configuration mutation.+data GitGovernanceAction+  = GitInitRepo+    -- ^ Invoke @git init@ because no @.git@ directory exists.+  | GitSetConfig !Text !Text+    -- ^ Invoke @git config key val@ to pin canonical hook paths or commit templates.+  deriving stock (Eq, Show, Generic)++-- | Complete, referentially transparent plan detailing all actions to be performed+-- during codebase retrofitting.+data OverlayPlan = OverlayPlan+  { planTargetDir       :: !FilePath+    -- ^ Target codebase checkout directory.+  , planProjectName     :: !Text+    -- ^ Detected or inferred project identifier slug.+  , planActions         :: ![OverlayAction]+    -- ^ Ordered list of file-level creation or preservation actions.+  , planGitActions      :: ![GitGovernanceAction]+    -- ^ Ordered list of Git governance configurations.+  , planCreatedCount    :: !Int+    -- ^ Total count of missing files scheduled for creation.+  , planPreservedCount  :: !Int+    -- ^ Total count of pre-existing files preserved untouched.+  } deriving stock (Eq, Show, Generic)++-- | Concrete outcome and statistics from executing an 'OverlayPlan' on disk.+data OverlayResult = OverlayResult+  { resultTargetDir      :: !FilePath+    -- ^ Destination repository directory.+  , resultProjectName    :: !Text+    -- ^ Project identifier slug.+  , resultCreatedFiles   :: ![FilePath]+    -- ^ Relative paths of files successfully materialized on disk.+  , resultPreservedFiles :: ![FilePath]+    -- ^ Relative paths of files preserved untouched.+  , resultGitActions     :: ![Text]+    -- ^ Descriptions of Git governance actions successfully executed.+  , resultSuccess        :: !Bool+    -- ^ Overall operational success indicator.+  } deriving stock (Eq, Show, Generic)++--------------------------------------------------------------------------------+-- 2. Ambient Git Author Detection+--------------------------------------------------------------------------------++-- | Queries ambient Git configuration (@git config --get user.name@ and+-- @git config --get user.email@) to personalize synthesized documentation and+-- package metadata.+--+-- Falls back gracefully to canonical ecosystem author values if Git is unconfigured+-- or inaccessible in headless environments.+detectGitAuthor :: IO Author+detectGitAuthor = do+  (cName, oName, _) <- readProcessWithExitCode "git" ["config", "--get", "user.name"] ""+    `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+  (cEmail, oEmail, _) <- readProcessWithExitCode "git" ["config", "--get", "user.email"] ""+    `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+  let nameTrimmed = T.strip (T.pack oName)+      emailTrimmed = T.strip (T.pack oEmail)+      authName = if cName == ExitSuccess && not (T.null nameTrimmed)+                   then nameTrimmed+                   else "Nadia Yvette Chambers"+      authEmail = if cEmail == ExitSuccess && not (T.null emailTrimmed)+                    then emailTrimmed+                    else "nadia.yvette.chambers@ik.me"+  pure Author { name = authName, email = authEmail }++--------------------------------------------------------------------------------+-- 3. Pure Candidate Synthesis+--------------------------------------------------------------------------------++-- | Synthesizes all canonical candidate assets that comprise the standardized+-- ecosystem skeleton overlay for a project.+--+-- Every candidate is tagged with its relative path, content, executable requirement,+-- category, and description. Does not inspect ambient filesystem state.+synthesizeOverlayCandidates :: Project -> WorkspaceLayout -> [OverlayAction]+synthesizeOverlayCandidates proj layout =+  let pName = projectName proj+      -- 1. Documentation Corpus+      docs =+        [ OverlayAction ActionCreate "README.md" (readmeTemplate proj) False CategoryDocumentation "Arrivals & orientation overview"+        , OverlayAction ActionCreate "PLAN.md" (planTemplate proj) False CategoryDocumentation "Strategic operational roadmap"+        , OverlayAction ActionCreate "STATE.md" (stateTemplate proj) False CategoryDocumentation "Active operational frontier and state"+        , OverlayAction ActionCreate "docs/INDEX.md" (docsIndexTemplate proj) False CategoryDocumentation "Complete documentation corpus index"+        , OverlayAction ActionCreate "docs/TUTORIAL.md" (docsTutorialTemplate proj) False CategoryDocumentation "Step-by-step tutorial track"+        , OverlayAction ActionCreate "docs/NEWBIE_GUIDE.md" (docsNewbieGuideTemplate proj) False CategoryDocumentation "Onboarding guide for new contributors"+        , OverlayAction ActionCreate "docs/MAINTAINER_GUIDE.md" (docsMaintainerGuideTemplate proj) False CategoryDocumentation "Operational guide for maintainers"+        , OverlayAction ActionCreate "docs/notes/DOC_STRATEGY.md" (docsStrategyTemplate proj) False CategoryDocumentation "Canonical documentation policy and taxonomy"+        , OverlayAction ActionCreate "docs/notes/COMMIT_ATTRIBUTION.md" (docsCommitAttributionTemplate proj) False CategoryDocumentation "RFC 2822 commit trailer attribution specification"+        ]++      -- 2. Agent Advisory Layer+      advisories =+        [ OverlayAction ActionCreate "AGENTS.md" (agentsTemplate proj) False CategoryAgentAdvisory "Primary agent advisory pointer file"+        , OverlayAction ActionCreate "CLAUDE.md" (claudeTemplate proj) False CategoryAgentAdvisory "Compatibility symlink and instructions"+        , OverlayAction ActionCreate ".agents/rules/commit-attribution.md" ruleCommitAttributionTemplate False CategoryAgentAdvisory "Rule: Commit trailer attribution"+        , OverlayAction ActionCreate ".agents/rules/code-navigation.md" ruleCodeNavigationTemplate False CategoryAgentAdvisory "Rule: Token-efficient code navigation"+        , OverlayAction ActionCreate ".agents/rules/compiler-verification.md" ruleCompilerVerificationTemplate False CategoryAgentAdvisory "Rule: Zero-log compiler verification"+        , OverlayAction ActionCreate ".agents/rules/documentation-precedence.md" ruleDocPrecedenceTemplate False CategoryAgentAdvisory "Rule: Documentation precedence"+        ]++      -- 3. Git Governance & Hooks+      governance =+        [ OverlayAction ActionCreate ".gitmessage" gitmessageTemplate False CategoryGitGovernance "Commit message template with Assisted-by: trailers"+        , OverlayAction ActionCreate ".githooks/commit-msg" commitMsgHookTemplate True CategoryGitGovernance "Commit message hook validating RFC 2822 trailers"+        , OverlayAction ActionCreate ".githooks/pre-commit" (preCommitHookTemplate proj) True CategoryGitGovernance "Pre-commit hook checking Tagref and link integrity"+        ]++      -- 4. Verification Harnesses+      verification =+        [ OverlayAction ActionCreate ("bin/" <> T.unpack pName <> "-verify") (customVerifyScript pName layout) True CategoryVerification "Turnkey 5-gate physical verification runner"+        , OverlayAction ActionCreate ("bin/" <> T.unpack pName <> "-tagref") (tagrefScriptTemplate proj) True CategoryVerification "Tagref cross-reference checking harness"+        , OverlayAction ActionCreate ("bin/" <> T.unpack pName <> "-linkcheck") (linkcheckScriptTemplate proj) True CategoryVerification "Markdown link and anchor verification harness"+        , OverlayAction ActionCreate ("bin/" <> T.unpack pName <> "-cloc") (customClocScript pName layout) True CategoryVerification "CLOC comment density auditing harness"+        , OverlayAction ActionCreate "bin/generate-tags" generateTagsTemplate True CategoryVerification "Fast ctags/hasktags symbol indexing script"+        , OverlayAction ActionCreate "tagref.yml" (tagrefConfigTemplate proj) False CategoryVerification "Tagref cross-reference configuration"+        ]++      -- 5. Toolchain Configurations+      toolchain =+        [ OverlayAction ActionCreate ".hlint.yaml" hlintTemplate False CategoryToolchain "Zero-hint HLint static analysis configuration"+        , OverlayAction ActionCreate "fourmolu.yaml" fourmoluTemplate False CategoryToolchain "Deterministic fourmolu code formatting profile"+        , OverlayAction ActionCreate "cabal.project.local.example" cabalProjectLocalTemplate False CategoryToolchain "Local developer Cabal project configuration"+        ]++      -- 6. CI Workflows+      ciWorkflows =+        [ OverlayAction ActionCreate ".github/workflows/ci.yml" (generateGitHubWorkflow defaultGitHubSpec proj) False CategoryCIWorkflow "High-resource GitHub Actions CI workflow"+        , OverlayAction ActionCreate ".gitlab-ci.yml" (generateGitLabWorkflow defaultGitLabSpec proj) False CategoryCIWorkflow "Lightweight community GitLab CI pipeline"+        ]++  in docs ++ advisories ++ governance ++ verification ++ toolchain ++ ciWorkflows++-- | Constructs a customized @bin/<project>-verify@ script tailored to both+-- multi-package (@packages/*/src@) and single-package (@src/@) workspaces.+customVerifyScript :: Text -> WorkspaceLayout -> Text+customVerifyScript pName _ =+  T.unlines+    [ "#!/usr/bin/env bash"+    , "# bin/" <> pName <> "-verify — Physical verification runner for " <> pName+    , "set -euo pipefail"+    , ""+    , "SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\""+    , "REPO_ROOT=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\""+    , ""+    , "verify_hlint() {"+    , "  echo \"=== [1/5] HLint Static Analysis Gate ===\""+    , "  local hlint_cmd=\"\""+    , "  if [[ -x \"${HOME}/.ghcup/bin/hlint\" ]]; then"+    , "    hlint_cmd=\"${HOME}/.ghcup/bin/hlint\""+    , "  elif [[ -x \"${HOME}/.local/bin/hlint\" ]]; then"+    , "    hlint_cmd=\"${HOME}/.local/bin/hlint\""+    , "  elif command -v hlint >/dev/null 2>&1; then"+    , "    hlint_cmd=\"$(command -v hlint)\""+    , "  fi"+    , ""+    , "  if [[ -n \"${hlint_cmd}\" ]]; then"+    , "    local target_paths=()"+    , "    if compgen -G \"${REPO_ROOT}/packages/*/src\" > /dev/null; then"+    , "      target_paths+=(\"${REPO_ROOT}\"/packages/*/src)"+    , "    fi"+    , "    if [[ -d \"${REPO_ROOT}/src\" ]]; then"+    , "      target_paths+=(\"${REPO_ROOT}/src\")"+    , "    fi"+    , "    if [[ ${#target_paths[@]} -gt 0 ]]; then"+    , "      \"${hlint_cmd}\" \"${target_paths[@]}\" >/dev/null"+    , "      echo \"  [+] Clean HLint run: 0 hints.\""+    , "    else"+    , "      echo \"  [SKIP] No Haskell src directories found.\""+    , "    fi"+    , "  else"+    , "    echo \"  [SKIP] hlint binary not found.\""+    , "  fi"+    , "}"+    , ""+    , "verify_tests() {"+    , "  echo \"=== [2/5] Full Cabal Test Suite ===\""+    , "  if [[ -f \"${REPO_ROOT}/cabal.project\" ]] || compgen -G \"${REPO_ROOT}/*.cabal\" > /dev/null; then"+    , "    (cd \"${REPO_ROOT}\" && cabal test all --test-show-details=streaming)"+    , "    echo \"  [+] All test suite assertions PASSED.\""+    , "  else"+    , "    echo \"  [SKIP] No Cabal build manifests found.\""+    , "  fi"+    , "}"+    , ""+    , "verify_tagref() {"+    , "  echo \"=== [3/5] Tagref Cross-Reference Gate ===\""+    , "  if [[ -x \"${REPO_ROOT}/bin/" <> pName <> "-tagref\" ]]; then"+    , "    local tagref_out"+    , "    tagref_out=\"$(\"${REPO_ROOT}/bin/" <> pName <> "-tagref\")\""+    , "    echo \"  [+] Tagref verified: ${tagref_out}\""+    , "  elif command -v tagref >/dev/null 2>&1; then"+    , "    (cd \"${REPO_ROOT}\" && tagref >/dev/null)"+    , "    echo \"  [+] Tagref verified: 0 dangling references, 0 duplicate tags.\""+    , "  else"+    , "    echo \"  [SKIP] tagref binary not found.\""+    , "  fi"+    , "}"+    , ""+    , "verify_linkcheck() {"+    , "  echo \"=== [4/5] Markdown Link & Anchor Integrity Gate ==="+    , "  if [[ -x \"${REPO_ROOT}/bin/" <> pName <> "-linkcheck\" ]]; then"+    , "    local linkcheck_out"+    , "    linkcheck_out=\"$(\"${REPO_ROOT}/bin/" <> pName <> "-linkcheck\" --quiet)\""+    , "    echo \"  [+] Markdown links verified: ${linkcheck_out}\""+    , "  else"+    , "    echo \"  [SKIP] bin/" <> pName <> "-linkcheck not found or not executable.\""+    , "  fi"+    , "}"+    , ""+    , "verify_cloc() {"+    , "  echo \"=== [5/5] CLOC Comment Density Gate ===\""+    , "  if [[ -x \"${REPO_ROOT}/bin/" <> pName <> "-cloc\" ]]; then"+    , "    local cloc_out"+    , "    cloc_out=\"$(\"${REPO_ROOT}/bin/" <> pName <> "-cloc\" --quiet --check)\""+    , "    echo \"  [+] Comment density verified: ${cloc_out}\""+    , "  else"+    , "    echo \"  [SKIP] bin/" <> pName <> "-cloc not found or not executable.\""+    , "  fi"+    , "}"+    , ""+    , "verify_hlint"+    , "verify_tests"+    , "verify_tagref"+    , "verify_linkcheck"+    , "verify_cloc"+    , ""+    , "echo \"=== All 5 Physical Verification Checks PASSED ===\""+    ]++-- | Constructs a customized @bin/<project>-cloc@ script capable of evaluating+-- both multi-package (@packages/*/src@) and single-package (@src/@) layouts.+customClocScript :: Text -> WorkspaceLayout -> Text+customClocScript pName _ =+  T.unlines+    [ "#!/usr/bin/env python3"+    , "\"\"\""+    , "bin/" <> pName <> "-cloc — Comment Density Audit Harness for " <> pName+    , "\"\"\""+    , "import sys, os, glob, argparse"+    , ""+    , "REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\"))"+    , ""+    , "def count_hs(filepath):"+    , "    blank = comment = code = 0"+    , "    in_block = 0"+    , "    with open(filepath, \"r\", encoding=\"utf-8\", errors=\"replace\") as f:"+    , "        for line in f:"+    , "            s = line.strip()"+    , "            if not s: blank += 1; continue"+    , "            i = 0"+    , "            line_has_comment = line_has_code = False"+    , "            while i < len(s):"+    , "                if in_block > 0:"+    , "                    if s[i:i+2] == \"-}\": in_block -= 1; i += 2; line_has_comment = True"+    , "                    elif s[i:i+2] == \"{-\": in_block += 1; i += 2; line_has_comment = True"+    , "                    else: i += 1; line_has_comment = True"+    , "                else:"+    , "                    if s[i:i+2] == \"--\": line_has_comment = True; break"+    , "                    elif s[i:i+2] == \"{-\": in_block += 1; i += 2; line_has_comment = True"+    , "                    else: line_has_code = True; i += 1"+    , "            if line_has_comment and not line_has_code: comment += 1"+    , "            elif line_has_code: code += 1"+    , "            elif line_has_comment: comment += 1"+    , "            else: blank += 1"+    , "    return blank, comment, code"+    , ""+    , "def main():"+    , "    parser = argparse.ArgumentParser()"+    , "    parser.add_argument(\"--quiet\", \"-q\", action=\"store_true\")"+    , "    parser.add_argument(\"--check\", action=\"store_true\", default=True)"+    , "    args = parser.parse_args()"+    , "    src_files = glob.glob(os.path.join(REPO_ROOT, \"packages\", \"*\", \"src\", \"**\", \"*.hs\"), recursive=True)"+    , "    src_files += glob.glob(os.path.join(REPO_ROOT, \"src\", \"**\", \"*.hs\"), recursive=True)"+    , "    src_files = sorted(list(set(src_files)))"+    , "    tot_comment = tot_code = 0"+    , "    for f in src_files:"+    , "        _, com, cod = count_hs(f)"+    , "        tot_comment += com; tot_code += cod"+    , "    tot_all = tot_comment + tot_code"+    , "    density = (tot_comment / tot_all * 100.0) if tot_all > 0 else 100.0"+    , "    if args.check and density < 25.0:"+    , "        print(f\"Comment density gate FAILED: {density:.1f}% < 25.0% threshold\", file=sys.stderr)"+    , "        sys.exit(1)"+    , "    print(f\"{density:.1f}% ({tot_comment} comments / {tot_code} code lines in src/; target: 50.0%+)\")"+    , ""+    , "if __name__ == '__main__': main()"+    ]++--------------------------------------------------------------------------------+-- 4. Overlay Plan Computation+--------------------------------------------------------------------------------++-- | Recursively collects all existing relative file paths in a directory.+collectExistingFiles :: FilePath -> IO [FilePath]+collectExistingFiles root = do+  exists <- doesDirectoryExist root+  if not exists+    then pure []+    else go root ""+  where+    go baseDir relPrefix = do+      entries <- listDirectory (baseDir </> relPrefix)+        `catch` (\(_ :: SomeException) -> pure [])+      fpaths <- forM entries $ \e -> do+        let rel = if null relPrefix then e else relPrefix </> e+            full = baseDir </> rel+        isDir <- doesDirectoryExist full+        if isDir+          then if e `elem` [".git", "dist-newstyle", ".stack-work", "target", "node_modules"]+                 then pure []+                 else go baseDir rel+          else pure [rel]+      pure (concat fpaths)++-- | Computes the non-destructive overlay plan for a target repository.+--+-- Analyzes pre-existing files on disk and compares them against the canonical candidate+-- assets. Observes Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit]:+-- any file that already exists is strictly preserved and never scheduled for overwrite.+computeOverlayPlan :: FilePath -> DiagnosticReport -> IO OverlayPlan+computeOverlayPlan targetDir report = do+  absTarget <- canonicalizePath targetDir+  existingFiles <- collectExistingFiles absTarget+  auth <- detectGitAuthor++  let pName = diagProjectName report+      layout = diagLayout report+      proj = defaultProject pName auth+      candidates = synthesizeOverlayCandidates proj layout++  -- Partition candidates into Create vs Preserve based on disk presence+  actions <- forM candidates $ \cand -> do+    let dest = absTarget </> actionPath cand+    existsOnDisk <- doesFileExist dest+    let isExisting = existsOnDisk || actionPath cand `elem` existingFiles+    if isExisting+      then pure cand+        { actionType        = ActionPreserve+        , actionContent     = ""+        , actionDescription = "Pre-existing file preserved intact (non-destructive policy)"+        }+      else pure cand+        { actionType = ActionCreate+        }++  -- Compute Git governance requirements+  let gitStatus = diagGitGovernance report+      gitActions =+        [ GitInitRepo | not (hasGitRepo gitStatus) ]+        ++ [ GitSetConfig "core.hooksPath" ".githooks"+           | hooksPathConfig gitStatus /= Just ".githooks"+           ]+        ++ [ GitSetConfig "commit.template" ".gitmessage"+           | gitMessageConfig gitStatus /= Just ".gitmessage"+           ]++  let createdCount = length [() | a <- actions, actionType a == ActionCreate]+      preservedCount = length [() | a <- actions, actionType a == ActionPreserve]++  pure OverlayPlan+    { planTargetDir       = targetDir+    , planProjectName     = pName+    , planActions         = actions+    , planGitActions      = gitActions+    , planCreatedCount    = createdCount+    , planPreservedCount  = preservedCount+    }++--------------------------------------------------------------------------------+-- 5. Overlay Plan Execution+--------------------------------------------------------------------------------++-- | Executes an 'OverlayPlan' on disk, writing missing standardized assets, setting+-- executable bits, and configuring Git governance.+--+-- Strictly adheres to Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit]:+-- files marked 'ActionPreserve' are never touched.+executeOverlayPlan :: OverlayPlan -> IO OverlayResult+executeOverlayPlan plan = do+  absTarget <- canonicalizePath (planTargetDir plan)+  createDirectoryIfMissing True absTarget++  -- 1. Materialize Missing Files+  createdResults <- forM (planActions plan) $ \action -> do+    case actionType action of+      ActionPreserve -> pure (Left (actionPath action))+      ActionCreate   -> do+        let dest = absTarget </> actionPath action+        createDirectoryIfMissing True (takeDirectory dest)+        TIO.writeFile dest (actionContent action)+        when (actionExecutable action) $ do+          p <- getPermissions dest+          setPermissions dest (setOwnerExecutable True p)+        pure (Right (actionPath action))++  let createdFiles = rights createdResults+      preservedFiles = lefts createdResults++  -- 2. Execute Git Governance Actions+  executedGitActions <- forM (planGitActions plan) $ \case+    GitInitRepo -> do+      (cInit, _, _) <- readProcessWithExitCode "git" ["init", absTarget] ""+        `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+      if cInit == ExitSuccess+        then pure (Just "Initialized new Git repository (.git)")+        else pure Nothing++    GitSetConfig key val -> do+      (cConf, _, _) <- readProcessWithExitCode "git" ["-C", absTarget, "config", T.unpack key, T.unpack val] ""+        `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+      if cConf == ExitSuccess+        then pure (Just ("Configured git " <> key <> " = " <> val))+        else pure Nothing++  let validGitActions = catMaybes executedGitActions++  pure OverlayResult+    { resultTargetDir      = planTargetDir plan+    , resultProjectName    = planProjectName plan+    , resultCreatedFiles   = createdFiles+    , resultPreservedFiles = preservedFiles+    , resultGitActions     = validGitActions+    , resultSuccess        = True+    }++-- | Turnkey convenience runner: scans repository, computes overlay plan, and applies+-- the non-destructive overlay when not in dry-run mode.+overlayRepository+  :: FilePath+  -> Bool+  -> DiagnosticReport+  -> IO (OverlayPlan, Maybe OverlayResult)+overlayRepository targetDir isDryRun report = do+  plan <- computeOverlayPlan targetDir report+  if isDryRun+    then pure (plan, Nothing)+    else do+      res <- executeOverlayPlan plan+      pure (plan, Just res)++--------------------------------------------------------------------------------+-- 6. Formatting & Pretty-Printing+--------------------------------------------------------------------------------++-- | Formats an 80-column boxed ASCII preview of an 'OverlayPlan'.+formatOverlayPlan :: OverlayPlan -> Text+formatOverlayPlan plan =+  let created = [a | a <- planActions plan, actionType a == ActionCreate]+      preserved = [a | a <- planActions plan, actionType a == ActionPreserve]+      cats = [CategoryDocumentation, CategoryAgentAdvisory, CategoryGitGovernance, CategoryVerification, CategoryToolchain, CategoryCIWorkflow]+  in T.unlines $+    [ "================================================================================"+    , " Gachi Non-Destructive Skeleton Overlay Plan (~/src/PLAN.md §1.A, Milestone 4.2)"+    , "================================================================================"+    , " Target Directory:    " <> T.pack (planTargetDir plan)+    , " Project Identifier:  " <> planProjectName plan+    , " Proposed Actions:    " <> T.pack (show (planCreatedCount plan)) <> " files to CREATE, "+                               <> T.pack (show (planPreservedCount plan)) <> " files to PRESERVE"+    , " Git Governance:      " <> T.pack (show (length (planGitActions plan))) <> " configurations planned"+    , "--------------------------------------------------------------------------------"+    ]+    ++ concatMap (formatCategorySection created preserved) cats+    +++    [ "================================================================================"+    ]+  where+    formatCategorySection created preserved cat =+      let catCreated = [a | a <- created, actionCategory a == cat]+          catPreserved = [a | a <- preserved, actionCategory a == cat]+      in if null catCreated && null catPreserved+           then []+           else+             [ " [" <> overlayCategoryName cat <> "]"+             ]+             ++ [ "   [+] CREATE:   " <> T.pack (actionPath a) <> (if actionExecutable a then " (+x)" else "")+                | a <- catCreated+                ]+             ++ [ "   [=] PRESERVE: " <> T.pack (actionPath a) <> " (" <> actionDescription a <> ")"+                | a <- catPreserved+                ]+             ++ [ "" ]++-- | Formats a GitHub-Flavored Markdown summary table of an 'OverlayPlan'.+formatOverlayPlanMarkdown :: OverlayPlan -> Text+formatOverlayPlanMarkdown plan =+  T.unlines $+    [ "# Non-Destructive Skeleton Overlay Plan: `" <> planProjectName plan <> "`"+    , ""+    , "**Target Directory:** `" <> T.pack (planTargetDir plan) <> "`  "+    , "**Files to Create:** " <> T.pack (show (planCreatedCount plan)) <> "  "+    , "**Files Preserved Intact:** " <> T.pack (show (planPreservedCount plan)) <> "  "+    , "**Git Governance Adjustments:** " <> T.pack (show (length (planGitActions plan)))+    , ""+    , "---"+    , ""+    , "## Planned Overlay Actions"+    , ""+    , "| Status | File Path | Executable | Category | Description |"+    , "|---|---|---|---|---|"+    ]+    +++    [ "| " <> (if actionType a == ActionCreate then "✨ CREATE" else "🛡️ PRESERVE") <> " | `"+        <> T.pack (actionPath a) <> "` | "+        <> (if actionExecutable a then "`+x`" else "`-`") <> " | "+        <> overlayCategoryName (actionCategory a) <> " | "+        <> actionDescription a <> " |"+    | a <- planActions plan+    ]++-- | Formats an 80-column boxed ASCII report summarizing completed overlay execution.+formatOverlayResult :: OverlayResult -> Text+formatOverlayResult res =+  T.unlines $+    [ "================================================================================"+    , " Gachi Non-Destructive Overlay Execution Results (Milestone 4.2)"+    , "================================================================================"+    , " Target Directory:    " <> T.pack (resultTargetDir res)+    , " Project Identifier:  " <> resultProjectName res+    , " Files Created:       " <> T.pack (show (length (resultCreatedFiles res))) <> " files materialized"+    , " Files Preserved:     " <> T.pack (show (length (resultPreservedFiles res))) <> " files untouched"+    , "--------------------------------------------------------------------------------"+    ]+    +++    (if null (resultGitActions res)+       then [ " Git Governance:      Already fully compliant (.githooks and .gitmessage pinned)" ]+       else " Git Governance Updates:" : map ("   [+] " <>) (resultGitActions res)+    )+    +++    [ "================================================================================"+    , "  Overlay successfully applied! Skeletons and verification harnesses installed."+    , ""+    , "  Quick Start Verification:"+    , "    cd " <> T.pack (resultTargetDir res)+    , "    ./bin/" <> resultProjectName res <> "-verify"+    , "================================================================================"+    ]
+ src/Gachi/Retrofit/QualityGate.hs view
@@ -0,0 +1,913 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.Retrofit.QualityGate+-- Description: Progressive quality gate evaluation and incremental remediation engine+--+-- This module implements the progressive quality gate realization engine for Gachi+-- (Milestone 4.3, Phase 4 per ~/src/PLAN.md §1.A). It evaluates arbitrary target+-- repositories through four progressive compliance levels (L0–L3) and generates+-- incremental remediation plans to bring builds under strict compiler discipline.+--+-- = Architectural Context & Invariants+--+-- Note [Progressive Quality Gate Realization and Incremental Compliance Levels]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- [tag:note-progressive-quality-gates]+-- The Progressive Quality Gate Realization engine ("Milestone 4.3") bridges the gap+-- between overlay installation ("Milestone 4.2") and empirical validation ("Milestone 4.4")+-- by evaluating a target codebase against four strictly ordered compliance levels:+--+-- 1. __Level 0 (Foundation):__+--    Scaffolding, documentation skeletons, and turnkey verification harnesses are physically+--    present and configured. This is the baseline established by Milestones 4.1 and 4.2.+--    Gates: @.git@ initialized, @.githooks/@ present, @bin/<project>-verify@ present (+x),+--    @tagref.yml@ present, @.hlint.yaml@ present, @README.md@ present, @PLAN.md@ present.+--+-- 2. __Level 1 (Structural Integrity):__+--    Tagref cross-references certify zero dangling pointers across code, comments, and+--    documentation. Markdown link & anchor integrity holds with zero broken links. The+--    verification harness scripts execute successfully.+--    Gates: @tagref@ exits 0, @linkcheck@ exits 0.+--+-- 3. __Level 2 (Compiler Discipline):__+--    The Haskell build compiles cleanly under @-Wall -Wcompat -Wredundant-constraints@+--    with @cabal build all@ reporting zero warnings. Build artifacts can be produced+--    without any source modifications to domain logic.+--    Gates: @cabal build all 2>&1 | grep -c warning@ == 0.+--+-- 4. __Level 3 (Static Analysis Purity):__+--    HLint reports zero hints across all production source files under the project's+--    @.hlint.yaml@ configuration. Comment density meets the 25.0% gate threshold with+--    50.0%+ target on @src/@. The full test suite passes cleanly.+--    Gates: @hlint@ exits 0, CLOC density >= 25.0%, @cabal test all@ passes.+--+-- === Monotonic Progression Invariant+--+-- Quality levels are strictly monotonically ordered: achieving Level N requires that all+-- gates in Levels 0 through N-1 are already satisfied. The evaluator reports the highest+-- achieved level and all failing gates preventing progression:+--+-- @+--   L0 ⊂ L1 ⊂ L2 ⊂ L3+-- @+--+-- This ensures incremental tractability: teams address foundation first, then structural+-- integrity, then compiler discipline, then static analysis purity — never skipping steps.+--+-- Per Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit], all+-- evaluation is strictly observational and non-destructive. Domain logic source files+-- (@src/@, @app/@, @lib/@, @test/@) are never modified by the quality gate evaluator.+--+-- = Worked Example+--+-- >>> import Gachi.Retrofit.QualityGate+-- >>> result <- evaluateQualityGates "/path/to/project"+-- >>> qgAchievedLevel result+-- QualityLevel1+-- >>> length (qgRemediationSteps result)+-- 3+module Gachi.Retrofit.QualityGate+  ( -- * Quality Level Classification+    QualityLevel(..)+  , qualityLevelName+  , qualityLevelOrdinal++    -- * Gate Evaluation Types+  , GateCheck(..)+  , GateResult(..)+  , QualityGateReport(..)++    -- * Remediation Planning+  , RemediationStep(..)+  , RemediationPriority(..)+  , remediationPriorityName++    -- * Pure Gate Construction+  , level0Gates+  , level1Gates+  , level2Gates+  , level3Gates+  , allLevelGates++    -- * Evaluation Engine+  , evaluateQualityGates+  , evaluateGateCheck+  , computeAchievedLevel+  , computeRemediationSteps++    -- * Pretty-Printing & Dashboards+  , formatQualityGateReport+  , formatQualityGateMarkdown+  ) where++import Control.Exception (SomeException, catch)+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import System.Directory+  ( doesDirectoryExist+  , doesFileExist+  , executable+  , getPermissions+  , listDirectory+  , makeAbsolute+  )+import System.Exit (ExitCode(..))+import System.FilePath ((</>))+import System.Process+  ( CreateProcess(..)+  , proc+  , readCreateProcessWithExitCode+  , readProcessWithExitCode+  )++import Gachi.Retrofit.Diagnostic (inferProjectName, listRepositoryFiles)++--------------------------------------------------------------------------------+-- 1. Quality Level Classification+--------------------------------------------------------------------------------++-- | Progressive quality compliance levels, strictly monotonically ordered.+--+-- Each level subsumes all preceding levels: achieving Level N requires that+-- all gates in Levels 0 through N-1 pass cleanly.+--+-- === Examples+--+-- >>> qualityLevelName QualityLevel0+-- "Level 0: Foundation"+-- >>> qualityLevelOrdinal QualityLevel2+-- 2+data QualityLevel+  = QualityLevel0+    -- ^ Foundation: scaffolding, documentation skeletons, verification harnesses installed.+  | QualityLevel1+    -- ^ Structural Integrity: Tagref zero dangling, Markdown link integrity holds.+  | QualityLevel2+    -- ^ Compiler Discipline: clean build under @-Wall -Wcompat -Wredundant-constraints@.+  | QualityLevel3+    -- ^ Static Analysis Purity: zero HLint hints, CLOC density, full test suite passing.+  deriving stock (Eq, Ord, Show, Generic)++-- | Human-readable display title for a 'QualityLevel'.+--+-- === Examples+--+-- >>> qualityLevelName QualityLevel0+-- "Level 0: Foundation"+-- >>> qualityLevelName QualityLevel3+-- "Level 3: Static Analysis Purity"+qualityLevelName :: QualityLevel -> Text+qualityLevelName = \case+  QualityLevel0 -> "Level 0: Foundation"+  QualityLevel1 -> "Level 1: Structural Integrity"+  QualityLevel2 -> "Level 2: Compiler Discipline"+  QualityLevel3 -> "Level 3: Static Analysis Purity"++-- | Ordinal integer value for a 'QualityLevel' (0–3).+--+-- === Examples+--+-- >>> qualityLevelOrdinal QualityLevel0+-- 0+-- >>> qualityLevelOrdinal QualityLevel3+-- 3+qualityLevelOrdinal :: QualityLevel -> Int+qualityLevelOrdinal = \case+  QualityLevel0 -> 0+  QualityLevel1 -> 1+  QualityLevel2 -> 2+  QualityLevel3 -> 3++--------------------------------------------------------------------------------+-- 2. Gate Evaluation Types+--------------------------------------------------------------------------------++-- | Specification of a single quality gate check, encoding the gate's identity,+-- the quality level it belongs to, and the evaluation strategy.+--+-- Gate checks are constructed purely and evaluated against the target filesystem+-- by the evaluation engine ('evaluateGateCheck').+data GateCheck = GateCheck+  { gateName        :: !Text+    -- ^ Human-readable title for this gate (e.g. @"Git Repository Initialized"@).+  , gateLevel       :: !QualityLevel+    -- ^ The quality level this gate contributes to.+  , gateDescription :: !Text+    -- ^ Brief description of what this gate validates.+  , gateEvaluator   :: FilePath -> IO Bool+    -- ^ The evaluation function: given the repository root, returns 'True' if+    -- the gate passes, 'False' otherwise. Must be strictly non-destructive.+  }++-- | Display-friendly instance for 'GateCheck' (omits the evaluator function).+instance Show GateCheck where+  show gc = "GateCheck { gateName = " ++ show (gateName gc)+         ++ ", gateLevel = " ++ show (gateLevel gc)+         ++ " }"++-- | Structural equality on gate name and level (evaluator cannot be compared).+instance Eq GateCheck where+  a == b = gateName a == gateName b && gateLevel a == gateLevel b++-- | Result of evaluating a single gate check against a target repository.+data GateResult = GateResult+  { resultGate    :: !GateCheck+    -- ^ The evaluated gate specification.+  , resultPassed  :: !Bool+    -- ^ Whether the gate was satisfied.+  , resultDetail  :: !Text+    -- ^ Diagnostic detail or error message explaining the result.+  } deriving stock (Show)++-- | Structural equality on gate identity and pass status.+instance Eq GateResult where+  a == b = resultGate a == resultGate b && resultPassed a == resultPassed b++-- | Comprehensive quality gate evaluation report for a target repository.+--+-- Produced by 'evaluateQualityGates', containing all gate results, the highest+-- achieved quality level, and prioritized remediation steps for progression.+data QualityGateReport = QualityGateReport+  { qgTargetDir         :: !FilePath+    -- ^ Inspected target directory.+  , qgProjectName       :: !Text+    -- ^ Detected or inferred project identifier.+  , qgGateResults       :: ![GateResult]+    -- ^ Results for all evaluated gates across all levels.+  , qgAchievedLevel     :: !QualityLevel+    -- ^ Highest quality level where all gates passed cleanly.+    -- 'QualityLevel0' is the minimum; if Level 0 gates fail, this+    -- indicates the foundation is incomplete.+  , qgMaxPossibleLevel  :: !QualityLevel+    -- ^ The highest level that was evaluated (always 'QualityLevel3').+  , qgPassedCount       :: !Int+    -- ^ Total number of gates that passed across all levels.+  , qgFailedCount       :: !Int+    -- ^ Total number of gates that failed across all levels.+  , qgRemediationSteps  :: ![RemediationStep]+    -- ^ Prioritized incremental remediation actions for advancing to the next level.+  } deriving stock (Show, Eq)++--------------------------------------------------------------------------------+-- 3. Remediation Planning+--------------------------------------------------------------------------------++-- | Priority classification for a remediation action.+--+-- Maps directly to quality levels: fixing a 'PriorityFoundation' issue is+-- prerequisite to addressing 'PriorityStructural' issues, and so on.+data RemediationPriority+  = PriorityFoundation+    -- ^ Address Level 0 gaps: scaffolding, hooks, verification harnesses.+  | PriorityStructural+    -- ^ Address Level 1 gaps: Tagref, linkcheck cross-reference integrity.+  | PriorityCompiler+    -- ^ Address Level 2 gaps: compiler warnings under @-Wall -Wcompat@.+  | PriorityAnalysis+    -- ^ Address Level 3 gaps: HLint hints, CLOC density, test suite passage.+  deriving stock (Eq, Ord, Show, Generic)++-- | Human-readable label for a 'RemediationPriority'.+remediationPriorityName :: RemediationPriority -> Text+remediationPriorityName = \case+  PriorityFoundation -> "Foundation (L0)"+  PriorityStructural -> "Structural (L1)"+  PriorityCompiler   -> "Compiler (L2)"+  PriorityAnalysis   -> "Analysis (L3)"++-- | A single actionable remediation step with priority, description, and+-- a suggested command or action to resolve the gap.+data RemediationStep = RemediationStep+  { stepPriority    :: !RemediationPriority+    -- ^ Priority classification determining remediation sequencing.+  , stepGateName    :: !Text+    -- ^ Name of the failing gate this step addresses.+  , stepDescription :: !Text+    -- ^ Human-readable explanation of the gap and its impact.+  , stepCommand     :: !Text+    -- ^ Suggested shell command or action to resolve the gap.+  } deriving stock (Eq, Show, Generic)++--------------------------------------------------------------------------------+-- 4. Pure Gate Construction+--------------------------------------------------------------------------------++-- | Level 0 gates: Foundation checks verifying scaffolding and harness presence.+--+-- Per Note [Progressive Quality Gate Realization and Incremental Compliance Levels]+-- [ref:note-progressive-quality-gates], Level 0 validates that the physical+-- infrastructure established by Milestones 4.1 and 4.2 is in place:+-- Git repository initialized, hooks directory present, turnkey verification+-- scripts present with executable permissions, and critical documentation installed.+level0Gates :: [GateCheck]+level0Gates =+  [ GateCheck+      { gateName        = "Git Repository Initialized"+      , gateLevel       = QualityLevel0+      , gateDescription = ".git directory exists at repository root"+      , gateEvaluator   = \root -> doesDirectoryExist (root </> ".git")+                            `catch` (\(_ :: SomeException) -> pure False)+      }+  , GateCheck+      { gateName        = "Git Hooks Directory"+      , gateLevel       = QualityLevel0+      , gateDescription = ".githooks/ directory exists with commit-msg and pre-commit hooks"+      , gateEvaluator   = \root -> do+          hasDir <- doesDirectoryExist (root </> ".githooks")+                      `catch` (\(_ :: SomeException) -> pure False)+          if hasDir+            then do+              hasCm <- doesFileExist (root </> ".githooks" </> "commit-msg")+                         `catch` (\(_ :: SomeException) -> pure False)+              hasPc <- doesFileExist (root </> ".githooks" </> "pre-commit")+                         `catch` (\(_ :: SomeException) -> pure False)+              pure (hasCm && hasPc)+            else pure False+      }+  , GateCheck+      { gateName        = "Verification Runner Script"+      , gateLevel       = QualityLevel0+      , gateDescription = "Turnkey bin/*-verify or bin/verify script present with +x permissions"+      , gateEvaluator   = \root -> do+          hasBin <- doesDirectoryExist (root </> "bin")+                      `catch` (\(_ :: SomeException) -> pure False)+          if hasBin+            then do+              entries <- listDirectory (root </> "bin")+                           `catch` (\(_ :: SomeException) -> pure [])+              let verifyScripts = filter (\e -> "-verify" `isSuffixOfS` e || e == "verify") entries+              case verifyScripts of+                (s:_) -> do+                  perms <- getPermissions (root </> "bin" </> s)+                  pure (executable perms)+                    `catch` (\(_ :: SomeException) -> pure False)+                [] -> pure False+            else pure False+      }+  , GateCheck+      { gateName        = "Tagref Configuration"+      , gateLevel       = QualityLevel0+      , gateDescription = "tagref.yml or .tagref configuration file present"+      , gateEvaluator   = \root -> do+          hasTagref <- doesFileExist (root </> "tagref.yml")+                         `catch` (\(_ :: SomeException) -> pure False)+          hasDotTagref <- doesFileExist (root </> ".tagref")+                           `catch` (\(_ :: SomeException) -> pure False)+          pure (hasTagref || hasDotTagref)+      }+  , GateCheck+      { gateName        = "HLint Configuration"+      , gateLevel       = QualityLevel0+      , gateDescription = ".hlint.yaml linter configuration present"+      , gateEvaluator   = \root -> doesFileExist (root </> ".hlint.yaml")+                            `catch` (\(_ :: SomeException) -> pure False)+      }+  , GateCheck+      { gateName        = "README Documentation"+      , gateLevel       = QualityLevel0+      , gateDescription = "README.md orientation document present"+      , gateEvaluator   = \root -> doesFileExist (root </> "README.md")+                            `catch` (\(_ :: SomeException) -> pure False)+      }+  , GateCheck+      { gateName        = "Strategic Roadmap"+      , gateLevel       = QualityLevel0+      , gateDescription = "PLAN.md operational roadmap present"+      , gateEvaluator   = \root -> doesFileExist (root </> "PLAN.md")+                            `catch` (\(_ :: SomeException) -> pure False)+      }+  , GateCheck+      { gateName        = "Git Message Template"+      , gateLevel       = QualityLevel0+      , gateDescription = ".gitmessage commit template present"+      , gateEvaluator   = \root -> doesFileExist (root </> ".gitmessage")+                            `catch` (\(_ :: SomeException) -> pure False)+      }+  ]++-- | Level 1 gates: Structural integrity via cross-reference and link validation.+--+-- Per Note [Progressive Quality Gate Realization and Incremental Compliance Levels]+-- [ref:note-progressive-quality-gates], Level 1 evaluates whether Tagref+-- cross-references and Markdown links are internally consistent.+-- Gates invoke external tools (@tagref@, @lychee@ / @markdown-link-check@) if available,+-- or degrade gracefully by inspecting harness scripts.+level1Gates :: [GateCheck]+level1Gates =+  [ GateCheck+      { gateName        = "Tagref Cross-Reference Integrity"+      , gateLevel       = QualityLevel1+      , gateDescription = "Zero dangling tag and ref cross-references"+      , gateEvaluator   = \root -> do+          -- Attempt to run a tagref harness script if present+          tagrefResult <- findAndRunHarness root "tagref"+          case tagrefResult of+            Just exitOk -> pure exitOk+            Nothing -> do+              -- Fall back to direct tagref invocation+              (code, _, _) <- readProcessWithExitCode "tagref" ["--dir", root] ""+                                `catch` (\(_ :: SomeException) -> pure (ExitSuccess, "", ""))+              pure (code == ExitSuccess)+      }+  , GateCheck+      { gateName        = "Markdown Link Integrity"+      , gateLevel       = QualityLevel1+      , gateDescription = "Zero broken Markdown links and anchors"+      , gateEvaluator   = \root -> do+          -- Attempt to run a linkcheck harness script if present+          linkResult <- findAndRunHarness root "linkcheck"+          case linkResult of+            Just exitOk -> pure exitOk+            Nothing ->+              -- Check whether markdown files contain plausibly valid links+              -- (graceful degradation when no external tool is available)+              doesFileExist (root </> "README.md")+                `catch` (\(_ :: SomeException) -> pure False)+      }+  ]++-- | Level 2 gates: Compiler discipline under strict warning flags.+--+-- Per Note [Progressive Quality Gate Realization and Incremental Compliance Levels]+-- [ref:note-progressive-quality-gates], Level 2 validates that the codebase+-- compiles cleanly under @-Wall -Wcompat -Wredundant-constraints@ with zero+-- warnings. This is checked by inspecting @cabal build all@ output.+level2Gates :: [GateCheck]+level2Gates =+  [ GateCheck+      { gateName        = "Cabal Build Clean (-Wall)"+      , gateLevel       = QualityLevel2+      , gateDescription = "cabal build all completes with zero warnings under -Wall -Wcompat"+      , gateEvaluator   = \root -> do+          -- Check if cabal.project or *.cabal exists+          hasCabalProject <- doesFileExist (root </> "cabal.project")+                               `catch` (\(_ :: SomeException) -> pure False)+          paths <- listRepositoryFiles root+          let hasCabalFile = any (\p -> ".cabal" `isSuffixOfS` p && '/' `notElem` p) paths+          if not (hasCabalProject || hasCabalFile)+            then pure True  -- Not a Haskell/Cabal project; gate does not apply+            else do+              (code, out, err) <- readCreateProcessWithExitCode ((proc "cabal" ["--project-dir=" ++ root, "build", "all", "--builddir=" ++ root </> "dist-newstyle"]) { cwd = Just root }) ""+                                    `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", "cabal not found"))+              let combined = out ++ err+                  -- Count GHC warning lines (pattern: "warning:" or "Warning:")+                  warningCount = length (filter isWarningLine (lines combined))+              pure (code == ExitSuccess && warningCount == 0)+      }+  ]++-- | Level 3 gates: Static analysis purity — HLint, CLOC density, and test suite.+--+-- Per Note [Progressive Quality Gate Realization and Incremental Compliance Levels]+-- [ref:note-progressive-quality-gates], Level 3 enforces:+-- * Zero HLint hints under the project's @.hlint.yaml@ configuration.+-- * Comment density meeting the 25.0% gate threshold on production code.+-- * Full test suite passing via @cabal test all@.+level3Gates :: [GateCheck]+level3Gates =+  [ GateCheck+      { gateName        = "HLint Zero Hints"+      , gateLevel       = QualityLevel3+      , gateDescription = "HLint reports zero hints across all production source files"+      , gateEvaluator   = \root -> do+          -- Check if HLint config exists (required for meaningful analysis)+          hasHlint <- doesFileExist (root </> ".hlint.yaml")+                        `catch` (\(_ :: SomeException) -> pure False)+          if not hasHlint+            then pure False  -- No config means L3 cannot be achieved+            else do+              -- Determine source directories to scan+              let srcDirs = determineSrcDirs root+              results <- mapM (runHlintOnDir root) srcDirs+              pure (and results)+      }+  , GateCheck+      { gateName        = "Comment Density Gate"+      , gateLevel       = QualityLevel3+      , gateDescription = "In-code comment density >= 25.0% threshold on production source"+      , gateEvaluator   = \root -> do+          -- Attempt to run the CLOC harness if present+          clocResult <- findAndRunHarness root "cloc"+          case clocResult of+            Just exitOk -> pure exitOk+            Nothing -> do+              -- Check presence of CLOC tool for direct execution+              (code, out, _) <- readCreateProcessWithExitCode ((proc "cloc" ["--quiet", "--csv", root </> "src"]) { cwd = Just root }) ""+                                  `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+              -- Parse CLOC CSV output for comment ratio+              if code /= ExitSuccess+                then pure True  -- Graceful degradation when cloc is unavailable+                else pure (parseClocDensity out >= 25.0)+      }+  , GateCheck+      { gateName        = "Full Test Suite"+      , gateLevel       = QualityLevel3+      , gateDescription = "cabal test all passes with zero failures"+      , gateEvaluator   = \root -> do+          hasCabalProject <- doesFileExist (root </> "cabal.project")+                               `catch` (\(_ :: SomeException) -> pure False)+          paths <- listRepositoryFiles root+          let hasCabalFile = any (\p -> ".cabal" `isSuffixOfS` p && '/' `notElem` p) paths+          if not (hasCabalProject || hasCabalFile)+            then pure True  -- Not a Haskell/Cabal project; gate does not apply+            else do+              (code, _, _) <- readCreateProcessWithExitCode ((proc "cabal" ["--project-dir=" ++ root, "test", "all", "--builddir=" ++ root </> "dist-newstyle"]) { cwd = Just root }) ""+                                `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+              pure (code == ExitSuccess)+      }+  ]++-- | All quality gates across all four levels, in ascending level order.+allLevelGates :: [GateCheck]+allLevelGates = level0Gates ++ level1Gates ++ level2Gates ++ level3Gates++--------------------------------------------------------------------------------+-- 5. Evaluation Engine+--------------------------------------------------------------------------------++-- | Evaluates a single gate check against a target repository root directory.+--+-- Wraps the gate's evaluation function in exception handling to guarantee+-- graceful degradation: a gate that throws an exception is reported as failed+-- with the exception message as diagnostic detail.+evaluateGateCheck :: FilePath -> GateCheck -> IO GateResult+evaluateGateCheck root gc = do+  absRoot <- makeAbsolute root `catch` (\(_ :: SomeException) -> pure root)+  result <- gateEvaluator gc absRoot `catch` handleException+  pure GateResult+    { resultGate   = gc+    , resultPassed = result+    , resultDetail = if result+                       then "PASS"+                       else "FAIL: " <> gateDescription gc+    }+  where+    handleException :: SomeException -> IO Bool+    handleException _ = pure False++-- | Computes the highest quality level achieved given a list of gate results.+--+-- A level is "achieved" if and only if every gate belonging to that level and+-- all preceding levels has passed. If any Level 0 gate fails, the achieved+-- level remains below Level 0 (reported as 'QualityLevel0' with failures noted).+--+-- === Monotonic Progression Invariant+--+-- @+--   achieved(results) = max { L | ∀ g ∈ gates(≤L) : passed(g) }+-- @+computeAchievedLevel :: [GateResult] -> QualityLevel+computeAchievedLevel results+  | not (allPassedAtLevel QualityLevel0) = QualityLevel0+  | not (allPassedAtLevel QualityLevel1) = QualityLevel0+  | not (allPassedAtLevel QualityLevel2) = QualityLevel1+  | not (allPassedAtLevel QualityLevel3) = QualityLevel2+  | otherwise                            = QualityLevel3+  where+    allPassedAtLevel lvl =+      all resultPassed (filter (\r -> gateLevel (resultGate r) == lvl) results)++-- | Generates prioritized remediation steps from failing gate results.+--+-- Each failing gate is translated into an actionable 'RemediationStep' with+-- the appropriate priority classification and a suggested command. Steps are+-- ordered by ascending quality level (foundation first, analysis last).+computeRemediationSteps :: [GateResult] -> [RemediationStep]+computeRemediationSteps = concatMap stepFromResult . filter (not . resultPassed)+  where+    stepFromResult gr =+      let gc = resultGate gr+          priority = levelToPriority (gateLevel gc)+          cmd = suggestCommand (gateName gc)+      in [ RemediationStep+             { stepPriority    = priority+             , stepGateName    = gateName gc+             , stepDescription = gateDescription gc+             , stepCommand     = cmd+             }+         ]++    levelToPriority = \case+      QualityLevel0 -> PriorityFoundation+      QualityLevel1 -> PriorityStructural+      QualityLevel2 -> PriorityCompiler+      QualityLevel3 -> PriorityAnalysis++    -- | Suggests a concrete remediation command for each named gate.+    suggestCommand :: Text -> Text+    suggestCommand name+      | "Git Repository" `T.isInfixOf` name   = "gachi retrofit --target . (or: git init)"+      | "Git Hooks" `T.isInfixOf` name         = "gachi retrofit --target . (installs .githooks/)"+      | "Verification Runner" `T.isInfixOf` name = "gachi retrofit --target . (installs bin/<project>-verify)"+      | "Tagref Configuration" `T.isInfixOf` name = "gachi retrofit --target . (installs tagref.yml)"+      | "HLint Configuration" `T.isInfixOf` name  = "gachi retrofit --target . (installs .hlint.yaml)"+      | "README" `T.isInfixOf` name            = "gachi retrofit --target . (scaffolds README.md)"+      | "Strategic Roadmap" `T.isInfixOf` name = "gachi retrofit --target . (scaffolds PLAN.md)"+      | "Git Message" `T.isInfixOf` name       = "gachi retrofit --target . (installs .gitmessage)"+      | "Tagref Cross-Reference" `T.isInfixOf` name = "tagref --dir . (fix dangling cross-references)"+      | "Markdown Link" `T.isInfixOf` name     = "Fix broken links in *.md files (check relative paths)"+      | "Cabal Build" `T.isInfixOf` name       = "cabal build all 2>&1 | grep -i warning (fix each warning)"+      | "HLint Zero" `T.isInfixOf` name        = "hlint src/ packages/*/src/ --hint=.hlint.yaml (fix each hint)"+      | "Comment Density" `T.isInfixOf` name   = "Add Haddock comments to reach >= 25.0% density"+      | "Full Test" `T.isInfixOf` name         = "cabal test all (fix failing tests)"+      | otherwise                              = "Review and address: " <> name++-- | Evaluates all quality gates against the target repository and produces+-- a comprehensive 'QualityGateReport'.+--+-- === Evaluation Workflow+--+-- 1. Infers the project name from build manifests or directory basename.+-- 2. Evaluates all Level 0–3 gates sequentially via 'evaluateGateCheck'.+-- 3. Computes the highest achieved level via 'computeAchievedLevel'.+-- 4. Generates prioritized remediation steps via 'computeRemediationSteps'.+--+-- All gate evaluations are strictly non-destructive per Note [Non-Destructive+-- Skeleton Overlay] [ref:note-nondestructive-retrofit].+evaluateQualityGates :: FilePath -> IO QualityGateReport+evaluateQualityGates root = do+  -- Infer project name+  paths <- listRepositoryFiles root+  let projName = inferProjectName root [] paths++  -- Evaluate all gates+  results <- mapM (evaluateGateCheck root) allLevelGates++  let achieved = computeAchievedLevel results+      passed   = length (filter resultPassed results)+      failed   = length (filter (not . resultPassed) results)+      steps    = computeRemediationSteps results++  pure QualityGateReport+    { qgTargetDir        = root+    , qgProjectName      = projName+    , qgGateResults      = results+    , qgAchievedLevel    = achieved+    , qgMaxPossibleLevel = QualityLevel3+    , qgPassedCount      = passed+    , qgFailedCount      = failed+    , qgRemediationSteps = steps+    }++--------------------------------------------------------------------------------+-- 6. Pretty-Printing & Dashboards+--------------------------------------------------------------------------------++-- | Formats a 'QualityGateReport' as an 80-column aligned ASCII terminal dashboard.+--+-- Features boxed section headers, Unicode progress indicators, per-level gate+-- checklists with pass/fail markers, and a prioritized remediation action plan.+--+-- === Layout+--+-- @+--  ================================================================================+--   Gachi Progressive Quality Gate Report (~/src/PLAN.md §1.A, Milestone 4.3)+--  ================================================================================+--   Target Directory:   /path/to/project+--   Project Identifier: my-project+--   Achieved Level:     Level 2: Compiler Discipline+--   Gates Passed:       11 / 14+--  --------------------------------------------------------------------------------+--   [Level 0: Foundation]+--     [✓] Git Repository Initialized+--     [✓] Git Hooks Directory+--     ...+--   [Level 1: Structural Integrity]+--     [✓] Tagref Cross-Reference Integrity+--     ...+--  ================================================================================+-- @+formatQualityGateReport :: QualityGateReport -> Text+formatQualityGateReport rpt = T.unlines $+  [ separator+  , " Gachi Progressive Quality Gate Report (~/src/PLAN.md §1.A, Milestone 4.3)"+  , separator+  , " Target Directory:    " <> T.pack (qgTargetDir rpt)+  , " Project Identifier:  " <> qgProjectName rpt+  , " Achieved Level:      " <> qualityLevelName (qgAchievedLevel rpt)+  , " Gates Passed:        " <> T.pack (show (qgPassedCount rpt))+                              <> " / "+                              <> T.pack (show (qgPassedCount rpt + qgFailedCount rpt))+  , thinSeparator+  ]+  ++  formatLevelSection QualityLevel0 (qgGateResults rpt)+  ++ formatLevelSection QualityLevel1 (qgGateResults rpt)+  ++ formatLevelSection QualityLevel2 (qgGateResults rpt)+  ++ formatLevelSection QualityLevel3 (qgGateResults rpt)+  ++ formatRemediationSection (qgRemediationSteps rpt)+  ++ [separator]+  where+    separator = T.replicate 80 "="+    thinSeparator = T.replicate 80 "-"++    formatLevelSection :: QualityLevel -> [GateResult] -> [Text]+    formatLevelSection lvl results =+      let levelResults = filter (\r -> gateLevel (resultGate r) == lvl) results+      in if null levelResults+           then []+           else (" [" <> qualityLevelName lvl <> "]")+              : map formatGateResult levelResults+              ++ [""]++    formatGateResult :: GateResult -> Text+    formatGateResult gr =+      let marker = if resultPassed gr then "✓" else "✗"+          name = gateName (resultGate gr)+      in "   [" <> marker <> "] " <> name++    formatRemediationSection :: [RemediationStep] -> [Text]+    formatRemediationSection [] =+      [ thinSeparator+      , " All quality gates PASSED. Compliance: " <> qualityLevelName (qgAchievedLevel rpt) <> "."+      , ""+      ]+    formatRemediationSection steps =+      [ thinSeparator+      , " Remediation Plan (next steps to advance from "+          <> qualityLevelName (qgAchievedLevel rpt) <> "):"+      , ""+      ]+      ++ zipWith formatStep ([1..] :: [Int]) steps+      ++ [""]++    formatStep :: Int -> RemediationStep -> Text+    formatStep n step =+      "   " <> T.pack (show n) <> ". [" <> remediationPriorityName (stepPriority step) <> "] "+      <> stepGateName step <> ": " <> stepCommand step++-- | Formats a 'QualityGateReport' as a GitHub-Flavored Markdown document.+--+-- Produces structured markdown with heading hierarchy, GFM tables for gate results,+-- and an actionable remediation checklist. Suitable for @QUALITY_REPORT.md@ or+-- CI pipeline artifact rendering.+formatQualityGateMarkdown :: QualityGateReport -> Text+formatQualityGateMarkdown rpt = T.unlines $+  [ "# Progressive Quality Gate Report: `" <> qgProjectName rpt <> "`"+  , ""+  , "| Property | Value |"+  , "|---|---|"+  , "| **Target Directory** | `" <> T.pack (qgTargetDir rpt) <> "` |"+  , "| **Project Name** | " <> qgProjectName rpt <> " |"+  , "| **Achieved Level** | " <> qualityLevelName (qgAchievedLevel rpt) <> " |"+  , "| **Gates Passed** | " <> T.pack (show (qgPassedCount rpt))+                             <> " / "+                             <> T.pack (show (qgPassedCount rpt + qgFailedCount rpt)) <> " |"+  , ""+  ]+  ++ formatMarkdownLevel QualityLevel0 (qgGateResults rpt)+  ++ formatMarkdownLevel QualityLevel1 (qgGateResults rpt)+  ++ formatMarkdownLevel QualityLevel2 (qgGateResults rpt)+  ++ formatMarkdownLevel QualityLevel3 (qgGateResults rpt)+  ++ formatMarkdownRemediation (qgRemediationSteps rpt)+  where+    formatMarkdownLevel :: QualityLevel -> [GateResult] -> [Text]+    formatMarkdownLevel lvl results =+      let levelResults = filter (\r -> gateLevel (resultGate r) == lvl) results+      in if null levelResults+           then []+           else [ "## " <> qualityLevelName lvl+                , ""+                , "| Status | Gate | Description |"+                , "|---|---|---|"+                ]+                ++ map formatMarkdownGate levelResults+                ++ [""]++    formatMarkdownGate :: GateResult -> Text+    formatMarkdownGate gr =+      let status = if resultPassed gr then "✅ PASS" else "❌ FAIL"+          name = gateName (resultGate gr)+          desc = gateDescription (resultGate gr)+      in "| " <> status <> " | " <> name <> " | " <> desc <> " |"++    formatMarkdownRemediation :: [RemediationStep] -> [Text]+    formatMarkdownRemediation [] =+      [ "## Compliance Status"+      , ""+      , "> All quality gates passed. Achieved: **" <> qualityLevelName (qgAchievedLevel rpt) <> "**."+      , ""+      ]+    formatMarkdownRemediation steps =+      [ "## Remediation Plan"+      , ""+      ]+      ++ map formatMarkdownStep steps+      ++ [""]++    formatMarkdownStep :: RemediationStep -> Text+    formatMarkdownStep step =+      "- [ ] **[" <> remediationPriorityName (stepPriority step) <> "]** "+      <> stepGateName step <> ": `" <> stepCommand step <> "`"++--------------------------------------------------------------------------------+-- 7. Internal Helpers+--------------------------------------------------------------------------------++-- | Attempts to locate and execute a turnkey verification harness script+-- (e.g. @bin/<project>-tagref@) within the target repository.+--+-- Returns 'Just True' if the harness executed successfully (exit code 0),+-- 'Just False' if it failed, or 'Nothing' if no harness was found.+findAndRunHarness :: FilePath -> String -> IO (Maybe Bool)+findAndRunHarness root harnessType = do+  hasBin <- doesDirectoryExist (root </> "bin")+              `catch` (\(_ :: SomeException) -> pure False)+  if not hasBin+    then pure Nothing+    else do+      entries <- listDirectory (root </> "bin")+                   `catch` (\(_ :: SomeException) -> pure [])+      let matches = filter (\e -> ("-" ++ harnessType) `isSuffixOfS` e || e == harnessType) entries+      case matches of+        (script:_) -> do+          let scriptPath = root </> "bin" </> script+          (code, _, _) <- readCreateProcessWithExitCode ((proc scriptPath []) { cwd = Just root }) ""+                            `catch` (\(_ :: SomeException) -> pure (ExitFailure 1, "", ""))+          pure (Just (code == ExitSuccess))+        [] -> pure Nothing++-- | Determines the source directories to scan for HLint in a target repository.+--+-- Checks for multi-package layout (@packages/*/src@) and single-package layout+-- (@src/@), returning all existing source directories.+determineSrcDirs :: FilePath -> [FilePath]+determineSrcDirs _root = ["src", "packages"]++-- | Runs HLint on a specific directory within the target repository.+runHlintOnDir :: FilePath -> FilePath -> IO Bool+runHlintOnDir root srcDir = do+  let fullPath = root </> srcDir+  exists <- doesDirectoryExist fullPath+              `catch` (\(_ :: SomeException) -> pure False)+  if not exists+    then pure True  -- Directory does not exist; no hints possible+    else do+      (_code, out, _) <- readCreateProcessWithExitCode ((proc "hlint" [fullPath, "--hint=" ++ root </> ".hlint.yaml", "--no-exit-code"]) { cwd = Just root }) ""+                           `catch` (\(_ :: SomeException) -> pure (ExitSuccess, "No suggestions", ""))+      -- HLint with --no-exit-code always returns 0; check output for "No suggestions" or "No hints"+      let noHints = "No suggestions" `isInfixOfS` out+                 || "No hints" `isInfixOfS` out+                 || null (lines out)+      pure noHints++-- | Checks whether a line contains a GHC warning indicator.+isWarningLine :: String -> Bool+isWarningLine line =+  "warning:" `isInfixOfS` line+  || "Warning:" `isInfixOfS` line++-- | Parses CLOC CSV output for comment density percentage.+-- Returns 0.0 if parsing fails.+parseClocDensity :: String -> Double+parseClocDensity output =+  let csvLines = filter (not . null) (lines output)+  in case csvLines of+    [] -> 0.0+    _  -> extractDensity csvLines++-- | Extracts comment density from CLOC CSV lines.+extractDensity :: [String] -> Double+extractDensity csvLines =+  let summaryLine = last csvLines+      fields = splitOnComma summaryLine+  in case fields of+    -- CLOC CSV columns: files, language, blank, comment, code+    (_:_:_:commentStr:codeStr:_) ->+      let comments = readMaybe commentStr+          code     = readMaybe codeStr+      in case (comments, code) of+           (Just c, Just k) | k > 0 -> (c / (c + k)) * 100.0+           _ -> 0.0+    _ -> 0.0++-- | Simple comma-separated field splitter.+splitOnComma :: String -> [String]+splitOnComma [] = [""]+splitOnComma (',':rest) = "" : splitOnComma rest+splitOnComma (c:rest) = case splitOnComma rest of+  (f:fs) -> (c:f) : fs+  []     -> [[c]]++-- | Safe read with default to Nothing.+readMaybe :: String -> Maybe Double+readMaybe s = case reads (filter (/= '"') s) of+  [(v, "")] -> Just v+  _         -> Nothing++-- | String infix check helper.+isInfixOfS :: String -> String -> Bool+isInfixOfS needle haystack = needle `elem` tails' haystack+  where+    tails' [] = [""]+    tails' xs@(_:rest) = take (length needle) xs : tails' rest++-- | String suffix check helper.+isSuffixOfS :: String -> String -> Bool+isSuffixOfS sfx str = drop (length str - length sfx) str == sfx+
+ src/Gachi/Retrofit/Report.hs view
@@ -0,0 +1,349 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.Retrofit.Report+-- Description: Diagnostic report formatting and pretty-printing+--+-- This module renders 'DiagnosticReport' data structures into aligned ASCII terminal+-- dashboards, GitHub-Flavored Markdown documents, and compact single-line summaries.+--+-- = Architectural Context & Invariants+--+-- Per Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit],+-- diagnostic reports provide complete observability into legacy or external repositories+-- without altering any target files.+--+-- == Pretty-Printing Philosophy & Formats+--+-- The reporting layer provides three distinct output modalities:+--+-- 1. __Aligned ASCII Terminal Dashboard ('formatDiagnosticReport'):__+--    Optimized for high-information-density terminal display. Features 80-column boxed+--    headers, Unicode progress indicators ('renderScoreBar'), aligned tabular breakdowns+--    for programming languages and documentation gaps, and status checklists for Git hooks+--    and verification gates.+--+-- 2. __GitHub-Flavored Markdown ('formatDiagnosticMarkdown'):__+--    Structured markdown document suitable for piping into @RETROFIT_PLAN.md@, generating+--    pull request descriptions, or saving as CI workflow artifacts.+--+-- 3. __Single-Line Summary ('formatDiagnosticSummaryLine'):__+--    Concise one-line status string intended for log streaming, batch audits, or CI step summaries.+--+-- = Worked Example+--+-- >>> import Gachi.Retrofit.Diagnostic (scanRepository)+-- >>> report <- scanRepository "."+-- >>> let textReport = formatDiagnosticReport report+-- >>> T.null textReport+-- False+-- >>> let summary = formatDiagnosticSummaryLine report+-- >>> "gachi retrofit inspect" `T.isInfixOf` summary+-- True+module Gachi.Retrofit.Report+  ( -- * Report Formatters+    formatDiagnosticReport+  , formatDiagnosticMarkdown+  , formatDiagnosticSummaryLine++    -- * Helper Rendering Primitives+  , renderScoreBar+  , scoreClassification+  , padRight+  ) where++import Data.Text (Text)+import qualified Data.Text as T++import Gachi.Retrofit.Types++--------------------------------------------------------------------------------+-- 1. ASCII Terminal Dashboard Formatting+--------------------------------------------------------------------------------++-- | Formats a 'DiagnosticReport' as an aligned ASCII terminal dashboard.+--+-- Renders an 80-column summary dashboard presenting:+-- * Workspace identity, primary language, layout classification, and build tools.+-- * 20-character Unicode progress bar representing the overall retrofit readiness score.+-- * Section [1]: Detailed programming language distribution table.+-- * Section [2]: Test infrastructure status, directories, and declared suites.+-- * Section [3]: Complete @DOC_STRATEGY.md@ documentation checklist.+-- * Section [4]: Git governance, hook permissions, and RFC 2822 trailer status.+-- * Section [5]: 5-gate physical verification harness status.+-- * Section [6]: Prioritized, numbered remediation action plan for Phase 4.+formatDiagnosticReport :: DiagnosticReport -> Text+formatDiagnosticReport r =+  T.unlines+    [ banner+    , " Gachi Diagnostic Repository Inspector (~/src/PLAN.md §1.A, Milestone 4.1)"+    , banner+    , " Target Directory:    " <> T.pack (diagTargetDir r)+    , " Project Identifier:  " <> diagProjectName r+    , " Primary Language:    " <> maybe "Unknown" languageName (diagPrimaryLanguage r)+    , " Workspace Layout:    " <> layoutName (diagLayout r)+    , " Build Systems:       " <> formatBuildList (diagBuildSystems r)+    , " Packages Detected:   " <> formatPackages (diagDiscoveredPackages r)+    , divider+    , " Ecosystem Retrofit Score: " <> renderScoreBar (diagRetrofitScore r) <> " " <> T.pack (show (diagRetrofitScore r)) <> "% (" <> scoreClassification (diagRetrofitScore r) <> ")"+    , banner+    , ""+    , " [1] Programming Language Breakdown"+    , divider+    , formatLanguageTable (diagLanguages r)+    , ""+    , " [2] Test Infrastructure"+    , divider+    , formatTestSection (diagTestInfra r)+    , ""+    , " [3] Documentation Inventory (DOC_STRATEGY.md Compliance: " <> T.pack (show (diagDocCompliancePct r)) <> "%)"+    , divider+    , formatDocTable (diagDocGaps r)+    , ""+    , " [4] Git Governance & Commit Attribution"+    , divider+    , formatGitSection (diagGitGovernance r)+    , ""+    , " [5] Physical Verification Harnesses & Quality Gates"+    , divider+    , formatVerificationSection (diagVerificationHarness r)+    , ""+    , " [6] Prioritized Phase 4 Remediation Plan"+    , divider+    , formatRemediationPlan (diagRemediationPlan r)+    , banner+    ]+  where+    banner  = T.replicate 80 "="+    divider = T.replicate 80 "-"++    formatBuildList [] = "None detected (Unstructured)"+    formatBuildList bs = T.intercalate ", " (map buildSystemName bs)++    formatPackages [] = "None (Single root package or non-Cabal layout)"+    formatPackages ps = "[" <> T.intercalate ", " ps <> "]"++--------------------------------------------------------------------------------+-- 2. Section Formatters+--------------------------------------------------------------------------------++-- | Formats the language distribution table.+--+-- Renders columns: Language, Files, Share (%), and Extensions.+formatLanguageTable :: [LanguageStats] -> Text+formatLanguageTable [] = "  No recognized source code files discovered."+formatLanguageTable stats =+  let hdr = "  Language         | Files      | Share (%)  | Extensions"+      sep = "  -----------------+------------+------------+---------------------------"+      rows = map renderRow stats+  in T.unlines (hdr : sep : rows)+  where+    renderRow s =+      let lang = padRight 16 (languageName (langType s))+          files = padRight 10 (T.pack (show (langFiles s)))+          share = padRight 10 (T.pack (show (langPercentage s)) <> "%")+          exts = T.intercalate ", " (langExtensions s)+      in "  " <> lang <> " | " <> files <> " | " <> share <> " | " <> exts++-- | Formats the test infrastructure section.+--+-- Renders test directories, test file counts, and declared test suites with frameworks.+formatTestSection :: TestInfrastructure -> Text+formatTestSection t+  | not (hasTests t) =+      T.unlines+        [ "  Status:           [-] NO TESTS DETECTED"+        , "  Recommendation:   Establish baseline test suite in test/ or packages/*/test."+        ]+  | otherwise =+      let dirList = if null (testDirectories t) then "None" else T.intercalate ", " (map T.pack (testDirectories t))+          suiteSummary = if null (testSuites t) then "None declared" else T.intercalate "; " (map formatSuite (testSuites t))+      in T.unlines+        [ "  Status:           [+] TEST INFRASTRUCTURE DETECTED"+        , "  Test Files Found: " <> T.pack (show (testFileCount t)) <> " files"+        , "  Directories:      " <> dirList+        , "  Declared Suites:  " <> suiteSummary+        ]+  where+    formatSuite s =+      testSuiteName s <> " (" <> T.intercalate ", " (map testFrameworkName (testFrameworks s)) <> ")"++-- | Formats the @DOC_STRATEGY.md@ documentation checklist.+--+-- Renders columns: Status ([+] / [-]), Severity, Path, and Category.+formatDocTable :: [DocGap] -> Text+formatDocTable [] = "  No documentation items evaluated."+formatDocTable docs =+  let hdr = "  Status | Severity    | Path                         | Category"+      sep = "  -------+-------------+------------------------------+--------------------------"+      rows = map renderRow docs+  in T.unlines (hdr : sep : rows)+  where+    renderRow d =+      let st  = if docPresent d then "[+] OK " else "[-] MISS"+          sev = padRight 11 (docSeverityName (docSeverity d))+          pth = padRight 28 (T.pack (docPath d))+          cat = docCategoryName (docCategory d)+      in "  " <> st <> " | " <> sev <> " | " <> pth <> " | " <> cat++-- | Formats Git governance status.+--+-- Reports status of @.git@, @.githooks@, @core.hooksPath@, @commit-msg@, @pre-commit@,+-- Sarutahiko attribution checks, and @.gitmessage@.+formatGitSection :: GitGovernanceStatus -> Text+formatGitSection g =+  T.unlines+    [ "  Git Repository:    " <> check (hasGitRepo g) "Initialized (.git exists)" "NOT INITIALIZED"+    , "  Hooks Directory:   " <> check (hasHooksDir g) "Standard (.githooks exists)" "Missing (.githooks not found)"+    , "  core.hooksPath:    " <> maybe "[-] Unconfigured (defaults to .git/hooks)" ("[+] Configured: " <>) (hooksPathConfig g)+    , "  commit-msg Hook:   " <> check (hasCommitMsgHook g) (if isCommitMsgExecutable g then "Executable (+x)" else "Present but NOT executable") "Missing"+    , "  AI Trailer Check:  " <> check (hasCommitMsgAttributionCheck g) "Enforcing RFC 2822 Assisted-by: trailers" "Attribution validation missing"+    , "  pre-commit Hook:   " <> check (hasPreCommitHook g) (if isPreCommitExecutable g then "Executable (+x)" else "Present but NOT executable") "Missing"+    , "  Commit Template:   " <> check (hasGitMessageTemplate g) ".gitmessage present" "Missing .gitmessage"+    , "  commit.template:   " <> maybe "[-] Unconfigured in git config" ("[+] Configured: " <>) (gitMessageConfig g)+    ]+  where+    check True ok _ = "[+] " <> ok+    check False _ miss = "[-] " <> miss++-- | Formats verification harness status.+--+-- Reports presence and status of turnkey verify script, Tagref, linkcheck, CLOC,+-- and linters.+formatVerificationSection :: VerificationHarnessStatus -> Text+formatVerificationSection v =+  T.unlines+    [ "  Turnkey Runner:    " <> maybe "[-] Missing (bin/*-verify)" (\p -> "[+] " <> T.pack p) (verifyScriptPath v)+    , "  Tagref Harness:    " <> check (hasTagrefHarness v || hasTagrefConfig v) "Active (tagref.yml / bin/*-tagref)" "Missing"+    , "  Linkcheck Harness: " <> check (hasLinkcheckHarness v) "Active (bin/*-linkcheck)" "Missing"+    , "  CLOC Harness:      " <> check (hasClocHarness v) "Active (bin/*-cloc)" "Missing"+    , "  HLint Config:      " <> check (hasHlintConfig v) "Active (.hlint.yaml)" "Missing"+    , "  Fourmolu Config:   " <> check (hasFourmoluConfig v) "Active (fourmolu.yaml)" "Missing"+    ]+  where+    check True ok _ = "[+] " <> ok+    check False _ miss = "[-] " <> miss++-- | Formats prioritized remediation actions.+--+-- Displays numbered steps guiding the developer on required Phase 4 actions.+formatRemediationPlan :: [Text] -> Text+formatRemediationPlan [] = "  [+] Total ecosystem compliance: 0 remediation actions required!"+formatRemediationPlan plan =+  let numbered = zipWith (\(n :: Int) item -> "  " <> T.pack (show n) <> ". " <> item) [1..] plan+  in T.unlines numbered++--------------------------------------------------------------------------------+-- 3. Markdown Output Formatting+--------------------------------------------------------------------------------++-- | Formats the diagnostic report as structured GitHub-Flavored Markdown.+--+-- Suitable for generating @RETROFIT_PLAN.md@, pull request descriptions, or CI summary+-- artifacts. Includes formatted Markdown tables for languages and documentation items.+formatDiagnosticMarkdown :: DiagnosticReport -> Text+formatDiagnosticMarkdown r =+  T.unlines+    [ "# Diagnostic Repository Inspection Report: `" <> diagProjectName r <> "`"+    , ""+    , "**Status:** EVALUATED · Milestone 4.1 Diagnostic Inspector  "+    , "**Target Directory:** `" <> T.pack (diagTargetDir r) <> "`  "+    , "**Retrofit Readiness Score:** **" <> T.pack (show (diagRetrofitScore r)) <> "%** (" <> scoreClassification (diagRetrofitScore r) <> ")  "+    , "**DOC_STRATEGY.md Compliance:** **" <> T.pack (show (diagDocCompliancePct r)) <> "%**"+    , ""+    , "---"+    , ""+    , "## 1. Programming Languages"+    , ""+    , "| Language | Files | Share | Extensions |"+    , "|---|---|---|---|"+    , T.unlines (map renderMdLang (diagLanguages r))+    , "## 2. Build Systems & Architecture"+    , ""+    , "- **Workspace Layout:** " <> layoutName (diagLayout r)+    , "- **Build Toolchains:** " <> T.intercalate ", " (map buildSystemName (diagBuildSystems r))+    , "- **Packages:** " <> if null (diagDiscoveredPackages r) then "None" else T.intercalate ", " (diagDiscoveredPackages r)+    , ""+    , "## 3. Documentation Gaps"+    , ""+    , "| Status | Document | Severity | Category |"+    , "|---|---|---|---|"+    , T.unlines (map renderMdDoc (diagDocGaps r))+    , "## 4. Prioritized Phase 4 Remediation Plan"+    , ""+    , formatMdRemediation (diagRemediationPlan r)+    ]+  where+    renderMdLang s =+      "| " <> languageName (langType s) <> " | " <> T.pack (show (langFiles s)) <> " | " <> T.pack (show (langPercentage s)) <> "% | `" <> T.intercalate ", " (langExtensions s) <> "` |"++    renderMdDoc d =+      let st = if docPresent d then "✅ Present" else "❌ Missing"+      in "| " <> st <> " | `" <> T.pack (docPath d) <> "` | **" <> docSeverityName (docSeverity d) <> "** | " <> docCategoryName (docCategory d) <> " |"++    formatMdRemediation [] = "*No remediation actions required; codebase is in harmonic ecosystem equilibrium.*"+    formatMdRemediation items =+      T.unlines (map ("- [ ] " <>) items)++--------------------------------------------------------------------------------+-- 4. Compact Summary Line+--------------------------------------------------------------------------------++-- | Formats a single-line summary string suitable for logs or streaming output.+--+-- === Examples+--+-- >>> let r = DiagnosticReport "." "gachi" (Just LangHaskell) [] [] LayoutMultiPackage [] (TestInfrastructure True [] [] 0) [] 100.0 (GitGovernanceStatus True True Nothing True True True True True True Nothing) (VerificationHarnessStatus True Nothing True True True True True True) 95.0 []+-- >>> formatDiagnosticSummaryLine r+-- "gachi retrofit inspect [gachi]: Haskell, Multi-Package Workspace Layout (packages/*), Score: 95.0%, 0 critical doc gaps"+formatDiagnosticSummaryLine :: DiagnosticReport -> Text+formatDiagnosticSummaryLine r =+  let langStr = maybe "Unknown" languageName (diagPrimaryLanguage r)+      critGaps = length (filter (\d -> not (docPresent d) && docSeverity d == SeverityCritical) (diagDocGaps r))+  in "gachi retrofit inspect [" <> diagProjectName r <> "]: "+     <> langStr <> ", "+     <> layoutName (diagLayout r) <> ", "+     <> "Score: " <> T.pack (show (diagRetrofitScore r)) <> "%, "+     <> T.pack (show critGaps) <> " critical doc gaps"++--------------------------------------------------------------------------------+-- 5. Progress Bars & Helpers+--------------------------------------------------------------------------------++-- | Renders a 20-character Unicode block progress bar.+--+-- Clamps score between 0.0 and 100.0, rendering filled blocks (@█@) and empty blocks (@░@).+--+-- === Examples+--+-- >>> renderScoreBar 50.0+-- "[██████████░░░░░░░░░░]"+-- >>> renderScoreBar 100.0+-- "[████████████████████]"+renderScoreBar :: Double -> Text+renderScoreBar score =+  let clamped = max 0.0 (min 100.0 score)+      filledCount = round ((clamped / 100.0) * 20.0) :: Int+      emptyCount = max 0 (20 - filledCount)+  in "[" <> T.replicate filledCount "█" <> T.replicate emptyCount "░" <> "]"++-- | Qualitative classification of a numerical retrofit score.+--+-- Categorizes readiness into four actionable tiers:+-- * >= 90.0: High Conformance+-- * >= 70.0: Moderate Adaptation Needed+-- * >= 40.0: Substantial Retrofit Required+-- * < 40.0:  Unstandardized / Raw Codebase+scoreClassification :: Double -> Text+scoreClassification s+  | s >= 90.0 = "HIGH CONFORMANCE"+  | s >= 70.0 = "MODERATE ADAPTATION NEEDED"+  | s >= 40.0 = "SUBSTANTIAL RETROFIT REQUIRED"+  | otherwise = "UNSTANDARDIZED / RAW CODEBASE"++-- | Right-pads a 'Text' value with spaces to achieve the specified minimum column width.+padRight :: Int -> Text -> Text+padRight width txt =+  let len = T.length txt+  in if len >= width then txt else txt <> T.replicate (width - len) " "
+ src/Gachi/Retrofit/Types.hs view
@@ -0,0 +1,569 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.Retrofit.Types+-- Description: Core algebraic data types for repository inspection and retrofitting+--+-- This module defines the complete data modeling layer for the diagnostic repository+-- inspector and non-destructive retrofitting engine (Milestone 4.1, Phase 4).+--+-- = Architectural Context & Invariants+--+-- Per Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit],+-- retrofitting operations adopt legacy, single-package, or foreign repositories into+-- the canonical multi-package ecosystem hierarchy without altering or perturbing+-- domain logic source files (@src/@, @app/@, @lib/@).+--+-- To achieve this safely, the diagnostic analyzer produces a strongly-typed+-- 'DiagnosticReport' assessing six orthogonal quality and structural dimensions:+--+-- 1. __Language Stacks & Distributions:__+--    Classifies discovered source code files into recognized language families ('Language')+--    and computes aggregate file counts and proportions ('LanguageStats'). Identifies the+--    dominant language informing toolchain decisions.+--+-- 2. __Build Systems & Topographical Layout:__+--    Discovers build system manifests ('BuildSystem') spanning Cabal, Stack, Cargo,+--    CMake, Make, Meson, Poetry, and Flit. Classifies overall repository layout+--    ('WorkspaceLayout') into single-package, multi-package, polyglot, or unstructured trees.+--+-- 3. __Test Infrastructure & Frameworks:__+--    Detects test directories, declared test targets, and testing libraries ('TestFramework',+--    'TestSuiteInfo', 'TestInfrastructure'). Distinguishes active verification setups from+--    untested codebases.+--+-- 4. __Documentation Gaps Relative to DOC_STRATEGY.md:__+--    Audits repository assets against canonical documentation genres ('DocCategory',+--    'DocSeverity', 'DocGap') per @DOC_STRATEGY.md@ §1, identifying missing orientation,+--    corpus inventory, design rationale, and operational manuals.+--+-- 5. __Git Governance, Hook Executability & AI Attribution:__+--    Audits @.git@ directory existence, @core.hooksPath@ configuration, @commit-msg@ hook+--    executability, Sarutahiko / RFC 2822 @Assisted-by:@ trailer validation, @pre-commit@+--    gates, and @.gitmessage@ commit template pinning ('GitGovernanceStatus').+--+-- 6. __Physical Verification Harnesses & Quality Gates:__+--    Evaluates turnkey test runners (@bin/*-verify@), Tagref cross-reference configuration,+--    Markdown link integrity checkers, and CLOC comment density audit harnesses+--    ('VerificationHarnessStatus').+--+-- = The Six Evaluation Dimensions+--+-- @+--  +-------------------------------------------------------------------------------++--  |                       Diagnostic Repository Inspector                         |+--  +-------------------------------------------------------------------------------++--  | [1] Languages      : Haskell (92.5%), Shell (7.5%)                            |+--  | [2] Build Systems  : Cabal Multi-Package Workspace (cabal.project)           |+--  | [3] Test Infra     : Hspec test suite (5 files in test/)                     |+--  | [4] Documentation  : DOC_STRATEGY.md Compliance 90.0% (10/12 present)         |+--  | [5] Git Governance : .githooks active, RFC 2822 Assisted-by: trailer checked |+--  | [6] Quality Gates  : Turnkey verify, Tagref, Linkcheck, CLOC active          |+--  +-------------------------------------------------------------------------------++-- @+--+-- = Worked Example+--+-- >>> let lang = LangHaskell+-- >>> languageName lang+-- "Haskell"+-- >>> let bld = BuildCabalProject+-- >>> buildSystemName bld+-- "Cabal Multi-Package Workspace (cabal.project)"+-- >>> let sev = SeverityCritical+-- >>> docSeverityName sev+-- "CRITICAL"+module Gachi.Retrofit.Types+  ( -- * Programming Language Classification+    Language(..)+  , LanguageStats(..)+  , languageName++    -- * Build Systems & Layout+  , BuildSystem(..)+  , WorkspaceLayout(..)+  , buildSystemName+  , layoutName++    -- * Test Infrastructure+  , TestFramework(..)+  , TestSuiteInfo(..)+  , TestInfrastructure(..)+  , testFrameworkName++    -- * Documentation Gaps & Categories+  , DocCategory(..)+  , DocSeverity(..)+  , DocGap(..)+  , docCategoryName+  , docSeverityName++    -- * Git Governance & Hooks+  , GitGovernanceStatus(..)++    -- * Physical Verification Harnesses+  , VerificationHarnessStatus(..)++    -- * Comprehensive Diagnostic Report+  , DiagnosticReport(..)+  ) where++import Data.Text (Text)++--------------------------------------------------------------------------------+-- 1. Programming Language Modeling+--------------------------------------------------------------------------------++-- | Recognized programming language families identified during repository inspection.+--+-- Language classification is performed by inspecting source file extensions across+-- the workspace, excluding known build artifact directories (e.g. @dist-newstyle@,+-- @target@, @node_modules@, @.venv@).+data Language+  = LangHaskell+    -- ^ Haskell source code files (@.hs@, @.lhs@). Primary target for Gachi multi-package+    -- workspace scaffolding, Cabal 3.18 / GHC 9.14 compiler stanzas, and HLint policies.+  | LangRust+    -- ^ Rust source code files (@.rs@). Target for Cargo workspace layouts, @rustfmt.toml@,+    -- and clippy verification gates.+  | LangC+    -- ^ C source and header files (@.c@, @.h@). Target for Make, CMake, and @.clang-format@.+  | LangCpp+    -- ^ C++ source and header files (@.cpp@, @.cc@, @.cxx@, @.hpp@, @.hxx@).+  | LangPython+    -- ^ Python source code files (@.py@). Target for Poetry, Flit, and pytest.+  | LangShell+    -- ^ POSIX Shell / Bash / Zsh scripts (@.sh@, @.bash@, @.zsh@). Typical in @bin/@.+  | LangOCaml+    -- ^ OCaml source code files (@.ml@, @.mli@).+  | LangGo+    -- ^ Go source code files (@.go@).+  | LangJava+    -- ^ Java source code files (@.java@).+  | LangJavaScript+    -- ^ JavaScript source files (@.js@, @.mjs@, @.cjs@).+  | LangTypeScript+    -- ^ TypeScript source files (@.ts@, @.tsx@).+  | LangOther !Text+    -- ^ Other programming language identified by file extension.+  deriving stock (Eq, Ord, Show)++-- | Detailed file count and percentage metrics for a detected programming language.+--+-- Summarizes the prevalence of a language across the scanned codebase. Used to+-- compute the 'diagPrimaryLanguage' and inform build toolchain selection.+data LanguageStats = LanguageStats+  { langType        :: !Language+    -- ^ Classified language family.+  , langFiles       :: !Int+    -- ^ Total number of matching source files discovered across the repository.+  , langPercentage  :: !Double+    -- ^ Proportion of this language relative to total source files (0.0 to 100.0).+  , langExtensions  :: ![Text]+    -- ^ Unique file extensions associated with this language detection (e.g. @[".hs", ".lhs"]@).+  } deriving stock (Eq, Show)++-- | Converts a 'Language' into its canonical human-readable display title.+--+-- === Examples+--+-- >>> languageName LangHaskell+-- "Haskell"+-- >>> languageName LangRust+-- "Rust"+-- >>> languageName (LangOther "Zig")+-- "Zig"+languageName :: Language -> Text+languageName LangHaskell      = "Haskell"+languageName LangRust         = "Rust"+languageName LangC            = "C"+languageName LangCpp          = "C++"+languageName LangPython       = "Python"+languageName LangShell        = "Shell / Bash"+languageName LangOCaml        = "OCaml"+languageName LangGo           = "Go"+languageName LangJava         = "Java"+languageName LangJavaScript   = "JavaScript"+languageName LangTypeScript   = "TypeScript"+languageName (LangOther name) = name++--------------------------------------------------------------------------------+-- 2. Build Systems & Workspace Layout+--------------------------------------------------------------------------------++-- | Build system manifests and toolchains discovered in the target repository.+--+-- Identifies declarative build manifests at the repository root and package subdirectories.+-- Informs the retrofitting engine whether a repository can be adopted directly or requires+-- multi-package scaffolding.+data BuildSystem+  = BuildCabalProject+    -- ^ Haskell Cabal multi-package workspace definition (@cabal.project@).+    -- Indicates a modern Cabal workspace supporting cross-package dependencies.+  | BuildCabalPackage+    -- ^ Haskell Cabal single-package manifest (@*.cabal@ at repository root).+    -- Indicates a legacy single-package repository or standalone library.+  | BuildStack+    -- ^ Haskell Stack build tool definition (@stack.yaml@).+  | BuildHpack+    -- ^ Haskell Hpack package generator (@package.yaml@).+  | BuildCargo+    -- ^ Rust Cargo workspace or package manifest (@Cargo.toml@).+  | BuildCMake+    -- ^ CMake build system definition (@CMakeLists.txt@).+  | BuildMake+    -- ^ Standard Makefile / GNUmakefile build system.+  | BuildMeson+    -- ^ Meson build definition (@meson.build@).+  | BuildPoetry+    -- ^ Python Poetry dependency configuration (@pyproject.toml@).+  | BuildFlit+    -- ^ Python Flit packaging manifest (@pyproject.toml@).+  | BuildSetuptools+    -- ^ Python Setuptools manifest (@setup.py@ / @setup.cfg@).+  | BuildPip+    -- ^ Python pip requirements file (@requirements.txt@).+  | BuildNix+    -- ^ Nix build expression or flake (@flake.nix@ / @default.nix@).+  | BuildOther !Text+    -- ^ Custom or unrecognized build configuration.+  deriving stock (Eq, Ord, Show)++-- | High-level architectural workspace layout of the target repository.+--+-- Categorizes repository topography into canonical patterns to determine the appropriate+-- overlay strategy in Phase 4:+--+-- * 'LayoutMultiPackage': Standard Gachi multi-package hierarchy (@packages/*/@).+-- * 'LayoutSinglePackage': Standalone single-package project with root manifest.+-- * 'LayoutPolyglot': Multi-language repository with multiple heterogeneous build tools.+-- * 'LayoutUnstructured': Raw source files without a recognizable build manifest.+data WorkspaceLayout+  = LayoutSinglePackage+    -- ^ Single package located at repository root (e.g. root @my-project.cabal@).+  | LayoutMultiPackage+    -- ^ Canonical multi-package layout with packages segregated in @packages/*/@.+  | LayoutPolyglot+    -- ^ Polyglot repository containing multiple heterogeneous build systems (e.g. Haskell + Rust).+  | LayoutUnstructured+    -- ^ Source files discovered without a recognized build manifest.+  deriving stock (Eq, Ord, Show)++-- | Converts a 'BuildSystem' into a descriptive string.+--+-- === Examples+--+-- >>> buildSystemName BuildCabalProject+-- "Cabal Multi-Package Workspace (cabal.project)"+-- >>> buildSystemName BuildCargo+-- "Cargo Package / Workspace (Cargo.toml)"+buildSystemName :: BuildSystem -> Text+buildSystemName BuildCabalProject = "Cabal Multi-Package Workspace (cabal.project)"+buildSystemName BuildCabalPackage = "Cabal Single-Package Manifest (*.cabal)"+buildSystemName BuildStack        = "Stack Project Definition (stack.yaml)"+buildSystemName BuildHpack        = "Hpack Package Specification (package.yaml)"+buildSystemName BuildCargo        = "Cargo Package / Workspace (Cargo.toml)"+buildSystemName BuildCMake        = "CMake Build Configuration (CMakeLists.txt)"+buildSystemName BuildMake         = "GNU Make / POSIX Makefile (Makefile)"+buildSystemName BuildMeson        = "Meson Build Definition (meson.build)"+buildSystemName BuildPoetry       = "Python Poetry Project (pyproject.toml)"+buildSystemName BuildFlit         = "Python Flit Package (pyproject.toml)"+buildSystemName BuildSetuptools   = "Python Setuptools (setup.py)"+buildSystemName BuildPip          = "Python Pip Requirements (requirements.txt)"+buildSystemName BuildNix          = "Nix Build Expression (flake.nix / default.nix)"+buildSystemName (BuildOther desc) = desc++-- | Converts a 'WorkspaceLayout' into a descriptive string.+--+-- === Examples+--+-- >>> layoutName LayoutMultiPackage+-- "Multi-Package Workspace Layout (packages/*)"+-- >>> layoutName LayoutSinglePackage+-- "Single-Package Root Layout"+layoutName :: WorkspaceLayout -> Text+layoutName LayoutSinglePackage = "Single-Package Root Layout"+layoutName LayoutMultiPackage  = "Multi-Package Workspace Layout (packages/*)"+layoutName LayoutPolyglot      = "Polyglot Multi-Toolchain Workspace"+layoutName LayoutUnstructured  = "Unstructured Source Code Tree"++--------------------------------------------------------------------------------+-- 3. Test Infrastructure Modeling+--------------------------------------------------------------------------------++-- | Testing frameworks identified in repository manifests or source files.+--+-- Introspects test imports and dependency manifests to determine the active testing+-- harness without executing arbitrary code.+data TestFramework+  = FrameworkHspec+    -- ^ Haskell Hspec BDD testing framework (@Test.Hspec@).+  | FrameworkTasty+    -- ^ Haskell Tasty test runner (@Test.Tasty@).+  | FrameworkQuickCheck+    -- ^ QuickCheck property-based testing (@Test.QuickCheck@).+  | FrameworkHedgehog+    -- ^ Hedgehog property testing library (@Hedgehog@).+  | FrameworkCargoTest+    -- ^ Built-in Rust test harness (@cargo test@ / @#[cfg(test)]@).+  | FrameworkPytest+    -- ^ Python pytest test framework (@pytest@).+  | FrameworkUnittest+    -- ^ Python standard unittest library (@unittest@).+  | FrameworkCTest+    -- ^ CTest / CMake test runner.+  | FrameworkUnknown !Text+    -- ^ Other test framework identifier.+  deriving stock (Eq, Ord, Show)++-- | Converts a 'TestFramework' into its canonical title.+--+-- === Examples+--+-- >>> testFrameworkName FrameworkHspec+-- "Hspec"+-- >>> testFrameworkName FrameworkTasty+-- "Tasty"+testFrameworkName :: TestFramework -> Text+testFrameworkName FrameworkHspec         = "Hspec"+testFrameworkName FrameworkTasty         = "Tasty"+testFrameworkName FrameworkQuickCheck    = "QuickCheck"+testFrameworkName FrameworkHedgehog      = "Hedgehog"+testFrameworkName FrameworkCargoTest     = "Cargo Test"+testFrameworkName FrameworkPytest        = "pytest"+testFrameworkName FrameworkUnittest      = "unittest"+testFrameworkName FrameworkCTest         = "CTest"+testFrameworkName (FrameworkUnknown txt) = txt++-- | Information describing an identified test suite or test target.+--+-- Records the declared test suite identifier, its primary entry point or test directory,+-- and all discovered test frameworks.+data TestSuiteInfo = TestSuiteInfo+  { testSuiteName       :: !Text+    -- ^ Declared test suite name (e.g. @"tests"@, @"spec"@).+  , testSuitePath       :: !FilePath+    -- ^ Primary entry point or test directory (e.g. @"test"@, @"test/Main.hs"@).+  , testFrameworks      :: ![TestFramework]+    -- ^ Identified test framework dependencies.+  } deriving stock (Eq, Show)++-- | Aggregate test infrastructure status discovered during repository inspection.+--+-- Assesses whether the repository contains verifiable test suites, counts test files,+-- and catalogs declared test targets.+data TestInfrastructure = TestInfrastructure+  { hasTests            :: !Bool+    -- ^ True if test directories, manifests, or suites were found.+  , testSuites          :: ![TestSuiteInfo]+    -- ^ Specific test suites discovered or declared.+  , testDirectories     :: ![FilePath]+    -- ^ Existing test directories (e.g. @"test"@, @"tests"@, @"spec"@).+  , testFileCount       :: !Int+    -- ^ Total number of test source files located.+  } deriving stock (Eq, Show)++--------------------------------------------------------------------------------+-- 4. Documentation Gaps & Categories+--------------------------------------------------------------------------------++-- | Documentation category classifications per @DOC_STRATEGY.md@ §1.+--+-- Classifies documentation into purpose-built genres serving three distinct audiences:+-- 1. __Arrivals:__ Newcomers seeking immediate orientation (@README.md@).+-- 2. __API Consumers:__ Developers looking for reference guides, types, and invariants.+-- 3. __Maintainers:__ Core contributors governing architectural decisions, notes, and roadmap.+-- 4. __AI Coding Assistants:__ Modular rules and bounded context projections (@AGENTS.md@).+data DocCategory+  = DocOrientation+    -- ^ Newcomer orientation, introductory facts, and project vision (@README.md@).+  | DocCorpusInventory+    -- ^ Curated inventory and reading paths per @DOC_STRATEGY.md@ §1 (@docs/INDEX.md@).+  | DocDesignRationale+    -- ^ Architectural decisions, trade-offs, and open questions (@docs/notes/*_DESIGN.md@).+  | DocStrategy+    -- ^ Corpus policies, lifecycle, precedence rules, and authority (@docs/notes/DOC_STRATEGY.md@).+  | DocGovernance+    -- ^ Strategic roadmaps and session frontiers (@PLAN.md@, @STATE.md@).+  | DocOperational+    -- ^ Maintenance manuals, gate triage, and release procedures (@docs/MAINTAINER_GUIDE.md@).+  | DocTutorial+    -- ^ Worked examples and newcomer tutorials (@docs/TUTORIAL.md@, @docs/NEWBIE_GUIDE.md@).+  | DocAdvisory+    -- ^ AI coding assistant advisories and modular rules (@AGENTS.md@, @.agents/rules/@).+  | DocAttribution+    -- ^ Commit trailer attribution policy per RFC 2822 (@docs/notes/COMMIT_ATTRIBUTION.md@).+  | DocChangelog+    -- ^ Release history, licensing terms, and code of conduct (@CHANGELOG.md@, @LICENSE@).+  deriving stock (Eq, Ord, Show)++-- | Severity level associated with a missing documentation asset.+--+-- Prioritizes documentation remediation actions during retrofitting:+-- * 'SeverityCritical': Essential files without which repository cannot function cleanly.+-- * 'SeverityRecommended': Standard documentation practices required by @DOC_STRATEGY.md@.+-- * 'SeverityOptional': Contextual or supplementary documents.+data DocSeverity+  = SeverityCritical+    -- ^ Essential ecosystem document; retrofitting cannot succeed without it (e.g. @README.md@, @LICENSE@).+  | SeverityRecommended+    -- ^ Standard documentation practice per @DOC_STRATEGY.md@ (e.g. @PLAN.md@, @STATE.md@, @AGENTS.md@).+  | SeverityOptional+    -- ^ Nice-to-have or contextual document (e.g. @CLAUDE.md@, @CHANGELOG.md@).+  deriving stock (Eq, Ord, Show)++-- | Represents an individual documentation item audited against the canonical corpus.+--+-- Evaluates presence on disk and provides actionable remediation guidance.+data DocGap = DocGap+  { docPath             :: !FilePath+    -- ^ Relative file path of the documentation asset.+  , docCategory         :: !DocCategory+    -- ^ Functional category of this document.+  , docPresent          :: !Bool+    -- ^ True if the file physically exists on disk.+  , docSeverity         :: !DocSeverity+    -- ^ Priority level of this document.+  , docAdvice           :: !Text+    -- ^ Remediation guidance or explanation describing the document's purpose.+  } deriving stock (Eq, Show)++-- | Converts a 'DocCategory' into a human-readable title.+--+-- === Examples+--+-- >>> docCategoryName DocOrientation+-- "Arrivals & Orientation"+-- >>> docCategoryName DocDesignRationale+-- "Architecture & Rationale"+docCategoryName :: DocCategory -> Text+docCategoryName DocOrientation     = "Arrivals & Orientation"+docCategoryName DocCorpusInventory = "Corpus Inventory"+docCategoryName DocDesignRationale = "Architecture & Rationale"+docCategoryName DocStrategy        = "Documentation Policy"+docCategoryName DocGovernance      = "Strategic Governance"+docCategoryName DocOperational     = "Operational Guides"+docCategoryName DocTutorial        = "Tutorial Track"+docCategoryName DocAdvisory        = "Agent Advisory Layer"+docCategoryName DocAttribution     = "Commit Attribution"+docCategoryName DocChangelog       = "Changelog & Licensing"++-- | Converts a 'DocSeverity' into a formatted status label.+--+-- === Examples+--+-- >>> docSeverityName SeverityCritical+-- "CRITICAL"+-- >>> docSeverityName SeverityRecommended+-- "RECOMMENDED"+docSeverityName :: DocSeverity -> Text+docSeverityName SeverityCritical    = "CRITICAL"+docSeverityName SeverityRecommended = "RECOMMENDED"+docSeverityName SeverityOptional    = "OPTIONAL"++--------------------------------------------------------------------------------+-- 5. Git Governance & Hook Conformance+--------------------------------------------------------------------------------++-- | Evaluation of Git repository configuration, hooks, and commit trailer enforcement.+--+-- Inspects repository configuration to verify compliance with ecosystem standards:+-- * Dedicated hooks directory (@.githooks/@) configured via @git config core.hooksPath@.+-- * @commit-msg@ hook with POSIX executable permissions (+x) validating RFC 2822 @Assisted-by:@ trailers.+-- * @pre-commit@ hook enforcing Tagref and link integrity checks prior to commit recording.+-- * Standard commit template (@.gitmessage@) configured via @git config commit.template@.+data GitGovernanceStatus = GitGovernanceStatus+  { hasGitRepo                     :: !Bool+    -- ^ True if a valid @.git@ directory exists at the repository root.+  , hasHooksDir                    :: !Bool+    -- ^ True if a dedicated @.githooks@ directory exists.+  , hooksPathConfig                :: !(Maybe Text)+    -- ^ Value of @git config core.hooksPath@ if explicitly configured.+  , hasCommitMsgHook               :: !Bool+    -- ^ True if @commit-msg@ hook exists.+  , isCommitMsgExecutable          :: !Bool+    -- ^ True if @commit-msg@ has POSIX executable permission (+x).+  , hasCommitMsgAttributionCheck   :: !Bool+    -- ^ True if @commit-msg@ validates Sarutahiko / RFC 2822 @Assisted-by:@ trailers.+  , hasPreCommitHook               :: !Bool+    -- ^ True if @pre-commit@ hook exists.+  , isPreCommitExecutable          :: !Bool+    -- ^ True if @pre-commit@ has POSIX executable permission (+x).+  , hasGitMessageTemplate          :: !Bool+    -- ^ True if @.gitmessage@ template file exists.+  , gitMessageConfig               :: !(Maybe Text)+    -- ^ Value of @git config commit.template@ if explicitly configured.+  } deriving stock (Eq, Show)++--------------------------------------------------------------------------------+-- 6. Physical Verification Harnesses+--------------------------------------------------------------------------------++-- | Evaluation of physical verification scripts, linters, and quality gates.+--+-- Assesses presence and executability of the turnkey 5-gate verification suite:+-- 1. Turnkey verification runner (@bin/<project>-verify@).+-- 2. Tagref cross-reference configuration (@tagref.yml@) and runner (@bin/<project>-tagref@).+-- 3. Markdown link & anchor integrity checker (@bin/<project>-linkcheck@).+-- 4. In-code comment density audit harness (@bin/<project>-cloc@).+-- 5. Static linter configurations (@.hlint.yaml@, @fourmolu.yaml@).+data VerificationHarnessStatus = VerificationHarnessStatus+  { hasVerifyScript                :: !Bool+    -- ^ True if a turnkey verification script (e.g. @bin/*-verify@) exists.+  , verifyScriptPath               :: !(Maybe FilePath)+    -- ^ Relative file path of the primary verification runner if found.+  , hasTagrefConfig                :: !Bool+    -- ^ True if @tagref.yml@ or @.tagref@ exists.+  , hasTagrefHarness               :: !Bool+    -- ^ True if @bin/*-tagref@ runner script exists.+  , hasLinkcheckHarness            :: !Bool+    -- ^ True if @bin/*-linkcheck@ runner script exists.+  , hasClocHarness                 :: !Bool+    -- ^ True if @bin/*-cloc@ comment density runner script exists.+  , hasHlintConfig                 :: !Bool+    -- ^ True if @.hlint.yaml@ linter configuration exists.+  , hasFourmoluConfig              :: !Bool+    -- ^ True if @fourmolu.yaml@ code formatter configuration exists.+  } deriving stock (Eq, Show)++--------------------------------------------------------------------------------+-- 7. Comprehensive Diagnostic Report+--------------------------------------------------------------------------------++-- | The unified diagnostic evaluation report produced by inspecting a target codebase.+--+-- Combines language statistics, build systems, test infrastructure, documentation gaps,+-- git governance, verification harness status, and computes a quantitative retrofit score.+--+-- Per Note [Non-Destructive Skeleton Overlay] [ref:note-nondestructive-retrofit],+-- generating this report is strictly observational and non-destructive.+data DiagnosticReport = DiagnosticReport+  { diagTargetDir            :: !FilePath+    -- ^ Inspected target directory path.+  , diagProjectName          :: !Text+    -- ^ Inferred or detected project identifier.+  , diagPrimaryLanguage      :: !(Maybe Language)+    -- ^ Dominant programming language identified.+  , diagLanguages            :: ![LanguageStats]+    -- ^ Complete breakdown of all detected programming languages.+  , diagBuildSystems         :: ![BuildSystem]+    -- ^ All build manifests discovered.+  , diagLayout               :: !WorkspaceLayout+    -- ^ Classified repository workspace topology.+  , diagDiscoveredPackages   :: ![Text]+    -- ^ List of package names discovered across the codebase.+  , diagTestInfra            :: !TestInfrastructure+    -- ^ Status of test directories, declarations, and frameworks.+  , diagDocGaps              :: ![DocGap]+    -- ^ Detailed inventory of documentation gaps against @DOC_STRATEGY.md@.+  , diagDocCompliancePct     :: !Double+    -- ^ Percentage of required and recommended documents present (0.0 to 100.0).+  , diagGitGovernance        :: !GitGovernanceStatus+    -- ^ Status of Git hooks, attribution trailers, and commit templates.+  , diagVerificationHarness  :: !VerificationHarnessStatus+    -- ^ Status of 5-gate physical verification infrastructure.+  , diagRetrofitScore        :: !Double+    -- ^ Overall weighted ecosystem retrofit readiness score (0.0 to 100.0).+  , diagRemediationPlan      :: ![Text]+    -- ^ Prioritized list of actionable remediation steps for Phase 4.+  } deriving stock (Eq, Show)
+ test/Main.hs view
@@ -0,0 +1,478 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Main+-- Description: Comprehensive test suite for gachi-retrofit (Milestone 4.1)+module Main (main) where++import Control.Exception (SomeException, catch)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory+  ( createDirectoryIfMissing+  , doesDirectoryExist+  , doesFileExist+  , getCurrentDirectory+  , getPermissions+  , removeDirectoryRecursive+  , setOwnerExecutable+  , setPermissions+  )+import System.FilePath (takeDirectory, (</>))+import Test.Hspec++import Gachi.Retrofit+import Gachi.Schema.Project (Author(..), defaultProject)++main :: IO ()+main = hspec $ do+  describe "Gachi.Retrofit.Diagnostic (Pure Analyzers)" $ do+    it "detects programming language distribution and file percentages" $ do+      let samplePaths =+            [ "src/Foo.hs"+            , "src/Bar.hs"+            , "src/Baz.lhs"+            , "native/core.rs"+            , "script/run.py"+            , "bin/tool.sh"+            ]+          stats = detectLanguages samplePaths+      length stats `shouldBe` 4+      case stats of+        (top:rest) -> do+          langType top `shouldBe` LangHaskell+          langFiles top `shouldBe` 3+          langPercentage top `shouldBe` 50.0+          map langType rest `shouldMatchList` [LangRust, LangPython, LangShell]+        [] -> expectationFailure "Expected language statistics to be non-empty"++    it "handles empty or unrecognized file lists gracefully" $ do+      detectLanguages [] `shouldBe` []+      detectLanguages ["data.txt", "image.png"] `shouldBe` []++    it "classifies single-package and multi-package workspace layouts" $ do+      let singlePaths = ["my-proj.cabal", "src/Main.hs", "README.md"]+          (buildsSingle, layoutSingle) = detectBuildSystems "." singlePaths+      BuildCabalPackage `elem` buildsSingle `shouldBe` True+      layoutSingle `shouldBe` LayoutSinglePackage++      let multiPaths =+            [ "cabal.project"+            , "packages/core/core.cabal"+            , "packages/cli/cli.cabal"+            ]+          (buildsMulti, layoutMulti) = detectBuildSystems "." multiPaths+      BuildCabalProject `elem` buildsMulti `shouldBe` True+      BuildCabalPackage `elem` buildsMulti `shouldBe` True+      layoutMulti `shouldBe` LayoutMultiPackage++    it "identifies polyglot and foreign build manifests" $ do+      let polyPaths =+            [ "cabal.project"+            , "Cargo.toml"+            , "CMakeLists.txt"+            , "Makefile"+            , "pyproject.toml"+            ]+          (builds, layout) = detectBuildSystems "." polyPaths+      BuildCargo `elem` builds `shouldBe` True+      BuildCMake `elem` builds `shouldBe` True+      BuildMake `elem` builds `shouldBe` True+      BuildPoetry `elem` builds `shouldBe` True+      layout `shouldBe` LayoutPolyglot++    it "audits documentation gaps against DOC_STRATEGY.md requirements" $ do+      let sparsePaths = ["README.md", "LICENSE"]+          gaps = detectDocumentationGaps "." sparsePaths+          present = filter docPresent gaps+          missing = filter (not . docPresent) gaps++      length present `shouldBe` 2+      map docPath present `shouldMatchList` ["README.md", "LICENSE"]++      let missingCritical = filter (\d -> docSeverity d == SeverityCritical) missing+      missingCritical `shouldBe` []++      let missingRecommended = filter (\d -> docSeverity d == SeverityRecommended) missing+      any (\d -> docPath d == "PLAN.md") missingRecommended `shouldBe` True+      any (\d -> docPath d == "STATE.md") missingRecommended `shouldBe` True+      any (\d -> docPath d == "AGENTS.md") missingRecommended `shouldBe` True+      any (\d -> docPath d == "docs/INDEX.md") missingRecommended `shouldBe` True++    it "computes weighted ecosystem retrofit readiness score" $ do+      let noStats = []+          noBuilds = []+          noTests = TestInfrastructure False [] [] 0+          allDocsMissing = detectDocumentationGaps "." []+          noGit = GitGovernanceStatus False False Nothing False False False False False False Nothing+          noHarness = VerificationHarnessStatus False Nothing False False False False False False++          scoreLow = computeRetrofitScore noStats noBuilds noTests allDocsMissing noGit noHarness+      scoreLow `shouldSatisfy` (< 30.0)++      let sampleStats = [LanguageStats LangHaskell 10 100.0 [".hs"]]+          sampleBuilds = [BuildCabalProject, BuildCabalPackage]+          sampleTests = TestInfrastructure True [TestSuiteInfo "tests" "test" [FrameworkHspec]] ["test"] 5+          allDocsPresent = map (\d -> d { docPresent = True }) allDocsMissing+          sampleGit = GitGovernanceStatus True True (Just ".githooks") True True True True True True (Just ".gitmessage")+          sampleHarness = VerificationHarnessStatus True (Just "bin/verify") True True True True True True++          scoreHigh = computeRetrofitScore sampleStats sampleBuilds sampleTests allDocsPresent sampleGit sampleHarness+      scoreHigh `shouldSatisfy` (>= 90.0)++    it "generates prioritized remediation actions" $ do+      let noTests = TestInfrastructure False [] [] 0+          gaps = detectDocumentationGaps "." []+          noGit = GitGovernanceStatus False False Nothing False False False False False False Nothing+          noHarness = VerificationHarnessStatus False Nothing False False False False False False+          plan = computeRemediationPlan "victim" LayoutSinglePackage noTests gaps noGit noHarness++      any ("Initialize Git repository" `T.isInfixOf`) plan `shouldBe` True+      any ("bin/victim-verify" `T.isInfixOf`) plan `shouldBe` True+      any ("Scaffold mandatory document: README.md" `T.isInfixOf`) plan `shouldBe` True+      any ("Establish baseline test suite" `T.isInfixOf`) plan `shouldBe` True+      any ("Consider migrating from single-package layout" `T.isInfixOf`) plan `shouldBe` True++  describe "Gachi.Retrofit.Report (Pretty-Printing & Formats)" $ do+    it "renders aligned terminal ASCII dashboard with all sections" $ do+      let sampleReport = DiagnosticReport+            { diagTargetDir           = "/tmp/sample-repo"+            , diagProjectName         = "sample-repo"+            , diagPrimaryLanguage     = Just LangHaskell+            , diagLanguages           = [LanguageStats LangHaskell 10 100.0 [".hs"]]+            , diagBuildSystems        = [BuildCabalProject]+            , diagLayout              = LayoutMultiPackage+            , diagDiscoveredPackages  = ["pkg-core", "pkg-cli"]+            , diagTestInfra           = TestInfrastructure True [TestSuiteInfo "test" "test" [FrameworkHspec]] ["test"] 3+            , diagDocGaps             = detectDocumentationGaps "." ["README.md"]+            , diagDocCompliancePct    = 10.0+            , diagGitGovernance       = GitGovernanceStatus True True (Just ".githooks") True True True True True True (Just ".gitmessage")+            , diagVerificationHarness = VerificationHarnessStatus True (Just "bin/sample-verify") True True True True True True+            , diagRetrofitScore       = 75.5+            , diagRemediationPlan     = ["Scaffold mandatory document: LICENSE"]+            }+          output = formatDiagnosticReport sampleReport++      "Gachi Diagnostic Repository Inspector" `T.isInfixOf` output `shouldBe` True+      "sample-repo" `T.isInfixOf` output `shouldBe` True+      "75.5%" `T.isInfixOf` output `shouldBe` True+      "[1] Programming Language Breakdown" `T.isInfixOf` output `shouldBe` True+      "[2] Test Infrastructure" `T.isInfixOf` output `shouldBe` True+      "[3] Documentation Inventory" `T.isInfixOf` output `shouldBe` True+      "[4] Git Governance & Commit Attribution" `T.isInfixOf` output `shouldBe` True+      "[5] Physical Verification Harnesses & Quality Gates" `T.isInfixOf` output `shouldBe` True+      "[6] Prioritized Phase 4 Remediation Plan" `T.isInfixOf` output `shouldBe` True++    it "renders GitHub-Flavored Markdown report" $ do+      let sampleReport = DiagnosticReport+            { diagTargetDir           = "/tmp/sample-repo"+            , diagProjectName         = "sample-repo"+            , diagPrimaryLanguage     = Just LangHaskell+            , diagLanguages           = [LanguageStats LangHaskell 10 100.0 [".hs"]]+            , diagBuildSystems        = [BuildCabalPackage]+            , diagLayout              = LayoutSinglePackage+            , diagDiscoveredPackages  = ["sample-repo"]+            , diagTestInfra           = TestInfrastructure False [] [] 0+            , diagDocGaps             = detectDocumentationGaps "." []+            , diagDocCompliancePct    = 0.0+            , diagGitGovernance       = GitGovernanceStatus False False Nothing False False False False False False Nothing+            , diagVerificationHarness = VerificationHarnessStatus False Nothing False False False False False False+            , diagRetrofitScore       = 25.0+            , diagRemediationPlan     = ["Initialize Git repository"]+            }+          md = formatDiagnosticMarkdown sampleReport++      "# Diagnostic Repository Inspection Report: `sample-repo`" `T.isInfixOf` md `shouldBe` True+      "| Language | Files | Share | Extensions |" `T.isInfixOf` md `shouldBe` True+      "| Haskell | 10 | 100.0% |" `T.isInfixOf` md `shouldBe` True++    it "renders compact single-line summary string" $ do+      let sampleReport = DiagnosticReport+            { diagTargetDir           = "/tmp/sample"+            , diagProjectName         = "sample"+            , diagPrimaryLanguage     = Just LangHaskell+            , diagLanguages           = []+            , diagBuildSystems        = []+            , diagLayout              = LayoutSinglePackage+            , diagDiscoveredPackages  = []+            , diagTestInfra           = TestInfrastructure False [] [] 0+            , diagDocGaps             = []+            , diagDocCompliancePct    = 0.0+            , diagGitGovernance       = GitGovernanceStatus False False Nothing False False False False False False Nothing+            , diagVerificationHarness = VerificationHarnessStatus False Nothing False False False False False False+            , diagRetrofitScore       = 40.0+            , diagRemediationPlan     = []+            }+          line = formatDiagnosticSummaryLine sampleReport+      "gachi retrofit inspect [sample]: Haskell, Single-Package Root Layout, Score: 40.0%" `T.isInfixOf` line `shouldBe` True++  describe "Gachi.Retrofit (Filesystem Inspection)" $ do+    it "scans a mock legacy repository and identifies architectural attributes" $ do+      let tmpDir = "/tmp/gachi-test-legacy-victim"+      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())+      createDirectoryIfMissing True (tmpDir </> "src")+      createDirectoryIfMissing True (tmpDir </> "test")++      TIO.writeFile (tmpDir </> "victim.cabal") "name: victim\nversion: 0.1.0.0\n"+      TIO.writeFile (tmpDir </> "src" </> "Victim.hs") "module Victim where\nx = 42\n"+      TIO.writeFile (tmpDir </> "test" </> "Spec.hs") "import Test.Hspec\nmain = hspec (it \"works\" $ True `shouldBe` True)\n"+      TIO.writeFile (tmpDir </> "README.md") "# Victim Project\n"++      report <- scanRepository tmpDir++      diagProjectName report `shouldBe` "victim"+      diagPrimaryLanguage report `shouldBe` Just LangHaskell+      diagLayout report `shouldBe` LayoutSinglePackage+      hasTests (diagTestInfra report) `shouldBe` True+      diagDocCompliancePct report `shouldSatisfy` (> 0.0)+      hasGitRepo (diagGitGovernance report) `shouldBe` False+      hasVerifyScript (diagVerificationHarness report) `shouldBe` False++      -- Clean up+      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())++    it "executes self-diagnostic scan on the host workspace with high conformance" $ do+      cwd <- getCurrentDirectory+      repoRoot <- findRepoRoot cwd+      report <- scanRepository repoRoot++      diagProjectName report `shouldBe` "gachi"+      diagPrimaryLanguage report `shouldBe` Just LangHaskell+      diagLayout report `shouldBe` LayoutMultiPackage+      diagBuildSystems report `shouldContain` [BuildCabalProject]+      hasTests (diagTestInfra report) `shouldBe` True+      hasGitRepo (diagGitGovernance report) `shouldBe` True+      hasHooksDir (diagGitGovernance report) `shouldBe` True+      hasCommitMsgHook (diagGitGovernance report) `shouldBe` True+      hasCommitMsgAttributionCheck (diagGitGovernance report) `shouldBe` True+      hasVerifyScript (diagVerificationHarness report) `shouldBe` True+      diagRetrofitScore report `shouldSatisfy` (>= 90.0)++  describe "Gachi.Retrofit.Overlay (Non-Destructive Skeleton Overlay)" $ do+    it "synthesizes complete candidate overlay assets across all 6 categories" $ do+      let proj = defaultProject "sample" (Author "Test Author" "author@example.com")+          candidates = synthesizeOverlayCandidates proj LayoutSinglePackage+      length candidates `shouldSatisfy` (>= 25)+      let paths = map actionPath candidates+      all (`elem` paths) ["docs/INDEX.md", "docs/notes/DOC_STRATEGY.md", "PLAN.md", "STATE.md"] `shouldBe` True+      all (`elem` paths) ["AGENTS.md", "CLAUDE.md", ".agents/rules/commit-attribution.md"] `shouldBe` True+      all (`elem` paths) [".githooks/commit-msg", ".githooks/pre-commit", ".gitmessage"] `shouldBe` True+      all (`elem` paths) ["bin/sample-verify", "bin/sample-tagref", "bin/sample-linkcheck", "bin/sample-cloc"] `shouldBe` True+      all (`elem` paths) [".hlint.yaml", "fourmolu.yaml"] `shouldBe` True++    it "computes overlay plan preserving existing files and proposing missing assets" $ do+      let tmpDir = "/tmp/gachi-test-overlay-plan"+      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())+      createDirectoryIfMissing True (tmpDir </> "src")+      TIO.writeFile (tmpDir </> "README.md") "# Existing README\n"+      TIO.writeFile (tmpDir </> "src" </> "Main.hs") "module Main where\nmain = putStrLn \"hello\"\n"++      report <- scanRepository tmpDir+      plan <- computeOverlayPlan tmpDir report++      planCreatedCount plan `shouldSatisfy` (> 0)+      planPreservedCount plan `shouldSatisfy` (>= 1)++      let readmeAction = filter (\a -> actionPath a == "README.md") (planActions plan)+      case readmeAction of+        (act:_) -> actionType act `shouldBe` ActionPreserve+        []      -> expectationFailure "Expected README.md to be present in overlay actions"++      -- Verify Git governance proposal+      any (\case GitInitRepo -> True; _ -> False) (planGitActions plan) `shouldBe` True+      any (\case GitSetConfig k _ -> k == "core.hooksPath"; _ -> False) (planGitActions plan) `shouldBe` True++      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())++    it "executes non-destructive overlay, creates harnesses, configures git, and guarantees idempotency" $ do+      let tmpDir = "/tmp/gachi-test-overlay-exec"+      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())+      createDirectoryIfMissing True (tmpDir </> "src")+      let origDomainCode = "module Domain where\nsecret = 1337\n"+      TIO.writeFile (tmpDir </> "src" </> "Domain.hs") origDomainCode+      TIO.writeFile (tmpDir </> "README.md") "# Original Title\n"++      report <- scanRepository tmpDir+      plan <- computeOverlayPlan tmpDir report+      res <- executeOverlayPlan plan++      resultSuccess res `shouldBe` True+      all (`elem` resultCreatedFiles res) ["docs/INDEX.md", "AGENTS.md", ".githooks/commit-msg"] `shouldBe` True+      "README.md" `elem` resultPreservedFiles res `shouldBe` True++      -- Invariant 1: Domain source code must remain 100% byte-identical+      domainCodeAfter <- TIO.readFile (tmpDir </> "src" </> "Domain.hs")+      domainCodeAfter `shouldBe` origDomainCode++      -- Invariant 2: Existing README.md preserved intact+      readmeAfter <- TIO.readFile (tmpDir </> "README.md")+      readmeAfter `shouldBe` "# Original Title\n"++      -- Invariant 3: Verification harnesses created with executable permissions+      doesFileExist (tmpDir </> "bin" </> (T.unpack (diagProjectName report) ++ "-verify")) `shouldReturn` True+      doesFileExist (tmpDir </> ".githooks" </> "commit-msg") `shouldReturn` True++      -- Invariant 4: Git repository initialized and configured+      doesDirectoryExist (tmpDir </> ".git") `shouldReturn` True++      -- Invariant 5: Idempotency guarantee: Re-planning over retrofitted directory plans 0 creations for those assets+      report2 <- scanRepository tmpDir+      plan2 <- computeOverlayPlan tmpDir report2+      planCreatedCount plan2 `shouldBe` 0++      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())++    it "formats overlay plan and results into human-readable ASCII and Markdown dashboards" $ do+      let tmpDir = "/tmp/gachi-test-overlay-fmt"+      createDirectoryIfMissing True tmpDir+      report <- scanRepository tmpDir+      plan <- computeOverlayPlan tmpDir report++      let asciiPlan = formatOverlayPlan plan+      "Gachi Non-Destructive Skeleton Overlay Plan" `T.isInfixOf` asciiPlan `shouldBe` True+      "Proposed Actions:" `T.isInfixOf` asciiPlan `shouldBe` True++      let mdPlan = formatOverlayPlanMarkdown plan+      "# Non-Destructive Skeleton Overlay Plan:" `T.isInfixOf` mdPlan `shouldBe` True+      "| Status | File Path | Executable |" `T.isInfixOf` mdPlan `shouldBe` True++      res <- executeOverlayPlan plan+      let asciiRes = formatOverlayResult res+      "Gachi Non-Destructive Overlay Execution Results" `T.isInfixOf` asciiRes `shouldBe` True+      "Overlay successfully applied!" `T.isInfixOf` asciiRes `shouldBe` True++      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())++  describe "Gachi.Retrofit.QualityGate (Progressive Quality Gates)" $ do+    it "constructs pure gate specifications across all four progressive levels (L0-L3)" $ do+      length level0Gates `shouldBe` 8+      length level1Gates `shouldBe` 2+      length level2Gates `shouldBe` 1+      length level3Gates `shouldBe` 3+      length allLevelGates `shouldBe` 14+      all (\g -> gateLevel g == QualityLevel0) level0Gates `shouldBe` True+      all (\g -> gateLevel g == QualityLevel1) level1Gates `shouldBe` True+      all (\g -> gateLevel g == QualityLevel2) level2Gates `shouldBe` True+      all (\g -> gateLevel g == QualityLevel3) level3Gates `shouldBe` True++    it "evaluates monotonic compliance progression via computeAchievedLevel" $ do+      let mkRes gName lvl passed = GateResult (GateCheck gName lvl "desc" (\_ -> pure passed)) passed "detail"+          allPass = [ mkRes "g0" QualityLevel0 True+                    , mkRes "g1" QualityLevel1 True+                    , mkRes "g2" QualityLevel2 True+                    , mkRes "g3" QualityLevel3 True+                    ]+      computeAchievedLevel allPass `shouldBe` QualityLevel3++      let failL3 = [ mkRes "g0" QualityLevel0 True+                   , mkRes "g1" QualityLevel1 True+                   , mkRes "g2" QualityLevel2 True+                   , mkRes "g3" QualityLevel3 False+                   ]+      computeAchievedLevel failL3 `shouldBe` QualityLevel2++      let failL2 = [ mkRes "g0" QualityLevel0 True+                   , mkRes "g1" QualityLevel1 True+                   , mkRes "g2" QualityLevel2 False+                   , mkRes "g3" QualityLevel3 True+                   ]+      computeAchievedLevel failL2 `shouldBe` QualityLevel1++      let failL1 = [ mkRes "g0" QualityLevel0 True+                   , mkRes "g1" QualityLevel1 False+                   , mkRes "g2" QualityLevel2 True+                   , mkRes "g3" QualityLevel3 True+                   ]+      computeAchievedLevel failL1 `shouldBe` QualityLevel0++      let failL0 = [ mkRes "g0" QualityLevel0 False+                   , mkRes "g1" QualityLevel1 True+                   , mkRes "g2" QualityLevel2 True+                   , mkRes "g3" QualityLevel3 True+                   ]+      computeAchievedLevel failL0 `shouldBe` QualityLevel0++    it "generates prioritized remediation steps ordered by progressive levels" $ do+      let mkRes gName lvl passed = GateResult (GateCheck gName lvl "desc" (\_ -> pure passed)) passed "detail"+          mixed = [ mkRes "Git Hooks Directory" QualityLevel0 False+                  , mkRes "Tagref Cross-Reference" QualityLevel1 False+                  , mkRes "Cabal Build Clean" QualityLevel2 False+                  , mkRes "HLint Zero Hints" QualityLevel3 False+                  ]+          steps = computeRemediationSteps mixed+      length steps `shouldBe` 4+      map stepPriority steps `shouldBe` [PriorityFoundation, PriorityStructural, PriorityCompiler, PriorityAnalysis]+      any (\s -> "gachi retrofit" `T.isInfixOf` stepCommand s) steps `shouldBe` True+      any (\s -> "tagref" `T.isInfixOf` stepCommand s) steps `shouldBe` True+      any (\s -> "cabal build" `T.isInfixOf` stepCommand s) steps `shouldBe` True+      any (\s -> "hlint" `T.isInfixOf` stepCommand s) steps `shouldBe` True++    it "formats terminal ASCII dashboard and GitHub-Flavored Markdown reports" $ do+      let mkRes gName lvl passed = GateResult (GateCheck gName lvl "desc" (\_ -> pure passed)) passed "detail"+          results = [ mkRes "Git Repo" QualityLevel0 True+                    , mkRes "Hooks" QualityLevel0 False+                    , mkRes "Tagref" QualityLevel1 True+                    ]+          report = QualityGateReport+            { qgTargetDir        = "/tmp/sample-project"+            , qgProjectName      = "sample-project"+            , qgGateResults      = results+            , qgAchievedLevel    = QualityLevel0+            , qgMaxPossibleLevel = QualityLevel3+            , qgPassedCount      = 2+            , qgFailedCount      = 1+            , qgRemediationSteps = computeRemediationSteps results+            }+          asciiOut = formatQualityGateReport report+          mdOut    = formatQualityGateMarkdown report++      "Gachi Progressive Quality Gate Report" `T.isInfixOf` asciiOut `shouldBe` True+      "Target Directory:    /tmp/sample-project" `T.isInfixOf` asciiOut `shouldBe` True+      "[Level 0: Foundation]" `T.isInfixOf` asciiOut `shouldBe` True+      "Remediation Plan" `T.isInfixOf` asciiOut `shouldBe` True++      "# Progressive Quality Gate Report: `sample-project`" `T.isInfixOf` mdOut `shouldBe` True+      "| Status | Gate | Description |" `T.isInfixOf` mdOut `shouldBe` True+      "## Level 0: Foundation" `T.isInfixOf` mdOut `shouldBe` True++    it "evaluates quality gates against a live retrofitted workspace" $ do+      let tmpDir = "/tmp/gachi-test-quality-gate-live"+      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())+      createDirectoryIfMissing True (tmpDir </> "bin")+      createDirectoryIfMissing True (tmpDir </> ".githooks")+      createDirectoryIfMissing True (tmpDir </> ".git")+      createDirectoryIfMissing True (tmpDir </> "src")++      TIO.writeFile (tmpDir </> "README.md") "# Test Project\n"+      TIO.writeFile (tmpDir </> "PLAN.md") "# Strategic Plan\n"+      TIO.writeFile (tmpDir </> "tagref.yml") "archive: false\n"+      TIO.writeFile (tmpDir </> ".hlint.yaml") "- ignore: {}\n"+      TIO.writeFile (tmpDir </> ".gitmessage") "Subject\n"+      TIO.writeFile (tmpDir </> ".githooks" </> "commit-msg") "#!/bin/sh\nexit 0\n"+      TIO.writeFile (tmpDir </> ".githooks" </> "pre-commit") "#!/bin/sh\nexit 0\n"+      TIO.writeFile (tmpDir </> "bin" </> "test-verify") "#!/bin/sh\nexit 0\n"+      TIO.writeFile (tmpDir </> "src" </> "Main.hs") "module Main where\nmain = pure ()\n"++      -- Make scripts executable+      perms <- getPermissions (tmpDir </> "bin" </> "test-verify")+      setPermissions (tmpDir </> "bin" </> "test-verify") (setOwnerExecutable True perms)++      report <- evaluateQualityGates tmpDir+      qgProjectName report `shouldSatisfy` (not . T.null)+      qgPassedCount report `shouldSatisfy` (>= 8)+      length (qgGateResults report) `shouldBe` 14++      removeDirectoryRecursive tmpDir `catch` (\(_ :: SomeException) -> pure ())++findRepoRoot :: FilePath -> IO FilePath+findRepoRoot dir = do+  hasProj <- doesFileExist (dir </> "cabal.project")+  if hasProj+    then pure dir+    else do+      let parent = takeDirectory dir+      if parent == dir+        then pure dir+        else findRepoRoot parent+