packages feed

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 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)