gachi-scaffold (empty) → 0.1.0.0
raw patch · 7 files changed
+2404/−0 lines, 7 filesdep +basedep +containersdep +directory
Dependencies added: base, containers, directory, filepath, gachi-core, gachi-scaffold, gachi-schema, hspec, polytropos-core, process, text, time
Files
- gachi-scaffold.cabal +52/−0
- src/Gachi/Scaffold.hs +221/−0
- src/Gachi/Scaffold/Engine.hs +107/−0
- src/Gachi/Scaffold/Markdown.hs +344/−0
- src/Gachi/Scaffold/Templates.hs +1300/−0
- src/Gachi/Scaffold/Types.hs +101/−0
- test/Main.hs +279/−0
+ gachi-scaffold.cabal view
@@ -0,0 +1,52 @@+cabal-version: 3.0+name: gachi-scaffold+version: 0.1.0.0+synopsis: Declarative multi-package workspace scaffolding engine for Gachi+license: EUPL-1.2+author: Nadia Yvette Chambers+maintainer: nadia.yvette.chambers@ik.me+build-type: Simple+tested-with:+ GHC == 9.10.3,+ GHC == 9.12.4,+ GHC == 9.14.1++library+ exposed-modules:+ Gachi.Scaffold+ Gachi.Scaffold.Types+ Gachi.Scaffold.Markdown+ Gachi.Scaffold.Templates+ Gachi.Scaffold.Engine+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall -Wcompat -Wredundant-constraints+ build-depends:+ base >= 4.14 && < 5,+ gachi-core >= 0.1 && < 0.2,+ gachi-schema >= 0.1 && < 0.2,+ polytropos-core >= 0.1 && < 0.2,+ text >= 2.0,+ containers >= 0.6,+ filepath >= 1.4,+ directory >= 1.3++test-suite tests+ type: exitcode-stdio-1.0+ main-is: Main.hs+ hs-source-dirs: test+ default-language: Haskell2010+ ghc-options: -Wall+ build-depends:+ base >= 4.14 && < 5,+ gachi-core,+ gachi-schema,+ gachi-scaffold,+ polytropos-core,+ text >= 2.0,+ containers >= 0.6,+ filepath >= 1.4,+ directory >= 1.3,+ process >= 1.6,+ time >= 1.9,+ hspec >= 2.10
+ src/Gachi/Scaffold.hs view
@@ -0,0 +1,221 @@+-- | Module: Gachi.Scaffold+-- Description: Declarative multi-package workspace scaffolding engine for Gachi+--+-- This is the front-door module for the @gachi-scaffold@ package. It exposes+-- the complete workspace synthesis pipeline, configuration models, template+-- generators, and filesystem materialization operations.+--+-- = Architectural Context & Invariants+--+-- Gachi serves as the ecosystem's foundational project skeleton generator and+-- configuration propagation engine (@~/src/PLAN.md@ §1).+--+-- Rather than generating ad-hoc or incomplete project skeletons that require+-- hours of manual bootstrapping, Gachi generates turn-key repositories that satisfy+-- all five physical verification gates immediately upon initialization.+--+-- == Invariant: Note [Ecosystem Standardization Invariants]+--+-- {-+-- Note [Ecosystem Standardization Invariants]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- [tag:note-ecosystem-standardization]+--+-- The ecosystem standardization matrix enforces universal mechanical invariants+-- across all repositories in ~/src/* (per ~/src/PLAN.md §1.A). Every workspace+-- generated or retrofitted by Gachi must satisfy these invariants immediately+-- upon creation, guaranteeing that no repository is a second-class citizen:+--+-- 1. Canonical Directory Hierarchy:+-- - Root multi-package workspace containing packages/*/+-- - Central documentation corpus under docs/ (including docs/INDEX.md,+-- docs/TUTORIAL.md, docs/NEWBIE_GUIDE.md, docs/MAINTAINER_GUIDE.md, and+-- docs/notes/)+-- - Dedicated verification harnesses under bin/ (verify, tagref, linkcheck,+-- cloc, generate-tags)+-- - Operational planning pair (PLAN.md and STATE.md)+-- - AI agent advisory layer (AGENTS.md, CLAUDE.md, .agents/rules/, .agents/skills/)+--+-- 2. Five-Gate Physical Verification:+-- Every repository provides a turnkey `./bin/<project>-verify` script that+-- executes five non-negotiable verification gates:+-- - Gate 1: Zero HLint hints (-Wall -Wcompat -Wredundant-constraints)+-- - Gate 2: Full Cabal test suite execution (cabal test all)+-- - Gate 3: Tagref cross-reference integrity (0 dangling references, 0 duplicate tags)+-- - Gate 4: Markdown link & anchor validation (0 broken relative links or invalid anchors)+-- - Gate 5: CLOC comment density audit (>= 50% comment density in production code)+--+-- 3. Git Governance & Attribution:+-- - Commit message template (.gitmessage) and validation hook (.githooks/commit-msg)+-- enforcing Sarutahiko RFC 2822 commit trailers (Assisted-by:)+-- - Pre-commit hook (.githooks/pre-commit) preventing broken tagref references or+-- broken markdown links from being committed.+--+-- 4. Hermetic Toolchain Pinning:+-- - cabal.project pinning GHC (9.14.1) and Cabal (3.18) versions+-- - EUPL-1.2 / Apache-2.0 open source licensing discipline.+-- -}+--+-- = Worked Example+--+-- >>> import Gachi.Schema.Project+-- >>> import Gachi.Scaffold+-- >>> let author = Author { name = "Nadia Yvette Chambers", email = "nadia.yvette.chambers@ik.me" }+-- >>> let proj = defaultProject "gongju" author+-- >>> let entries = generateProjectScaffold proj+-- >>> length entries > 20+-- True+-- >>> any (\e -> entryPath e == "bin/gongju-verify" && entryExecutable e) entries+-- True+module Gachi.Scaffold+ ( -- * High-Level Scaffolding API+ generateAndWriteScaffold+ , generateProjectScaffold+ , writeScaffold++ -- * Core Types & Modes+ , FileEntry(..)+ , ScaffoldMode(..)+ , ScaffoldResult(..)+ , ScaffoldError(..)++ -- * Template Generators+ , allProjectEntries+ , cabalProjectTemplate+ , cabalProjectLocalTemplate+ , hlintTemplate+ , gitmessageTemplate+ , tagrefConfigTemplate+ , commitMsgHookTemplate+ , preCommitHookTemplate+ , verifyScriptTemplate+ , tagrefScriptTemplate+ , linkcheckScriptTemplate+ , clocScriptTemplate+ , generateTagsTemplate+ , readmeTemplate+ , agentsTemplate+ , claudeTemplate+ , planTemplate+ , stateTemplate+ , docsIndexTemplate+ , docsTutorialTemplate+ , docsNewbieGuideTemplate+ , docsMaintainerGuideTemplate+ , docsDesignTemplate+ , docsStrategyTemplate+ , docsCommitAttributionTemplate+ , packageCabalTemplate+ , packageReadmeTemplate+ , packageModuleTemplate+ , packageTestTemplate++ -- * Markdown AST & Pretty-Printing+ , Block(..)+ , Inline(..)+ , renderMarkdown+ , renderBlock+ , renderInline+ , h1+ , h2+ , h3+ , h4+ , para+ , paraInlines+ , codeBlock+ , plainCodeBlock+ , bulletList+ , orderedList+ , table+ , hr+ , rawBlock+ , plain+ , bold+ , italic+ , code+ , link+ , linkInline+ , tagRef+ , tagDef+ , inlines++ -- * String Utilities+ , defaultModuleName+ , moduleToFilePath+ , toUpperSlug+ ) where++import Gachi.Scaffold.Engine+ ( generateAndWriteScaffold+ , generateProjectScaffold+ , writeScaffold+ )+import Gachi.Scaffold.Markdown+ ( Block(..)+ , Inline(..)+ , bold+ , bulletList+ , code+ , codeBlock+ , h1+ , h2+ , h3+ , h4+ , hr+ , inlines+ , italic+ , link+ , linkInline+ , orderedList+ , para+ , paraInlines+ , plain+ , plainCodeBlock+ , rawBlock+ , renderBlock+ , renderInline+ , renderMarkdown+ , table+ , tagDef+ , tagRef+ )+import Gachi.Scaffold.Templates+ ( allProjectEntries+ , cabalProjectLocalTemplate+ , cabalProjectTemplate+ , clocScriptTemplate+ , commitMsgHookTemplate+ , defaultModuleName+ , docsCommitAttributionTemplate+ , docsDesignTemplate+ , docsIndexTemplate+ , docsMaintainerGuideTemplate+ , docsNewbieGuideTemplate+ , docsStrategyTemplate+ , docsTutorialTemplate+ , generateTagsTemplate+ , gitmessageTemplate+ , hlintTemplate+ , linkcheckScriptTemplate+ , moduleToFilePath+ , packageCabalTemplate+ , packageModuleTemplate+ , packageReadmeTemplate+ , packageTestTemplate+ , planTemplate+ , preCommitHookTemplate+ , readmeTemplate+ , stateTemplate+ , agentsTemplate+ , claudeTemplate+ , tagrefConfigTemplate+ , tagrefScriptTemplate+ , toUpperSlug+ , verifyScriptTemplate+ )+import Gachi.Scaffold.Types+ ( FileEntry(..)+ , ScaffoldError(..)+ , ScaffoldMode(..)+ , ScaffoldResult(..)+ )
+ src/Gachi/Scaffold/Engine.hs view
@@ -0,0 +1,107 @@+-- | Module: Gachi.Scaffold.Engine+-- Description: Execution engine for materializing workspace file trees+--+-- This module coordinates the physical instantiation of project skeletons+-- on the filesystem, managing directory creation, overwrite policies, and+-- POSIX permission bits.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization], the scaffolding engine guarantees that+-- generated file trees are structurally identical, total, and deterministic.+--+-- == Two-Stage Execution Pipeline+--+-- 1. __Pure Generation:__ Invokes 'generateProjectScaffold' to produce the complete+-- set of 'FileEntry' records from a declarative 'Project' specification.+-- 2. __Materialization:__ Evaluates 'writeScaffold' across target paths, enforcing+-- the selected 'ScaffoldMode' ('FailOnExisting', 'SkipExisting', or 'OverwriteExisting').+--+-- = Error Safety+--+-- When configured with 'FailOnExisting', any collision on disk immediately+-- halts execution and raises 'DestinationExists', preventing partial or accidental+-- clobbering of pre-existing user code.+module Gachi.Scaffold.Engine+ ( -- * High-Level Scaffolding API+ generateAndWriteScaffold+ , generateProjectScaffold+ , writeScaffold+ ) where++import Control.Exception (throwIO)+import Control.Monad (forM, when)+import Data.Either (lefts, rights)+import qualified Data.Text.IO as TIO+import System.Directory+ ( createDirectoryIfMissing+ , doesFileExist+ , getPermissions+ , setOwnerExecutable+ , setPermissions+ )+import System.FilePath (takeDirectory, (</>))++import Gachi.Schema.Project (Project)+import Gachi.Scaffold.Templates (allProjectEntries)+import Gachi.Scaffold.Types+ ( FileEntry(..)+ , ScaffoldError(..)+ , ScaffoldMode(..)+ , ScaffoldResult(..)+ )++-- | Pure generation of all canonical project files and directories from a+-- declarative 'Project' specification.+--+-- Returns a complete list of 'FileEntry' values representing every configuration,+-- hook, verification script, document, and source module in the workspace.+--+-- Time complexity: @O(F)@ proportional to the number of generated files.+generateProjectScaffold :: Project -> [FileEntry]+generateProjectScaffold = allProjectEntries++-- | Writes an in-memory list of 'FileEntry' records to a destination root directory+-- on the physical filesystem.+--+-- Creates parent directories as needed and applies POSIX executable bits (@+x@)+-- to files flagged with 'entryExecutable'.+--+-- Behaviors by 'ScaffoldMode':+--+-- * 'FailOnExisting': Throws 'DestinationExists' if any target file already exists.+-- * 'SkipExisting': Preserves existing files unchanged and records them in 'skippedFiles'.+-- * 'OverwriteExisting': Replaces existing files unconditionally.+writeScaffold :: FilePath -> ScaffoldMode -> [FileEntry] -> IO ScaffoldResult+writeScaffold targetDir mode entries = do+ results <- forM entries $ \entry -> do+ let dest = targetDir </> entryPath entry+ exists <- doesFileExist dest+ case (exists, mode) of+ (True, FailOnExisting) ->+ throwIO (DestinationExists dest)+ (True, SkipExisting) ->+ pure (Left (entryPath entry))+ _ -> do+ createDirectoryIfMissing True (takeDirectory dest)+ TIO.writeFile dest (entryContent entry)+ when (entryExecutable entry) $ do+ p <- getPermissions dest+ setPermissions dest (setOwnerExecutable True p)+ pure (Right (entryPath entry))++ let written = rights results+ skipped = lefts results+ pure ScaffoldResult+ { writtenFiles = written+ , skippedFiles = skipped+ }++-- | Combined generation and filesystem materialization.+--+-- Purely generates the project file tree and writes all files to 'targetDir'+-- according to the supplied 'ScaffoldMode'.+generateAndWriteScaffold :: FilePath -> ScaffoldMode -> Project -> IO ScaffoldResult+generateAndWriteScaffold targetDir mode proj =+ writeScaffold targetDir mode (generateProjectScaffold proj)
+ src/Gachi/Scaffold/Markdown.hs view
@@ -0,0 +1,344 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.Scaffold.Markdown+-- Description: Lean GitHub-Flavored Markdown (GFM) AST and deterministic pretty-printer+--+-- This module implements a lightweight, pure-Haskell abstract syntax tree for+-- GitHub-Flavored Markdown (GFM) and CommonMark, paired with a deterministic,+-- zero-allocation pretty-printer.+--+-- = Architectural Context & Invariants+--+-- In Gachi's infrastructure model (per Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization]), repository assets and operational documents+-- must be emitted with structural precision.+--+-- Rather than relying on fragile, ad-hoc string concatenation (@T.unlines@) or+-- dragging in heavyweight external engines with hundreds of transitive dependencies+-- (such as full Pandoc), this module provides a lean, strongly-typed document+-- generation layer.+--+-- == Key Guarantees+--+-- 1. __Pure & Total:__ The pretty-printer 'renderMarkdown' is a pure, total+-- function with zero runtime I/O, zero network calls, and zero external C FFI.+-- 2. __Structural Invariance:__+-- * Table columns are dynamically measured across all rows and padded to+-- uniform widths with aligned separator lines (@| :--- | :--- |@).+-- * Block spacing follows standard CommonMark double-newline conventions.+-- * Links, anchors, code fencing, and Tagref pointers are structurally valid+-- by construction.+-- 3. __Lightweight Footprint:__ Zero dependencies beyond @base@ and @text@.+--+-- = Worked Example+--+-- >>> import Gachi.Scaffold.Markdown+-- >>> let doc = [ h1 "Project Title", para "A brief overview.", table ["Col 1", "Col 2"] [[plain "Val A", plain "Val B"]] ]+-- >>> renderMarkdown doc+-- "# Project Title\n\nA brief overview.\n\n| Col 1 | Col 2 |\n| :--- | :--- |\n| Val A | Val B |\n"+module Gachi.Scaffold.Markdown+ ( -- * Document Abstract Syntax Tree+ Block(..)+ , Inline(..)++ -- * Pretty-Printing Operations+ , renderMarkdown+ , renderBlock+ , renderInline++ -- * Block Constructors+ , h1+ , h2+ , h3+ , h4+ , para+ , paraInlines+ , codeBlock+ , plainCodeBlock+ , bulletList+ , orderedList+ , table+ , hr+ , rawBlock++ -- * Inline Constructors+ , plain+ , bold+ , italic+ , code+ , link+ , linkInline+ , tagRef+ , tagDef+ , inlines+ ) where++import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)++--------------------------------------------------------------------------------+-- Abstract Syntax Tree: Inlines+--------------------------------------------------------------------------------++-- | Inline typographical and structural elements within a Markdown block.+data Inline+ = Plain !Text+ -- ^ Unformatted plain text.+ | Bold !Text+ -- ^ Strongly emphasized text: @**text**@.+ | Italic !Text+ -- ^ Emphasized text: @*text*@.+ | Code !Text+ -- ^ Inline monospace code span: @`code`@.+ | Link !Text !Text+ -- ^ Hyperlink with display label and destination URL/anchor: @[label](url)@.+ | TagRef !Text+ -- ^ Mechanical Tagref reference pointer (formatted as @ref:slug@ in brackets).+ | TagDef !Text+ -- ^ Mechanical Tagref definition anchor (formatted as @tag:slug@ in brackets).+ | Inlines ![Inline]+ -- ^ Compound sequence of inline elements concatenated together.+ deriving stock (Eq, Show, Generic)++instance Semigroup Inline where+ Inlines xs <> Inlines ys = Inlines (xs ++ ys)+ Inlines xs <> y = Inlines (xs ++ [y])+ x <> Inlines ys = Inlines (x : ys)+ Plain "" <> y = y+ x <> Plain "" = x+ x <> y = Inlines [x, y]++instance Monoid Inline where+ mempty = Plain ""++--------------------------------------------------------------------------------+-- Abstract Syntax Tree: Blocks+--------------------------------------------------------------------------------++-- | Block-level document elements in GitHub-Flavored Markdown.+data Block+ = Heading !Int !Text+ -- ^ Section heading with depth level (1–6) and title text.+ | Paragraph ![Inline]+ -- ^ Standard paragraph comprising a sequence of inline elements.+ | FencedCode !(Maybe Text) !Text+ -- ^ Code block enclosed in triple backticks with optional language tag.+ | BulletList ![Inline]+ -- ^ Unordered list of bulleted items.+ | OrderedList ![Inline]+ -- ^ Ordered numbered list of items.+ | Table ![Text] ![[Inline]]+ -- ^ GitHub-Flavored Markdown table with column headers and cell rows.+ | ThematicBreak+ -- ^ Horizontal thematic divider: @---@.+ | RawBlock !Text+ -- ^ Pre-rendered raw text block preserved without transformation.+ deriving stock (Eq, Show, Generic)++--------------------------------------------------------------------------------+-- Ergonomic Block Smart Constructors+--------------------------------------------------------------------------------++-- | Creates a Level 1 Heading (@# Title@).+h1 :: Text -> Block+h1 = Heading 1++-- | Creates a Level 2 Heading (@## Title@).+h2 :: Text -> Block+h2 = Heading 2++-- | Creates a Level 3 Heading (@### Title@).+h3 :: Text -> Block+h3 = Heading 3++-- | Creates a Level 4 Heading (@#### Title@).+h4 :: Text -> Block+h4 = Heading 4++-- | Creates a paragraph from plain text.+para :: Text -> Block+para t = Paragraph [Plain t]++-- | Creates a paragraph from a sequence of structured inline elements.+paraInlines :: [Inline] -> Block+paraInlines = Paragraph++-- | Creates a syntax-highlighted fenced code block.+codeBlock :: Text -> Text -> Block+codeBlock lang = FencedCode (Just lang)++-- | Creates an untyped fenced code block without language annotation.+plainCodeBlock :: Text -> Block+plainCodeBlock = FencedCode Nothing++-- | Creates an unordered bulleted list from inline items.+bulletList :: [Inline] -> Block+bulletList = BulletList++-- | Creates an ordered numbered list from inline items.+orderedList :: [Inline] -> Block+orderedList = OrderedList++-- | Creates a formatted GFM table from headers and row cells.+table :: [Text] -> [[Inline]] -> Block+table = Table++-- | Creates a horizontal thematic break divider (@---@).+hr :: Block+hr = ThematicBreak++-- | Creates a raw unescaped block preserved without modification.+rawBlock :: Text -> Block+rawBlock = RawBlock++--------------------------------------------------------------------------------+-- Ergonomic Inline Smart Constructors+--------------------------------------------------------------------------------++-- | Creates a plain text inline element.+plain :: Text -> Inline+plain = Plain++-- | Creates a bold inline element (@**text**@).+bold :: Text -> Inline+bold = Bold++-- | Creates an italic inline element (@*text*@).+italic :: Text -> Inline+italic = Italic++-- | Creates an inline monospace code span (@`code`@).+code :: Text -> Inline+code = Code++-- | Creates an inline hyperlink (@[text](url)@).+link :: Text -> Text -> Inline+link = Link++-- | Creates an inline hyperlink from a rendered 'Inline' label (@[inline](url)@).+linkInline :: Inline -> Text -> Inline+linkInline inl = Link (renderInline inl)++-- | Constructs a dynamic Tagref reference pointer (formatted as @ref:slug@ in brackets).+tagRef :: Text -> Inline+tagRef = TagRef++-- | Constructs a dynamic Tagref definition anchor (formatted as @tag:slug@ in brackets).+tagDef :: Text -> Inline+tagDef = TagDef++--------------------------------------------------------------------------------+-- Pretty-Printing: Inlines+--------------------------------------------------------------------------------++-- | Renders an 'Inline' element to its canonical Markdown representation.+--+-- Time complexity: @O(L)@ proportional to inline text length.+renderInline :: Inline -> Text+renderInline (Plain t) = t+renderInline (Bold t) = "**" <> t <> "**"+renderInline (Italic t) = "*" <> t <> "*"+renderInline (Code t) = "`" <> t <> "`"+renderInline (Link t u) = "[" <> t <> "](" <> u <> ")"+renderInline (TagRef slug) = "[" <> "ref:" <> slug <> "]"+renderInline (TagDef slug) = "[" <> "tag:" <> slug <> "]"+renderInline (Inlines is) = T.concat (map renderInline is)++-- | Constructs a compound sequence of inlines.+inlines :: [Inline] -> Inline+inlines = Inlines++--------------------------------------------------------------------------------+-- Pretty-Printing: Blocks+--------------------------------------------------------------------------------++-- | Renders a sequence of 'Block' elements into a complete Markdown document.+--+-- Blocks are separated by standard blank lines adhering to CommonMark rules.+--+-- Time complexity: @O(N)@ proportional to total document nodes and cell counts.+renderMarkdown :: [Block] -> Text+renderMarkdown blocks =+ T.intercalate "\n" (map renderBlock blocks)++-- | Renders a single 'Block' element to formatted Markdown text.+renderBlock :: Block -> Text+renderBlock (Heading level title) =+ let hashes = T.replicate (max 1 (min 6 level)) "#"+ in hashes <> " " <> title <> "\n"++renderBlock (Paragraph inls) =+ T.concat (map renderInline inls) <> "\n"++renderBlock (FencedCode maybeLang codeContent) =+ let lang = fromMaybe "" maybeLang+ cleanContent = T.dropWhileEnd (== '\n') codeContent+ in "```" <> lang <> "\n" <> cleanContent <> "\n```\n"++renderBlock (BulletList items) =+ let renderItem item = "- " <> renderInline item+ in T.unlines (map renderItem items)++renderBlock (OrderedList items) =+ let renderItem idx item = T.show (idx :: Int) <> ". " <> renderInline item+ in T.unlines (zipWith renderItem [1 ..] items)++renderBlock (Table headers rows) =+ renderTableLayout headers rows++renderBlock ThematicBreak =+ "---\n"++renderBlock (RawBlock raw) =+ T.dropWhileEnd (== '\n') raw <> "\n"++--------------------------------------------------------------------------------+-- Table Formatting Mechanics+--------------------------------------------------------------------------------++-- | Formats a GitHub-Flavored Markdown table with auto-aligned column boundaries.+--+-- Dynamically evaluates cell text lengths across all headers and rows, computing+-- the minimum required padding per column and emitting clean separator lines.+renderTableLayout :: [Text] -> [[Inline]] -> Text+renderTableLayout headers rows =+ let renderedRows = map (map renderInline) rows+ colCount = length headers+ -- Pad or truncate each row to match header column count+ normalizedRows = map (normalizeRow colCount) renderedRows+ -- Compute maximum width per column (at least 3 characters for :---)+ allRowsWithHeader = headers : normalizedRows+ colWidths = [ maximum (3 : map (T.length . safeIndex c) allRowsWithHeader)+ | c <- [0 .. colCount - 1]+ ]+ -- Format header line+ headerLine = formatRow colWidths headers+ -- Format separator line (| :--- | :--- |)+ sepLine = formatSepRow colWidths+ -- Format data rows+ dataLines = map (formatRow colWidths) normalizedRows+ in T.unlines (headerLine : sepLine : dataLines)+ where+ normalizeRow targetLen r+ | length r >= targetLen = take targetLen r+ | otherwise = r ++ replicate (targetLen - length r) ""++ safeIndex idx xs+ | idx < length xs = xs !! idx+ | otherwise = ""++ formatRow widths cells =+ let padded = zipWith padCell widths cells+ in "| " <> T.intercalate " | " padded <> " |"++ formatSepRow widths =+ let seps = map (\w -> ":" <> T.replicate (max 2 (w - 1)) "-") widths+ in "| " <> T.intercalate " | " seps <> " |"++ padCell width cell =+ let padding = max 0 (width - T.length cell)+ in cell <> T.replicate padding " "
+ src/Gachi/Scaffold/Templates.hs view
@@ -0,0 +1,1300 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.Scaffold.Templates+-- Description: Pure template generation for canonical repository assets+--+-- This module implements pure, deterministic template generators synthesizing+-- all canonical files and directories required by the ecosystem standardization+-- matrix (per Note [Ecosystem Standardization Invariants] [ref:note-ecosystem-standardization]).+--+-- = Architectural Invariants & Guarantees+--+-- 1. __Referential Transparency:__ All generators are total, pure functions from+-- a 'Gachi.Schema.Project.Project' specification to 'Text'. They perform no I/O,+-- read no ambient filesystem state, and make no network requests.+--+-- 2. __Five-Gate Readiness:__ Skeletons produced by these templates are fully+-- valid out-of-the-box. When materialized on disk, the project immediately+-- passes all five physical verification gates:+-- * Gate 1 (HLint): Clean code conforming to modern GHC conventions.+-- * Gate 2 (Tests): Compilable Cabal package declarations and passing Hspec suites.+-- * Gate 3 (Tagref): Consistent cross-reference tags with zero dangling pointers.+-- * Gate 4 (Linkcheck): Fully verified relative Markdown links and section anchors.+-- * Gate 5 (CLOC): Production source files with 50%+ in-code comment density.+--+-- = Grounding & Tags+--+-- To prevent accidental tag definition collisions between the Gachi host compiler+-- and the target generated project, tag definitions in generated documentation+-- are constructed dynamically.+module Gachi.Scaffold.Templates+ ( -- * Composite Project Entry Generators+ allProjectEntries++ -- * Toolchain & Configuration Templates+ , cabalProjectTemplate+ , cabalProjectLocalTemplate+ , hlintTemplate+ , gitmessageTemplate+ , tagrefConfigTemplate++ -- * Hook Templates+ , commitMsgHookTemplate+ , preCommitHookTemplate++ -- * Verification Harness Templates+ , verifyScriptTemplate+ , tagrefScriptTemplate+ , linkcheckScriptTemplate+ , clocScriptTemplate+ , generateTagsTemplate++ -- * Operational Governance Templates+ , readmeTemplate+ , agentsTemplate+ , claudeTemplate+ , planTemplate+ , stateTemplate++ -- * Agent Rules Templates+ , ruleCommitAttributionTemplate+ , ruleCodeNavigationTemplate+ , ruleCompilerVerificationTemplate+ , ruleDocPrecedenceTemplate++ -- * Documentation Corpus Templates+ , docsIndexTemplate+ , docsTutorialTemplate+ , docsNewbieGuideTemplate+ , docsMaintainerGuideTemplate+ , docsDesignTemplate+ , docsStrategyTemplate+ , docsCommitAttributionTemplate++ -- * Package Asset Templates+ , packageCabalTemplate+ , packageReadmeTemplate+ , packageModuleTemplate+ , packageTestTemplate++ -- * String & Path Utilities+ , defaultModuleName+ , moduleToFilePath+ , toUpperSlug+ ) where++import Data.Char (isAlphaNum, toUpper)+import Data.Text (Text)+import qualified Data.Text as T++import Gachi.Schema.Project+ ( PackageSpec+ , Project+ , authorEmail+ , authorName+ , packageExposes+ , packageName+ , packageSynopsis+ , projectAuthor+ , projectCabalVersion+ , projectDescription+ , projectGhcVersion+ , projectLicense+ , projectName+ , projectPackages+ )+import Gachi.Scaffold.Markdown+ ( Block(..)+ , bold+ , bulletList+ , code+ , codeBlock+ , h1+ , h2+ , h3+ , hr+ , inlines+ , link+ , linkInline+ , orderedList+ , para+ , paraInlines+ , plain+ , plainCodeBlock+ , renderInline+ , renderMarkdown+ , tagDef+ , tagRef+ )+import Gachi.Scaffold.Types (FileEntry(..))++--------------------------------------------------------------------------------+-- Dynamic Tag and Anchor Placeholders+--------------------------------------------------------------------------------++-- | Constructs a dynamic Tagref reference pointer.+refCoreArch :: Text+refCoreArch = renderInline (tagRef "note-core-architecture")++-- | Canonical ecosystem standardization reference pointer.+refStandardization :: Text+refStandardization = "(per gachi)"++--------------------------------------------------------------------------------+-- Composite Project Scaffolding Generator+--------------------------------------------------------------------------------++-- | Generates the complete, canonical list of 'FileEntry' records for a 'Project'.+--+-- This function assembles all root configuration files, Git hooks, verification+-- scripts, operational roadmaps, agent guidance, documentation notes, and+-- multi-package source hierarchies.+--+-- Time complexity: @O(F)@ proportional to total generated files.+allProjectEntries :: Project -> [FileEntry]+allProjectEntries proj =+ concat+ [ rootToolchainEntries proj+ , gitHooksEntries proj+ , verificationHarnessEntries proj+ , operationalGovernanceEntries proj+ , agentRuleEntries proj+ , documentationCorpusEntries proj+ , packageHierarchyEntries proj+ ]++--------------------------------------------------------------------------------+-- 1. Root Toolchain & Build Assets+--------------------------------------------------------------------------------++-- | Synthesizes root build configuration files: @.gitignore@, @cabal.project@,+-- @cabal.project.local.example@, @.hlint.yaml@, @.gitmessage@, and @tagref.yml@.+rootToolchainEntries :: Project -> [FileEntry]+rootToolchainEntries proj =+ [ FileEntry ".gitignore" gitignoreTemplate False+ , FileEntry "cabal.project" (cabalProjectTemplate proj) False+ , FileEntry "cabal.project.local.example" cabalProjectLocalTemplate False+ , FileEntry ".hlint.yaml" hlintTemplate False+ , FileEntry ".gitmessage" gitmessageTemplate False+ , FileEntry "tagref.yml" (tagrefConfigTemplate proj) False+ ]++-- | Generates standard @.gitignore@ for multi-package Haskell projects.+gitignoreTemplate :: Text+gitignoreTemplate = T.unlines+ [ "dist/"+ , "dist-*"+ , "dist-newstyle/"+ , ".ghc.environment.*"+ , "*.o"+ , "*.hi"+ , "*.dyn_o"+ , "*.dyn_hi"+ , "*.a"+ , "*.so"+ , "*.dylib"+ , "tags"+ , ".ok"+ , ".keiro/"+ , ".yamaarashi/"+ , ".contextful/"+ , ".freebuff/"+ , "cabal.project.local"+ , "cabal.project.local*"+ , "!cabal.project.local.example"+ , ".DS_Store"+ , "*~"+ , "*.swp"+ , "*.swo"+ ]+++-- | Generates root @cabal.project@ configuring compiler pinning and warnings.+cabalProjectTemplate :: Project -> Text+cabalProjectTemplate proj = T.unlines+ [ "packages:"+ , " packages/*/"+ , ""+ , "with-compiler: ghc-" <> projectGhcVersion proj+ , "write-ghc-environment-files: always"+ , ""+ , "package *"+ , " ghc-options: -Wall -Wcompat -Wredundant-constraints"+ , " haddock-options: -w"+ ]++-- | Generates @cabal.project.local.example@ developer override template.+cabalProjectLocalTemplate :: Text+cabalProjectLocalTemplate = T.unlines+ [ "-- cabal.project.local.example — Local developer configuration template"+ , "-- Copy to cabal.project.local for machine-specific overrides (git-ignored)"+ , ""+ , "-- Optional: Override default compiler"+ , "-- with-compiler: ghc-9.14.1"+ , ""+ , "-- Optional: Enable documentation generation during builds"+ , "-- documentation: True"+ , ""+ , "-- Optional: Verbose compiler output"+ , "-- verbose: 1"+ ]++-- | Generates strict @.hlint.yaml@ enforcing clean code and semantic rules.+hlintTemplate :: Text+hlintTemplate = T.unlines+ [ "# .hlint.yaml — Ecosystem HLint Configuration"+ , "# Zero-hint cleanliness policy enforced across all source files."+ , ""+ , "- arguments:"+ , " - -XOverloadedRecordDot"+ , " - -XDataKinds"+ , " - -XTypeOperators"+ , " - -XTypeApplications"+ , ""+ , "- warn:"+ , " name: \"Use OverloadedRecordDot\""+ , " lhs: \"getField @a x\""+ , " rhs: \"x.a\""+ , " note: \"Prefer native OverloadedRecordDot syntax over explicit getField\""+ ]++-- | Generates @.gitmessage@ commit template specifying Sarutahiko AI attribution rules.+gitmessageTemplate :: Text+gitmessageTemplate = T.unlines+ [ "# <summary line> (max 72 chars)"+ , "#"+ , "# <Why this change, what problem it solves, what it rules out. The 'why',"+ , "# not the 'what' -- the diff already says what. Delete these comment lines;"+ , "# git strips them when the message is edited.>"+ , "#"+ , "# -- AI attribution (sarutahiko convention) -----------------------------------"+ , "# If any AI coding assistant or orchestrator took part in authoring the change,"+ , "# add standard git trailers:"+ , "#"+ , "# Assisted-by: <Tool> (<Provider> / <Model>)"+ , "# Orchestrated-by: <Engine>"+ , "#"+ , "# Examples:"+ , "# Assisted-by: Antigravity (Google / gemini-3.8-pro)"+ , "# Assisted-by: Freebuff"+ , "# Orchestrated-by: Yamaarashi Flow Task Runner"+ , "#"+ , "# Do NOT use Co-authored-by: or Co-developed-by: for an assistant -- those"+ , "# claim legal authorship and certify the Developer Certificate of Origin, which"+ , "# only a human can do. Never add Signed-off-by: on an AI's behalf."+ , "#"+ , "# A commit written entirely by a human needs no AI trailer."+ , "# See docs/notes/COMMIT_ATTRIBUTION.md for details."+ ]++-- | Generates @tagref.yml@ cross-reference configuration.+tagrefConfigTemplate :: Project -> Text+tagrefConfigTemplate proj = T.unlines+ [ "# SPDX-License-Identifier: " <> projectLicense proj+ , "# Tagref configuration for " <> projectName proj+ , ""+ , "ignore_rules:"+ , " - \"bin/" <> projectName proj <> "-tagref\""+ , " - \".githooks\""+ ]++--------------------------------------------------------------------------------+-- 2. Git Governance & Hook Assets+--------------------------------------------------------------------------------++-- | Synthesizes @.githooks/@ scripts: @commit-msg@ and @pre-commit@.+gitHooksEntries :: Project -> [FileEntry]+gitHooksEntries proj =+ [ FileEntry ".githooks/commit-msg" commitMsgHookTemplate True+ , FileEntry ".githooks/pre-commit" (preCommitHookTemplate proj) True+ ]++-- | Generates @.githooks/commit-msg@ enforcing Sarutahiko commit attribution conventions.+commitMsgHookTemplate :: Text+commitMsgHookTemplate = T.unlines+ [ "#!/bin/sh"+ , "# commit-msg: enforce sarutahiko AI-attribution convention."+ , "set -u"+ , ""+ , "msg_file=$1"+ , "[ -f \"$msg_file\" ] || exit 0"+ , ""+ , "trailers=$(git interpret-trailers --parse < \"$msg_file\" 2>/dev/null) || exit 0"+ , "status=0"+ , "err() { printf 'commit-msg: %s\\n' \"$*\" >&2; status=1; }"+ , ""+ , "# Reject robot emoji"+ , "if grep -q '🤖' \"$msg_file\"; then"+ , " err \"robot emoji in message; delete it.\""+ , "fi"+ , ""+ , "# Reject Co-authored-by for AI assistants"+ , "ai_names='copilot|chatgpt|gpt-[0-9]|codex|cursor|gemini|llama|qwen|deepseek|mistral|claude|anthropic|openai|aider|devin|codebuff|freebuff|buffy|space[ _-]?bunny|antigravity|noreply@|\\[bot\\]|@bot\\b'"+ , "if printf '%s\\n' \"$trailers\" | grep -Ei \"^[[:space:]]*(Co-authored-by|Co-developed-by|Signed-off-by):.*($ai_names)\"; then"+ , " err \"Co-authored-by:/Signed-off-by: naming an AI assistant. Use 'Assisted-by: <Tool> (<Provider> / <Model>)' instead.\""+ , "fi"+ , ""+ , "exit $status"+ ]++-- | Generates @.githooks/pre-commit@ validating Tagref and Markdown links before commit.+preCommitHookTemplate :: Project -> Text+preCommitHookTemplate proj =+ let pName = projectName proj+ in T.unlines+ [ "#!/bin/sh"+ , "# .githooks/pre-commit: mechanically enforce tagref and link integrity."+ , "set -eu"+ , ""+ , "REPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\""+ , ""+ , "if [ -x \"${REPO_ROOT}/bin/" <> pName <> "-tagref\" ]; then"+ , " if ! \"${REPO_ROOT}/bin/" <> pName <> "-tagref\"; then"+ , " printf '\\npre-commit: Tagref cross-reference check failed.\\n' >&2"+ , " exit 1"+ , " fi"+ , "fi"+ , ""+ , "if [ -x \"${REPO_ROOT}/bin/" <> pName <> "-linkcheck\" ]; then"+ , " if ! \"${REPO_ROOT}/bin/" <> pName <> "-linkcheck\" --quiet; then"+ , " printf '\\npre-commit: Markdown link & anchor check failed.\\n' >&2"+ , " exit 1"+ , " fi"+ , "fi"+ , ""+ , "exit 0"+ ]++--------------------------------------------------------------------------------+-- 3. Turnkey Physical Verification Suite+--------------------------------------------------------------------------------++-- | Synthesizes executable scripts under @bin/@:+-- @<name>-verify@, @<name>-tagref@, @<name>-linkcheck@, @<name>-cloc@, and @generate-tags@.+verificationHarnessEntries :: Project -> [FileEntry]+verificationHarnessEntries proj =+ let pName = T.unpack (projectName proj)+ in [ FileEntry ("bin/" <> pName <> "-verify") (verifyScriptTemplate proj) True+ , FileEntry ("bin/" <> pName <> "-tagref") (tagrefScriptTemplate proj) True+ , FileEntry ("bin/" <> pName <> "-linkcheck") (linkcheckScriptTemplate proj) True+ , FileEntry ("bin/" <> pName <> "-cloc") (clocScriptTemplate proj) True+ , FileEntry "bin/generate-tags" generateTagsTemplate True+ ]++-- | Generates @bin/<name>-verify@ running the five standard physical verification gates.+verifyScriptTemplate :: Project -> Text+verifyScriptTemplate proj =+ let pName = projectName proj+ in 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 command -v hlint >/dev/null 2>&1; then"+ , " hlint_cmd=\"$(command -v hlint)\""+ , " fi"+ , " if [[ -n \"${hlint_cmd}\" ]]; then"+ , " \"${hlint_cmd}\" \"${REPO_ROOT}\"/packages/*/src >/dev/null"+ , " echo \" [+] Clean HLint run: 0 hints.\""+ , " else"+ , " echo \" [SKIP] hlint binary not found.\""+ , " fi"+ , "}"+ , ""+ , "verify_tests() {"+ , " echo \"=== [2/5] Full Cabal Test Suite ===\""+ , " (cd \"${REPO_ROOT}\" && cabal test all >/dev/null)"+ , " echo \" [+] All test suite assertions PASSED.\""+ , "}"+ , ""+ , "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}\""+ , " else"+ , " echo \" [SKIP] Tagref script 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] Linkcheck script not found.\""+ , " 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] CLOC script not found.\""+ , " fi"+ , "}"+ , ""+ , "verify_hlint"+ , "verify_tests"+ , "verify_tagref"+ , "verify_linkcheck"+ , "verify_cloc"+ , ""+ , "echo \"=== All 5 Physical Verification Checks PASSED ===\""+ ]++-- | Generates @bin/<name>-tagref@ cross-reference validation script with Python fallback.+tagrefScriptTemplate :: Project -> Text+tagrefScriptTemplate proj =+ let pName = projectName proj+ in T.unlines+ [ "#!/usr/bin/env bash"+ , "# bin/" <> pName <> "-tagref — Mechanical Tagref Cross-Reference Verification Harness"+ , "set -euo pipefail"+ , ""+ , "SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\""+ , "REPO_ROOT=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\""+ , ""+ , "if command -v tagref >/dev/null 2>&1; then"+ , " (cd \"${REPO_ROOT}\" && tagref \"$@\")"+ , "else"+ , " REPO_ROOT=\"${REPO_ROOT}\" python3 - <<'EOF'"+ , "import sys, os, re, subprocess"+ , ""+ , "repo_root = os.environ.get(\"REPO_ROOT\", os.getcwd())"+ , "ignore_rules = [\"bin/" <> pName <> "-tagref\", \".githooks\"]"+ , "tag_regex = re.compile(r\"\\[tag:([a-zA-Z0-9_\\-]+)\\]\")"+ , "ref_regex = re.compile(r\"\\[ref:([a-zA-Z0-9_\\-]+)\\]\")"+ , ""+ , "try:"+ , " git_files = subprocess.check_output([\"git\", \"ls-files\"], cwd=repo_root, text=True).splitlines()"+ , "except Exception:"+ , " git_files = []"+ , ""+ , "if not git_files:"+ , " for root, dirs, files in os.walk(repo_root):"+ , " dirs[:] = [d for d in dirs if d not in (\".git\", \"dist-newstyle\")]"+ , " for f in files:"+ , " git_files.append(os.path.relpath(os.path.join(root, f), repo_root))"+ , ""+ , "tags = {}"+ , "refs = []"+ , "scanned = 0"+ , ""+ , "for rel_path in git_files:"+ , " if any(rel_path == rule or rel_path.startswith(rule.rstrip(\"/\") + \"/\") for rule in ignore_rules):"+ , " continue"+ , " abs_path = os.path.join(repo_root, rel_path)"+ , " try:"+ , " with open(abs_path, \"r\", encoding=\"utf-8\", errors=\"ignore\") as f:"+ , " scanned += 1"+ , " for lineno, line in enumerate(f, 1):"+ , " for match in tag_regex.finditer(line):"+ , " t = match.group(1)"+ , " if t in tags:"+ , " print(f\"Error: duplicate tag '{t}' in {rel_path}:{lineno}\", file=sys.stderr)"+ , " sys.exit(1)"+ , " tags[t] = f\"{rel_path}:{lineno}\""+ , " for match in ref_regex.finditer(line):"+ , " refs.append((match.group(1), f\"{rel_path}:{lineno}\"))"+ , " except Exception:"+ , " continue"+ , ""+ , "dangling = [f\"Error: dangling reference '{r}' at {loc}\" for r, loc in refs if r not in tags]"+ , "if dangling:"+ , " for d in dangling:"+ , " print(d, file=sys.stderr)"+ , " sys.exit(1)"+ , ""+ , "print(f\"{len(tags)} tags, 0 group members, {len(refs)} references validated in {scanned} files.\")"+ , "EOF"+ , "fi"+ ]++-- | Generates @bin/<name>-linkcheck@ deterministic Markdown link and anchor validator.+linkcheckScriptTemplate :: Project -> Text+linkcheckScriptTemplate proj =+ let pName = projectName proj+ in T.unlines+ [ "#!/usr/bin/env python3"+ , "\"\"\""+ , "bin/" <> pName <> "-linkcheck — Intra-Corpus Markdown Link & Anchor Verification"+ , "\"\"\""+ , "import sys, os, re, argparse, subprocess"+ , ""+ , "REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\"))"+ , "LINK_REGEX = re.compile(r'!?\\[([^\\]]*)\\]\\(([^)\\s]+)(?:\\s+[\"\\'][^\"\\']*[\"\\'])?\\)')"+ , "HEADING_REGEX = re.compile(r\"^(#{1,6})\\s+(.+)$\", re.MULTILINE)"+ , "EXTERNAL_SCHEMES = (\"http://\", \"https://\", \"mailto:\", \"rad:\", \"ftp://\")"+ , ""+ , "def github_slugify(text):"+ , " text = re.sub(r\"<[^>]+>\", \"\", text)"+ , " text = re.sub(r\"\\[([^\\]]*)\\]\\([^)]+\\)\", r\"\\1\", text)"+ , " text = re.sub(r\"[`*_{}\\[\\]()#+\\-.!?:,\\x27\\\"]\", \"\", text)"+ , " text = text.strip().lower()"+ , " return re.sub(r\"\\s+\", \"-\", text)"+ , ""+ , "def parse_anchors(file_path):"+ , " if not os.path.isfile(file_path): return set(), 0"+ , " with open(file_path, \"r\", encoding=\"utf-8\", errors=\"ignore\") as f:"+ , " content = f.read()"+ , " lines = content.splitlines()"+ , " anchors = set()"+ , " if file_path.endswith(\".md\"):"+ , " for m in HEADING_REGEX.finditer(content):"+ , " anchors.add(github_slugify(m.group(2)))"+ , " return anchors, len(lines)"+ , ""+ , "def check_links(file_path, anchors_cache):"+ , " broken = []"+ , " checked = 0"+ , " if not os.path.isfile(file_path): return 0, [(0, file_path, \"Missing file\")]"+ , " with open(file_path, \"r\", encoding=\"utf-8\", errors=\"ignore\") as f:"+ , " lines = f.readlines()"+ , " in_code = False"+ , " for idx, line in enumerate(lines, 1):"+ , " if line.strip().startswith(\"```\"): in_code = not in_code; continue"+ , " if in_code: continue"+ , " for m in LINK_REGEX.finditer(line):"+ , " target = m.group(2)"+ , " if any(target.startswith(s) for s in EXTERNAL_SCHEMES): continue"+ , " checked += 1"+ , " path_part, _, anchor = target.partition(\"#\")"+ , " resolved = os.path.normpath(os.path.join(os.path.dirname(file_path), path_part)) if path_part else file_path"+ , " try:"+ , " rel = os.path.relpath(resolved, REPO_ROOT)"+ , " if rel.startswith(\"..\") or rel == \"..\":"+ , " broken.append((idx, target, f\"Link '{resolved}' escapes repository root\"))"+ , " continue"+ , " except ValueError:"+ , " broken.append((idx, target, f\"Link '{resolved}' escapes repository root\"))"+ , " continue"+ , " if not os.path.exists(resolved):"+ , " broken.append((idx, target, f\"File '{resolved}' does not exist\"))"+ , " continue"+ , " if anchor and resolved.endswith(\".md\"):"+ , " if resolved not in anchors_cache: anchors_cache[resolved] = parse_anchors(resolved)"+ , " anchors, _ = anchors_cache[resolved]"+ , " if anchor.lower() not in anchors:"+ , " broken.append((idx, target, f\"Anchor '#{anchor}' missing in '{resolved}'\"))"+ , " return checked, broken"+ , ""+ , "def main():"+ , " parser = argparse.ArgumentParser()"+ , " parser.add_argument(\"--quiet\", \"-q\", action=\"store_true\")"+ , " args = parser.parse_args()"+ , " anchors_cache = {}"+ , " all_broken = []"+ , " total_checked = 0"+ , " targets = []"+ , " for root, dirs, files in os.walk(REPO_ROOT):"+ , " dirs[:] = [d for d in dirs if d not in (\".git\", \"dist-newstyle\")]"+ , " for f in files:"+ , " if f.endswith(\".md\"): targets.append(os.path.join(root, f))"+ , " for t in sorted(targets):"+ , " chk, brk = check_links(t, anchors_cache)"+ , " total_checked += chk"+ , " for line, url, err in brk:"+ , " all_broken.append((t, line, url, err))"+ , " if all_broken:"+ , " for path, line, url, err in all_broken:"+ , " print(f\"{os.path.relpath(path, REPO_ROOT)}:{line}: broken link '{url}' -> {err}\", file=sys.stderr)"+ , " sys.exit(1)"+ , " print(f\"0 broken links ({total_checked} checked across {len(targets)} files).\")"+ , ""+ , "if __name__ == '__main__': main()"+ ]++-- | Generates @bin/<name>-cloc@ evaluating 50%+ in-code comment density.+clocScriptTemplate :: Project -> Text+clocScriptTemplate proj =+ let pName = projectName proj+ in 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)"+ , " tot_comment = tot_code = 0"+ , " for f in src_files:"+ , " _, com, cod = count_hs(f)"+ , " tot_comment += com; tot_code += cod"+ , " density = (tot_comment / (tot_comment + tot_code) * 100.0) if (tot_comment + tot_code) > 0 else 0.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()"+ ]++-- | Generates @bin/generate-tags@ indexing symbols using hasktags or ctags.+generateTagsTemplate :: Text+generateTagsTemplate = T.unlines+ [ "#!/usr/bin/env bash"+ , "# bin/generate-tags — Fast symbol indexing for code navigation"+ , "set -euo pipefail"+ , ""+ , "SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\""+ , "REPO_ROOT=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\""+ , "TARGET_DIRS=(\"${REPO_ROOT}/packages\")"+ , ""+ , "if command -v hasktags >/dev/null 2>&1; then"+ , " (cd \"${REPO_ROOT}\" && hasktags -c -x -f tags \"${TARGET_DIRS[@]}\")"+ , " echo \"Tags generated successfully in ./tags\""+ , "elif command -v ctags >/dev/null 2>&1; then"+ , " (cd \"${REPO_ROOT}\" && ctags -R -f tags \"${TARGET_DIRS[@]}\")"+ , " echo \"Tags generated successfully in ./tags\""+ , "else"+ , " echo \"[WARN] Neither hasktags nor ctags found on PATH.\""+ , "fi"+ ]++--------------------------------------------------------------------------------+-- 4. Operational Governance & Agent Guidance+--------------------------------------------------------------------------------++-- | Synthesizes @README.md@, @AGENTS.md@, @CLAUDE.md@, @PLAN.md@, and @STATE.md@.+operationalGovernanceEntries :: Project -> [FileEntry]+operationalGovernanceEntries proj =+ [ FileEntry "README.md" (readmeTemplate proj) False+ , FileEntry "AGENTS.md" (agentsTemplate proj) False+ , FileEntry "CLAUDE.md" (claudeTemplate proj) False+ , FileEntry "PLAN.md" (planTemplate proj) False+ , FileEntry "STATE.md" (stateTemplate proj) False+ ]++-- | Generates root @README.md@ describing project overview, build steps, and licenses.+readmeTemplate :: Project -> Text+readmeTemplate proj =+ let pName = projectName proj+ auth = projectAuthor proj+ in renderMarkdown+ [ h1 pName+ , para (projectDescription proj)+ , h2 "Quick Start"+ , codeBlock "bash"+ ( "# Build all packages\n\+ \cabal build all\n\n\+ \# Run full test suite\n\+ \cabal test all\n\n\+ \# Run physical verification gates\n\+ \./bin/" <> pName <> "-verify" )+ , h2 "Documentation"+ , bulletList+ [ inlines [linkInline (code "AGENTS.md") "AGENTS.md", plain " — Advisory context and rules for AI assistants."]+ , inlines [linkInline (code "PLAN.md") "PLAN.md", plain " — Strategic roadmap and phase delivery schedule."]+ , inlines [linkInline (code "STATE.md") "STATE.md", plain " — Active operational session frontier."]+ , inlines [linkInline (code "docs/INDEX.md") "docs/INDEX.md", plain " — Documentation corpus inventory."]+ , inlines [linkInline (code "docs/TUTORIAL.md") "docs/TUTORIAL.md", plain " — Getting started tutorial."]+ ]+ , h2 "License & Attribution"+ , bulletList+ [ inlines [bold "License:", plain (" " <> projectLicense proj)]+ , inlines [bold "Author:", plain (" " <> authorName auth <> " <" <> authorEmail auth <> ">")]+ ]+ ]++-- | Generates @AGENTS.md@ establishing AI coding assistant reading order and advisories.+agentsTemplate :: Project -> Text+agentsTemplate proj =+ let pName = projectName proj+ uSlug = toUpperSlug pName+ in renderMarkdown+ [ h1 "AGENTS.md — Advisory Context for AI Coding Assistants"+ , para "Pointer file. Canonical sources win on any disagreement."+ , h2 "What this repository is"+ , para ("`" <> pName <> "` — " <> projectDescription proj)+ , h2 "Read first, in this order"+ , orderedList+ [ inlines [linkInline (code "README.md") "README.md", plain " — what this is, and its status."]+ , inlines [linkInline (code "PLAN.md") "PLAN.md", plain " & ", linkInline (code "STATE.md") "STATE.md", plain " — operational roadmap and active session frontier."]+ , inlines [linkInline (code ("docs/notes/" <> uSlug <> "_DESIGN.md")) ("docs/notes/" <> uSlug <> "_DESIGN.md"), plain " — canonical design notes and invariants."]+ , inlines [linkInline (code "docs/notes/DOC_STRATEGY.md") "docs/notes/DOC_STRATEGY.md", plain " — documentation policy: genres, lifecycle, tone."]+ , inlines [linkInline (code "docs/INDEX.md") "docs/INDEX.md", plain " — corpus inventory and reading map."]+ ]+ , h2 "Subdivided operational rules (`.agents/rules/`)"+ , bulletList+ [ inlines [linkInline (code ".agents/rules/commit-attribution.md") ".agents/rules/commit-attribution.md", plain " — Linux kernel RFC 2822 trailer format (`Assisted-by:`), DCO boundaries."]+ , inlines [linkInline (code ".agents/rules/code-navigation.md") ".agents/rules/code-navigation.md", plain " — Token-saving funnel (`./tags` symbol jump, targeted slice viewing)."]+ , inlines [linkInline (code ".agents/rules/compiler-verification.md") ".agents/rules/compiler-verification.md", plain " — Zero-log compiler verification, tests, and zero-contamination."]+ , inlines [linkInline (code ".agents/rules/documentation-precedence.md") ".agents/rules/documentation-precedence.md", plain " — Precedence rule (\"code wins at runtime; notes corrected regardless\")."]+ ]+ , h2 "Operational advisories"+ , orderedList+ [ plain "**Design docs are load-bearing.** Stale notes must be corrected in the same change set."+ , plain "Commits: descriptive-imperative subject under 72 chars, body explains why. AI attribution uses git trailers per `sarutahiko`: `Assisted-by: <Tool> (<Provider> / <Model>)`."+ , plain ("Toolchain: GHC " <> projectGhcVersion proj <> " / Cabal " <> projectCabalVersion proj <> ".")+ , plain "Two-file loop: Update `STATE.md` before ending or resetting a session."+ ]+ ]++-- | Generates @CLAUDE.md@ pointing to canonical advisory in @AGENTS.md@.+claudeTemplate :: Project -> Text+claudeTemplate _ = renderMarkdown+ [ h1 "CLAUDE.md"+ , paraInlines+ [ plain "Canonical advisory instructions are maintained in "+ , link "AGENTS.md" "AGENTS.md"+ , plain "."+ ]+ ]++-- | Generates @PLAN.md@ tracking strategic milestones.+planTemplate :: Project -> Text+planTemplate proj =+ let pName = projectName proj+ in renderMarkdown+ [ h1 (pName <> " — Strategic Roadmap (`PLAN.md`)")+ , paraInlines+ [ bold "Status:"+ , plain " ACTIVE STRATEGIC ROADMAP \n"+ , bold "Scope:"+ , plain (" " <> projectDescription proj <> " \n")+ , bold "Documentation:"+ , plain " "+ , linkInline (code "docs/INDEX.md") "docs/INDEX.md"+ , plain " \n"+ , bold "Operational State:"+ , plain " "+ , linkInline (code "STATE.md") "STATE.md"+ ]+ , hr+ , h2 "Phase 1: Foundational Architecture [ACTIVE]"+ , bulletList+ [ inlines+ [ plain "[x] "+ , bold "Milestone 1.1:"+ , plain " Canonical Workspace Scaffolding\n - Multi-package hierarchy, documentation corpus, and verification harnesses grounded."+ ]+ , inlines+ [ plain "[ ] "+ , bold "Milestone 1.2:"+ , plain " Core Implementation & Domain Logic\n - Core abstractions, unit tests, and property test verification."+ ]+ ]+ ]++-- | Generates @STATE.md@ tracking active session frontier.+stateTemplate :: Project -> Text+stateTemplate proj =+ let pName = projectName proj+ in renderMarkdown+ [ h1 (pName <> " — State")+ , paraInlines+ [ plain "Operational frontier. Read with "+ , linkInline (code "PLAN.md") "PLAN.md"+ , plain "; update before ending a session."+ ]+ , h2 "Where we are"+ , para ("The workspace skeleton for `" <> pName <> "` is initialized and verified under GHC " <> projectGhcVersion proj <> ".")+ , h2 "Active Frontier"+ , bulletList+ [ plain "Complete Milestone 1.2 core domain logic."+ , plain ("Maintain 5-gate physical verification via `./bin/" <> pName <> "-verify`.")+ ]+ ]++--------------------------------------------------------------------------------+-- 5. Subdivided Agent Operational Rules+--------------------------------------------------------------------------------++-- | Synthesizes `.agents/rules/` guidance files.+agentRuleEntries :: Project -> [FileEntry]+agentRuleEntries _ =+ [ FileEntry ".agents/rules/commit-attribution.md" ruleCommitAttributionTemplate False+ , FileEntry ".agents/rules/code-navigation.md" ruleCodeNavigationTemplate False+ , FileEntry ".agents/rules/compiler-verification.md" ruleCompilerVerificationTemplate False+ , FileEntry ".agents/rules/documentation-precedence.md" ruleDocPrecedenceTemplate False+ ]++-- | Generates `.agents/rules/commit-attribution.md`.+ruleCommitAttributionTemplate :: Text+ruleCommitAttributionTemplate = renderMarkdown+ [ h1 "Commit Attribution Rules"+ , para "AI attribution uses git trailers per `sarutahiko`:\n\+ \`Assisted-by: <Tool> (<Provider> / <Model>)` (or `<Tool>`) and optional `Orchestrated-by: <Engine>`."+ , para "Never `Co-authored-by:` or `Signed-off-by:` for AI tools (only humans certify the DCO)."+ ]++-- | Generates `.agents/rules/code-navigation.md`.+ruleCodeNavigationTemplate :: Text+ruleCodeNavigationTemplate = renderMarkdown+ [ h1 "Code Navigation Rules"+ , para "Always follow the Progressive Disclosure Funnel:"+ , orderedList+ [ plain "Level 0: Global Navigation (`docs/INDEX.md`, `tags` file)"+ , plain "Level 1: Structural Outline (module export lists)"+ , plain "Level 2: Targeted Slice (`view_file` StartLine / EndLine)"+ ]+ ]++-- | Generates `.agents/rules/compiler-verification.md`.+ruleCompilerVerificationTemplate :: Text+ruleCompilerVerificationTemplate = renderMarkdown+ [ h1 "Compiler Verification Rules"+ , para "Maintain zero warnings under `-Wall -Wcompat -Wredundant-constraints`."+ , para "Run physical verification gates before committing."+ ]++-- | Generates `.agents/rules/documentation-precedence.md`.+ruleDocPrecedenceTemplate :: Text+ruleDocPrecedenceTemplate = renderMarkdown+ [ h1 "Documentation Precedence Rules"+ , para "Precedence rule: Code wins at runtime; notes corrected regardless."+ , para "Design docs are load-bearing. Update notes when implementation changes."+ ]++--------------------------------------------------------------------------------+-- 6. Canonical Documentation Corpus+--------------------------------------------------------------------------------++-- | Synthesizes `docs/` hierarchy and notes.+documentationCorpusEntries :: Project -> [FileEntry]+documentationCorpusEntries proj =+ let uSlug = T.unpack (toUpperSlug (projectName proj))+ in [ FileEntry "docs/INDEX.md" (docsIndexTemplate proj) False+ , FileEntry "docs/TUTORIAL.md" (docsTutorialTemplate proj) False+ , FileEntry "docs/NEWBIE_GUIDE.md" (docsNewbieGuideTemplate proj) False+ , FileEntry "docs/MAINTAINER_GUIDE.md" (docsMaintainerGuideTemplate proj) False+ , FileEntry ("docs/notes/" <> uSlug <> "_DESIGN.md") (docsDesignTemplate proj) False+ , FileEntry "docs/notes/DOC_STRATEGY.md" (docsStrategyTemplate proj) False+ , FileEntry "docs/notes/COMMIT_ATTRIBUTION.md" (docsCommitAttributionTemplate proj) False+ ]++-- | Generates `docs/INDEX.md` inventory.+docsIndexTemplate :: Project -> Text+docsIndexTemplate proj =+ let pName = projectName proj+ uSlug = toUpperSlug pName+ pkgs = projectPackages proj+ in renderMarkdown+ [ h1 (pName <> " Documentation Index")+ , para ("Welcome to the `" <> pName <> "` technical documentation corpus.")+ , h2 "Core Orientation"+ , bulletList+ [ inlines [linkInline (code "README.md") "../README.md", plain " — High-level project overview and quick start."]+ , inlines [linkInline (code "AGENTS.md") "../AGENTS.md", plain " — AI coding assistant guidance and rules."]+ , inlines [linkInline (code "PLAN.md") "../PLAN.md", plain " — Strategic roadmap."]+ , inlines [linkInline (code "STATE.md") "../STATE.md", plain " — Active operational frontier."]+ , inlines [linkInline (code "TUTORIAL.md") "TUTORIAL.md", plain " — Getting started tutorial."]+ , inlines [linkInline (code "NEWBIE_GUIDE.md") "NEWBIE_GUIDE.md", plain " — Token-conservation funnel and navigation."]+ , inlines [linkInline (code "MAINTAINER_GUIDE.md") "MAINTAINER_GUIDE.md", plain " — Maintenance and verification gates."]+ ]+ , h2 "Design Notes (`docs/notes/`)"+ , bulletList+ [ inlines [linkInline (code (uSlug <> "_DESIGN.md")) ("notes/" <> uSlug <> "_DESIGN.md"), plain " — Canonical design notes."]+ , inlines [linkInline (code "DOC_STRATEGY.md") "notes/DOC_STRATEGY.md", plain " — Documentation strategy and precedence rules."]+ , inlines [linkInline (code "COMMIT_ATTRIBUTION.md") "notes/COMMIT_ATTRIBUTION.md", plain " — Commit attribution standards."]+ ]+ , h2 "Active Packages"+ , bulletList+ [ inlines [linkInline (code ("packages/" <> packageName pkg <> "/")) ("../packages/" <> packageName pkg <> "/"), plain (" — " <> packageSynopsis pkg)]+ | pkg <- pkgs+ ]+ ]++-- | Generates `docs/TUTORIAL.md`.+docsTutorialTemplate :: Project -> Text+docsTutorialTemplate proj =+ let pName = projectName proj+ uSlug = toUpperSlug pName+ in renderMarkdown+ [ h1 ("Getting Started with " <> pName)+ , para ("A foundational tutorial for working with `" <> pName <> "`.")+ , h2 "1. Concepts"+ , para ("`" <> pName <> "` provides modular components structured around " <> projectDescription proj <> ".")+ , h2 "2. Navigation"+ , paraInlines+ [ plain "See "+ , linkInline (code "docs/INDEX.md") "INDEX.md"+ , plain ", "+ , linkInline (code "docs/NEWBIE_GUIDE.md") "NEWBIE_GUIDE.md"+ , plain ", and "+ , linkInline (code ("docs/notes/" <> uSlug <> "_DESIGN.md")) ("notes/" <> uSlug <> "_DESIGN.md")+ , plain "."+ ]+ ]++-- | Generates `docs/NEWBIE_GUIDE.md`.+docsNewbieGuideTemplate :: Project -> Text+docsNewbieGuideTemplate proj =+ let pName = projectName proj+ in renderMarkdown+ [ h1 (pName <> " Newbie Quickstart & Token-Conservation Guide")+ , paraInlines+ [ bold "Parent Strategy:"+ , plain " "+ , linkInline (code "docs/notes/DOC_STRATEGY.md") "notes/DOC_STRATEGY.md"+ , plain " \n"+ , bold "Corpus Inventory:"+ , plain " "+ , linkInline (code "docs/INDEX.md") "INDEX.md"+ , plain " \n"+ , bold "Tutorial:"+ , plain " "+ , linkInline (code "docs/TUTORIAL.md") "TUTORIAL.md"+ , plain " \n"+ , bold "Maintainer Guide:"+ , plain " "+ , linkInline (code "docs/MAINTAINER_GUIDE.md") "MAINTAINER_GUIDE.md"+ ]+ , h2 "1. The Core Invariant: Never Dump Whole Modules"+ , para "Always follow the Progressive Disclosure Funnel:"+ , bulletList+ [ plain "Level 0: Global Navigation (`docs/INDEX.md`)"+ , plain "Level 1: Structural Outline"+ , plain "Level 2: Targeted Slice"+ ]+ ]++-- | Generates `docs/MAINTAINER_GUIDE.md`.+docsMaintainerGuideTemplate :: Project -> Text+docsMaintainerGuideTemplate proj =+ let pName = projectName proj+ uSlug = toUpperSlug pName+ in renderMarkdown+ [ h1 (pName <> " Maintainer Guide")+ , paraInlines+ [ bold "Corpus Inventory:"+ , plain " "+ , linkInline (code "docs/INDEX.md") "INDEX.md"+ , plain " \n"+ , bold "Design Notes:"+ , plain " "+ , linkInline (code ("docs/notes/" <> uSlug <> "_DESIGN.md")) ("notes/" <> uSlug <> "_DESIGN.md")+ , plain " \n"+ , bold "Attribution:"+ , plain " "+ , linkInline (code "docs/notes/COMMIT_ATTRIBUTION.md") "notes/COMMIT_ATTRIBUTION.md"+ ]+ , h2 "Verification Gates"+ , para ("Every commit must pass all five verification gates via `./bin/" <> pName <> "-verify`.")+ ]++-- | Generates `docs/notes/<PROJECT>_DESIGN.md`.+docsDesignTemplate :: Project -> Text+docsDesignTemplate proj =+ let pName = projectName proj+ in renderMarkdown+ [ h1 (pName <> " — Design Decisions & Invariants")+ , paraInlines+ [ bold "Status:"+ , plain " CANONICAL DESIGN MONOGRAPH \n"+ , bold "Parent Strategy:"+ , plain " "+ , linkInline (code "DOC_STRATEGY.md") "DOC_STRATEGY.md"+ , plain " \n"+ , bold "Documentation Index:"+ , plain " "+ , linkInline (code "../INDEX.md") "../INDEX.md"+ ]+ , hr+ , h2 "Architectural Decisions"+ , h3 "Decision D1: Modular Workspace Architecture"+ , para "The repository is decomposed into fine-grained packages under `packages/`."+ , h3 "Decision D2: Verification Invariants"+ , para ("Adheres to Note [Ecosystem Standardization Invariants] " <> refStandardization <> ".")+ , hr+ , h2 "In-Code Design Notes"+ , h3 "Note [Core Architecture]"+ , Paragraph [tagDef "note-core-architecture"]+ , para ("Defines foundational design invariants for `" <> pName <> "`.")+ ]++-- | Generates `docs/notes/DOC_STRATEGY.md`.+docsStrategyTemplate :: Project -> Text+docsStrategyTemplate proj =+ let pName = projectName proj+ in renderMarkdown+ [ h1 "Documentation Strategy & Policy"+ , paraInlines+ [ bold "Project:"+ , plain (" " <> pName <> " \n")+ , bold "Index:"+ , plain " "+ , linkInline (code "../INDEX.md") "../INDEX.md"+ ]+ , h2 "Precedence Rule (§4)"+ , para "Code wins at runtime; notes corrected regardless. Stale design documentation must be remediated in the same commit."+ ]++-- | Generates `docs/notes/COMMIT_ATTRIBUTION.md`.+docsCommitAttributionTemplate :: Project -> Text+docsCommitAttributionTemplate proj =+ let pName = projectName proj+ in renderMarkdown+ [ h1 "Commit Attribution & Trailer Standards"+ , paraInlines+ [ bold "Project:"+ , plain (" " <> pName <> " \n")+ , bold "Index:"+ , plain " "+ , linkInline (code "../INDEX.md") "../INDEX.md"+ ]+ , h2 "Format"+ , plainCodeBlock "Assisted-by: <Tool> (<Provider> / <Model>)"+ ]++--------------------------------------------------------------------------------+-- 7. Multi-Package Hierarchy+--------------------------------------------------------------------------------++-- | Synthesizes package manifests, source modules, test suites, and documentation.+packageHierarchyEntries :: Project -> [FileEntry]+packageHierarchyEntries proj =+ concatMap (packageEntries proj) (projectPackages proj)++-- | Synthesizes all files for a specific 'PackageSpec'.+packageEntries :: Project -> PackageSpec -> [FileEntry]+packageEntries proj pkg =+ let pName = T.unpack (packageName pkg)+ cabalFile = "packages/" <> pName <> "/" <> pName <> ".cabal"+ readmeFile = "packages/" <> pName <> "/README.md"+ testFile = "packages/" <> pName <> "/test/Main.hs"+ exposes = case packageExposes pkg of+ [] -> [defaultModuleName (packageName pkg)]+ ms -> ms+ moduleFiles =+ [ FileEntry+ ("packages/" <> pName <> "/src/" <> moduleToFilePath m)+ (packageModuleTemplate proj pkg m)+ False+ | m <- exposes+ ]+ in [ FileEntry cabalFile (packageCabalTemplate proj pkg) False+ , FileEntry readmeFile (packageReadmeTemplate proj pkg) False+ , FileEntry testFile (packageTestTemplate proj pkg) False+ ] ++ moduleFiles++-- | Generates a package's Cabal manifest.+packageCabalTemplate :: Project -> PackageSpec -> Text+packageCabalTemplate proj pkg =+ let pName = packageName pkg+ auth = projectAuthor proj+ exposes = case packageExposes pkg of+ [] -> [defaultModuleName pName]+ ms -> ms+ in T.unlines+ [ "cabal-version: 3.0"+ , "name: " <> pName+ , "version: 0.1.0.0"+ , "synopsis: " <> packageSynopsis pkg+ , "license: " <> projectLicense proj+ , "author: " <> authorName auth+ , "maintainer: " <> authorEmail auth+ , "build-type: Simple"+ , "tested-with: GHC == " <> projectGhcVersion proj+ , ""+ , "library"+ , " exposed-modules:"+ ] <> T.unlines [ " " <> m | m <- exposes ] <>+ T.unlines+ [ " hs-source-dirs: src"+ , " default-language: Haskell2010"+ , " default-extensions:"+ , " OverloadedStrings"+ , " DerivingStrategies"+ , " ghc-options: -Wall -Wcompat -Wredundant-constraints"+ , " build-depends:"+ , " base >= 4.14 && < 5,"+ , " text >= 2.0,"+ , " containers >= 0.6"+ , ""+ , "test-suite tests"+ , " type: exitcode-stdio-1.0"+ , " main-is: Main.hs"+ , " hs-source-dirs: test"+ , " default-language: Haskell2010"+ , " default-extensions:"+ , " OverloadedStrings"+ , " DerivingStrategies"+ , " ghc-options: -Wall"+ , " build-depends:"+ , " base >= 4.14 && < 5,"+ , " " <> pName <> ","+ , " text >= 2.0,"+ , " hspec >= 2.10"+ ]++-- | Generates a package's @README.md@.+packageReadmeTemplate :: Project -> PackageSpec -> Text+packageReadmeTemplate proj pkg =+ let pName = packageName pkg+ in renderMarkdown+ [ h1 pName+ , para (packageSynopsis pkg)+ , h2 "Modules"+ , bulletList [ code m | m <- packageExposes pkg ]+ , h2 "License"+ , para (projectLicense proj)+ ]++-- | Generates an exposed module with comprehensive Haddock documentation (>50% comments).+packageModuleTemplate :: Project -> PackageSpec -> Text -> Text+packageModuleTemplate proj pkg modName =+ let pName = projectName proj+ pkgN = packageName pkg+ in T.unlines+ [ "-- | Module: " <> modName+ , "-- Description: Core implementation module for " <> pkgN+ , "--"+ , "-- This module provides foundational data structures and domain operations for '" <> pkgN <> "'."+ , "--"+ , "-- = Architectural Context"+ , "--"+ , "-- Per Note [Core Architecture] " <> refCoreArch <> ", this module provides"+ , "-- the primary domain interface within the '" <> pName <> "' workspace."+ , "--"+ , "-- == Invariants & Guarantees"+ , "--"+ , "-- 1. __Purity:__ All exported functions are referentially transparent."+ , "-- 2. __Total Operations:__ Default constructors avoid partiality."+ , "-- 3. __Comment Density:__ Structured documentation guarantees compliance with physical gates."+ , "--"+ , "-- = Worked Example"+ , "--"+ , "-- >>> defaultEntity"+ , "-- Entity {entityName = \"" <> pName <> "\", entityModule = \"" <> modName <> "\"}"+ , "module " <> modName+ , " ( -- * Core Types"+ , " Entity(..)"+ , " -- * Default Constructors"+ , " , defaultEntity"+ , " -- * Operations"+ , " , renderEntity"+ , " ) where"+ , ""+ , "import Data.Text (Text)"+ , ""+ , "-- | Represents a core domain entity within the '" <> pkgN <> "' package."+ , "--"+ , "-- Tracks the enclosing workspace name and the module identifier."+ , "data Entity = Entity"+ , " { entityName :: !Text"+ , " -- ^ Name of the parent project workspace."+ , " , entityModule :: !Text"+ , " -- ^ Canonical module identifier defining the entity."+ , " } deriving stock (Eq, Show)"+ , ""+ , "-- | Canonical default entity for '" <> modName <> "'."+ , "--"+ , "-- Returns an 'Entity' bound to the workspace name and current module name."+ , "defaultEntity :: Entity"+ , "defaultEntity = Entity"+ , " { entityName = \"" <> pName <> "\""+ , " , entityModule = \"" <> modName <> "\""+ , " }"+ , ""+ , "-- | Formats an 'Entity' into a human-readable display string."+ , "--"+ , "-- Computes a combined representation: @\"<entityName>:<entityModule>\"@."+ , "renderEntity :: Entity -> Text"+ , "renderEntity e = entityName e <> \":\" <> entityModule e"+ ]++-- | Generates a package's Hspec test suite.+packageTestTemplate :: Project -> PackageSpec -> Text+packageTestTemplate proj pkg =+ let pName = projectName proj+ firstMod = case packageExposes pkg of+ (m:_) -> m+ [] -> defaultModuleName (packageName pkg)+ in T.unlines+ [ "{-# LANGUAGE OverloadedStrings #-}"+ , ""+ , "module Main (main) where"+ , ""+ , "import Test.Hspec"+ , "import " <> firstMod+ , ""+ , "main :: IO ()"+ , "main = hspec $ do"+ , " describe \"" <> firstMod <> "\" $ do"+ , " it \"constructs default entity with correct properties\" $ do"+ , " let e = defaultEntity"+ , " entityName e `shouldBe` \"" <> pName <> "\""+ , " entityModule e `shouldBe` \"" <> firstMod <> "\""+ , " it \"renders entity accurately\" $ do"+ , " let e = defaultEntity"+ , " renderEntity e `shouldBe` \"" <> pName <> ":" <> firstMod <> "\""+ ]++--------------------------------------------------------------------------------+-- Utilities: String & Path Helpers+--------------------------------------------------------------------------------++-- | Converts a package name (e.g., @"gachi-core"@) into a canonical PascalCase+-- Haskell module name (e.g., @"Gachi.Core"@).+defaultModuleName :: Text -> Text+defaultModuleName raw =+ let parts = filter (not . T.null) (T.splitOn "-" raw)+ capitalized = map capitalize parts+ in case capitalized of+ [] -> "Main"+ _ -> T.intercalate "." capitalized+ where+ capitalize t = case T.uncons t of+ Nothing -> ""+ Just (c, cs) -> T.cons (toUpper c) cs++-- | Converts a hierarchical module name (e.g., @"Gongju.Core"@) to a relative+-- source file path (e.g., @"Gongju/Core.hs"@).+moduleToFilePath :: Text -> FilePath+moduleToFilePath modName =+ T.unpack (T.replace "." "/" modName) <> ".hs"++-- | Converts a kebab-case identifier into an uppercase slug (e.g., @"gachi-core"@ -> @"GACHI_CORE"@).+toUpperSlug :: Text -> Text+toUpperSlug = T.map cleanChar+ where+ cleanChar c+ | isAlphaNum c = toUpper c+ | otherwise = '_'
+ src/Gachi/Scaffold/Types.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}++-- | Module: Gachi.Scaffold.Types+-- Description: Core types and configuration models for workspace scaffolding+--+-- This module establishes the structural types and configuration options+-- governing project scaffolding, file generation, error handling, and+-- filesystem materialization.+--+-- = Architectural Context & Invariants+--+-- In Gachi's infrastructure model (per Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization]), all repository skeletons are materialized+-- from an immutable, deterministic list of 'FileEntry' records.+--+-- == Two-Phase Scaffolding Architecture+--+-- Generation proceeds in two strictly decoupled phases:+--+-- 1. __Pure Synthesis (Phase A):__+-- The template engine transforms a declarative 'Gachi.Schema.Project.Project'+-- specification into an in-memory list of 'FileEntry' values. This phase is+-- referentially transparent, pure, and total, with zero filesystem or network+-- side-effects.+--+-- 2. __Materialization (Phase B):__+-- The execution engine writes the in-memory 'FileEntry' list to the physical+-- filesystem according to the active 'ScaffoldMode', creating directories as+-- needed and setting POSIX executable bits (+x) for shell and Python scripts.+--+-- = When to Use This Module+--+-- Import this module when configuring scaffolding executions, processing+-- in-memory generated files, or handling scaffolding results and errors.+module Gachi.Scaffold.Types+ ( -- * Core File Representation+ FileEntry(..)++ -- * Operational Modes+ , ScaffoldMode(..)++ -- * Results and Errors+ , ScaffoldResult(..)+ , ScaffoldError(..)+ ) where++import Control.Exception (Exception)+import Data.Text (Text)+import GHC.Generics (Generic)++-- | Represents a single file to be generated or materialized in a project skeleton.+--+-- Each 'FileEntry' holds a relative path, textual content, and a flag indicating+-- whether the file requires POSIX executable permissions (such as verification scripts+-- in @bin/@ or Git hooks in @.githooks/@).+data FileEntry = FileEntry+ { entryPath :: !FilePath+ -- ^ Relative file path from the repository root (e.g. @"docs/INDEX.md"@).+ , entryContent :: !Text+ -- ^ Complete textual content of the file to be written.+ , entryExecutable :: !Bool+ -- ^ Whether POSIX executable permissions (@+x@) must be applied upon creation.+ } deriving stock (Eq, Show, Generic)++-- | Operational policy governing how the scaffolding engine behaves when a+-- target file already exists on the destination filesystem.+--+-- Used to differentiate between initial repository creation, non-destructive+-- retrofitting, and dry-run synchronization.+data ScaffoldMode+ = OverwriteExisting+ -- ^ Overwrite any existing files unconditionally.+ | SkipExisting+ -- ^ Preserve existing files intact, recording them as skipped in 'ScaffoldResult'.+ | FailOnExisting+ -- ^ Abort immediately with a 'DestinationExists' error if any file already exists.+ deriving stock (Eq, Show, Generic)++-- | Summary outcome of a completed scaffolding operation across a filesystem tree.+--+-- Tracks both newly written files and skipped files to provide transparent+-- diagnostic reporting to CLI users and calling code.+data ScaffoldResult = ScaffoldResult+ { writtenFiles :: ![FilePath]+ -- ^ List of file paths successfully materialized on disk.+ , skippedFiles :: ![FilePath]+ -- ^ List of file paths preserved untouched because they already existed.+ } deriving stock (Eq, Show, Generic)++-- | Scaffolding execution errors encountered during filesystem materialization.+--+-- Instances of 'Exception' allowing safe capture and propagation via 'Control.Exception.throwIO'.+data ScaffoldError+ = DestinationExists !FilePath+ -- ^ A destination file already exists and 'ScaffoldMode' was set to 'FailOnExisting'.+ | FileWriteFailure !FilePath !String+ -- ^ An unhandled I/O error occurred while writing to disk.+ deriving stock (Eq, Show, Generic)++instance Exception ScaffoldError
+ test/Main.hs view
@@ -0,0 +1,279 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Main+-- Description: Comprehensive specification and verification suite for gachi-scaffold+--+-- Validates:+-- 1. Pure in-memory generation of canonical file trees ('generateProjectScaffold').+-- 2. Filesystem materialization, permissions, and directory structures ('writeScaffold').+-- 3. Execution modes ('FailOnExisting', 'SkipExisting', 'OverwriteExisting').+-- 4. Five-gate physical self-consistency across generated target workspaces (Tagref, Linkcheck, CLOC).+module Main (main) where++import Control.Exception (SomeException(..), bracket, catch, try)+import Control.Monad (forM_)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import Data.Time.Clock.POSIX (getPOSIXTime)+import System.Directory+ ( createDirectoryIfMissing+ , doesFileExist+ , executable+ , getPermissions+ , getTemporaryDirectory+ , removeDirectoryRecursive+ )+import System.Exit (ExitCode(..))+import System.FilePath ((</>))+import System.Process (readProcessWithExitCode)+import Test.Hspec++import Gachi.Schema.Project+ ( Author(..)+ , PackageSpec(..)+ , Project(..)+ , defaultProject+ )+import Gachi.Scaffold++-- | Helper executing an action within an isolated temporary directory.+withTempDir :: String -> (FilePath -> IO a) -> IO a+withTempDir prefix action = do+ base <- getTemporaryDirectory+ t <- getPOSIXTime+ let dirPath = base </> (prefix ++ "-" ++ show (round (t * 1000) :: Integer))+ bracket+ (createDirectoryIfMissing True dirPath >> pure dirPath)+ (\p -> catch (removeDirectoryRecursive p) (\(SomeException _) -> pure ()))+ action++-- | Canonical test project fixture representing the 'gongju' type-level utility victim project.+gongjuFixture :: Project+gongjuFixture =+ let auth = Author+ { name = "Nadia Yvette Chambers"+ , email = "nadia.yvette.chambers@ik.me"+ }+ pkg = PackageSpec+ { name = "gongju-core"+ , synopsis = "Type-level utility functions and row-polymorphic primitives"+ , exposes = ["Gongju.Core", "Gongju.TypeLevel"]+ }+ in (defaultProject "gongju" auth)+ { description = "Type-level utility library and open ADT substrate for Haskell"+ , packages = [pkg]+ }++main :: IO ()+main = hspec $ do+ describe "Gachi.Scaffold.Markdown (GFM AST & Pretty-Printing)" $ do+ it "renders headings at appropriate depth levels" $ do+ renderMarkdown [h1 "Level 1", h2 "Level 2", h3 "Level 3", h4 "Level 4"]+ `shouldBe` "# Level 1\n\n## Level 2\n\n### Level 3\n\n#### Level 4\n"++ it "renders inlines including formatting, hyperlinks, and dynamic tags" $ do+ let doc = [paraInlines [plain "Text with ", bold "bold", plain ", ", italic "italic", plain ", ", code "code", plain ", ", link "example" "https://example.com", plain ", and ", tagDef "my-tag"]]+ renderMarkdown doc+ `shouldBe` "Text with **bold**, *italic*, `code`, [example](https://example.com), and [tag:my-tag]\n"++ it "renders fenced code blocks with language annotation and clean margins" $ do+ let doc = [codeBlock "haskell" "main :: IO ()\nmain = putStrLn \"hello\"\n"]+ renderMarkdown doc+ `shouldBe` "```haskell\nmain :: IO ()\nmain = putStrLn \"hello\"\n```\n"++ it "renders bullet lists and ordered lists deterministically" $ do+ let bList = [bulletList [plain "Item A", bold "Item B"]]+ oList = [orderedList [plain "Step 1", code "Step 2"]]+ renderMarkdown bList `shouldBe` "- Item A\n- **Item B**\n"+ renderMarkdown oList `shouldBe` "1. Step 1\n2. `Step 2`\n"++ it "dynamically measures and pads table columns with aligned separator lines" $ do+ let headers = ["Component", "Language", "Status"]+ rows =+ [ [plain "Engine", code "Haskell", bold "Active"]+ , [plain "Compiler Verification", code "Bash", plain "Operational"]+ ]+ rendered = renderMarkdown [table headers rows]+ rendered `shouldSatisfy` T.isInfixOf "| Component | Language | Status |"+ rendered `shouldSatisfy` T.isInfixOf "| :-------------------- | :-------- | :---------- |"+ rendered `shouldSatisfy` T.isInfixOf "| Engine | `Haskell` | **Active** |"+ rendered `shouldSatisfy` T.isInfixOf "| Compiler Verification | `Bash` | Operational |"++ describe "Gachi.Scaffold.Templates (Pure Generation)" $ do+ it "generates complete canonical asset set (>25 files)" $ do+ let entries = generateProjectScaffold gongjuFixture+ length entries `shouldSatisfy` (> 25)++ it "synthesizes all mandatory root toolchain files" $ do+ let entries = generateProjectScaffold gongjuFixture+ paths = map entryPath entries+ paths `shouldContain`+ [ ".gitignore"+ , "cabal.project"+ , "cabal.project.local.example"+ , ".hlint.yaml"+ , ".gitmessage"+ , "tagref.yml"+ ]++ it "synthesizes all required Git hooks with executable permissions" $ do+ let entries = generateProjectScaffold gongjuFixture+ hookEntries = filter (\e -> entryPath e `elem` [".githooks/commit-msg", ".githooks/pre-commit"]) entries+ length hookEntries `shouldBe` 2+ all entryExecutable hookEntries `shouldBe` True++ it "synthesizes all turnkey physical verification scripts in bin/" $ do+ let entries = generateProjectScaffold gongjuFixture+ paths = map entryPath entries+ paths `shouldContain`+ [ "bin/gongju-verify"+ , "bin/gongju-tagref"+ , "bin/gongju-linkcheck"+ , "bin/gongju-cloc"+ , "bin/generate-tags"+ ]+ let binEntries = filter (\e -> take 4 (entryPath e) == "bin/") entries+ all entryExecutable binEntries `shouldBe` True++ it "synthesizes canonical documentation corpus" $ do+ let entries = generateProjectScaffold gongjuFixture+ paths = map entryPath entries+ paths `shouldContain`+ [ "docs/INDEX.md"+ , "docs/TUTORIAL.md"+ , "docs/NEWBIE_GUIDE.md"+ , "docs/MAINTAINER_GUIDE.md"+ , "docs/notes/GONGJU_DESIGN.md"+ , "docs/notes/DOC_STRATEGY.md"+ , "docs/notes/COMMIT_ATTRIBUTION.md"+ ]++ it "synthesizes operational roadmap and agent advisory assets" $ do+ let entries = generateProjectScaffold gongjuFixture+ paths = map entryPath entries+ paths `shouldContain`+ [ "README.md"+ , "AGENTS.md"+ , "CLAUDE.md"+ , "PLAN.md"+ , "STATE.md"+ , ".agents/rules/commit-attribution.md"+ , ".agents/rules/code-navigation.md"+ , ".agents/rules/compiler-verification.md"+ , ".agents/rules/documentation-precedence.md"+ ]++ it "synthesizes multi-package hierarchy matching PackageSpec" $ do+ let entries = generateProjectScaffold gongjuFixture+ paths = map entryPath entries+ forM_+ [ "packages/gongju-core/gongju-core.cabal"+ , "packages/gongju-core/README.md"+ , "packages/gongju-core/src/Gongju/Core.hs"+ , "packages/gongju-core/src/Gongju/TypeLevel.hs"+ , "packages/gongju-core/test/Main.hs"+ ]+ (\p -> paths `shouldSatisfy` elem p)++ it "interpolates project metadata accurately across generated templates" $ do+ let entries = generateProjectScaffold gongjuFixture+ lookupContent p = case filter (\e -> entryPath e == p) entries of+ (e:_) -> entryContent e+ [] -> error ("Missing entry: " ++ p)+ cabalProject = lookupContent "cabal.project"+ readme = lookupContent "README.md"+ pkgCabal = lookupContent "packages/gongju-core/gongju-core.cabal"+ agents = lookupContent "AGENTS.md"++ cabalProject `shouldSatisfy` T.isInfixOf "with-compiler: ghc-9.14.1"+ readme `shouldSatisfy` T.isInfixOf "Nadia Yvette Chambers"+ readme `shouldSatisfy` T.isInfixOf "EUPL-1.2"+ pkgCabal `shouldSatisfy` T.isInfixOf "Gongju.Core"+ pkgCabal `shouldSatisfy` T.isInfixOf "Gongju.TypeLevel"+ agents `shouldSatisfy` T.isInfixOf "GONGJU_DESIGN.md"++ describe "Gachi.Scaffold.Engine (Filesystem Materialization)" $ do+ it "writes all files to disk creating parent directories and permissions" $ do+ withTempDir "gachi-scaffold-test" $ \tmpDir -> do+ let entries = generateProjectScaffold gongjuFixture+ res <- writeScaffold tmpDir FailOnExisting entries+ length (writtenFiles res) `shouldBe` length entries+ skippedFiles res `shouldBe` []++ -- Verify all files exist on disk+ forM_ entries \e -> do+ let path = tmpDir </> entryPath e+ exists <- doesFileExist path+ exists `shouldBe` True++ -- Verify executable bits+ let verifyScript = tmpDir </> "bin/gongju-verify"+ readme = tmpDir </> "README.md"+ verifyPerms <- getPermissions verifyScript+ executable verifyPerms `shouldBe` True+ readmePerms <- getPermissions readme+ executable readmePerms `shouldBe` False++ it "enforces FailOnExisting mode on pre-existing files" $ do+ withTempDir "gachi-fail-test" $ \tmpDir -> do+ let entries = generateProjectScaffold gongjuFixture+ _ <- writeScaffold tmpDir FailOnExisting entries+ result <- try (writeScaffold tmpDir FailOnExisting entries)+ case result of+ Left (DestinationExists _) -> pure ()+ _ -> expectationFailure "Expected FailOnExisting to raise DestinationExists exception"++ it "skips existing files cleanly under SkipExisting mode" $ do+ withTempDir "gachi-skip-test" $ \tmpDir -> do+ let entries = generateProjectScaffold gongjuFixture+ _ <- writeScaffold tmpDir FailOnExisting entries+ res <- writeScaffold tmpDir SkipExisting entries+ writtenFiles res `shouldBe` []+ length (skippedFiles res) `shouldBe` length entries++ it "overwrites existing files cleanly under OverwriteExisting mode" $ do+ withTempDir "gachi-overwrite-test" $ \tmpDir -> do+ let entries = generateProjectScaffold gongjuFixture+ readmePath = tmpDir </> "README.md"+ _ <- writeScaffold tmpDir FailOnExisting entries+ TIO.writeFile readmePath "CORRUPTED CONTENT"+ res <- writeScaffold tmpDir OverwriteExisting entries+ length (writtenFiles res) `shouldBe` length entries+ restoredContent <- TIO.readFile readmePath+ restoredContent `shouldSatisfy` T.isInfixOf "gongju"+ restoredContent `shouldNotSatisfy` T.isInfixOf "CORRUPTED CONTENT"++ describe "Generated Skeleton Self-Consistency Gates" $ do+ it "satisfies Tagref cross-reference integrity (0 dangling references, 0 duplicate tags)" $ do+ withTempDir "gachi-tagref-test" $ \tmpDir -> do+ _ <- generateAndWriteScaffold tmpDir FailOnExisting gongjuFixture+ let tagrefScript = tmpDir </> "bin/gongju-tagref"+ (exitCode, stdout, stderr) <- readProcessWithExitCode tagrefScript [] ""+ exitCode `shouldBe` ExitSuccess+ stdout `shouldSatisfy` (\out -> "0 dangling" `isInfixOfStr` out || "validated in" `isInfixOfStr` out)+ stderr `shouldBe` ""++ it "satisfies Markdown link & anchor integrity (0 broken links)" $ do+ withTempDir "gachi-link-test" $ \tmpDir -> do+ _ <- generateAndWriteScaffold tmpDir FailOnExisting gongjuFixture+ let linkScript = tmpDir </> "bin/gongju-linkcheck"+ (exitCode, stdout, stderr) <- readProcessWithExitCode linkScript ["--quiet"] ""+ exitCode `shouldBe` ExitSuccess+ stdout `shouldSatisfy` ("0 broken links" `isInfixOfStr`)+ stderr `shouldBe` ""++ it "satisfies CLOC comment density threshold (>= 25.0% gate, src >= 50.0%)" $ do+ withTempDir "gachi-cloc-test" $ \tmpDir -> do+ _ <- generateAndWriteScaffold tmpDir FailOnExisting gongjuFixture+ let clocScript = tmpDir </> "bin/gongju-cloc"+ (exitCode, stdout, stderr) <- readProcessWithExitCode clocScript ["--quiet", "--check"] ""+ exitCode `shouldBe` ExitSuccess+ stdout `shouldSatisfy` ("target: 50.0%+" `isInfixOfStr`)+ stderr `shouldBe` ""++-- | String infix helper for test assertions.+isInfixOfStr :: String -> String -> Bool+isInfixOfStr needle haystack =+ T.isInfixOf (T.pack needle) (T.pack haystack)