gachi-retrofit-0.1.0.0: src/Gachi/Retrofit/Report.hs
{-# 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) " "