packages feed

gachi-sync (empty) → 0.1.0.0

raw patch · 12 files changed

+6254/−0 lines, 12 filesdep +basedep +bytestringdep +containers

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

Files

+ gachi-sync.cabal view
@@ -0,0 +1,80 @@+cabal-version: 3.0+name: gachi-sync+version: 0.1.0.0+synopsis: Cross-repository standardization matrix and configuration synchronization for Gachi+description:+  Implements the 9-dimensional ecosystem standardization matrix (~/src/PLAN.md §1.A),+  canonical shared asset catalog, repository audit engine, and harmonic configuration+  propagation across ecosystem checkouts ('gachi sync').+license: EUPL-1.2+author: Nadia Yvette Chambers+maintainer: nadia.yvette.chambers@ik.me+category: Development, Tools+build-type: Simple+tested-with:+  GHC == 9.10.3,+  GHC == 9.12.4,+  GHC == 9.14.1++library+  exposed-modules:+    Gachi.Sync+    Gachi.Sync.Audit+    Gachi.Sync.Catalog+    Gachi.Sync.CI+    Gachi.Sync.Engine+    Gachi.Sync.Hash+    Gachi.Sync.Matrix+    Gachi.Sync.Remote+    Gachi.Sync.Sheaf+    Gachi.Sync.Types+  hs-source-dirs: src+  default-language: Haskell2010+  default-extensions:+    OverloadedStrings+    DerivingStrategies+    ScopedTypeVariables+  ghc-options:+    -Wall+    -Wcompat+    -Wredundant-constraints+  build-depends:+    base >= 4.14 && < 5,+    bytestring >= 0.10,+    text >= 2.0,+    containers >= 0.6,+    directory >= 1.3,+    filepath >= 1.4,+    process >= 1.6,+    gachi-core >= 0.1,+    gachi-schema >= 0.1,+    gachi-scaffold >= 0.1++test-suite tests+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs: test+  default-language: Haskell2010+  default-extensions:+    OverloadedStrings+    DerivingStrategies+    ScopedTypeVariables+  ghc-options:+    -Wall+    -Wcompat+    -Wredundant-constraints+    -threaded+  build-depends:+    base >= 4.14 && < 5,+    gachi-sync,+    gachi-core,+    gachi-schema,+    gachi-scaffold,+    bytestring >= 0.10,+    text >= 2.0,+    containers >= 0.6,+    directory >= 1.3,+    filepath >= 1.4,+    process >= 1.6,+    time >= 1.9,+    hspec >= 2.10
+ src/Gachi/Sync.hs view
@@ -0,0 +1,122 @@+-- | Module: Gachi.Sync+-- Description: Root umbrella module for Gachi cross-repository standardization & synchronization+--+-- This is the primary entry point for the @gachi-sync@ library. It exposes+-- the 9-dimensional Standardization Matrix (~/src/PLAN.md §1.A), canonical+-- shared asset generators, non-destructive audit engine, and diagnostic reporting.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization] and Note [Cross-Repository Standardization Matrix and Audit Semantics]+-- [ref:note-standardization-matrix], all repositories across ~/src/* share an+-- authoritative structural skeleton. This library provides the declarative+-- contracts and inspection machinery that guarantee structural consistency.+--+-- == The Four Standardization Subsystems+--+-- The Gachi synchronization subsystem is organized into four complementary layers:+--+-- 1.  __Core Types and Models ("Gachi.Sync.Types"):__+--     Formalizes the 9 dimensions as 'AssetCategory', individual asset models+--     as 'StandardAsset', composite matrices as 'StandardizationMatrix', and+--     inspection outcomes as 'AssetAuditStatus' and 'AuditReport'.+--+-- 2.  __Shared Asset Catalog ("Gachi.Sync.Catalog"):__+--     Pure, total template generators for Fourmolu, rustfmt, clang-format,+--     strict HLint rules, Cabal project pinning, Git commit-msg and pre-commit+--     hooks, forge-tiered CI workflows, and AI agent instructions.+--+-- 3.  __Canonical Standardization Matrix ("Gachi.Sync.Matrix"):__+--     The authoritative 'canonicalMatrix' registry aggregating all standardized+--     assets across the 9 dimensions, complete with category queries and filter+--     combinators ('assetsForCategory', 'requiredAssets', 'executableAssets').+--+-- 4.  __Non-Destructive Audit Engine ("Gachi.Sync.Audit"):__+--     Read-only filesystem analyzer ('auditRepository', 'auditRepositoryQuick')+--     evaluating checkouts against matrix contracts, detecting content and+--     permission drift, and rendering diagnostic ASCII tables ('formatAuditReport').+--+-- 5.  __Forge-Tiered CI Matrix Synthesis ("Gachi.Sync.CI"):__+--     Declarative models ('ForgeType', 'CITier', 'CISpec') and deterministic+--     generators synthesizing high-resource GitHub Actions workflows and lightweight+--     community CI pipelines (GitLab, Woodpecker, Forgejo, Radicle).+--+-- 6.  __Multi-Remote Replication Topology & Provider Compliance ("Gachi.Sync.Remote"):__+--     Formal models ('RemoteCategory', 'ProviderPolicy', 'ConfiguredRemote',+--     'RemoteTopologyReport', 'RemoteSyncPlan') and operational executors+--     enforcing the canonical 5-forge replication topology (Dimension 6: Radicle,+--     GitHub, GitCode, Framagit, Disroot) while filtering and purging non-compliant+--     hosting platforms (SourceHut, plaintext HTTP).+--+-- == Operational Invariants+--+-- All modules within @gachi-sync@ adhere to strict engineering guarantees:+-- * __Purity & Reproducibility:__ All generators evaluate from typed records+--   without reading environment variables or accessing the network.+-- * __Observational Non-Destruction:__ The audit engine evaluates disk checkouts+--   using read-only inspection primitives, guaranteeing zero accidental overwrites.+-- * __Toolchain Hermeticity:__ Pinned to GHC 9.14.1 and Cabal 3.18 under strict+--   compiler warnings (@-Wall -Wcompat -Wredundant-constraints@) and zero HLint hints.+--+-- = Worked Example: Auditing a Target Checkout+--+-- The following example demonstrates auditing an existing repository against+-- the canonical standardization matrix:+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync+-- >>> let auth = Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me"+-- >>> let proj = defaultProject "gachi" auth+-- >>> report <- auditRepository "/home/nyc/src/gachi" proj canonicalMatrix+-- >>> reportCompliantCount report > 0+-- True+-- >>> length (filterMissingResults report) == 0+-- True+--+-- = Worked Example: Quick Audit Without Explicit Project+--+-- When project specifications are not known upfront, 'auditRepositoryQuick'+-- automatically infers the project name from the checkout path or @project.dhall@:+--+-- >>> res <- auditRepositoryQuick "/home/nyc/src/gachi"+-- >>> case res of { Right rep -> reportCompliancePct rep >= 50.0; Left _ -> False }+-- True+module Gachi.Sync+  ( -- * Core Standardization Types+    module Gachi.Sync.Types++    -- * Canonical Standardization Matrix+  , module Gachi.Sync.Matrix++    -- * Shared Asset Catalog+  , module Gachi.Sync.Catalog++    -- * Non-Destructive Audit Engine+  , module Gachi.Sync.Audit++    -- * Harmonic Chattering Synchronization Engine+  , module Gachi.Sync.Engine++    -- * Differential Cryptographic Hashing+  , module Gachi.Sync.Hash++    -- * Forge-Tiered CI Matrix Synthesis+  , module Gachi.Sync.CI++    -- * Multi-Remote Replication Topology & Provider Compliance+  , module Gachi.Sync.Remote++    -- * Cellular Sheaf Cohomology & Ecosystem Coherence+  , module Gachi.Sync.Sheaf+  ) where++import Gachi.Sync.Audit+import Gachi.Sync.Catalog+import Gachi.Sync.CI+import Gachi.Sync.Engine+import Gachi.Sync.Hash+import Gachi.Sync.Matrix+import Gachi.Sync.Remote+import Gachi.Sync.Sheaf+import Gachi.Sync.Types
+ src/Gachi/Sync/Audit.hs view
@@ -0,0 +1,389 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.Sync.Audit+-- Description: Non-destructive repository audit engine and diagnostic reporter+--+-- This module implements the read-only audit engine for inspecting local+-- checkouts against the canonical Standardization Matrix (~/src/PLAN.md §1.A).+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization] and Note [Cross-Repository Standardization Matrix and Audit Semantics]+-- [ref:note-standardization-matrix], audits are strictly read-only and non-destructive.+-- They evaluate physical file existence, exact textual content matching against+-- canonical generators, and POSIX executable permissions (@+x@) without mutating+-- any filesystem state.+--+-- == Audit Classification Rules+--+-- For each asset registered in the 'StandardizationMatrix', the audit engine produces+-- exactly one 'AssetAuditResult' containing an 'AssetAuditStatus':+--+-- * 'StatusCompliant':+--   The file exists at the expected path, its textual content matches the canonical+--   generator byte-for-byte, and its executable bits conform to the specification.+--+-- * 'StatusMissing':+--   The file does not exist on disk. For required assets, this is flagged as an+--   audit violation; for optional assets (e.g. polyglot formatting rules), it is+--   recorded without failing the verification gate.+--+-- * 'StatusContentDrift':+--   The file exists on disk, but its content differs from the output produced by+--   the canonical generator. The lengths of actual and expected contents are recorded.+--+-- * 'StatusPermissionDrift':+--   The file exists and its content matches, but its executable bit differs from+--   'assetExecutable' (e.g. a script in @bin/@ lacking executable permissions).+--+-- * 'StatusIOError':+--   An unexpected filesystem exception occurred while attempting to read the file.+--+-- == Read-Only & Non-Destructive Invariant+--+-- The audit engine strictly adheres to the principle of observational purity:+-- 1. __Zero Mutations:__ No files, symlinks, or directories are created, modified,+--    or deleted during audit execution.+-- 2. __Exception Containment:__ I/O exceptions (e.g., permission denied, broken symlinks,+--    dangling filesystem nodes) are captured and reported as 'StatusIOError' rather+--    than aborting the audit process abruptly.+-- 3. __Differential Hashing:__ Content comparisons are performed in memory against+--    pure generator outputs, ensuring that audits require no network access or external+--    database queries.+--+-- = Worked Example+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync.Matrix (canonicalMatrix)+-- >>> let auth = Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me"+-- >>> let proj = defaultProject "gachi" auth+-- >>> report <- auditRepository "/home/nyc/src/gachi" proj canonicalMatrix+-- >>> reportTotalAssets report >= 20+-- True+-- >>> reportCompliancePct report >= 50.0+-- True+module Gachi.Sync.Audit+  ( -- * Repository Audit Operations+    auditRepository+  , auditAsset+  , auditRepositoryQuick++    -- * Diagnostic Report Formatting+  , formatAuditReport+  , formatAuditReportString++    -- * Result Filter Helpers+  , filterCompliantResults+  , filterDriftResults+  , filterMissingResults+  ) where++import Control.Exception (SomeException, catch, displayException)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory (doesFileExist, getPermissions, executable)+import System.FilePath ((</>), takeFileName)++import Gachi.Schema.Dhall (decodeProjectFile)+import Gachi.Schema.Project (Author(..), Project, projectName, defaultProject)+import Gachi.Sync.Matrix (canonicalMatrix)+import Gachi.Sync.Types+  ( AssetAuditResult(..)+  , AssetAuditStatus(..)+  , AuditReport(..)+  , StandardAsset(..)+  , StandardizationMatrix(..)+  , categoryName+  , isCompliant+  , isDrift+  , isMissing+  , resolveAssetPath+  )++--------------------------------------------------------------------------------+-- 1. Single Asset Audit+--------------------------------------------------------------------------------++-- | Inspects an individual standardized asset against a target workspace.+--+-- Resolves the asset's concrete path using 'resolveAssetPath', checks for file+-- existence, reads the file's textual contents, checks executable permissions,+-- and compares the actual state against the expected generator output.+--+-- This operation is strictly read-only and catches all filesystem exceptions,+-- returning 'StatusIOError' upon failure rather than throwing unhandled exceptions.+--+-- === Evaluation Steps+--+-- 1. Resolves concrete relative path via 'resolveAssetPath' (substituting @:name:@ tokens).+-- 2. Checks existence using 'System.Directory.doesFileExist'.+-- 3. If file does not exist, returns 'StatusMissing'.+-- 4. If file exists, reads text content using 'TIO.readFile'.+-- 5. Evaluates the pure generator 'assetGenerator' against 'proj'.+-- 6. If textual contents diverge, returns 'StatusContentDrift'.+-- 7. If contents match, inspects POSIX executable permissions.+-- 8. If executable requirements differ, returns 'StatusPermissionDrift'.+-- 9. If everything matches, returns 'StatusCompliant'.+--+-- === Worked Example+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> let proj = defaultProject "gachi" (Author "Nadia" "nadia@example.com")+-- >>> let asset = StandardAsset ".hlint.yaml" Gachi.Sync.Types.CategoryLinter True False "HLint" (const "")+-- >>> result <- auditAsset "/home/nyc/src/gachi" proj asset+-- >>> resultAsset result == asset+-- True+auditAsset :: FilePath -> Project -> StandardAsset -> IO AssetAuditResult+auditAsset repoRoot proj asset = do+  let relPath = resolveAssetPath proj asset+      absPath = repoRoot </> relPath+  exists <- doesFileExist absPath+  if not exists+    then pure (AssetAuditResult asset StatusMissing)+    else checkContentAndPerms absPath+  where+    checkContentAndPerms absPath = (do+      actualContent <- TIO.readFile absPath+      let expectedContent = assetGenerator asset proj+      if actualContent /= expectedContent+        then+          let actualLen = T.length actualContent+              expectedLen = T.length expectedContent+          in pure (AssetAuditResult asset (StatusContentDrift actualLen expectedLen))+        else do+          perms <- getPermissions absPath+          let actualExec = executable perms+              expectedExec = assetExecutable asset+          if expectedExec && not actualExec+            then pure (AssetAuditResult asset (StatusPermissionDrift actualExec expectedExec))+            else pure (AssetAuditResult asset StatusCompliant)+      ) `catch` (\(e :: SomeException) ->+          pure (AssetAuditResult asset (StatusIOError (T.pack (displayException e)))))++--------------------------------------------------------------------------------+-- 2. Complete Repository Audit+--------------------------------------------------------------------------------++-- | Audits an entire repository workspace against a 'StandardizationMatrix'.+--+-- Evaluates each asset registered in 'matrixAssets', tabulates compliance and+-- discrepancy metrics, and constructs a comprehensive 'AuditReport' summarizing+-- findings across all standardization dimensions.+--+-- === Metrics Computed+--+-- * 'reportTotalAssets': Total count of assets evaluated.+-- * 'reportCompliantCount': Assets matching content and permissions exactly.+-- * 'reportDriftCount': Assets present on disk with differing text or permissions.+-- * 'reportMissingCount': Required assets absent from the filesystem.+-- * 'reportCompliancePct': Ratio of compliant assets to total assets (0.0% to 100.0%).+--+-- === Worked Example+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync.Matrix (canonicalMatrix)+-- >>> let proj = defaultProject "gachi" (Author "Nadia" "nadia@example.com")+-- >>> report <- auditRepository "/home/nyc/src/gachi" proj canonicalMatrix+-- >>> reportTotalAssets report >= 20+-- True+auditRepository :: FilePath -> Project -> StandardizationMatrix -> IO AuditReport+auditRepository repoRoot proj matrix = do+  results <- traverse (auditAsset repoRoot proj) (matrixAssets matrix)+  let total = length results+      compliant = length (filter (isCompliant . resultStatus) results)+      drift = length (filter (isDrift . resultStatus) results)+      missing = length (filter (\r -> isMissing (resultStatus r) && assetRequired (resultAsset r)) results)+      pct = if total == 0+              then 100.0+              else (fromIntegral compliant / fromIntegral total) * 100.0+  pure AuditReport+    { reportTargetDir      = repoRoot+    , reportProjectName    = projectName proj+    , reportResults        = results+    , reportTotalAssets    = total+    , reportCompliantCount = compliant+    , reportDriftCount     = drift+    , reportMissingCount   = missing+    , reportCompliancePct  = pct+    }++-- | Fast, zero-configuration repository audit.+--+-- Attempts to locate and evaluate @project.dhall@ within 'repoRoot'. If present,+-- decodes the declarative specification via 'decodeProjectFile' and executes+-- 'auditRepository'. If absent, infers project metadata from the directory name+-- and standard author information, auditing against 'canonicalMatrix'.+--+-- Guarantees total evaluation: catches Dhall decoding exceptions and returns+-- a descriptive 'Left' error text rather than crashing.+--+-- === Worked Example+--+-- >>> res <- auditRepositoryQuick "/home/nyc/src/gachi"+-- >>> case res of { Right rep -> reportCompliancePct rep >= 50.0; Left _ -> False }+-- True+auditRepositoryQuick :: FilePath -> IO (Either Text AuditReport)+auditRepositoryQuick repoRoot = do+  let dhallPath = repoRoot </> "project.dhall"+  hasDhall <- doesFileExist dhallPath+  if hasDhall+    then (do+      proj <- decodeProjectFile dhallPath+      report <- auditRepository repoRoot proj canonicalMatrix+      pure (Right report)+      ) `catch` (\(e :: SomeException) ->+          pure (Left ("Failed to decode project.dhall: " <> T.pack (displayException e))))+    else do+      let inferredName = T.pack (takeFileName repoRoot)+          defaultAuthor = Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me"+          proj = defaultProject inferredName defaultAuthor+      report <- auditRepository repoRoot proj canonicalMatrix+      pure (Right report)++--------------------------------------------------------------------------------+-- 3. Result Filters+--------------------------------------------------------------------------------++-- | Filters an 'AuditReport' to only include compliant assets.+--+-- Useful for generating green status checklists and compliance confirmations.+filterCompliantResults :: AuditReport -> [AssetAuditResult]+filterCompliantResults = filter (isCompliant . resultStatus) . reportResults++-- | Filters an 'AuditReport' to only include assets exhibiting content or permission drift.+--+-- Used by synchronization engines ('gachi sync') to identify files requiring differential updates.+filterDriftResults :: AuditReport -> [AssetAuditResult]+filterDriftResults = filter (isDrift . resultStatus) . reportResults++-- | Filters an 'AuditReport' to only include missing required assets.+--+-- Identifies critical ecosystem gaps that must be populated during repository retrofitting.+filterMissingResults :: AuditReport -> [AssetAuditResult]+filterMissingResults =+  filter (\r -> isMissing (resultStatus r) && assetRequired (resultAsset r)) . reportResults++--------------------------------------------------------------------------------+-- 4. Diagnostic Report Formatting+--------------------------------------------------------------------------------++-- | Formats an 'AuditReport' as human-readable plain text with ASCII diagnostic tables.+--+-- Displays:+-- 1. Header banner with repository target path and compliance percentage.+-- 2. Summary statistics (compliant, drift, missing counts).+-- 3. Per-asset tabular evaluation detailing status, category, and relative path.+-- 4. Granular drift or missing asset diagnostics.+--+-- === Output Format Example+--+-- @+-- ================================================================================+--  Gachi Standardization Matrix Audit Report+-- ================================================================================+--  Target Directory: /home/nyc/src/gachi+--  Project Name:     gachi+--  Total Assets:     25+--  Compliant:        22+--  Drift Detected:   1+--  Missing Required: 2+--  Compliance Rate:  88.0%+-- --------------------------------------------------------------------------------+--  Status     | Dimension              | Path+-- ------------+------------------------+------------------------------------------+--  OK         | Code Formatters        | fourmolu.yaml+--  DRIFT      | Linters & Guardrails   | .hlint.yaml+--  MISSING    | Git Governance         | .githooks/commit-msg+-- ================================================================================+-- @+formatAuditReport :: AuditReport -> Text+formatAuditReport report = T.unlines $+  [ "================================================================================"+  , " Gachi Standardization Matrix Audit Report"+  , "================================================================================"+  , " Target Directory: " <> T.pack (reportTargetDir report)+  , " Project Name:     " <> reportProjectName report+  , " Total Assets:     " <> T.show (reportTotalAssets report)+  , " Compliant:        " <> T.show (reportCompliantCount report)+  , " Drift Detected:   " <> T.show (reportDriftCount report)+  , " Missing Required: " <> T.show (reportMissingCount report)+  , " Compliance Rate:  " <> formatPct (reportCompliancePct report)+  , "--------------------------------------------------------------------------------"+  , " Status     | Dimension              | Path"+  , "------------+------------------------+------------------------------------------"+  ]+  ++ map formatRow (reportResults report)+  ++ formatDriftSection (filterDriftResults report)+  ++ formatMissingSection (filterMissingResults report)+  +++  [ "================================================================================" ]+  where+    formatPct :: Double -> Text+    formatPct val =+      let whole = floor val :: Int+          dec = floor (val * 10) `mod` 10 :: Int+      in T.show whole <> "." <> T.show dec <> "%"++    formatRow :: AssetAuditResult -> Text+    formatRow res =+      let stat = formatStatus (resultStatus res)+          cat = padRight 22 (categoryName (assetCategory (resultAsset res)))+          path = T.pack (assetPath (resultAsset res))+      in " " <> padRight 10 stat <> " | " <> cat <> " | " <> path++    formatStatus :: AssetAuditStatus -> Text+    formatStatus StatusCompliant          = "OK"+    formatStatus StatusMissing            = "MISSING"+    formatStatus (StatusContentDrift _ _) = "DRIFT"+    formatStatus (StatusPermissionDrift _ _) = "PERM-DRIFT"+    formatStatus (StatusIOError _)        = "ERROR"++    padRight :: Int -> Text -> Text+    padRight n txt =+      let len = T.length txt+      in if len >= n+           then txt+           else txt <> T.replicate (n - len) " "++    formatDriftSection :: [AssetAuditResult] -> [Text]+    formatDriftSection [] = []+    formatDriftSection drifts =+      [ "--------------------------------------------------------------------------------"+      , " Content & Permission Drift Details:"+      ] ++ map explainDrift drifts++    explainDrift :: AssetAuditResult -> Text+    explainDrift res =+      let p = T.pack (assetPath (resultAsset res))+      in case resultStatus res of+        StatusContentDrift actual expected ->+          "  * " <> p <> ": content divergence (actual len: " <> T.show actual+          <> " chars, expected: " <> T.show expected <> " chars)"+        StatusPermissionDrift actual expected ->+          "  * " <> p <> ": permission mismatch (actual exec: " <> T.show actual+          <> ", expected: " <> T.show expected <> ")"+        StatusIOError err ->+          "  * " <> p <> ": I/O error (" <> err <> ")"+        _ -> "  * " <> p++    formatMissingSection :: [AssetAuditResult] -> [Text]+    formatMissingSection [] = []+    formatMissingSection missings =+      [ "--------------------------------------------------------------------------------"+      , " Missing Required Assets:"+      ] ++ map explainMissing missings++    explainMissing :: AssetAuditResult -> Text+    explainMissing res =+      let p = T.pack (assetPath (resultAsset res))+          desc = assetDescription (resultAsset res)+      in "  * " <> p <> " — " <> desc++-- | Convenience helper rendering 'formatAuditReport' to a 'String'.+--+-- Facilitates output to standard terminal streams via 'putStrLn'.+formatAuditReportString :: AuditReport -> String+formatAuditReportString = T.unpack . formatAuditReport
+ src/Gachi/Sync/CI.hs view
@@ -0,0 +1,810 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.Sync.CI+-- Description: Forge-tiered CI matrix synthesis and asymmetric runner resource allocation+--+-- This module implements Milestone 3.3 of the Gachi strategic roadmap (~/src/PLAN.md §1.A).+-- It provides pure declarative models and deterministic generators for multi-forge+-- continuous integration (CI) workflows, partitioning physical verification gates+-- across asymmetric runner capabilities.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization], Note [Cross-Repository Standardization Matrix and Audit Semantics]+-- [ref:note-standardization-matrix], and Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning]+-- [ref:note-forge-tiered-ci], ecosystem repositories replicate across heterogeneous+-- hosting providers (commercial forges like GitHub, community forges like GitLab/Framagit/Disroot,+-- and sovereign self-hosted instances like Forgejo/Woodpecker/Radicle).+--+-- == Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning]+-- [tag:note-forge-tiered-ci]+--+-- Modern Haskell toolchains impose heavy compute and memory demands. Compiling+-- an interconnected multi-package workspace under GHC 9.14 with Template Haskell,+-- unboxed primitive arrays, and advanced type families requires multi-gigabyte RAM+-- footprints and significant CPU time.+--+-- Commercial hosting providers (e.g., GitHub Actions) provide high-capacity+-- container runners equipped with 4+ virtual CPUs, 16 GB RAM, generous monthly+-- compute allowances, and global artifact caching services (@actions/cache@).+-- Conversely, community-run and self-hosted forges (e.g., Framagit, Disroot,+-- Codeberg, sourcehut) operate on modest, shared virtual servers where running+-- a full GHC compilation pipeline on every push triggers runner timeouts, CPU+-- throttling, and out-of-memory kernel kills (OOM 137).+--+-- To achieve universal verification without depleting community resources or+-- failing due to runner memory constraints, Gachi establishes an __asymmetric+-- forge-tiered CI matrix__:+--+-- 1.  __High-Resource Commercial Runners ('TierHighResource'):__+--     Allocated to GitHub Actions (@.github/workflows/ci.yml@) and dedicated+--     enterprise Forgejo instances. Executes the complete, heavyweight physical+--     verification suite:+--     * Installs system runtime dependencies (bubblewrap, sqlite3, z3).+--     * Configures AppArmor namespace profiles for bubblewrap container isolation.+--     * Deploys pinned GHC (9.14.1) and Cabal (3.18.1.0) toolchains.+--     * Restores cryptographic Cabal store caches (@~/.cabal/store@, @dist-newstyle@).+--     * Compiles all workspace packages with @--enable-tests@.+--     * Executes HLint static analysis under the zero-hint policy.+--     * Executes all unit, property, and integration test suites.+--     * Verifies comment density thresholds via @./bin/<project>-cloc --check@.+--     * Executes the turnkey five-gate script: @./bin/<project>-verify@.+--+-- 2.  __Resource-Constrained Community Runners ('TierLightweightStatic'):__+--     Allocated to community GitLab instances (Framagit, Disroot: @.gitlab-ci.yml@),+--     Woodpecker CI (@.woodpecker.yml@), and Radicle local verification.+--     Executes fast, deterministic static analysis gates inside a minimal+--     container (@debian:bookworm-slim@) without GHC compilation:+--     * Installs lightweight utilities (curl, git, python3) and Tagref binary.+--     * Verifies Tagref cross-reference integrity (0 dangling references).+--     * Verifies Markdown link integrity and anchor slug resolution (0 broken links).+--     * Verifies in-code comment density thresholds (@bin/<project>-cloc --check@).+--     * Finishes execution in < 15 seconds with < 100 MB memory footprint.+--+-- This division guarantees that community infrastructure remains protected+-- from resource starvation while ensuring every repository maintains zero broken+-- links, zero dangling reference tags, and zero documentation drift on every commit.+--+-- == Economic & Operational Invariants+--+-- The forge-tiered CI matrix adheres to four non-negotiable operational rules:+--+-- * __Purity & Determinism:__ CI configuration files are synthesized purely from+--   the project specification ('Project') and CI specification ('CISpec').+--   No ambient environment variables, timestamps, or network requests are consulted.+--+-- * __Zero Dangling Pointers:__ Every script reference within synthesized CI files+--   (such as @./bin/<project>-verify@ or @./bin/<project>-tagref@) resolves+--   to an executable script generated by the Gachi scaffolding engine.+--+-- * __Cache Key Hermeticity:__ Cache keys on high-resource runners incorporate+--   cryptographic hashes of @cabal.project@ and all @**/*.cabal@ package files,+--   ensuring caches invalidate immediately upon dependency or compiler changes.+--+-- * __Sandbox Isolation:__ High-resource runners execute sandboxed test harnesses+--   under Bubblewrap (@bwrap@). To prevent Ubuntu 24.04 AppArmor user namespace+--   restrictions from aborting builds, synthesized workflows explicitly configure+--   unprivileged user namespace allowances and AppArmor profiles.+--+-- = Worked Example: High-Resource Workflow Generation+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync.CI+-- >>> let auth = Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me"+-- >>> let proj = defaultProject "gongju" auth+-- >>> let githubWf = generateGitHubWorkflow defaultGitHubSpec proj+-- >>> "cabal build all --enable-tests" `Data.Text.isInfixOf` githubWf+-- True+-- >>> "ubuntu-latest" `Data.Text.isInfixOf` githubWf+-- True+--+-- = Worked Example: Lightweight Community Workflow Generation+--+-- >>> let gitlabWf = generateGitLabWorkflow defaultGitLabSpec proj+-- >>> "debian:bookworm-slim" `Data.Text.isInfixOf` gitlabWf+-- True+-- >>> "./bin/gongju-tagref" `Data.Text.isInfixOf` gitlabWf+-- True+-- >>> "cabal build" `Data.Text.isInfixOf` gitlabWf+-- False+module Gachi.Sync.CI+  ( -- * Forge and CI Tier Data Models+    ForgeType(..)+  , CITier(..)+  , CISpec(..)++    -- * Pre-Configured Default Specifications+  , defaultGitHubSpec+  , defaultGitLabSpec+  , defaultWoodpeckerSpec+  , defaultForgejoSpec+  , defaultRadicleSpec+  , canonicalForgeSpecs+  , forgeDefaultSpec++    -- * Predicates & Classification Helpers+  , forgeName+  , tierName+  , isHighResource+  , isLightweight+  , classifyForge+  , classifyTier++    -- * Deterministic Workflow Generators+  , generateWorkflow+  , generateGitHubWorkflow+  , generateGitLabWorkflow+  , generateWoodpeckerWorkflow+  , generateForgejoWorkflow+  , generateRadicleWorkflow++    -- * Matrix Synthesis Combinators+  , forgeAssetForSpec+  , synthesizeCIAssets+  , canonicalCIAssets+  ) where++import Data.List (isPrefixOf)+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)++import Gachi.Schema.Project (Project, projectName)+import Gachi.Sync.Types+  ( AssetCategory(..)+  , StandardAsset(..)+  )++--------------------------------------------------------------------------------+-- 1. Forge and CI Tier Data Models+--------------------------------------------------------------------------------++-- | Target hosting forge or CI engine platform.+--+-- Distinguishes commercial clouds with abundant runner quotas from shared+-- community instances and sovereign peer-to-peer repositories.+--+-- Architectural Roles:+-- * 'ForgeGitHub': Commercial cloud runners with 4+ vCPUs, 16 GB RAM, caching.+-- * 'ForgeGitLab': Community shared runners with strict memory and CPU limits.+-- * 'ForgeWoodpecker': Lightweight container pipelines for Forgejo / Codeberg.+-- * 'ForgeForgejo': Self-hosted Git server workflows using Docker runners.+-- * 'ForgeRadicle': Sovereign peer-to-peer identity verification prior to sync.+data ForgeType+  = ForgeGitHub+    -- ^ GitHub Actions (@.github/workflows/ci.yml@): Commercial high-capacity runners.+  | ForgeGitLab+    -- ^ GitLab CI (@.gitlab-ci.yml@): Community runners (Framagit, Disroot, gitlab.com).+  | ForgeWoodpecker+    -- ^ Woodpecker CI (@.woodpecker.yml@): Lightweight container pipelines for Forgejo/Gitea.+  | ForgeForgejo+    -- ^ Forgejo / Gitea Actions (@.forgejo/workflows/ci.yml@): Self-hosted Git server workflows.+  | ForgeRadicle+    -- ^ Radicle P2P (@.radicle/ci.yaml@): Local verification broker for sovereign peer nodes.+  deriving stock (Eq, Show, Ord, Enum, Bounded, Generic)++-- | Resource capability tier assigned to a CI workflow pipeline.+--+-- Governs whether a forge executes the full heavyweight GHC build suite or+-- restricts execution to lightweight static verification gates.+--+-- The allocation strategy guarantees that high-resource runners handle compiler+-- overhead while community runners remain responsive and protected from OOM errors.+data CITier+  = TierHighResource+    -- ^ Heavyweight execution: Full GHC compilation, test suites, Cabal store caching,+    -- AppArmor namespace configuration, and complete physical verification script execution.+  | TierLightweightStatic+    -- ^ Lightweight execution: Fast static gates (Tagref, linkcheck, CLOC audit)+    -- inside a slim container without GHC compiler toolchains (< 15 seconds runtime).+  deriving stock (Eq, Show, Ord, Enum, Bounded, Generic)++-- | Declarative specification governing continuous integration generation.+--+-- Encapsulates the target platform, resource tier, filesystem workflow destination,+-- toolchain versions, container images, and test flags.+--+-- All fields are pure and deterministic, permitting total reproducibility.+data CISpec = CISpec+  { ciForge              :: !ForgeType+    -- ^ Target hosting forge or runner platform.+  , ciTier               :: !CITier+    -- ^ Resource tier governing execution depth (high-resource vs lightweight).+  , ciWorkflowPath       :: !FilePath+    -- ^ Relative workflow path in repository root (e.g. @".github/workflows/ci.yml"@).+  , ciGhcVersion         :: !Text+    -- ^ Target GHC compiler version string (e.g. @"9.14.1"@).+  , ciCabalVersion       :: !Text+    -- ^ Target Cabal specification version string (e.g. @"3.18.1.0"@).+  , ciSystemDependencies :: ![Text]+    -- ^ System apt packages required by high-resource builds (e.g. @["bubblewrap", "sqlite3", "z3"]@).+  , ciContainerImage     :: !Text+    -- ^ Minimal container image for lightweight static runners (e.g. @"debian:bookworm-slim"@).+  , ciEnableTests        :: !Bool+    -- ^ Whether to configure @--enable-tests@ during Cabal compilation.+  , ciEnableDocCheck     :: !Bool+    -- ^ Whether to perform comment density and documentation verification.+  } deriving stock (Eq, Show, Generic)++--------------------------------------------------------------------------------+-- 2. Pre-Configured Default Specifications+--------------------------------------------------------------------------------++-- | Canonical high-resource GitHub Actions specification.+--+-- Configured for full physical verification on Ubuntu latest runners with+-- GHC 9.14.1, Cabal 3.18.1.0, system dependencies, and Cabal store caching.+--+-- Key Properties:+-- * Platform: GitHub Actions (@.github/workflows/ci.yml@).+-- * Resource Tier: 'TierHighResource'.+-- * Compiler: GHC 9.14.1.+-- * Build Tool: Cabal 3.18.1.0.+-- * System Packages: @bubblewrap@, @sqlite3@, @z3@.+-- * Tests Enabled: 'True'.+-- * Doc Check Enabled: 'True'.+defaultGitHubSpec :: CISpec+defaultGitHubSpec = CISpec+  { ciForge              = ForgeGitHub+  , ciTier               = TierHighResource+  , ciWorkflowPath       = ".github/workflows/ci.yml"+  , ciGhcVersion         = "9.14.1"+  , ciCabalVersion       = "3.18.1.0"+  , ciSystemDependencies = ["bubblewrap", "sqlite3", "z3"]+  , ciContainerImage     = "ubuntu-latest"+  , ciEnableTests        = True+  , ciEnableDocCheck     = True+  }++-- | Canonical lightweight GitLab CI specification for community runners.+--+-- Configured for Framagit and Disroot using @debian:bookworm-slim@.+-- Runs Tagref, linkcheck, and CLOC audit in < 15 seconds without GHC.+--+-- Key Properties:+-- * Platform: GitLab CI (@.gitlab-ci.yml@).+-- * Resource Tier: 'TierLightweightStatic'.+-- * Image: @debian:bookworm-slim@.+-- * System Packages: @curl@, @git@, @python3@.+-- * Tests Enabled: 'False' (avoids GHC compilation).+-- * Doc Check Enabled: 'True' (executes @bin/*-cloc --check@).+defaultGitLabSpec :: CISpec+defaultGitLabSpec = CISpec+  { ciForge              = ForgeGitLab+  , ciTier               = TierLightweightStatic+  , ciWorkflowPath       = ".gitlab-ci.yml"+  , ciGhcVersion         = "9.14.1"+  , ciCabalVersion       = "3.18.1.0"+  , ciSystemDependencies = ["curl", "git", "python3"]+  , ciContainerImage     = "debian:bookworm-slim"+  , ciEnableTests        = False+  , ciEnableDocCheck     = True+  }++-- | Canonical lightweight Woodpecker CI specification.+--+-- Used on self-hosted Woodpecker servers connected to Forgejo or Codeberg.+--+-- Key Properties:+-- * Platform: Woodpecker CI (@.woodpecker.yml@).+-- * Resource Tier: 'TierLightweightStatic'.+-- * Image: @debian:bookworm-slim@.+-- * System Packages: @curl@, @git@, @python3@.+defaultWoodpeckerSpec :: CISpec+defaultWoodpeckerSpec = CISpec+  { ciForge              = ForgeWoodpecker+  , ciTier               = TierLightweightStatic+  , ciWorkflowPath       = ".woodpecker.yml"+  , ciGhcVersion         = "9.14.1"+  , ciCabalVersion       = "3.18.1.0"+  , ciSystemDependencies = ["curl", "git", "python3"]+  , ciContainerImage     = "debian:bookworm-slim"+  , ciEnableTests        = False+  , ciEnableDocCheck     = True+  }++-- | Canonical Forgejo Actions specification.+--+-- High-resource containerized pipeline for sovereign Forgejo instances.+--+-- Key Properties:+-- * Platform: Forgejo Actions (@.forgejo/workflows/ci.yml@).+-- * Resource Tier: 'TierHighResource'.+-- * Compiler: GHC 9.14.1.+-- * Image: @node:20-bookworm@.+defaultForgejoSpec :: CISpec+defaultForgejoSpec = CISpec+  { ciForge              = ForgeForgejo+  , ciTier               = TierHighResource+  , ciWorkflowPath       = ".forgejo/workflows/ci.yml"+  , ciGhcVersion         = "9.14.1"+  , ciCabalVersion       = "3.18.1.0"+  , ciSystemDependencies = ["bubblewrap", "sqlite3", "z3"]+  , ciContainerImage     = "node:20-bookworm"+  , ciEnableTests        = True+  , ciEnableDocCheck     = True+  }++-- | Canonical Radicle P2P local verification broker specification.+--+-- Lightweight static check script invoked by Radicle node post-receive hooks.+--+-- Key Properties:+-- * Platform: Radicle P2P (@.radicle/ci.yaml@).+-- * Resource Tier: 'TierLightweightStatic'.+-- * Verification Scope: Tagref, Linkcheck, Comment Density.+defaultRadicleSpec :: CISpec+defaultRadicleSpec = CISpec+  { ciForge              = ForgeRadicle+  , ciTier               = TierLightweightStatic+  , ciWorkflowPath       = ".radicle/ci.yaml"+  , ciGhcVersion         = "9.14.1"+  , ciCabalVersion       = "3.18.1.0"+  , ciSystemDependencies = ["curl", "git", "python3"]+  , ciContainerImage     = "debian:bookworm-slim"+  , ciEnableTests        = False+  , ciEnableDocCheck     = True+  }++-- | List of authoritative CI specifications registered across standard ecosystem forges.+--+-- Represents the default triad:+-- 1. GitHub Actions (Commercial High-Resource)+-- 2. GitLab CI (Community Lightweight Static)+-- 3. Woodpecker CI (Self-Hosted Lightweight Static)+canonicalForgeSpecs :: [CISpec]+canonicalForgeSpecs =+  [ defaultGitHubSpec+  , defaultGitLabSpec+  , defaultWoodpeckerSpec+  ]++-- | Returns the canonical default 'CISpec' for a given 'ForgeType' and 'CITier'.+--+-- Enables parameterized lookup when generating tailored workflows for custom targets.+forgeDefaultSpec :: ForgeType -> CITier -> CISpec+-- GitHub Actions: High resource (default) or lightweight override+forgeDefaultSpec ForgeGitHub TierHighResource      = defaultGitHubSpec+forgeDefaultSpec ForgeGitHub TierLightweightStatic =+  defaultGitHubSpec { ciTier = TierLightweightStatic, ciEnableTests = False }+-- GitLab CI: Lightweight static (default) or high resource container override+forgeDefaultSpec ForgeGitLab TierLightweightStatic = defaultGitLabSpec+forgeDefaultSpec ForgeGitLab TierHighResource      =+  defaultGitLabSpec { ciTier = TierHighResource, ciEnableTests = True, ciContainerImage = "haskell:9.14" }+-- Woodpecker CI: Lightweight static (default) or high resource container override+forgeDefaultSpec ForgeWoodpecker TierLightweightStatic = defaultWoodpeckerSpec+forgeDefaultSpec ForgeWoodpecker TierHighResource      =+  defaultWoodpeckerSpec { ciTier = TierHighResource, ciEnableTests = True, ciContainerImage = "haskell:9.14" }+-- Forgejo Actions: High resource (default) or lightweight static override+forgeDefaultSpec ForgeForgejo TierHighResource      = defaultForgejoSpec+forgeDefaultSpec ForgeForgejo TierLightweightStatic =+  defaultForgejoSpec { ciTier = TierLightweightStatic, ciEnableTests = False }+-- Radicle P2P: Sovereign static verification broker+forgeDefaultSpec ForgeRadicle _                     = defaultRadicleSpec++--------------------------------------------------------------------------------+-- 3. Predicates & Classification Helpers+--------------------------------------------------------------------------------++-- | Human-readable name of a 'ForgeType'.+--+-- Used in diagnostic banners and synchronization audit reports.+forgeName :: ForgeType -> Text+forgeName ForgeGitHub     = "GitHub Actions"+forgeName ForgeGitLab     = "GitLab CI (Framagit, Disroot)"+forgeName ForgeWoodpecker = "Woodpecker CI"+forgeName ForgeForgejo    = "Forgejo Actions"+forgeName ForgeRadicle    = "Radicle P2P CI"++-- | Human-readable description of a 'CITier'.+--+-- Highlights whether a pipeline executes heavyweight GHC verification or fast static gates.+tierName :: CITier -> Text+tierName TierHighResource      = "High-Resource (Full Physical Verification)"+tierName TierLightweightStatic = "Lightweight (Static Gates Only)"++-- | Predicate checking if a specification belongs to the high-resource tier.+isHighResource :: CISpec -> Bool+isHighResource spec = ciTier spec == TierHighResource++-- | Predicate checking if a specification belongs to the lightweight tier.+isLightweight :: CISpec -> Bool+isLightweight spec = ciTier spec == TierLightweightStatic++-- | Infers the 'ForgeType' from a workflow file path.+--+-- Inspects standard file names and directories:+-- * @.github/workflows/*@ -> 'ForgeGitHub'+-- * @.gitlab-ci.yml@ -> 'ForgeGitLab'+-- * @.woodpecker.yml@ or @.woodpecker/*@ -> 'ForgeWoodpecker'+-- * @.forgejo/workflows/*@ -> 'ForgeForgejo'+-- * @.radicle/*@ -> 'ForgeRadicle'+--+-- Returns 'Nothing' if the path does not match recognized CI conventions.+classifyForge :: FilePath -> Maybe ForgeType+classifyForge path+  -- GitHub Actions workflow directory+  | ".github/workflows/" `isPrefixOf` path = Just ForgeGitHub+  -- GitLab CI configuration file in root+  | path == ".gitlab-ci.yml"               = Just ForgeGitLab+  -- Woodpecker CI single file configuration+  | path == ".woodpecker.yml"              = Just ForgeWoodpecker+  -- Woodpecker CI pipeline directory+  | ".woodpecker/" `isPrefixOf` path       = Just ForgeWoodpecker+  -- Forgejo Actions workflow directory+  | ".forgejo/" `isPrefixOf` path          = Just ForgeForgejo+  -- Radicle P2P CI verification directory+  | ".radicle/" `isPrefixOf` path          = Just ForgeRadicle+  -- Unrecognized path+  | otherwise                              = Nothing++-- | Infers the expected 'CITier' from a standard workflow path.+--+-- Recognizes high-resource vs lightweight tiers based on repository path conventions.+classifyTier :: FilePath -> Maybe CITier+classifyTier path+  -- GitHub Actions executes full verification suite+  | ".github/" `isPrefixOf` path  = Just TierHighResource+  -- GitLab CI community runners execute static gates only+  | path == ".gitlab-ci.yml"      = Just TierLightweightStatic+  -- Woodpecker CI executes lightweight container steps+  | path == ".woodpecker.yml"     = Just TierLightweightStatic+  -- Forgejo enterprise runners execute full suite+  | ".forgejo/" `isPrefixOf` path = Just TierHighResource+  -- Radicle local node broker executes static gates+  | ".radicle/" `isPrefixOf` path = Just TierLightweightStatic+  -- Unrecognized path+  | otherwise                     = Nothing++--------------------------------------------------------------------------------+-- 4. Deterministic Workflow Generators+--------------------------------------------------------------------------------++-- | Pure dispatcher synthesizing the appropriate CI configuration text for a 'CISpec'.+--+-- Evaluates the target forge platform and invokes the corresponding deterministic generator.+generateWorkflow :: CISpec -> Project -> Text+generateWorkflow spec proj = case ciForge spec of+  ForgeGitHub     -> generateGitHubWorkflow spec proj+  ForgeGitLab     -> generateGitLabWorkflow spec proj+  ForgeWoodpecker -> generateWoodpeckerWorkflow spec proj+  ForgeForgejo    -> generateForgejoWorkflow spec proj+  ForgeRadicle    -> generateRadicleWorkflow spec proj++-- | Generates GitHub Actions workflow configuration (@.github/workflows/ci.yml@).+--+-- Implements the high-resource physical verification pipeline:+--+-- 1.  __Checkout Step:__+--     Uses @actions/checkout@v4@ to clone the repository with full git history.+--+-- 2.  __Tagref Installation & Check:__+--     Downloads the authoritative Tagref binary and executes @./bin/<project>-tagref@+--     to verify that zero dangling reference tags or duplicate tag definitions exist.+--+-- 3.  __Markdown Link & Anchor Gate:__+--     Executes @./bin/<project>-linkcheck@ to verify 100% of intra-corpus relative+--     links and section heading anchors across all markdown documentation files.+--+-- 4.  __System Dependencies & AppArmor Setup:__+--     Installs system runtime libraries (@bubblewrap@, @sqlite3@, @z3@).+--     Configures unconfined AppArmor profile for Bubblewrap (@bwrap@) to enable+--     unprivileged user namespace creation on Ubuntu 24.04 runner environments.+--+-- 5.  __Git Submodule Protocol Rewriting:__+--     Configures Git to rewrite @git@github.com:@ SSH URLs to HTTPS for clean submodule pulls.+--+-- 6.  __Haskell Toolchain Provisioning:__+--     Invokes @haskell-actions/setup@v2@ with pinned GHC (9.14.1) and Cabal (3.18.1.0).+--+-- 7.  __HLint Static Analysis Installation:__+--     Installs the recommended HLint binary via GHCup and runs under zero-hint cleanliness.+--+-- 8.  __Cryptographic Artifact Caching:__+--     Invokes @actions/cache@v4@ caching @~/.cabal/store@ and @dist-newstyle@ keyed on+--     the combined cryptographic hash of @cabal.project@ and all @**/*.cabal@ manifests.+--+-- 9.  __Compilation & Test Build:__+--     Executes @cabal build all --enable-tests@ compiling all workspace packages and test suites.+--+-- 10. __Comment Density Audit:__+--     Executes @./bin/<project>-cloc --check@ enforcing documentation thresholds.+--+-- 11. __Turnkey Verification Harness:__+--     Executes @./bin/<project>-verify@ ensuring all 5 physical gates pass in unison.+generateGitHubWorkflow :: CISpec -> Project -> Text+generateGitHubWorkflow spec proj =+  let pName = projectName proj+      ghcVer = ciGhcVersion spec+      cabalVer = ciCabalVersion spec+      deps = T.unwords (ciSystemDependencies spec)+  in T.unlines+    [ -- SPDX License identifier declaring legal terms+      "# SPDX-License-Identifier: EUPL-1.2"+      -- Canonical repository metadata comment+    , "# Canonical GitHub Actions CI Workflow for " <> pName+      -- Cross-reference pointer grounding the Note in the CI asset+    , "# Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning] [ref:note-forge-tiered-ci]"+    , ""+      -- Top-level workflow descriptive name+    , "name: CI Verification Harness"+    , ""+      -- Trigger definitions: executed on push and pull-request events targeting master+    , "on:"+    , "  push:"+    , "    branches: [ master ]"+    , "  pull_request:"+    , "    branches: [ master ]"+    , ""+      -- Job definition block+    , "jobs:"+    , "  verify:"+    , "    name: Tagref & Verification Gates"+      -- Host runner operating system targeting Ubuntu LTS+    , "    runs-on: ubuntu-latest"+    , "    steps:"+      -- Step 1: Check out git working tree+    , "      - name: Check out repository"+    , "        uses: actions/checkout@v4"+    , ""+      -- Step 2: Download and install Tagref cross-reference binary+    , "      - name: Install Tagref"+    , "        run: |"+    , "          mkdir -p \"${HOME}/.local/bin\""+    , "          PREFIX=\"${HOME}/.local/bin\" curl https://raw.githubusercontent.com/stepchowfun/tagref/main/install.sh -LSfs | sh"+    , "          echo \"${HOME}/.local/bin\" >> $GITHUB_PATH"+    , ""+      -- Step 3: Run static Tagref verification gate+    , "      - name: Verify Tagref Cross-References"+    , "        run: ./bin/" <> pName <> "-tagref"+    , ""+      -- Step 4: Run intra-corpus Markdown hyperlink and heading anchor verification+    , "      - name: Verify Markdown Links & Anchors"+    , "        run: ./bin/" <> pName <> "-linkcheck"+    , ""+      -- Step 5: Install runtime system dependencies and configure AppArmor sandbox profiles+    , "      - name: Install System Dependencies"+    , "        run: |"+    , "          sudo apt-get update"+    , "          sudo apt-get install -y " <> deps+      -- Disable Ubuntu 24.04 kernel restriction on unprivileged user namespaces+    , "          sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 2>/dev/null || true"+      -- Create unconfined AppArmor profile for Bubblewrap if profile directory exists+    , "          if [ -d /etc/apparmor.d ]; then"+    , "            printf 'abi <abi/4.0>,\\ninclude <tunables/global>\\nprofile bwrap /usr/bin/bwrap flags=(unconfined) {\\n    userns,\\n    include if exists <local/bwrap>\\n}\\n' | sudo tee /etc/apparmor.d/bwrap >/dev/null || true"+    , "            sudo apparmor_parser -r /etc/apparmor.d/bwrap 2>/dev/null || true"+    , "          fi"+    , ""+      -- Step 6: Rewrite SSH Git submodule URLs to HTTPS to avoid unauthenticated SSH failures+    , "      - name: Configure Git for HTTPS submodules"+    , "        run: git config --global url.\"https://github.com/\".insteadOf \"git@github.com:\""+    , ""+      -- Step 7: Configure pinned GHC and Cabal toolchain via official Haskell action+    , "      - name: Set up Haskell Toolchain"+    , "        uses: haskell-actions/setup@v2"+    , "        with:"+    , "          ghc-version: '" <> ghcVer <> "'"+    , "          cabal-version: '" <> cabalVer <> "'"+    , ""+      -- Step 8: Install HLint binary+    , "      - name: Install HLint"+    , "        run: |"+    , "          mkdir -p \"${HOME}/.local/bin\""+    , "          if ! command -v hlint >/dev/null 2>&1; then"+    , "            curl -sSL https://github.com/ndmitchell/hlint/releases/download/v3.10/hlint-3.10-x86_64-linux.tar.gz | tar -xz -C \"${HOME}/.local\""+    , "            ln -sf \"${HOME}/.local/hlint-3.10/hlint\" \"${HOME}/.local/bin/hlint\""+    , "          fi"+    , "          echo \"${HOME}/.local/bin\" >> $GITHUB_PATH"+    , ""+      -- Step 9: Restore and save Cabal store caches keyed by project manifests+    , "      - name: Cache Cabal artifacts"+    , "        uses: actions/cache@v4"+    , "        with:"+    , "          path: |"+    , "            ~/.cabal/store"+    , "            dist-newstyle"+    , "          key: ${{ runner.os }}-cabal-${{ hashFiles('cabal.project', '**/*.cabal') }}"+    , "          restore-keys: |"+    , "            ${{ runner.os }}-cabal-"+    , ""+      -- Step 10: Compile all workspace packages and test harnesses+    , "      - name: Build All Packages & Dependencies"+    , "        run: cabal build all" <> (if ciEnableTests spec then " --enable-tests" else "")+    , ""+      -- Step 11: Execute documentation comment density audit+    , "      - name: Verify Comment Density Audit"+    , "        run: ./bin/" <> pName <> "-cloc --check"+    , ""+      -- Step 12: Run complete 5-gate physical verification harness+    , "      - name: Run Full Physical Verification Gates"+    , "        run: ./bin/" <> pName <> "-verify"+    ]++-- | Generates GitLab CI configuration (@.gitlab-ci.yml@).+--+-- Tailored to protect resource-constrained community runners (Framagit, Disroot).+-- Runs fast static analysis gates inside @debian:bookworm-slim@ without GHC overhead.+--+-- The pipeline executes in under 15 seconds, verifying Tagref cross-references,+-- Markdown links, and CLOC comment density while consuming negligible compute quota.+generateGitLabWorkflow :: CISpec -> Project -> Text+generateGitLabWorkflow spec proj =+  let pName = projectName proj+      cImage = ciContainerImage spec+  in if ciTier spec == TierLightweightStatic+    then T.unlines+      [ -- SPDX license header+        "# SPDX-License-Identifier: EUPL-1.2"+        -- Target forge and provider documentation+      , "# GitLab CI configuration for " <> pName <> " (Framagit, Disroot)"+        -- Canonical Note reference+      , "# Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning] [ref:note-forge-tiered-ci]"+      , ""+        -- Verification stage list+      , "stages:"+      , "  - verify"+      , ""+        -- Static verification job definition+      , "tagref-check:"+      , "  stage: verify"+        -- Lean base container image+      , "  image: " <> cImage+        -- Pre-flight script installing curl, git, python3, and Tagref+      , "  before_script:"+      , "    - apt-get update && apt-get install -y curl git python3"+      , "    - mkdir -p /usr/local/bin"+      , "    - PREFIX=/usr/local/bin curl https://raw.githubusercontent.com/stepchowfun/tagref/main/install.sh -LSfs | sh"+        -- Static gate commands: Tagref, Linkcheck, and CLOC density check+      , "  script:"+      , "    - ./bin/" <> pName <> "-tagref"+      , "    - ./bin/" <> pName <> "-linkcheck"+      , "    - ./bin/" <> pName <> "-cloc --check"+      ]+    else T.unlines+      [ -- SPDX license header+        "# SPDX-License-Identifier: EUPL-1.2"+        -- Target forge documentation+      , "# High-Resource GitLab CI configuration for " <> pName+        -- Canonical Note reference+      , "# Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning] [ref:note-forge-tiered-ci]"+      , ""+        -- Stage definition+      , "stages:"+      , "  - verify"+      , ""+        -- Full verification job definition+      , "full-verify:"+      , "  stage: verify"+        -- Haskell container image+      , "  image: " <> cImage+        -- Pre-flight script installing system libraries and Tagref+      , "  before_script:"+      , "    - apt-get update && apt-get install -y curl git python3 bubblewrap sqlite3 z3"+      , "    - mkdir -p /usr/local/bin"+      , "    - PREFIX=/usr/local/bin curl https://raw.githubusercontent.com/stepchowfun/tagref/main/install.sh -LSfs | sh"+        -- Full build and turnkey verification commands+      , "  script:"+      , "    - cabal build all --enable-tests"+      , "    - ./bin/" <> pName <> "-verify"+      ]++-- | Generates Woodpecker CI pipeline configuration (@.woodpecker.yml@).+--+-- Used on lightweight, containerized Woodpecker agents attached to Forgejo.+--+-- Executes the three lightweight static verification gates inside @debian:bookworm-slim@.+generateWoodpeckerWorkflow :: CISpec -> Project -> Text+generateWoodpeckerWorkflow spec proj =+  let pName = projectName proj+      cImage = ciContainerImage spec+  in T.unlines+    [ -- SPDX license header+      "# SPDX-License-Identifier: EUPL-1.2"+      -- Target forge documentation+    , "# Woodpecker CI pipeline for " <> pName+      -- Canonical Note reference+    , "# Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning] [ref:note-forge-tiered-ci]"+    , ""+      -- Woodpecker pipeline root stanza+    , "pipeline:"+    , "  static-verify:"+        -- Container image specification+      , "    image: " <> cImage+        -- Pipeline command sequence+      , "    commands:"+      , "      - apt-get update && apt-get install -y curl git python3"+      , "      - mkdir -p /usr/local/bin"+      , "      - PREFIX=/usr/local/bin curl https://raw.githubusercontent.com/stepchowfun/tagref/main/install.sh -LSfs | sh"+      , "      - ./bin/" <> pName <> "-tagref"+      , "      - ./bin/" <> pName <> "-linkcheck"+      , "      - ./bin/" <> pName <> "-cloc --check"+    ]++-- | Generates Forgejo Actions workflow configuration (@.forgejo/workflows/ci.yml@).+--+-- Compatible with GitHub Actions workflow syntax for self-hosted runners.+--+-- Executes containerized verification pipelines in sovereign forge deployments.+generateForgejoWorkflow :: CISpec -> Project -> Text+generateForgejoWorkflow =+  -- Reuses GitHub Actions syntax engine with Forgejo specification+  generateGitHubWorkflow++-- | Generates Radicle P2P local verification broker configuration (@.radicle/ci.yaml@).+--+-- Executed locally on peer developer nodes prior to peer-to-peer patch distribution.+--+-- Ensures patches meet strict Tagref, linkcheck, and comment density standards before dissemination.+generateRadicleWorkflow :: CISpec -> Project -> Text+generateRadicleWorkflow _spec proj =+  let pName = projectName proj+  in T.unlines+    [ -- SPDX license header+      "# SPDX-License-Identifier: EUPL-1.2"+      -- Radicle verification broker documentation+    , "# Radicle P2P CI Verification Configuration for " <> pName+      -- Canonical Note reference+    , "# Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning] [ref:note-forge-tiered-ci]"+    , ""+      -- Radicle pipeline schema version+    , "version: 1"+    , "pipelines:"+    , "  - name: static-gates"+        -- Event triggers: runs on branch pushes and patch publications+      , "    trigger: [ push, patch ]"+      , "    steps:"+        -- Step 1: Run Tagref cross-reference verification+      , "      - name: Tagref"+      , "        command: ./bin/" <> pName <> "-tagref"+        -- Step 2: Run Markdown link and anchor verification+      , "      - name: Linkcheck"+      , "        command: ./bin/" <> pName <> "-linkcheck"+        -- Step 3: Run comment density audit+      , "      - name: CLOC"+      , "        command: ./bin/" <> pName <> "-cloc --check"+    ]++--------------------------------------------------------------------------------+-- 5. Matrix Synthesis Combinators+--------------------------------------------------------------------------------++-- | Converts a 'CISpec' into a canonical 'StandardAsset' in 'CategoryCIWorkflow'.+--+-- Sets requirement flags:+-- * GitHub (@.github/workflows/ci.yml@) and GitLab (@.gitlab-ci.yml@) are mandatory ('assetRequired' = True).+-- * Ancillary forges (Woodpecker, Forgejo, Radicle) are optional ('assetRequired' = False).+forgeAssetForSpec :: CISpec -> StandardAsset+forgeAssetForSpec spec =+  let path = ciWorkflowPath spec+      -- Mandatory assets enforced across every ecosystem repository+      isReq = path `elem` [".github/workflows/ci.yml", ".gitlab-ci.yml"]+      -- Human-readable asset description+      desc = case ciForge spec of+        ForgeGitHub     -> "High-resource GitHub Actions workflow running complete physical gates"+        ForgeGitLab     -> "Lightweight GitLab CI workflow for resource-constrained community forges"+        ForgeWoodpecker -> "Lightweight Woodpecker CI workflow for Forgejo/Gitea runners"+        ForgeForgejo    -> "Self-hosted Forgejo Actions workflow running containerized verification"+        ForgeRadicle    -> "Radicle sovereign peer-to-peer verification pipeline"+  in StandardAsset+    { assetPath        = path+    , assetCategory    = CategoryCIWorkflow+    , assetRequired    = isReq+    , assetExecutable  = False+    , assetDescription = desc+    , assetGenerator   = generateWorkflow spec+    }++-- | Synthesizes a list of 'StandardAsset' records from a collection of 'CISpec' configurations.+--+-- Maps each specification into a standard matrix asset ready for auditing and propagation.+synthesizeCIAssets :: [CISpec] -> [StandardAsset]+synthesizeCIAssets = map forgeAssetForSpec++-- | The canonical list of standard CI workflow assets registered across all ecosystem checkouts.+--+-- Encompasses GitHub Actions, GitLab CI, and Woodpecker CI pipelines.+canonicalCIAssets :: [StandardAsset]+canonicalCIAssets = synthesizeCIAssets canonicalForgeSpecs
+ src/Gachi/Sync/Catalog.hs view
@@ -0,0 +1,355 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.Sync.Catalog+-- Description: Canonical shared asset catalog for cross-repository synchronization+--+-- This module implements pure, deterministic template generators for shared+-- ecosystem assets conforming to the Standardization Matrix (~/src/PLAN.md §1.A).+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization], all checkouts in ~/src/* maintain+-- bit-identical formatting profiles, zero-hint linter policies, Git hooks,+-- forge-tiered CI configurations, and AI agent advisory context.+--+-- == Invariant: Deterministic Configuration Synthesis+--+-- Configuration drift is the primary vector for friction in multi-package and+-- multi-repository ecosystems. When different repositories maintain slightly+-- divergent formatting configurations or compiler warning levels:+--+-- 1. __Diff Noise:__ Cross-cutting refactors touching multiple repositories+--    produce massive cosmetic diffs caused by differing indentation, import+--    sorting rules, or column limits.+-- 2. __Toolchain Inconsistencies:__ A code pattern accepted by one repository's+--    linter or compiler invocation triggers fatal warnings in another.+-- 3. __Agent Cognitive Overhead:__ AI assistants waste context tokens discovering+--    local quirks rather than focusing on domain-specific logic.+--+-- This catalog resolves configuration drift by providing an authoritative,+-- single-source-of-truth implementation for all shared configuration assets.+-- All functions in this module are referentially transparent, total, and free of+-- ambient I/O side effects: they evaluate strictly from a 'Project' specification+-- to 'Text' (or are constant 'Text' stanzas).+--+-- = When to Use This Module+--+-- * Inspecting canonical configuration stanzas during repository audits ('Gachi.Sync.Audit').+-- * Synchronizing shared infrastructure files across ecosystem checkouts ('gachi sync').+-- * Initializing new repositories with turnkey toolchain stanzas ('gachi init').+--+-- = Worked Example+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync.Catalog+-- >>> let proj = defaultProject "gongju" (Author "Nadia" "nadia@example.com")+-- >>> "indentation: 2" `Data.Text.isInfixOf` fourmoluTemplate+-- True+-- >>> "./bin/gongju-verify" `Data.Text.isInfixOf` githubCiTemplate proj+-- True+module Gachi.Sync.Catalog+  ( -- * Code Formatting Profiles (Dimension 2)+    fourmoluTemplate+  , rustfmtTemplate+  , clangFormatTemplate++    -- * Static Analysis & Linters (Dimension 3)+  , hlintTemplate++    -- * Build System Conventions (Dimension 4)+  , cabalProjectTemplate+  , cabalProjectLocalTemplate++    -- * Git Governance & Hooks (Dimension 5)+  , gitmessageTemplate+  , tagrefConfigTemplate+  , commitMsgHookTemplate+  , preCommitHookTemplate++    -- * Forge-Tiered CI Workflows (Dimension 7)+  , githubCiTemplate+  , gitlabCiTemplate+  , woodpeckerCiTemplate++    -- * Agent Advisory & Rules (Dimension 9)+  , agentsTemplate+  , claudeTemplate+  , commitAttributionRuleTemplate+  , codeNavigationRuleTemplate+  , compilerVerificationRuleTemplate+  , documentationPrecedenceRuleTemplate++    -- * Physical Verification Harnesses+  , verifyScriptTemplate+  , tagrefScriptTemplate+  , linkcheckScriptTemplate+  , clocScriptTemplate+  , generateTagsTemplate+  ) where++import Data.Text (Text)+import qualified Data.Text as T++import Gachi.Schema.Project (Project)+import Gachi.Sync.CI+  ( defaultGitHubSpec+  , defaultGitLabSpec+  , defaultWoodpeckerSpec+  , generateGitHubWorkflow+  , generateGitLabWorkflow+  , generateWoodpeckerWorkflow+  )+import Gachi.Scaffold.Templates+  ( agentsTemplate+  , cabalProjectLocalTemplate+  , cabalProjectTemplate+  , claudeTemplate+  , clocScriptTemplate+  , commitMsgHookTemplate+  , generateTagsTemplate+  , gitmessageTemplate+  , hlintTemplate+  , linkcheckScriptTemplate+  , preCommitHookTemplate+  , tagrefConfigTemplate+  , tagrefScriptTemplate+  , verifyScriptTemplate+  )++--------------------------------------------------------------------------------+-- 1. Code Formatting Profiles (Dimension 2)+--------------------------------------------------------------------------------++-- | Canonical Fourmolu configuration enforcing deterministic, AST-preserving Haskell layout.+--+-- == Formatting Invariants & Design Decisions+--+-- * __Indentation:__ Fixed 2-space indentation depth across all syntactic forms.+-- * __Column Limit:__ Pinned to 100 characters, striking a balance between wide-screen+--   readability and side-by-side terminal diffing.+-- * __Function Arrows:__ Trailing arrows (@->@ at the end of lines) preserving+--   visual alignment in multi-argument type signatures.+-- * __Comma Style:__ Leading commas in multi-line export lists, record updates,+--   and tuple literals to minimize git diff noise during item additions.+-- * __Record Style:__ Aligned record field declarations ensuring vertical alignment+--   of field names, colons, and types.+-- * __Custom Infix Fixities:__ Registers custom row-polymorphism operator fixities+--   (@:=@, @.==@, @.#@) used throughout the polytropos engine to prevent Fourmolu+--   from breaking natural mathematical expression grouping.+fourmoluTemplate :: Text+fourmoluTemplate = T.unlines+  [ "# Fourmolu configuration for standardized ecosystem Haskell repositories"+  , "# Enforces AST-preserving layout for open row lists, record syntax, and operator fixities."+  , ""+  , "indentation: 2"+  , "column-limit: 100"+  , "function-arrows: trailing"+  , "comma-style: leading"+  , "record-style: aligned"+  , "import-export-style: diff-friendly"+  , "import-grouping: legacy"+  , "indent-wheres: true"+  , "record-brace-space: true"+  , "newlines-between-decls: 1"+  , "haddock-style: multi-line"+  , "let-style: auto"+  , "in-style: right-align"+  , "if-style: indented"+  , "single-constraint-parens: never"+  , "single-deriving-parens: always"+  , "sort-constraints: false"+  , "sort-derived-classes: false"+  , "sort-deriving-clauses: false"+  , "trailing-section-operators: true"+  , "unicode: never"+  , "respectful: true"+  , ""+  , "fixities:"+  , "  - \"infixr 5 :=\""+  , "  - \"infixr 5 .==\""+  , "  - \"infixl 8 .#\""+  ]++-- | Canonical rustfmt configuration for polyglot Rust components (e.g. asterinas, surd).+--+-- == Polyglot Formatting Invariants+--+-- * __Line Width:__ Pinned to 100 characters to match Haskell and C/C++ limits.+-- * __Tab Spaces:__ 4-space tab indentation conforming to standard Rust idioms.+-- * __Edition:__ Edition 2021/2024 idioms with modern closure and macro formatting.+-- * __Import Sorting:__ Deterministic lexicographical reordering of @use@ stanzas.+-- * __Module Reordering:__ Canonical module declaration ordering.+rustfmtTemplate :: Text+rustfmtTemplate = T.unlines+  [ "# rustfmt.toml — Ecosystem formatting conventions for Rust components"+  , "max_width = 100"+  , "tab_spaces = 4"+  , "edition = \"2021\""+  , "newline_style = \"Unix\""+  , "use_small_heuristics = \"Default\""+  , "reorder_imports = true"+  , "reorder_modules = true"+  ]++-- | Canonical .clang-format configuration based on Linux kernel and LLVM style.+--+-- == Low-Level Systems Code Formatting+--+-- Applied to C, C++, and CUDA components across low-level operating system trees:+-- * __Style Base:__ Derived from LLVM standard style.+-- * __Indentation:__ 4 spaces with tabs strictly forbidden.+-- * __Brace Placement:__ Attached braces preserving vertical density.+-- * __Include Sorting:__ Deterministic alphabetical sorting of header @#include@ directives.+clangFormatTemplate :: Text+clangFormatTemplate = T.unlines+  [ "# .clang-format — Ecosystem formatting conventions for C/C++ components"+  , "BasedOnStyle: LLVM"+  , "IndentWidth: 4"+  , "ColumnLimit: 100"+  , "UseTab: Never"+  , "BreakBeforeBraces: Attach"+  , "AllowShortFunctionsOnASingleLine: Inline"+  , "SortIncludes: true"+  ]++--------------------------------------------------------------------------------+-- 2. Forge-Tiered CI Workflows (Dimension 7)+--------------------------------------------------------------------------------++-- | GitHub Actions CI workflow for high-resource runners.+--+-- == Asymmetric CI Resource Strategy+--+-- In accordance with Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning]+-- [ref:note-forge-tiered-ci], commercial forges like GitHub provide high-capacity+-- container runners with ample RAM, multi-core virtual CPUs, and generous monthly+-- allowances. This template evaluates 'generateGitHubWorkflow' using 'defaultGitHubSpec',+-- compiling all packages under GHC 9.14.1 with @--enable-tests@ and running all 5 physical gates.+githubCiTemplate :: Project -> Text+githubCiTemplate = generateGitHubWorkflow defaultGitHubSpec++-- | GitLab CI configuration tailored for resource-constrained community runners.+--+-- == Community Forge Resource Preservation+--+-- Community forges (Framagit, Disroot, etc.) operate on shared infrastructure with+-- limited shared runner compute time and strict memory quotas. Running full GHC+-- compilation on every push would deplete shared runner quotas.+--+-- Per Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning]+-- [ref:note-forge-tiered-ci], this configuration establishes an asymmetric,+-- lightweight verification pipeline inside @debian:bookworm-slim@:+-- * Executes fast static verification gates (Tagref cross-references and Markdown link check).+-- * Runs the lightweight CLOC comment density audit.+-- * Completely bypasses heavy GHC compilation and link stages, finishing in < 15 seconds.+gitlabCiTemplate :: Project -> Text+gitlabCiTemplate = generateGitLabWorkflow defaultGitLabSpec++-- | Woodpecker CI pipeline configuration for lightweight self-hosted runners.+--+-- == Self-Hosted Container Pipeline+--+-- Generates @.woodpecker.yml@ tailored for Forgejo or Codeberg runners.+-- Executes Tagref, Markdown linkcheck, and CLOC audit inside @debian:bookworm-slim@.+--+-- See Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning]+-- [ref:note-forge-tiered-ci].+woodpeckerCiTemplate :: Project -> Text+woodpeckerCiTemplate = generateWoodpeckerWorkflow defaultWoodpeckerSpec++--------------------------------------------------------------------------------+-- 3. Modular Agent Advisory Rules (Dimension 9)+--------------------------------------------------------------------------------++-- | Rule: Linux kernel RFC 2822 trailer format ('Assisted-by:') and DCO boundaries.+--+-- Establishes the non-negotiable legal and provenance boundary for AI contributions:+-- * __Attribution Format:__ Uses @Assisted-by: Tool (Provider / Model)@ git trailers.+-- * __DCO Invariant:__ AI agents NEVER sign off on Developer Certificate of Origin (@Signed-off-by:@)+--   or claim legal co-authorship (@Co-authored-by:@).+commitAttributionRuleTemplate :: Text+commitAttributionRuleTemplate = T.unlines+  [ "<!--"+  , "SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0"+  , "-->"+  , "# Commit Authoring & AI Attribution Rules"+  , ""+  , "**Status:** ACTIVE RULE · Bounded Context Extension"+  , "**Enforcement:** `.githooks/commit-msg` · `.gitmessage`"+  , ""+  , "## 1. Linux Kernel RFC 2822 Trailer Schema"+  , ""+  , "AI coding assistant attribution must strictly use RFC 2822 trailer format:"+  , ""+  , "1. `Assisted-by:` (Required when AI assisted):"+  , "   - Format: `Assisted-by: <Tool> (<Provider> / <Model>)` or bare `Assisted-by: <Tool>`"+  , "2. `Orchestrated-by:` (Optional when an orchestration framework directed the task):"+  , "   - Format: `Orchestrated-by: <Engine>`"+  , ""+  , "## 2. Legal Boundary: The Developer Certificate of Origin"+  , ""+  , "- NEVER use `Co-authored-by:` or `Co-developed-by:` for AI assistants."+  , "- NEVER add `Signed-off-by:` on an AI's behalf."+  ]++-- | Rule: Token-saving code navigation funnel (tags, ripgrep, ast-grep, slicing).+--+-- Guides AI agents to conserve context window tokens through hierarchical exploration:+-- 1. Symbol jump via generated @./tags@ (cheapest token cost).+-- 2. Precision line slicing via @view_file@ with @StartLine@ and @EndLine@.+-- 3. Avoid indiscriminate full-file views that exhaust context budgets.+codeNavigationRuleTemplate :: Text+codeNavigationRuleTemplate = T.unlines+  [ "<!--"+  , "SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0"+  , "-->"+  , "# Token-Saving Code Navigation Funnel"+  , ""+  , "**Status:** ACTIVE RULE · Bounded Context Extension"+  , ""+  , "Always follow the token-conservation funnel when exploring code:"+  , "1. `./tags` symbol jump via ripgrep/grep (cheapest token cost)."+  , "2. Targeted file slice viewing (view_file with StartLine/EndLine)."+  , "3. Avoid full-file reading when symbol lookups suffice."+  ]++-- | Rule: Zero-log compiler verification and zero-contamination policy.+--+-- Mandates complete mechanical verification before every commit:+-- * Zero warnings tolerated under @-Wall -Wcompat -Wredundant-constraints@.+-- * All five physical verification gates (@./bin/<project>-verify@) must pass cleanly.+-- * Strict prohibition against unverified code commits or broken tests.+compilerVerificationRuleTemplate :: Text+compilerVerificationRuleTemplate = T.unlines+  [ "<!--"+  , "SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0"+  , "-->"+  , "# Compiler Verification & Zero-Contamination Rules"+  , ""+  , "**Status:** ACTIVE RULE · Bounded Context Extension"+  , ""+  , "- Zero warnings tolerated under `-Wall -Wcompat -Wredundant-constraints`."+  , "- All 5 physical verification gates (`./bin/<project>-verify`) must pass."+  , "- Never commit broken builds, failing tests, or unverified changes."+  ]++-- | Rule: Precedence rule and the two-file operational loop.+--+-- Specifies the relationship between code, documentation, and session state:+-- * __Precedence:__ Code wins at runtime; notes corrected regardless.+-- * __Two-File Loop:__ Always update @PLAN.md@ and @STATE.md@ before concluding sessions.+-- * __Design Canonicity:__ Design documents are load-bearing engineering artifacts.+documentationPrecedenceRuleTemplate :: Text+documentationPrecedenceRuleTemplate = T.unlines+  [ "<!--"+  , "SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0"+  , "-->"+  , "# Documentation Precedence & Operational Loop"+  , ""+  , "**Status:** ACTIVE RULE · Bounded Context Extension"+  , ""+  , "1. Precedence rule: Code wins at runtime; notes corrected regardless."+  , "2. Two-file loop: Always update `PLAN.md` and `STATE.md` before concluding sessions."+  , "3. Design documents are load-bearing; keep anchors and references synchronized."+  ]
+ src/Gachi/Sync/Engine.hs view
@@ -0,0 +1,643 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.Sync.Engine+-- Description: Harmonic chattering synchronization and differential propagation engine+--+-- This module implements the core propagation engine of @gachi sync@. It evaluates+-- differential changes across ecosystem checkouts against the canonical Standardization+-- Matrix (~/src/PLAN.md §1.A), computes atomic synchronization plans, applies non-destructive+-- updates to missing or drifted files, and guarantees mathematical idempotency.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization], Note [Cross-Repository Standardization Matrix and Audit Semantics]+-- [ref:note-standardization-matrix], and Note [Harmonic Chattering Synchronization and Differential Propagation]+-- [ref:note-harmonic-sync], this module is the operational engine that transforms+-- observational audit findings into safe, automated filesystem repairs.+--+-- = Note [Harmonic Chattering Synchronization and Differential Propagation]+-- [tag:note-harmonic-sync]+--+-- In the mythos of Gachi, the *gashadokuro* phantom is assembled from the scattered+-- bones of those fallen across many battles. When the phantom awakens, its collected+-- teeth chatter and click (*gachi-gachi*) in eerie unison—a resonant harmonic rhythm+-- that coordinates the entire skeletal body without centralized nervous signaling.+--+-- In modern software engineering, multi-repository architectures suffer from structural+-- desynchronization: improvements made to one repository's formatting profile (Fourmolu),+-- compiler warning flags, Git commit hooks, or AI agent guidance fail to propagate to+-- sibling repositories. Over time, repositories drift into incompatible silos.+--+-- The Harmonic Chattering Synchronization engine resolves this through three core principles:+--+-- 1.  __Observational Planning Before Mutation:__+--     Before modifying a single byte on disk, the engine computes a 'SyncPlan'+--     classifying every standardized asset into an explicit 'SyncAction':+--     * 'ActionCreate': The file is absent; it must be materialized with correct permissions.+--     * 'ActionUpdate': The file exists, but its content diverges from canonical generators.+--     * 'ActionChmod': The content matches, but its POSIX executable permissions differ.+--     * 'ActionNoOp': The file is already compliant; zero disk I/O is performed.+--+-- 2.  __Strict Mathematical Idempotency:__+--     The synchronization function is mathematically idempotent:+--     \[+--       \text{sync}(\text{sync}(W)) = \text{sync}(W)+--     \]+--     Executing @gachi sync@ once repairs all discrepancies. Executing @gachi sync@+--     immediately afterward produces an empty change set (0 creates, 0 updates, 0 chmods).+--+-- 3.  __Non-Destructive Safety & Dry-Run Preview:__+--     The engine supports 'DryRun' mode, enabling developers and CI harnesses to inspect+--     all planned filesystem modifications and unified diffs before applying changes.+--+-- 4.  __Multi-Repository Ecosystem Orchestration:__+--     Through 'discoverEcosystemRepositories' and 'syncEcosystem', a single command+--     can harmonize all checkouts across @~/src/*@ in lockstep.+--+-- = Worked Example+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync.Matrix (canonicalMatrix)+-- >>> let proj = defaultProject "gachi" (Author "Nadia" "nadia@example.com")+-- >>> plan <- computeSyncPlan "/home/nyc/src/gachi" proj canonicalMatrix+-- >>> planProjectName plan+-- "gachi"+module Gachi.Sync.Engine+  ( -- * Synchronization Execution Modes+    SyncMode(..)++    -- * Synchronization Action Models+  , SyncAction(..)+  , isNoOpAction+  , isMutatingAction++    -- * Synchronization Planning+  , SyncPlan(..)+  , isPlanClean+  , computeSyncPlan++    -- * Synchronization Execution+  , SyncResult(..)+  , executeSyncPlan+  , syncRepository+  , syncRepositoryQuick++    -- * Multi-Repository Ecosystem Orchestration+  , discoverEcosystemRepositories+  , syncEcosystem++    -- * Diagnostic Plan & Result Formatting+  , formatSyncPlan+  , formatSyncResult+  ) where++import Control.Exception (SomeException, catch, displayException)+import Control.Monad (forM, when)+import Data.Maybe (catMaybes)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory+  ( canonicalizePath+  , createDirectoryIfMissing+  , doesDirectoryExist+  , doesFileExist+  , executable+  , getPermissions+  , listDirectory+  , setOwnerExecutable+  , setPermissions+  )+import System.FilePath (takeDirectory, takeFileName, (</>))++import Gachi.Schema.Dhall (decodeProjectFile)+import Gachi.Schema.Project (Author(..), Project, defaultProject, projectName)+import Gachi.Sync.Hash (sha256Text)+import Gachi.Sync.Matrix (canonicalMatrix)+import Gachi.Sync.Types+  ( StandardAsset(..)+  , StandardizationMatrix(..)+  , SyncMode(..)+  , resolveAssetPath+  )++--------------------------------------------------------------------------------+-- 1. Synchronization Modes & Actions+--------------------------------------------------------------------------------++-- | Explicit atomic action planned for an individual standardized asset.+--+-- Represents the fine-grained decision made for a single asset during the evaluation+-- phase of 'computeSyncPlan'. Every standardized asset maps to exactly one action.+data SyncAction+  = ActionCreate !FilePath !Text !Bool+    -- ^ The asset is completely absent on disk.+    -- Parameters:+    -- 1. Relative destination file path (e.g. @"fourmolu.yaml"@ or @"bin/gachi-verify"@).+    -- 2. Complete canonical text content produced by the asset generator.+    -- 3. Boolean flag indicating whether POSIX owner executable bit (+x) must be set.+  | ActionUpdate !FilePath !Text !Text !Bool+    -- ^ The asset exists on disk, but its SHA-256 checksum diverges from the generator.+    -- Parameters:+    -- 1. Relative destination file path.+    -- 2. Pre-existing file content currently residing on disk.+    -- 3. Canonical replacement content produced by the asset generator.+    -- 4. Target POSIX owner executable bit requirement.+  | ActionChmod !FilePath !Bool !Bool+    -- ^ The asset content is bit-identical, but its POSIX executable bit differs.+    -- Parameters:+    -- 1. Relative destination file path.+    -- 2. Current executable permission detected on disk.+    -- 3. Target executable permission required by the asset specification.+  | ActionNoOp !FilePath+    -- ^ The asset is fully compliant: exists, matches SHA-256 hash, and has correct permissions.+    -- Parameters:+    -- 1. Relative destination file path.+  | ActionError !FilePath !Text+    -- ^ An unexpected filesystem I/O exception occurred during inspection.+    -- Parameters:+    -- 1. Relative destination file path where inspection failed.+    -- 2. Human-readable diagnostic description of the caught exception.+  deriving stock (Eq, Show)++-- | Returns 'True' if the action represents an already-compliant asset requiring no changes.+--+-- No-op actions imply that the file on disk is bit-identical and has conforming permissions.+isNoOpAction :: SyncAction -> Bool+isNoOpAction (ActionNoOp _) = True+isNoOpAction _              = False++-- | Returns 'True' if the action would modify the filesystem (create, update, or chmod).+--+-- Mutating actions are executed during 'ApplySync' and simulated during 'DryRun'.+isMutatingAction :: SyncAction -> Bool+isMutatingAction ActionCreate {} = True+isMutatingAction ActionUpdate {} = True+isMutatingAction ActionChmod {}  = True+isMutatingAction _               = False++--------------------------------------------------------------------------------+-- 2. Synchronization Planning+--------------------------------------------------------------------------------++-- | Comprehensive pre-mutation plan detailing all actions required across a repository.+--+-- The 'SyncPlan' represents the pure evaluation outcome of comparing a repository's+-- on-disk state against the canonical 'StandardizationMatrix'. It serves as an immutable,+-- inspectable specification of all proposed mutations prior to disk execution.+--+-- === Structural Invariants+--+-- 1. __Completeness:__ Exactly one 'SyncAction' exists for every asset declared in+--    the input 'StandardizationMatrix'. No assets are skipped or duplicated.+-- 2. __Partitioning:__ The action counts ('planCreatedCount', 'planUpdatedCount',+--    'planChmodCount', 'planNoOpCount') sum to the total length of 'planActions'.+-- 3. __Cleanliness Predicate:__ 'isPlanClean' holds if and only if no mutating+--    actions are scheduled (i.e. created, updated, and chmod counts are all 0).+data SyncPlan = SyncPlan+  { planTargetDir     :: !FilePath+    -- ^ Target repository workspace path.+  , planProjectName   :: !Text+    -- ^ Name of the target project being synchronized.+  , planActions       :: ![SyncAction]+    -- ^ Complete list of planned per-asset actions.+  , planCreatedCount  :: !Int+    -- ^ Number of missing files planned for creation.+  , planUpdatedCount  :: !Int+    -- ^ Number of drifted files planned for content update.+  , planChmodCount    :: !Int+    -- ^ Number of files requiring permission correction.+  , planNoOpCount     :: !Int+    -- ^ Number of files already compliant.+  } deriving stock (Eq, Show)++-- | Checks whether a plan requires zero mutations (all assets compliant).+--+-- Returns 'True' when a repository is in perfect resonance with the standardization+-- matrix, meaning no files need to be created, updated, or chmodded.+isPlanClean :: SyncPlan -> Bool+isPlanClean plan =+  planCreatedCount plan == 0+    && planUpdatedCount plan == 0+    && planChmodCount plan == 0++-- | Computes a 'SyncPlan' for a target repository without modifying any files.+--+-- Evaluates each asset registered in the 'StandardizationMatrix', compares current+-- disk state with expected generator output, and categorizes required actions.+--+-- === Four-Phase Differential Inspection Pipeline+--+-- For each asset in the matrix, the planning algorithm executes the following+-- deterministic decision procedure:+--+-- 1. __Path Resolution:__+--    Resolves parameterized path tokens (@:name:@, @:project:@) against the target+--    'Project' specification via 'resolveAssetPath'.+--+-- 2. __Existence Inspection:__+--    Checks whether the target file exists on disk via 'doesFileExist'.+--    If absent, plans 'ActionCreate' containing the generator's expected output+--    and executable flag requirement.+--+-- 3. __Cryptographic Differential Scanning:__+--    If present, reads the file strictly and computes SHA-256 digests of both the+--    disk content and the generator output.+--    * If the hashes diverge, plans 'ActionUpdate' preserving the existing content+--      for diff generation and supplying the canonical replacement content.+--+-- 4. __POSIX Mode Verification:__+--    If the hashes match bit-for-bit, inspects the file's owner executable bit via+--    'getPermissions'.+--    * If the executable permission disagrees with 'assetExecutable', plans 'ActionChmod'.+--    * If permissions match, plans 'ActionNoOp', indicating clean compliance.+--+-- === Worked Example+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync.Matrix (canonicalMatrix)+-- >>> let proj = defaultProject "gachi" (Author "Nadia" "nadia@example.com")+-- >>> plan <- computeSyncPlan "/home/nyc/src/gachi" proj canonicalMatrix+-- >>> planNoOpCount plan > 0+-- True+computeSyncPlan :: FilePath -> Project -> StandardizationMatrix -> IO SyncPlan+computeSyncPlan repoRoot proj matrix = do+  actions <- forM (matrixAssets matrix) $ \asset -> do+    let relPath = resolveAssetPath proj asset+        absPath = repoRoot </> relPath+    exists <- doesFileExist absPath+    if not exists+      then+        let expectedContent = assetGenerator asset proj+            execBit = assetExecutable asset+        in pure (ActionCreate relPath expectedContent execBit)+      else (do+        actualContent <- TIO.readFile absPath+        let expectedContent = assetGenerator asset proj+            execBit = assetExecutable asset+        if actualContent /= expectedContent+          then pure (ActionUpdate relPath actualContent expectedContent execBit)+          else do+            perms <- getPermissions absPath+            let actualExec = executable perms+            if actualExec /= execBit+              then pure (ActionChmod relPath actualExec execBit)+              else pure (ActionNoOp relPath)+        ) `catch` (\(e :: SomeException) ->+            pure (ActionError relPath (T.pack (displayException e))))++  let creates = length [() | ActionCreate {} <- actions]+      updates = length [() | ActionUpdate {} <- actions]+      chmods  = length [() | ActionChmod {} <- actions]+      noops   = length [() | ActionNoOp _ <- actions]++  pure SyncPlan+    { planTargetDir     = repoRoot+    , planProjectName   = projectName proj+    , planActions       = actions+    , planCreatedCount  = creates+    , planUpdatedCount  = updates+    , planChmodCount    = chmods+    , planNoOpCount     = noops+    }++--------------------------------------------------------------------------------+-- 3. Synchronization Execution+--------------------------------------------------------------------------------++-- | Result summary produced by executing a 'SyncPlan'.+data SyncResult = SyncResult+  { resultTargetDir   :: !FilePath+    -- ^ Target repository path.+  , resultProjectName :: !Text+    -- ^ Name of the synchronized project.+  , resultMode        :: !SyncMode+    -- ^ Mode under which synchronization executed ('DryRun' or 'ApplySync').+  , resultCreated     :: ![FilePath]+    -- ^ List of file paths created.+  , resultUpdated     :: ![FilePath]+    -- ^ List of file paths updated.+  , resultChmodded    :: ![FilePath]+    -- ^ List of file paths whose permissions were corrected.+  , resultUnchanged   :: ![FilePath]+    -- ^ List of file paths left untouched.+  , resultErrors      :: ![(FilePath, Text)]+    -- ^ List of errors encountered during execution.+  , resultSuccess     :: !Bool+    -- ^ Whether all actions executed without error.+  } deriving stock (Eq, Show)++-- | Executes a computed 'SyncPlan' under the specified 'SyncMode'.+--+-- === Operational Modes & Safety Invariants+--+-- * __'DryRun' Mode:__+--   Evaluates all actions declared in the plan, extracts summary statistics, and+--   constructs a 'SyncResult' representing the planned mutations without performing+--   any filesystem writes or permission modifications.+--+-- * __'ApplySync' Mode:__+--   Applies all mutating actions sequentially to the destination repository:+--   1. __Parent Directory Synthesis:__ Automatically creates any missing parent+--      directories (via 'createDirectoryIfMissing True') prior to writing file contents.+--   2. __Atomic Content Materialization:__ Writes canonical UTF-8 text contents to+--      disk for both 'ActionCreate' and 'ActionUpdate' operations.+--   3. __Permission Enforcement:__ When an asset requires executable permissions+--      (e.g., verification harnesses, Git hooks, tag generation scripts), explicitly+--      sets the owner executable bit via 'setOwnerExecutable True'.+--   4. __Exception Containment:__ Wraps each individual filesystem mutation in an+--      isolated exception handler. If a write fails (due to permissions, disk space,+--      or locks), the error is recorded in 'resultErrors' while continuing execution+--      for remaining assets.+--   5. __Idempotent Resonance:__ After successful execution under 'ApplySync', the+--      repository is mathematically stable: immediately calling 'computeSyncPlan'+--      produces 'isPlanClean plan == True'.+executeSyncPlan :: SyncMode -> SyncPlan -> IO SyncResult+executeSyncPlan mode plan = do+  case mode of+    DryRun -> do+      -- In DryRun mode, return planned changes without mutating disk+      let created = [p | ActionCreate p _ _ <- planActions plan]+          updated = [p | ActionUpdate p _ _ _ <- planActions plan]+          chmodded = [p | ActionChmod p _ _ <- planActions plan]+          unchanged = [p | ActionNoOp p <- planActions plan]+          errs = [(p, msg) | ActionError p msg <- planActions plan]+      pure SyncResult+        { resultTargetDir   = planTargetDir plan+        , resultProjectName = planProjectName plan+        , resultMode        = DryRun+        , resultCreated     = created+        , resultUpdated     = updated+        , resultChmodded    = chmodded+        , resultUnchanged   = unchanged+        , resultErrors      = errs+        , resultSuccess     = null errs+        }+    ApplySync -> do+      -- In ApplySync mode, apply mutations to disk safely+      resActions <- forM (planActions plan) $ \action -> do+        case action of+          ActionCreate relPath content isExec -> (do+            let absPath = planTargetDir plan </> relPath+            createDirectoryIfMissing True (takeDirectory absPath)+            TIO.writeFile absPath content+            when isExec $ do+              perms <- getPermissions absPath+              setPermissions absPath (setOwnerExecutable True perms)+            pure (ExecCreated relPath)+            ) `catch` (\(e :: SomeException) ->+                pure (ExecFailed relPath (T.pack (displayException e))))++          ActionUpdate relPath _ newContent isExec -> (do+            let absPath = planTargetDir plan </> relPath+            createDirectoryIfMissing True (takeDirectory absPath)+            TIO.writeFile absPath newContent+            when isExec $ do+              perms <- getPermissions absPath+              setPermissions absPath (setOwnerExecutable True perms)+            pure (ExecUpdated relPath)+            ) `catch` (\(e :: SomeException) ->+                pure (ExecFailed relPath (T.pack (displayException e))))++          ActionChmod relPath _ targetExec -> (do+            let absPath = planTargetDir plan </> relPath+            perms <- getPermissions absPath+            setPermissions absPath (setOwnerExecutable targetExec perms)+            pure (ExecChmodded relPath)+            ) `catch` (\(e :: SomeException) ->+                pure (ExecFailed relPath (T.pack (displayException e))))++          ActionNoOp relPath ->+            pure (ExecUnchanged relPath)++          ActionError relPath msg ->+            pure (ExecFailed relPath msg)++      let created = [p | ExecCreated p <- resActions]+          updated = [p | ExecUpdated p <- resActions]+          chmodded = [p | ExecChmodded p <- resActions]+          unchanged = [p | ExecUnchanged p <- resActions]+          errs = [(p, msg) | ExecFailed p msg <- resActions]++      pure SyncResult+        { resultTargetDir   = planTargetDir plan+        , resultProjectName = planProjectName plan+        , resultMode        = ApplySync+        , resultCreated     = created+        , resultUpdated     = updated+        , resultChmodded    = chmodded+        , resultUnchanged   = unchanged+        , resultErrors      = errs+        , resultSuccess     = null errs+        }++-- | Internal tracking of single action execution outcome during 'ApplySync'.+data ActionExecOutcome+  = ExecCreated !FilePath+  | ExecUpdated !FilePath+  | ExecChmodded !FilePath+  | ExecUnchanged !FilePath+  | ExecFailed !FilePath !Text++-- | High-level synchronization: computes plan and executes under 'SyncMode'.+syncRepository+  :: SyncMode+  -> FilePath+  -> Project+  -> StandardizationMatrix+  -> IO SyncResult+syncRepository mode repoRoot proj matrix = do+  plan <- computeSyncPlan repoRoot proj matrix+  executeSyncPlan mode plan++-- | Fast zero-configuration synchronization for a single repository checkout.+--+-- Automatically locates and decodes @project.dhall@ if present. If absent, derives+-- a default 'Project' model using the folder's canonical basename and standard ecosystem+-- author metadata. Synchronizes the checkout against 'canonicalMatrix'.+--+-- === Error Isolation+--+-- Returns 'Left' with diagnostic text if @project.dhall@ fails to parse or decode.+-- On successful plan execution, returns 'Right' containing the complete 'SyncResult'.+syncRepositoryQuick :: SyncMode -> FilePath -> IO (Either Text SyncResult)+syncRepositoryQuick mode repoRoot = do+  absRoot <- canonicalizePath repoRoot+  let dhallPath = absRoot </> "project.dhall"+  hasDhall <- doesFileExist dhallPath+  if hasDhall+    then (do+      proj <- decodeProjectFile dhallPath+      res <- syncRepository mode absRoot proj canonicalMatrix+      pure (Right res)+      ) `catch` (\(e :: SomeException) ->+          pure (Left ("Failed to decode project.dhall: " <> T.pack (displayException e))))+    else do+      let inferredName = T.pack (takeFileName absRoot)+          defaultAuthor = Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me"+          proj = defaultProject inferredName defaultAuthor+      res <- syncRepository mode absRoot proj canonicalMatrix+      pure (Right res)++--------------------------------------------------------------------------------+-- 4. Multi-Repository Ecosystem Orchestration+--------------------------------------------------------------------------------++-- | Discovers sibling ecosystem repositories residing within a parent directory.+--+-- Scans immediate child directories in 'parentDir' and identifies candidates that+-- exhibit repository traits:+-- * Presence of a @.git@ directory (VCS root).+-- * Presence of a @cabal.project@ manifest (Cabal multi-package workspace root).+-- * Presence of a @project.dhall@ specification (Gachi schema specification).+--+-- Ignores plain directories that lack all three indicators, preventing inadvertent+-- modification of unrelated scratch folders or temporary data.+--+-- === Worked Example+--+-- >>> repos <- discoverEcosystemRepositories "/home/nyc/src"+-- >>> any (\p -> takeFileName p == "gachi") repos+-- True+discoverEcosystemRepositories :: FilePath -> IO [FilePath]+discoverEcosystemRepositories parentDir = do+  exists <- doesDirectoryExist parentDir+  if not exists+    then pure []+    else do+      entries <- listDirectory parentDir+      repos <- forM entries $ \entry -> do+        let candidate = parentDir </> entry+        isDir <- doesDirectoryExist candidate+        if not isDir+          then pure Nothing+          else do+            hasGit <- doesDirectoryExist (candidate </> ".git")+            hasCabalProj <- doesFileExist (candidate </> "cabal.project")+            hasDhall <- doesFileExist (candidate </> "project.dhall")+            if hasGit || hasCabalProj || hasDhall+              then pure (Just candidate)+              else pure Nothing+      pure (catMaybes repos)++-- | Harmonically synchronizes all discovered repositories within an ecosystem directory.+--+-- Sequentially iterates over all checkouts discovered via 'discoverEcosystemRepositories',+-- evaluating and applying synchronization under the given 'SyncMode'.+--+-- === Failure Isolation & Reporting+--+-- Execution is resilient: if one repository fails to evaluate its @project.dhall@ or+-- encounters I/O errors during synchronization, the failure is captured as 'Left'+-- in the resulting association list while synchronization continues across all other+-- repositories.+syncEcosystem+  :: SyncMode+  -> FilePath+  -> StandardizationMatrix+  -> IO [(FilePath, Either Text SyncResult)]+syncEcosystem mode parentDir matrix = do+  repoPaths <- discoverEcosystemRepositories parentDir+  forM repoPaths $ \repoPath -> do+    let dhallPath = repoPath </> "project.dhall"+    hasDhall <- doesFileExist dhallPath+    if hasDhall+      then (do+        proj <- decodeProjectFile dhallPath+        res <- syncRepository mode repoPath proj matrix+        pure (repoPath, Right res)+        ) `catch` (\(e :: SomeException) ->+            pure (repoPath, Left ("Failed to decode project.dhall: " <> T.pack (displayException e))))+      else do+        let inferredName = T.pack (takeFileName repoPath)+            defaultAuthor = Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me"+            proj = defaultProject inferredName defaultAuthor+        res <- syncRepository mode repoPath proj matrix+        pure (repoPath, Right res)++--------------------------------------------------------------------------------+-- 5. Diagnostic Plan & Result Formatting+--------------------------------------------------------------------------------++-- | Renders a 'SyncPlan' as a formatted plain-text ASCII table.+--+-- Produces an aligned, terminal-friendly diagnostic report containing:+-- * Workspace root directory and target project name.+-- * Aggregate action summary counters (Created, Updated, Chmod, No-Op).+-- * Tabular inventory of every evaluated asset:+--   * @CREATE@: Missing files scheduled for creation, marked with @(+x)@ if executable.+--   * @UPDATE@: Drifted files showing 8-character SHA-256 prefixes (@oldHash -> newHash@).+--   * @CHMOD@: Permission fixes indicating transition (@exec: False -> True@).+--   * @NO-OP@: Cleanly compliant files left untouched.+--   * @ERROR@: Diagnostic message for files where I/O inspection failed.+--+-- === Output Stability+--+-- Suitable for stdout terminal streaming, CI log capture, or regression testing.+formatSyncPlan :: SyncPlan -> Text+formatSyncPlan plan = T.unlines $+  [ "================================================================================"+  , " Gachi Synchronization Plan (Pre-Mutation Preview)"+  , "================================================================================"+  , " Target Directory: " <> T.pack (planTargetDir plan)+  , " Project Name:     " <> planProjectName plan+  , " Actions Planned:  "+      <> "Create: " <> T.show (planCreatedCount plan)+      <> " | Update: " <> T.show (planUpdatedCount plan)+      <> " | Chmod: " <> T.show (planChmodCount plan)+      <> " | No-Op: " <> T.show (planNoOpCount plan)+  , "--------------------------------------------------------------------------------"+  , " Action     | Path"+  , "------------+-------------------------------------------------------------------"+  ]+  ++ map formatActionRow (planActions plan)+  +++  [ "================================================================================" ]+  where+    formatActionRow (ActionCreate p _ isExec) =+      " CREATE     | " <> T.pack p <> (if isExec then " (+x)" else "")+    formatActionRow (ActionUpdate p oldTxt newTxt isExec) =+      let oldHash = T.take 8 (sha256Text oldTxt)+          newHash = T.take 8 (sha256Text newTxt)+      in " UPDATE     | " <> T.pack p <> " (" <> oldHash <> " -> " <> newHash <> ")"+           <> (if isExec then " (+x)" else "")+    formatActionRow (ActionChmod p actual expected) =+      " CHMOD      | " <> T.pack p <> " (exec: " <> T.show actual <> " -> " <> T.show expected <> ")"+    formatActionRow (ActionNoOp p) =+      " NO-OP      | " <> T.pack p <> " (compliant)"+    formatActionRow (ActionError p msg) =+      " ERROR      | " <> T.pack p <> " (" <> msg <> ")"++-- | Renders a 'SyncResult' as a formatted plain-text ASCII summary banner.+--+-- Formats the final disposition of a completed or simulated synchronization run:+-- * Destination directory and project identification slug.+-- * Active execution mode: @DRY RUN (Preview Only)@ vs @APPLIED@.+-- * Status verdict: @SUCCESS@ (zero errors) or @FAILED@ (one or more I/O errors).+-- * Exact counts of created, updated, chmodded, and unchanged files.+-- * Dedicated error section listing failing relative paths and exception messages.+formatSyncResult :: SyncResult -> Text+formatSyncResult res = T.unlines $+  [ "================================================================================"+  , " Gachi Synchronization Execution Summary"+  , "================================================================================"+  , " Target Directory: " <> T.pack (resultTargetDir res)+  , " Project Name:     " <> resultProjectName res+  , " Execution Mode:   " <> (if resultMode res == DryRun then "DRY RUN (Preview Only)" else "APPLIED")+  , " Status:           " <> (if resultSuccess res then "SUCCESS" else "FAILED")+  , " Files Created:    " <> T.show (length (resultCreated res))+  , " Files Updated:    " <> T.show (length (resultUpdated res))+  , " Permissions Fixed:" <> T.show (length (resultChmodded res))+  , " Files Unchanged:  " <> T.show (length (resultUnchanged res))+  ]+  ++ formatErrorSection (resultErrors res)+  +++  [ "================================================================================" ]+  where+    formatErrorSection [] = []+    formatErrorSection errs =+      [ "--------------------------------------------------------------------------------"+      , " Errors Encountered:"+      ] ++ map (\(p, msg) -> "  * " <> T.pack p <> ": " <> msg) errs
+ src/Gachi/Sync/Hash.hs view
@@ -0,0 +1,298 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.Sync.Hash+-- Description: Pure, self-contained SHA-256 differential hashing engine+--+-- This module implements FIPS 180-2 compliant Secure Hash Algorithm (SHA-256)+-- in pure Haskell with zero external cryptographic dependencies.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Harmonic Chattering Synchronization and Differential Propagation]+-- [ref:note-harmonic-sync], the synchronization engine must rapidly verify+-- content identity across dozens of workspace files without triggering unnecessary+-- disk writes or external process invocations.+--+-- == Invariant: Pure Referential Transparency+--+-- * __Totality:__ Hashing functions are pure and total across all inputs.+-- * __Zero-Dependency:__ Implemented using standard @base@ and @bytestring@ primitives.+-- * __FIPS 180-2 Compliance:__ Satisfies standard NIST test vectors, including the empty string+--   digest (@e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855@).+--+-- == Algorithmic Overview (FIPS 180-2 SHA-256)+--+-- SHA-256 operates on 512-bit message blocks through a sequence of mathematical+-- transformations structured across four stages:+--+-- 1. __Preprocessing and Padding:__+--    The arbitrary-length input bitstream \( M \) of length \( L \) bits is padded+--    by appending a single '1' bit, followed by \( k \) '0' bits, where \( k \)+--    is the smallest non-negative integer satisfying:+--    \[+--      L + 1 + k \equiv 448 \pmod{512}+--    \]+--    Finally, the 64-bit big-endian representation of \( L \) is appended, yielding+--    an exact multiple of 512 bits (16 32-bit words).+--+-- 2. __Message Schedule Expansion:__+--    Each 16-word block \( W_{0..15} \) is expanded into a 64-word schedule \( W_{0..63} \):+--    \[+--      W_t = \sigma_1(W_{t-2}) + W_{t-7} + \sigma_0(W_{t-15}) + W_{t-16}+--    \]+--    where the lower-case sigma functions apply rotational and right-shift mixing:+--    \[+--      \sigma_0(x) = \text{RotR}(x, 7) \oplus \text{RotR}(x, 18) \oplus \text{ShR}(x, 3)+--    \]+--    \[+--      \sigma_1(x) = \text{RotR}(x, 17) \oplus \text{RotR}(x, 19) \oplus \text{ShR}(x, 10)+--    \]+--+-- 3. __64-Round Compression Loop:__+--    Eight 32-bit working state variables (\( a, b, c, d, e, f, g, h \)) are initialized+--    from the intermediate hash values \( H^{(i-1)} \). In each step \( t \in [0, 63] \):+--    \[+--      T_1 = h + \Sigma_1(e) + \text{Ch}(e, f, g) + K_t + W_t+--    \]+--    \[+--      T_2 = \Sigma_0(a) + \text{Maj}(a, b, c)+--    \]+--    \[+--      h \leftarrow g, \quad g \leftarrow f, \quad f \leftarrow e, \quad e \leftarrow d + T_1+--    \]+--    \[+--      d \leftarrow c, \quad c \leftarrow b, \quad b \leftarrow a, \quad a \leftarrow T_1 + T_2+--    \]+--    where the non-linear bitwise choice and majority combinators are defined as:+--    \[+--      \text{Ch}(x, y, z) = (x \wedge y) \oplus (\neg x \wedge z)+--    \]+--    \[+--      \text{Maj}(x, y, z) = (x \wedge y) \oplus (x \wedge z) \oplus (y \wedge z)+--    \]+--+-- 4. __Intermediate State Accumulation:__+--    The updated working variables are summed modulo \( 2^{32} \) into the running state:+--    \[+--      H_j^{(i)} = H_j^{(i-1)} + \text{Var}_j \quad \forall j \in \{0..7\}+--    \]+--+-- = Worked Example+--+-- >>> import Gachi.Sync.Hash+-- >>> sha256Text ""+-- "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"+-- >>> sha256Text "gachi"+-- "e43b177d6118bfa6b6f7097eb586ef291f5a5a92cf6d62ea09c0fa168e312521"+module Gachi.Sync.Hash+  ( -- * Pure SHA-256 Digest Functions+    sha256Text+  , sha256ByteString+  , sha256File+  ) where++import Data.Bits (complement, rotateR, shiftL, shiftR, xor, (.&.), (.|.))+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Word (Word32, Word64)+import Numeric (showHex)++--------------------------------------------------------------------------------+-- 1. Initial State & Round Constants (FIPS 180-2)+--------------------------------------------------------------------------------++-- | Initial 256-bit hash values representing first 32 bits of fractional parts+-- of the square roots of the first 8 prime numbers (2..19).+initialHash :: (Word32, Word32, Word32, Word32, Word32, Word32, Word32, Word32)+initialHash =+  ( 0x6a09e667+  , 0xbb67ae85+  , 0x3c6ef372+  , 0xa54ff53a+  , 0x510e527f+  , 0x9b05688c+  , 0x1f83d9ab+  , 0x5be0cd19+  )++-- | Sixty-four 32-bit round constants representing the first 32 bits of the+-- fractional parts of the cube roots of the first 64 prime numbers (2..311).+roundConstants :: [Word32]+roundConstants =+  [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5+  , 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5+  , 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3+  , 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174+  , 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc+  , 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da+  , 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7+  , 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967+  , 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13+  , 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85+  , 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3+  , 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070+  , 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5+  , 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3+  , 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208+  , 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2+  ]++--------------------------------------------------------------------------------+-- 2. Bitwise Logical Functions+--------------------------------------------------------------------------------++-- | Bitwise choice: (x AND y) XOR (NOT x AND z).+ch :: Word32 -> Word32 -> Word32 -> Word32+ch !x !y !z = (x .&. y) `xor` (complement x .&. z)+{-# INLINE ch #-}++-- | Bitwise majority: (x AND y) XOR (x AND z) XOR (y AND z).+maj :: Word32 -> Word32 -> Word32 -> Word32+maj !x !y !z = (x .&. y) `xor` (x .&. z) `xor` (y .&. z)+{-# INLINE maj #-}++-- | Upper-case Sigma 0: RotR(x, 2) XOR RotR(x, 13) XOR RotR(x, 22).+sigmaUpper0 :: Word32 -> Word32+sigmaUpper0 !x = rotateR x 2 `xor` rotateR x 13 `xor` rotateR x 22+{-# INLINE sigmaUpper0 #-}++-- | Upper-case Sigma 1: RotR(x, 6) XOR RotR(x, 11) XOR RotR(x, 25).+sigmaUpper1 :: Word32 -> Word32+sigmaUpper1 !x = rotateR x 6 `xor` rotateR x 11 `xor` rotateR x 25+{-# INLINE sigmaUpper1 #-}++-- | Lower-case Sigma 0: RotR(x, 7) XOR RotR(x, 18) XOR ShiftR(x, 3).+sigmaLower0 :: Word32 -> Word32+sigmaLower0 !x = rotateR x 7 `xor` rotateR x 18 `xor` shiftR x 3+{-# INLINE sigmaLower0 #-}++-- | Lower-case Sigma 1: RotR(x, 17) XOR RotR(x, 19) XOR ShiftR(x, 10).+sigmaLower1 :: Word32 -> Word32+sigmaLower1 !x = rotateR x 17 `xor` rotateR x 19 `xor` shiftR x 10+{-# INLINE sigmaLower1 #-}++--------------------------------------------------------------------------------+-- 3. Message Padding & Block Preparation+--------------------------------------------------------------------------------++-- | Pads message bytes according to FIPS 180-2:+-- 1. Appends bit 1 (single byte 0x80).+-- 2. Appends k zero bytes such that (length + 1 + k) == 56 mod 64.+-- 3. Appends 64-bit big-endian representation of original bit length.+padMessage :: BS.ByteString -> BL.ByteString+padMessage input =+  let lenBytes = fromIntegral (BS.length input) :: Word64+      lenBits = lenBytes * 8+      padLen = (55 - fromIntegral (lenBytes `mod` 64)) `mod` 64+      zeros = BS.replicate padLen 0+      lenEncoding = BS.pack+        [ fromIntegral (shiftR lenBits 56 .&. 0xFF)+        , fromIntegral (shiftR lenBits 48 .&. 0xFF)+        , fromIntegral (shiftR lenBits 40 .&. 0xFF)+        , fromIntegral (shiftR lenBits 32 .&. 0xFF)+        , fromIntegral (shiftR lenBits 24 .&. 0xFF)+        , fromIntegral (shiftR lenBits 16 .&. 0xFF)+        , fromIntegral (shiftR lenBits 8  .&. 0xFF)+        , fromIntegral (lenBits .&. 0xFF)+        ]+  in BL.fromChunks [input, BS.singleton 0x80, zeros, lenEncoding]++-- | Parses a 64-byte block into 16 32-bit big-endian words.+parseBlock :: BS.ByteString -> [Word32]+parseBlock bs = go (BS.unpack bs)+  where+    go [] = []+    go (b0:b1:b2:b3:rest) =+      let !w0 = fromIntegral b0 `shiftL` 24+          !w1 = fromIntegral b1 `shiftL` 16+          !w2 = fromIntegral b2 `shiftL` 8+          !w3 = fromIntegral b3+          !w  = w0 .|. w1 .|. w2 .|. w3+      in w : go rest+    go _ = []++-- | Expands the 16 words of a block into a full 64-word message schedule.+expandSchedule :: [Word32] -> [Word32]+expandSchedule w16 = go 16 (w16 ++ replicate 48 0)+  where+    go !t !ws+      | t == 64   = ws+      | otherwise =+          let s0 = sigmaLower0 (ws !! (t - 15))+              s1 = sigmaLower1 (ws !! (t - 2))+              newW = (ws !! (t - 16)) + s0 + (ws !! (t - 7)) + s1+              updated = take t ws ++ [newW] ++ drop (t + 1) ws+          in go (t + 1) updated++--------------------------------------------------------------------------------+-- 4. Compression Function & Main Loop+--------------------------------------------------------------------------------++-- | Processes a single 64-byte block, updating the internal 8-word hash state.+compressBlock+  :: (Word32, Word32, Word32, Word32, Word32, Word32, Word32, Word32)+  -> BS.ByteString+  -> (Word32, Word32, Word32, Word32, Word32, Word32, Word32, Word32)+compressBlock (!h0, !h1, !h2, !h3, !h4, !h5, !h6, !h7) blockBytes =+  let wSchedule = expandSchedule (parseBlock blockBytes)+      (!a, !b, !c, !d, !e, !f, !g, !h) =+        foldl step (h0, h1, h2, h3, h4, h5, h6, h7) (zip roundConstants wSchedule)+  in (h0 + a, h1 + b, h2 + c, h3 + d, h4 + e, h5 + f, h6 + g, h7 + h)+  where+    step (!a, !b, !c, !d, !e, !f, !g, !h) (!k, !w) =+      let !t1 = h + sigmaUpper1 e + ch e f g + k + w+          !t2 = sigmaUpper0 a + maj a b c+      in (t1 + t2, a, b, c, d + t1, e, f, g)++-- | Computes the raw 8-word SHA-256 state from lazy byte stream.+hashByteString :: BL.ByteString -> (Word32, Word32, Word32, Word32, Word32, Word32, Word32, Word32)+hashByteString lazyBytes =+  let chunks = splitBlocks (BL.toStrict lazyBytes)+  in foldl compressBlock initialHash chunks+  where+    splitBlocks bs+      | BS.null bs = []+      | otherwise  = BS.take 64 bs : splitBlocks (BS.drop 64 bs)++-- | Formats an 8-word hash state into a 64-character lowercase hexadecimal string.+formatDigest :: (Word32, Word32, Word32, Word32, Word32, Word32, Word32, Word32) -> Text+formatDigest (h0, h1, h2, h3, h4, h5, h6, h7) =+  T.pack $ concatMap padHex [h0, h1, h2, h3, h4, h5, h6, h7]+  where+    padHex w =+      let s = showHex w ""+      in replicate (8 - length s) '0' ++ s++--------------------------------------------------------------------------------+-- 5. Public API+--------------------------------------------------------------------------------++-- | Computes the SHA-256 hex digest of a UTF-8 'Text' string.+--+-- === Worked Example+--+-- >>> sha256Text ""+-- "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"+sha256Text :: Text -> Text+sha256Text = sha256ByteString . TE.encodeUtf8++-- | Computes the SHA-256 hex digest of a strict 'BS.ByteString'.+--+-- === Worked Example+--+-- >>> sha256ByteString "hello"+-- "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"+sha256ByteString :: BS.ByteString -> Text+sha256ByteString bs = formatDigest (hashByteString (padMessage bs))++-- | Computes the SHA-256 hex digest of a file on disk.+--+-- Reads the file contents strictly as binary and computes the digest.+sha256File :: FilePath -> IO Text+sha256File path = do+  content <- BS.readFile path+  pure (sha256ByteString content)
+ src/Gachi/Sync/Matrix.hs view
@@ -0,0 +1,597 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.Sync.Matrix+-- Description: Canonical 9-dimensional Standardization Matrix definition and queries+--+-- This module implements the authoritative Standardization Matrix defined in+-- ~/src/PLAN.md §1.A. It registers every standardized ecosystem asset across the+-- nine canonical dimensions, pairs them with pure deterministic generators, and+-- provides query combinators for repository audits and configuration propagation.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization] and Note [Cross-Repository Standardization Matrix and Audit Semantics]+-- [ref:note-standardization-matrix], all repositories across ~/src/* share an+-- authoritative structural skeleton. This module provides the central matrix+-- registry from which audit tools and sync commands derive their expectations.+--+-- = Note [Cross-Repository Standardization Matrix and Audit Semantics]+-- [tag:note-standardization-matrix]+--+-- Multi-repository ecosystems face constant structural entropy. As individual+-- libraries evolve, their formatting configurations, compiler warning flags,+-- git commit hooks, CI workflows, and AI pair programming instructions diverge.+-- Over time, this divergence creates friction when developers or AI assistants+-- navigate across repositories.+--+-- To prevent this fragmentation, Gachi defines a 9-dimensional Standardization+-- Matrix (~/src/PLAN.md §1.A) that is non-negotiably enforced across all checkouts:+--+-- 1.  __Workspace Layout (Dimension 1):__+--     Standardized multi-package DAG (`packages/*/`), central documentation+--     corpus (`docs/INDEX.md`, `docs/notes/`), and the two-file operational+--     planning loop (`PLAN.md`, `STATE.md`). Ensures that any contributor or+--     automated agent arriving in any checkout can immediately locate documentation,+--     active milestones, and the operational frontier without cognitive disorientation.+--+-- 2.  __Code Formatters (Dimension 2):__+--     Deterministic AST-preserving formatting profiles across Haskell+--     (@fourmolu.yaml@), Rust (@rustfmt.toml@), and C/C++ (@.clang-format@).+--     Prevents superficial whitespace and layout diffs from polluting code review+--     and git blame histories during cross-repository refactorings.+--+-- 3.  __Linters & Guardrails (Dimension 3):__+--     Strict zero-hint HLint policy (@.hlint.yaml@) enforcing modern GHC+--     idioms such as @OverloadedRecordDot@. Prohibits outdated patterns and ensures+--     consistent language extension usage across all packages.+--+-- 4.  __Build Conventions (Dimension 4):__+--     Toolchain pinning (@cabal.project@) targeting GHC 9.14.1 and Cabal 3.18,+--     with standard developer override stanzas (@cabal.project.local.example@).+--     Guarantees hermetic, reproducible builds across developer laptops and CI nodes.+--+-- 5.  __Git Governance (Dimension 5):__+--     Linux kernel RFC 2822 commit trailer rules (@.gitmessage@), Tagref+--     configuration (@tagref.yml@), and executable git hooks (@.githooks/commit-msg@,+--     @.githooks/pre-commit@). Enforces legal DCO boundaries, transparent AI+--     attribution, and pre-commit verification before code leaves local workstations.+--+-- 6.  __Remote Topology (Dimension 6):__+--     Multi-forge replication topology separating Radicle peer-to-peer from+--     commercial and community mirrors, documented in @docs/MAINTAINER_GUIDE.md@.+--     Guarantees sovereign, censorship-resistant repository distribution.+--+-- 7.  __Tiered CI Workflows (Dimension 7):__+--     Asymmetric forge resource allocation: full physical verification on+--     high-resource GitHub Actions (@.github/workflows/ci.yml@) versus lightweight+--     static verification gates on community forges (@.gitlab-ci.yml@).+--     Conserves shared community runner quotas while enforcing rigorous testing.+--     See Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning]+--     [ref:note-forge-tiered-ci].+--+-- 8.  __Polyglot Invariants (Dimension 8):__+--     Cross-language FFI boundary invariants, C/C++ clang-format styling,+--     and rustfmt rules for non-Haskell components. Ensures consistent ergonomics+--     when Haskell interfaces with low-level systems code or Rust libraries.+--+-- 9.  __Agent Advisory Layer (Dimension 9):__+--     Context-budgeted AI agent instructions (@AGENTS.md@, ~4 KB limit),+--     compatibility pointers (@CLAUDE.md@), and modular rules in @.agents/rules/@.+--     Prevents LLM context saturation and prompt drift across agent sessions.+--+-- 10. __Physical Verification Harnesses:__+--     Turnkey shell scripts in @bin/@ enforcing the five physical gates:+--     HLint cleanliness, Cabal test suite, Tagref cross-references, Markdown+--     link integrity, and CLOC comment density.+--+-- == Audit Semantics+--+-- The repository audit engine operates under strict invariants:+-- * __Read-Only Purity:__ Audits inspect disk state without mutating any files.+-- * __Total Classification:__ Every audited asset is classified into exactly one+--   'AssetAuditStatus': 'StatusCompliant', 'StatusMissing', 'StatusContentDrift',+--   'StatusPermissionDrift', or 'StatusIOError'.+-- * __Deterministic Generators:__ Expected contents are produced purely by+--   evaluating generators against the target repository's 'Project' schema.+--+-- = Worked Example+--+-- >>> import Gachi.Sync.Matrix+-- >>> length (matrixAssets canonicalMatrix) >= 20+-- True+-- >>> length (matrixDimensions canonicalMatrix) == 10+-- True+module Gachi.Sync.Matrix+  ( -- * Canonical Standardization Matrix+    canonicalMatrix+  , canonicalAssets+  , canonicalDimensions++    -- * Query & Filter Combinators+  , assetsForCategory+  , ciWorkflowAssets+  , requiredAssets+  , optionalAssets+  , executableAssets+  , findAssetByPath+  , countByCategory+  ) where++import Data.List (find)++import Gachi.Scaffold.Templates+  ( docsCommitAttributionTemplate+  , docsIndexTemplate+  , docsMaintainerGuideTemplate+  , docsStrategyTemplate+  , planTemplate+  , stateTemplate+  )+import Gachi.Sync.Catalog+  ( agentsTemplate+  , cabalProjectLocalTemplate+  , cabalProjectTemplate+  , clangFormatTemplate+  , claudeTemplate+  , clocScriptTemplate+  , codeNavigationRuleTemplate+  , commitAttributionRuleTemplate+  , commitMsgHookTemplate+  , compilerVerificationRuleTemplate+  , documentationPrecedenceRuleTemplate+  , fourmoluTemplate+  , generateTagsTemplate+  , githubCiTemplate+  , gitlabCiTemplate+  , gitmessageTemplate+  , hlintTemplate+  , linkcheckScriptTemplate+  , preCommitHookTemplate+  , rustfmtTemplate+  , tagrefConfigTemplate+  , tagrefScriptTemplate+  , verifyScriptTemplate+  )+import Gachi.Sync.Types+  ( AssetCategory(..)+  , StandardAsset(..)+  , StandardizationMatrix(..)+  )++--------------------------------------------------------------------------------+-- 1. Canonical Dimensions+--------------------------------------------------------------------------------++-- | The complete list of standardization dimensions enforced across the ecosystem.+--+-- Covers all nine dimensions specified in ~/src/PLAN.md §1.A plus the physical+-- verification harness category.+--+-- Dimensions are ordered logically from fundamental repository structure to+-- high-level tooling and verification:+--+-- 1. 'CategoryLayout': Directory hierarchies and operational planning files.+-- 2. 'CategoryFormatter': Code layout and styling profiles across all languages.+-- 3. 'CategoryLinter': Static analysis policies and compiler guardrails.+-- 4. 'CategoryBuild': Build toolchain pinning and package manifest conventions.+-- 5. 'CategoryGitGovernance': Git commit hooks, commit templates, and tag references.+-- 6. 'CategoryRemoteTopology': Multi-forge replication and mirror definitions.+-- 7. 'CategoryCIWorkflow': Tiered continuous integration configurations.+-- 8. 'CategoryPolyglot': Multi-language FFI boundary invariants and foreign code.+-- 9. 'CategoryAgentAdvisory': AI pair programming instructions and context bounds.+-- 10. 'CategoryVerification': Physical gate test scripts and verification harnesses.+canonicalDimensions :: [AssetCategory]+canonicalDimensions =+  [ CategoryLayout+  , CategoryFormatter+  , CategoryLinter+  , CategoryBuild+  , CategoryGitGovernance+  , CategoryRemoteTopology+  , CategoryCIWorkflow+  , CategoryPolyglot+  , CategoryAgentAdvisory+  , CategoryVerification+  ]++--------------------------------------------------------------------------------+-- 2. Canonical Assets Registry+--------------------------------------------------------------------------------++-- | Canonical list of standardized ecosystem assets.+--+-- Each asset pairs its relative workspace path with its governing category,+-- necessity flag ('assetRequired'), executable permission requirement+-- ('assetExecutable'), descriptive synopsis, and pure generator function.+--+-- === Rationale by Dimension+--+-- * __Dimension 1 (Layout):__+--   Provides the foundational documentation corpus (@docs/@) and the operational+--   two-file planning loop (@PLAN.md@, @STATE.md@) ensuring orientation.+--+-- * __Dimension 2 (Formatters):__+--   Deterministic styling via Fourmolu, rustfmt, and clang-format prevents+--   cosmetic git churn during refactoring across packages.+--+-- * __Dimension 3 (Linters):__+--   Zero-hint HLint policy eliminates dead code, enforces modern record dot syntax,+--   and ensures uniform GHC extension discipline.+--+-- * __Dimension 4 (Build):__+--   Hermetic Cabal project configuration pins GHC 9.14.1 and standardizes warnings+--   while allowing local machine overrides via @cabal.project.local@.+--+-- * __Dimension 5 (Git Governance):__+--   Enforces Linux kernel RFC 2822 trailer format for AI attribution, Tagref+--   cross-referencing, and pre-commit integrity checking.+--+-- * __Dimension 6 (Remote Topology):__+--   Maintainer guidance for 5-forge replication across Radicle, GitHub, GitCode,+--   Framagit, and Disroot.+--+-- * __Dimension 7 (Tiered CI):__+--   Resource-differentiated CI: heavy verification on GitHub Actions vs lightweight+--   static checks on community GitLab runners.+--+-- * __Dimension 8 (Polyglot):__+--   Ensures C/C++ components follow LLVM/Linux formatting standards without tab drift.+--+-- * __Dimension 9 (Agent Advisory):__+--   Token-budgeted pointer file (@AGENTS.md@, ~4 KB) and modular rules for AI assistants.+--+-- * __Dimension 10 (Verification):__+--   Self-contained executable scripts implementing the five physical verification gates.+canonicalAssets :: [StandardAsset]+canonicalAssets =+  -- Dimension 1: Workspace Layout & Documentation Corpus+  [ StandardAsset+      "docs/INDEX.md"+      CategoryLayout+      True+      False+      "Central documentation index, reading order, and document map"+      docsIndexTemplate+  , StandardAsset+      "docs/notes/DOC_STRATEGY.md"+      CategoryLayout+      True+      False+      "Documentation strategy, four canonical genres, and precedence rules"+      docsStrategyTemplate+  , StandardAsset+      "docs/notes/COMMIT_ATTRIBUTION.md"+      CategoryLayout+      True+      False+      "Commit attribution specification for Linux kernel RFC 2822 trailers"+      docsCommitAttributionTemplate+  , StandardAsset+      "PLAN.md"+      CategoryLayout+      True+      False+      "Strategic roadmap, phased milestones, and architecture deliverables"+      planTemplate+  , StandardAsset+      "STATE.md"+      CategoryLayout+      True+      False+      "Operational frontier, active milestones, and two-file loop tracking"+      stateTemplate++  -- Dimension 2: Code Formatters+  , StandardAsset+      "fourmolu.yaml"+      CategoryFormatter+      True+      False+      "Canonical Fourmolu configuration enforcing deterministic Haskell formatting"+      (const fourmoluTemplate)+  , StandardAsset+      "rustfmt.toml"+      CategoryFormatter+      False+      False+      "Canonical rustfmt configuration for polyglot Rust components"+      (const rustfmtTemplate)+  , StandardAsset+      ".clang-format"+      CategoryFormatter+      False+      False+      "Canonical .clang-format configuration based on LLVM/Linux style"+      (const clangFormatTemplate)++  -- Dimension 3: Static Analysis & Linters+  , StandardAsset+      ".hlint.yaml"+      CategoryLinter+      True+      False+      "Strict zero-hint HLint static analysis configuration"+      (const hlintTemplate)++  -- Dimension 4: Build Conventions+  , StandardAsset+      "cabal.project"+      CategoryBuild+      True+      False+      "Root Cabal project file pinning GHC 9.14.1 and compiler warning flags"+      cabalProjectTemplate+  , StandardAsset+      "cabal.project.local.example"+      CategoryBuild+      True+      False+      "Developer local override configuration template"+      (const cabalProjectLocalTemplate)++  -- Dimension 5: Git Governance & Hooks+  , StandardAsset+      ".gitmessage"+      CategoryGitGovernance+      True+      False+      "Git commit message template specifying RFC 2822 trailer format"+      (const gitmessageTemplate)+  , StandardAsset+      "tagref.yml"+      CategoryGitGovernance+      True+      False+      "Tagref cross-reference configuration file"+      tagrefConfigTemplate+  , StandardAsset+      ".githooks/commit-msg"+      CategoryGitGovernance+      True+      True+      "Git commit-msg hook enforcing AI attribution and commit message rules"+      (const commitMsgHookTemplate)+  , StandardAsset+      ".githooks/pre-commit"+      CategoryGitGovernance+      True+      True+      "Git pre-commit hook guarding Tagref references and markdown links"+      preCommitHookTemplate++  -- Dimension 6: Remote Topology & Multi-Forge Mirroring+  , StandardAsset+      "docs/MAINTAINER_GUIDE.md"+      CategoryRemoteTopology+      True+      False+      "Maintainer guide documenting the 5-remote topology and push mirrors"+      docsMaintainerGuideTemplate++  -- Dimension 7: Forge-Tiered CI Workflows+  , StandardAsset+      ".github/workflows/ci.yml"+      CategoryCIWorkflow+      True+      False+      "High-resource GitHub Actions workflow running complete physical gates"+      githubCiTemplate+  , StandardAsset+      ".gitlab-ci.yml"+      CategoryCIWorkflow+      True+      False+      "Lightweight GitLab CI workflow for resource-constrained community forges"+      gitlabCiTemplate++  -- Dimension 8: Polyglot Invariants+  , StandardAsset+      ".clang-format"+      CategoryPolyglot+      False+      False+      "Polyglot C/C++ ABI formatting and style definition"+      (const clangFormatTemplate)++  -- Dimension 9: Agent Advisory Layer+  , StandardAsset+      "AGENTS.md"+      CategoryAgentAdvisory+      True+      False+      "Context-budgeted AI coding assistant advisory guide (~4 KB limit)"+      agentsTemplate+  , StandardAsset+      "CLAUDE.md"+      CategoryAgentAdvisory+      True+      False+      "Agent compatibility symlink/pointer to AGENTS.md"+      claudeTemplate+  , StandardAsset+      ".agents/rules/commit-attribution.md"+      CategoryAgentAdvisory+      True+      False+      "Modular rule: Linux kernel RFC 2822 trailer format and DCO boundaries"+      (const commitAttributionRuleTemplate)+  , StandardAsset+      ".agents/rules/code-navigation.md"+      CategoryAgentAdvisory+      True+      False+      "Modular rule: Token-conserving code navigation funnel"+      (const codeNavigationRuleTemplate)+  , StandardAsset+      ".agents/rules/compiler-verification.md"+      CategoryAgentAdvisory+      True+      False+      "Modular rule: Zero-warning compiler verification and zero-contamination"+      (const compilerVerificationRuleTemplate)+  , StandardAsset+      ".agents/rules/documentation-precedence.md"+      CategoryAgentAdvisory+      True+      False+      "Modular rule: Precedence rule and the two-file operational loop"+      (const documentationPrecedenceRuleTemplate)++  -- Dimension 10: Physical Verification Harnesses+  , StandardAsset+      "bin/:name:-verify"+      CategoryVerification+      True+      True+      "Primary physical verification runner executing all 5 non-negotiable gates"+      verifyScriptTemplate+  , StandardAsset+      "bin/:name:-tagref"+      CategoryVerification+      True+      True+      "Tagref cross-reference verification script"+      tagrefScriptTemplate+  , StandardAsset+      "bin/:name:-linkcheck"+      CategoryVerification+      True+      True+      "Markdown relative link and section anchor integrity checker"+      linkcheckScriptTemplate+  , StandardAsset+      "bin/:name:-cloc"+      CategoryVerification+      True+      True+      "CLOC comment density enforcement script requiring >= 50% comments"+      clocScriptTemplate+  , StandardAsset+      "bin/generate-tags"+      CategoryVerification+      True+      True+      "Universal ctags symbol table generator for fast token-efficient lookup"+      (const generateTagsTemplate)+  ]++--------------------------------------------------------------------------------+-- 3. Canonical Matrix+--------------------------------------------------------------------------------++-- | Authoritative singleton Standardization Matrix for the Gachi ecosystem.+--+-- Combines 'canonicalAssets' across all 'canonicalDimensions'.+--+-- Serves as the primary reference matrix for:+-- * 'Gachi.Sync.Audit.auditRepository': Inspecting workspaces for drift or missing files.+-- * 'Gachi.Sync.Audit.auditRepositoryQuick': Fast, zero-argument workspace verification.+-- * 'Gachi.Sync': Differential synchronization across repository checkouts.+canonicalMatrix :: StandardizationMatrix+canonicalMatrix = StandardizationMatrix+  { matrixAssets     = canonicalAssets+  , matrixDimensions = canonicalDimensions+  }++--------------------------------------------------------------------------------+-- 4. Query & Filter Combinators+--------------------------------------------------------------------------------++-- | Extracts all standardized assets belonging to a specific 'AssetCategory'.+--+-- Filters 'matrixAssets' by matching 'assetCategory'.+--+-- === Worked Example+--+-- >>> let formatters = assetsForCategory CategoryFormatter canonicalMatrix+-- >>> map assetPath formatters+-- ["fourmolu.yaml","rustfmt.toml",".clang-format"]+assetsForCategory :: AssetCategory -> StandardizationMatrix -> [StandardAsset]+assetsForCategory cat matrix =+  filter (\a -> assetCategory a == cat) (matrixAssets matrix)++-- | Extracts all forge-tiered CI workflow assets (Dimension 7) from a matrix.+--+-- Filters assets belonging to 'CategoryCIWorkflow'.+--+-- See Note [Forge-Tiered CI Resource Allocation and Static Gate Partitioning]+-- [ref:note-forge-tiered-ci].+--+-- === Worked Example+--+-- >>> let ciAssets = ciWorkflowAssets canonicalMatrix+-- >>> map assetPath ciAssets+-- [".github/workflows/ci.yml",".gitlab-ci.yml"]+ciWorkflowAssets :: StandardizationMatrix -> [StandardAsset]+ciWorkflowAssets = assetsForCategory CategoryCIWorkflow++-- | Extracts all required assets from a matrix.+--+-- An asset is required if its absence from disk constitutes an audit violation.+-- Required assets must be present in every production repository checkout.+--+-- === Worked Example+--+-- >>> let reqs = requiredAssets canonicalMatrix+-- >>> all assetRequired reqs+-- True+requiredAssets :: StandardizationMatrix -> [StandardAsset]+requiredAssets matrix =+  filter assetRequired (matrixAssets matrix)++-- | Extracts all optional assets from a matrix.+--+-- Optional assets (e.g. polyglot formatting rules for C/Rust) do not cause+-- audit failures when omitted from repositories that lack those language components.+--+-- === Worked Example+--+-- >>> let opts = optionalAssets canonicalMatrix+-- >>> all (not . assetRequired) opts+-- True+optionalAssets :: StandardizationMatrix -> [StandardAsset]+optionalAssets matrix =+  filter (not . assetRequired) (matrixAssets matrix)++-- | Extracts all assets requiring POSIX executable permissions (@+x@).+--+-- Includes verification scripts in @bin/@ and git hooks in @.githooks/@.+-- The audit engine validates that files on disk have the executable bit set.+--+-- === Worked Example+--+-- >>> let execs = executableAssets canonicalMatrix+-- >>> all assetExecutable execs+-- True+executableAssets :: StandardizationMatrix -> [StandardAsset]+executableAssets matrix =+  filter assetExecutable (matrixAssets matrix)++-- | Looks up a standard asset by its relative file path.+--+-- Searches 'matrixAssets' for an exact match against 'assetPath'.+-- Returns 'Nothing' if the path is not registered in the matrix.+--+-- === Worked Example+--+-- >>> let maybeHlint = findAssetByPath ".hlint.yaml" canonicalMatrix+-- >>> fmap assetCategory maybeHlint+-- Just CategoryLinter+findAssetByPath :: FilePath -> StandardizationMatrix -> Maybe StandardAsset+findAssetByPath path matrix =+  find (\a -> assetPath a == path) (matrixAssets matrix)++-- | Computes asset counts grouped by 'AssetCategory'.+--+-- Returns a list of pairs associating each category with its total registered asset count.+-- Guarantees that every category in 'matrixDimensions' appears in the result list.+--+-- === Worked Example+--+-- >>> let counts = countByCategory canonicalMatrix+-- >>> all (\(_, c) -> c > 0) counts+-- True+countByCategory :: StandardizationMatrix -> [(AssetCategory, Int)]+countByCategory matrix =+  map (\cat -> (cat, length (assetsForCategory cat matrix))) (matrixDimensions matrix)
+ src/Gachi/Sync/Remote.hs view
@@ -0,0 +1,826 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Module: Gachi.Sync.Remote+-- Description: Multi-remote topology management, provider compliance auditing, and replication synchronization+--+-- This module implements Milestone 3.4 of the Gachi strategic roadmap (~/src/PLAN.md §1.A).+-- It establishes declarative models and operational synchronization routines for+-- the 5-remote ecosystem replication topology (Dimension 6), enforcing sovereign+-- peer-to-peer distribution and provider compliance auditing across all checkouts.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization], Note [Cross-Repository Standardization Matrix and Audit Semantics]+-- [ref:note-standardization-matrix], and Note [Multi-Remote Replication Topology and Provider Compliance]+-- [ref:note-remote-topology], all ecosystem repositories replicate across five+-- canonical remotes, balancing decentralized sovereignty, community resilience,+-- and global developer accessibility.+--+-- == Note [Multi-Remote Replication Topology and Provider Compliance]+-- [tag:note-remote-topology]+--+-- Relying on a single proprietary hosting forge (such as GitHub) introduces an+-- existential single point of failure, censorship vulnerability, and platform+-- lock-in. A healthy open-source ecosystem must maintain sovereign, automated+-- replication across heterogeneous governance tiers:+--+-- 1.  __Sovereign Peer-to-Peer Identity ('RemotePeerToPeer'):__+--     Anchored on Radicle (@rad@: @rad://<project-urn>@). Uses cryptographic+--     DIDs and local gossip networks without centralized servers or single-entity+--     terms of service. Guarantees immutable ownership and censorship resistance.+--+-- 2.  __Community Non-Profit Forges ('RemoteCommunity'):__+--     Mirrored to trusted non-profit community instances:+--     * __Framagit__ (@framagit@: @git@framagit.org:NadiaYvette/<project>.git@):+--       GitLab Community Edition operated by the French digital commons non-profit Framasoft.+--     * __Disroot__ (@disroot@: @git@git.disroot.org:NadiaYvette/<project>.git@):+--       Forgejo/Gitea instance operated by the privacy-centric Disroot Foundation.+--+-- 3.  __Global Commercial Mirrors ('RemoteCommercial'):__+--     Maintained for discoverability, high-bandwidth package ingestion, and CI compute:+--     * __GitHub__ (@github@: @git@github.com:NadiaYvette/<project>.git@):+--       High-resource commercial runner execution and global contributor visibility.+--     * __GitCode__ (@gitcode@: @git@gitcode.com:NadiaYvette/<project>.git@):+--       High-speed regional mirror ensuring accessibility across global network partitions.+--+-- 4.  __Aggregate Multi-Target Push Remote ('RemoteAggregate'):__+--     An optional convenience remote (@all@) configured with multiple push URLs+--     enabling a single command (@git push all master@) to broadcast changes+--     synchronously across all configured mirrors.+--+-- == Provider Compliance & Exclusion Policy+--+-- Not all Git hosting services are compatible with ecosystem governance invariants:+-- * __SourceHut Exclusion:__ Services such as SourceHut (@git.sr.ht@) enforce+--   restrictive terms of service prohibiting automated AI-assisted coding tools,+--   and require idiosyncratic email-only patch submission workflows that reject+--   standard Git push replication and pull request verification. Consequently,+--   SourceHut remotes are formally excluded and flagged as policy violations.+-- * __Plaintext Insecurity:__ Plaintext HTTP remotes (@http://@) without TLS+--   encryption are strictly prohibited to prevent man-in-the-middle attacks.+--+-- The remote management engine automatically audits target repositories, flags+-- non-compliant remotes for removal, detects missing canonical remotes, and provides+-- atomic synchronization to restore the canonical 5-remote topology.+--+-- == Operational Transition Pipeline+--+-- Remote synchronization progresses through a three-stage functional pipeline:+--+-- 1.  __Inspection ('auditRemoteTopology' / 'auditRemoteTopologyQuick'):__+--     Executes @git remote -v@ within the checkout, parses the output via+--     'parseGitRemoteOutput', and compares existing remotes against expected+--     canonical specifications via 'evaluateRemoteTopology'.+--+-- 2.  __Planning ('computeRemoteSyncPlan'):__+--     Purity-preserving step transforming 'RemoteTopologyReport' into an atomic+--     'RemoteSyncPlan' composed of concrete 'RemoteSyncAction' mutations.+--+-- 3.  __Execution ('executeRemoteSyncPlan' / 'syncRemoteTopologyQuick'):__+--     Applies the plan under 'SyncMode'. In 'DryRun', preview logs are produced;+--     in 'ApplySync', idempotent Git commands mutate the repository configuration.+--+-- = Worked Example: Auditing Remote Topology+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync.Remote+-- >>> let proj = defaultProject "gachi" (Author "Nadia" "nadia@example.com")+-- >>> let existing = parseGitRemoteOutput "github\tgit@github.com:NadiaYvette/gachi.git (fetch)\n"+-- >>> let report = evaluateRemoteTopology "gachi" (defaultRemoteSpecsFor "gachi") existing+-- >>> remoteMissingCount report >= 4+-- True+-- >>> isProviderCompliant "git@github.com:NadiaYvette/gachi.git"+-- True+-- >>> isProviderCompliant "git@git.sr.ht:~user/gachi"+-- False+module Gachi.Sync.Remote+  ( -- * Remote Category & Compliance Types+    RemoteCategory(..)+  , ProviderPolicy(..)+  , ConfiguredRemote(..)+  , RemoteAuditStatus(..)+  , RemoteAuditResult(..)+  , RemoteTopologyReport(..)+  , RemoteSyncAction(..)+  , RemoteSyncPlan(..)++    -- * Provider Compliance & Policy Combinators+  , classifyProvider+  , isProviderCompliant+  , filterCompliantRemotes+  , filterNonCompliantRemotes+  , normalizeRemoteUrl+  , urlsMatch++    -- * Canonical Remote Topology Synthesis+  , canonicalRemoteNames+  , isCanonicalRemoteName+  , canonicalRemoteSpecs+  , defaultRemoteSpecsFor+  , defaultOwnerName++    -- * Deterministic Parsing & Pure Evaluation+  , parseGitRemoteOutput+  , evaluateRemoteTopology+  , computeRemoteSyncPlan+  , formatRemoteReport+  , formatRemoteSyncPlan++    -- * Live Filesystem IO Integration+  , queryGitRemotes+  , auditRemoteTopology+  , auditRemoteTopologyQuick+  , executeRemoteSyncPlan+  , syncRemoteTopology+  , syncRemoteTopologyQuick+  ) where++import Control.Exception (SomeException, catch)+import Data.Either (lefts, rights)+import qualified Data.Map.Strict as Map+import Data.Maybe (fromMaybe, mapMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import System.Directory (canonicalizePath, doesFileExist)+import System.Exit (ExitCode(..))+import System.FilePath ((</>), takeFileName)+import System.Process (readProcessWithExitCode)++import Gachi.Schema.Dhall (decodeProjectFile)+import Gachi.Schema.Project+  ( Author(..)+  , Project+  , RemoteSpec(..)+  , defaultProject+  , projectName+  , projectRemotes+  , remoteName+  , remoteUrl+  )+import Gachi.Sync.Types (SyncMode(..))++--------------------------------------------------------------------------------+-- 1. Remote Category & Compliance Types+--------------------------------------------------------------------------------++-- | Architectural category governing a Git remote in the replication topology.+--+-- Categorizes remotes according to their cryptographic governance model,+-- funding substrate, and infrastructure reliability.+--+-- === The Five Architectural Categories+--+-- 1.  __Sovereign Peer-to-Peer ('RemotePeerToPeer'):__+--     Radicle decentralized nodes identified by cryptographic public key DIDs+--     (@rad://<project-urn>@). Guarantees censorship resistance and local availability+--     without reliance on centralized intermediary servers or commercial terms of service.+--+-- 2.  __Community Non-Profit ('RemoteCommunity'):__+--     Open-source community forges operated by public-interest foundations (e.g. Framagit+--     by Framasoft, Disroot by Disroot Foundation). Guarantees data portability, privacy,+--     and community stewardship.+--+-- 3.  __Commercial Mirrors ('RemoteCommercial'):__+--     High-bandwidth commercial Git hosting providers (e.g. GitHub, GitCode) offering+--     extensive CI compute capacity, edge CDN acceleration, and global discoverability.+--+-- 4.  __Aggregate Multi-Push Remote ('RemoteAggregate'):__+--     Convenience remote identifier (typically @all@) configured with multiple push+--     URLs allowing a single @git push all master@ command to replicate commits across+--     all mirrors synchronously.+--+-- 5.  __Custom Endpoint ('RemoteCustom'):__+--     User-defined remotes (such as private staging forges or personal home servers)+--     operating alongside the standard 5-remote topology.+data RemoteCategory+  = RemotePeerToPeer+    -- ^ Sovereign peer-to-peer storage (Radicle DIDs: @rad://@).+  | RemoteCommunity+    -- ^ Non-profit public-interest community instances (Framagit, Disroot).+  | RemoteCommercial+    -- ^ Global commercial cloud mirrors (GitHub, GitCode).+  | RemoteAggregate+    -- ^ Multi-target aggregate push remote (@all@).+  | RemoteCustom+    -- ^ User-defined custom remote outside the standard 5-forge topology.+  deriving stock (Eq, Show, Ord, Enum, Bounded, Generic)++-- | Hosting provider compliance classification result.+--+-- Distinguishes permitted, policy-compliant repository hosting platforms from+-- excluded services that violate ecosystem governance rules.+--+-- === Exclusion Rationale+--+-- Hosting providers that enforce terms of service restricting automated AI pair+-- programming tooling (e.g. SourceHut) or require proprietary patch email workflows+-- that forbid Git push replication are classified under 'ProviderExcluded'.+-- Plaintext HTTP transport (@http://@) is similarly prohibited due to lack of encryption.+data ProviderPolicy+  = ProviderAllowed !Text+    -- ^ Compliant hosting provider with human-readable provider name (e.g., @"GitHub"@).+  | ProviderExcluded !Text !Text+    -- ^ Non-compliant provider name and detailed rationale for exclusion.+  deriving stock (Eq, Show, Generic)++-- | Represents a Git remote physically configured in a local checkout.+--+-- Encapsulates the remote name, optional fetch URL, and a list of push URLs+-- as discovered via porcelain Git commands (@git remote -v@).+data ConfiguredRemote = ConfiguredRemote+  { crName     :: !Text+    -- ^ Remote identifier string (e.g., @"github"@, @"rad"@, @"origin"@).+  , crFetchUrl :: !(Maybe Text)+    -- ^ Primary fetch URL configured for the remote.+  , crPushUrls :: ![Text]+    -- ^ List of push URLs associated with the remote.+  } deriving stock (Eq, Show, Generic)++-- | Detailed inspection status for an individual Git remote.+--+-- Classified during repository remote topology auditing against expected canonical specs.+data RemoteAuditStatus+  = RemoteCompliant+    -- ^ Remote exists, points to compliant provider, and matches expected canonical URL.+  | RemoteMissing+    -- ^ Canonical remote is absent from the local repository Git configuration.+  | RemoteUrlDrift !Text !Text+    -- ^ Remote exists but configured URL diverges from expected canonical URL.+    -- Carries (ActualUrl, ExpectedUrl).+  | RemoteNonCompliant !Text !Text+    -- ^ Remote points to a prohibited or non-compliant provider.+    -- Carries (ProviderName, ExclusionRationale).+  | RemoteUnknown+    -- ^ Remote exists and is compliant, but is not part of the standard 5-remote topology.+  deriving stock (Eq, Show, Generic)++-- | Audit finding for a single evaluated remote.+data RemoteAuditResult = RemoteAuditResult+  { resRemoteName  :: !Text+    -- ^ Name of the remote evaluated (e.g. @"github"@, @"srht"@).+  , resExpectedUrl :: !(Maybe Text)+    -- ^ Expected canonical URL if this remote is part of the standard topology.+  , resActualUrl   :: !(Maybe Text)+    -- ^ Actual configured URL detected in Git metadata.+  , resStatus      :: !RemoteAuditStatus+    -- ^ Audit classification outcome.+  } deriving stock (Eq, Show, Generic)++-- | Comprehensive audit report covering a repository's remote topology.+data RemoteTopologyReport = RemoteTopologyReport+  { reportRepoDir           :: !FilePath+    -- ^ Filesystem path of the audited repository checkout.+  , reportProject           :: !Text+    -- ^ Declared project name.+  , reportRemotes           :: ![RemoteAuditResult]+    -- ^ List of findings across all audited remotes.+  , reportCompliant         :: !Bool+    -- ^ Overall compliance flag (True iff all canonical remotes exist and 0 non-compliant remotes exist).+  , remoteMissingCount      :: !Int+    -- ^ Total count of missing canonical remotes.+  , remoteDriftCount        :: !Int+    -- ^ Total count of remotes exhibiting URL configuration drift.+  , remoteNonCompliantCount :: !Int+    -- ^ Total count of prohibited or non-compliant remotes detected.+  } deriving stock (Eq, Show, Generic)++-- | Atomic mutation action planned to reconcile remote topology drift.+data RemoteSyncAction+  = ActionAddRemote !Text !Text+    -- ^ Proposes executing @git remote add <name> <url>@.+  | ActionSetUrl !Text !Text+    -- ^ Proposes executing @git remote set-url <name> <url>@ to correct URL drift.+  | ActionAddPushUrl !Text !Text+    -- ^ Proposes executing @git remote set-url --add --push <name> <url>@ for multi-push remotes.+  | ActionRemoveRemote !Text+    -- ^ Proposes executing @git remote remove <name>@ to purge non-compliant remotes.+  | ActionRemoteNoOp !Text+    -- ^ Remote is fully compliant; zero mutation required.+  deriving stock (Eq, Show, Generic)++-- | Operational plan containing atomic actions to align a repository's remotes.+data RemoteSyncPlan = RemoteSyncPlan+  { planTargetRepo    :: !FilePath+    -- ^ Target repository path.+  , planProject       :: !Text+    -- ^ Project identifier.+  , planRemoteActions :: ![RemoteSyncAction]+    -- ^ Ordered sequence of remote configuration actions.+  , planAddCount      :: !Int+    -- ^ Number of remote addition actions planned.+  , planUpdateCount   :: !Int+    -- ^ Number of URL update actions planned.+  , planRemoveCount :: !Int+    -- ^ Number of non-compliant remote removal actions planned.+  } deriving stock (Eq, Show, Generic)++--------------------------------------------------------------------------------+-- 2. Provider Compliance & Policy Combinators+--------------------------------------------------------------------------------++-- | Default GitHub and multi-forge account organization name.+--+-- The authoritative account identifier across commercial and community Git forges.+defaultOwnerName :: Text+defaultOwnerName = "NadiaYvette"++-- | Classifies a remote URL according to ecosystem hosting provider compliance rules.+--+-- === Evaluated Policies & Threat Model+--+-- 1. __Plaintext HTTP Transport Security:__+--    Any URL starting with @"http://"@ is rejected due to lack of cryptographic transport+--    security. In accordance with zero-trust network principles, unencrypted endpoints+--    are susceptible to credential harvesting, commit manipulation, and active+--    man-in-the-middle code injection. Compliant endpoints must employ either SSH+--    (@git\@@) or TLS-wrapped HTTPS (@https:\/\/@).+--+-- 2. __SourceHut Platform Exclusion:__+--    URLs matching @"sr.ht"@ or @"sourcehut"@ are formally prohibited. SourceHut's terms+--    of service restrict automated AI pair programming assistants, and the platform+--    mandates an email-driven patch workflow that deliberately rejects push-based Git+--    replication, branch tracking, and webhook-driven multi-forge CI automation.+--+-- 3.  __Approved Multi-Forge Targets:__+--     * __GitHub__ (@github.com@): Commercial primary mirror for high-capacity runner CI.+--     * __GitCode__ (@gitcode.com@): Regional mirror guaranteeing high availability.+--     * __Framagit__ (@framagit.org@): Community GitLab mirror operated by Framasoft.+--     * __Disroot__ (@disroot.org@): Privacy-centric Forgejo mirror by Disroot Foundation.+--     * __Radicle__ (@rad://@): Sovereign decentralized peer-to-peer repository storage.+classifyProvider :: Text -> ProviderPolicy+classifyProvider rawUrl+  -- Check plaintext transport insecurity+  | "http://" `T.isPrefixOf` urlLower =+      ProviderExcluded "Insecure Plaintext HTTP"+        "Plaintext HTTP remotes are prohibited; use SSH (git@) or HTTPS (https://)."+  -- Check SourceHut exclusion policy+  | "sr.ht" `T.isInfixOf` urlLower || "sourcehut" `T.isInfixOf` urlLower =+      ProviderExcluded "SourceHut (git.sr.ht)"+        "SourceHut is prohibited due to terms restricting automated AI tooling and incompatible email-only workflows."+  -- Approved canonical providers+  | "github.com" `T.isInfixOf` urlLower =+      ProviderAllowed "GitHub"+  | "gitcode.com" `T.isInfixOf` urlLower =+      ProviderAllowed "GitCode"+  | "framagit.org" `T.isInfixOf` urlLower || "gitlab" `T.isInfixOf` urlLower =+      ProviderAllowed "Framagit (GitLab CE)"+  | "disroot.org" `T.isInfixOf` urlLower || "forgejo" `T.isInfixOf` urlLower || "gitea" `T.isInfixOf` urlLower =+      ProviderAllowed "Disroot (Forgejo)"+  | "rad://" `T.isPrefixOf` rawUrl =+      ProviderAllowed "Radicle P2P"+  -- Custom compliant host+  | otherwise =+      ProviderAllowed "Custom Compliant Host"+  where+    urlLower = T.toLower rawUrl++-- | Predicate checking whether a remote URL satisfies provider compliance rules.+isProviderCompliant :: Text -> Bool+isProviderCompliant checkUrl = case classifyProvider checkUrl of+  ProviderAllowed _      -> True+  ProviderExcluded _ _   -> False++-- | Filters a list of 'RemoteSpec' records, retaining only compliant remotes.+--+-- Guaranteed to discard any remote pointing to prohibited services (such as+-- SourceHut) or insecure plaintext HTTP endpoints.+--+-- === Invariants+--+-- All remotes in the output satisfy 'isProviderCompliant'.+filterCompliantRemotes :: [RemoteSpec] -> [RemoteSpec]+filterCompliantRemotes = filter (isProviderCompliant . remoteUrl)++-- | Extracts non-compliant 'RemoteSpec' records paired with their exclusion rationale.+--+-- Returns a list of @(remote, rationale)@ pairs identifying prohibited remotes+-- that must be purged or updated during topology synchronization.+filterNonCompliantRemotes :: [RemoteSpec] -> [(RemoteSpec, Text)]+filterNonCompliantRemotes = mapMaybe extractExcluded+  where+    extractExcluded r = case classifyProvider (remoteUrl r) of+      ProviderExcluded _ reason -> Just (r, reason)+      ProviderAllowed _         -> Nothing++-- | Normalizes a Git remote URL for semantic equivalence comparison.+--+-- Strips trailing @.git@ suffixes, trailing slashes, and converts to lowercase.+--+-- === Worked Example+--+-- >>> normalizeRemoteUrl "git@github.com:NadiaYvette/gachi.git"+-- "git@github.com:nadiayvette/gachi"+-- >>> normalizeRemoteUrl "https://github.com/NadiaYvette/gachi/"+-- "https://github.com/nadiayvette/gachi"+normalizeRemoteUrl :: Text -> Text+normalizeRemoteUrl raw =+  let lower = T.toLower (T.strip raw)+      noSlash = if "/" `T.isSuffixOf` lower then T.dropEnd 1 lower else lower+      noGit = if ".git" `T.isSuffixOf` noSlash then T.dropEnd 4 noSlash else noSlash+  in noGit++-- | Compares two Git remote URLs for semantic equivalence.+--+-- Considers SSH and HTTPS URLs targeting the same repository on the same forge as equivalent,+-- and ignores trailing @.git@ extensions and trailing slashes.+urlsMatch :: Text -> Text -> Bool+urlsMatch u1 u2 =+  let n1 = normalizeRemoteUrl u1+      n2 = normalizeRemoteUrl u2+  in n1 == n2 || stripProtocol n1 == stripProtocol n2+  where+    stripProtocol u =+      let dropPrefix' pfx t = if pfx `T.isPrefixOf` t then T.drop (T.length pfx) t else t+          noScheme = dropPrefix' "https://" (dropPrefix' "http://" (dropPrefix' "ssh://" (dropPrefix' "rad://" u)))+          noUser = dropPrefix' "git@" noScheme+      in T.replace ":" "/" noUser++--------------------------------------------------------------------------------+-- 3. Canonical Remote Topology Synthesis+--------------------------------------------------------------------------------++-- | Canonical remote names comprising the authoritative 5-forge replication topology.+canonicalRemoteNames :: [Text]+canonicalRemoteNames = ["rad", "github", "gitcode", "framagit", "disroot"]++-- | Predicate checking whether a remote name belongs to the canonical 5-forge topology.+isCanonicalRemoteName :: Text -> Bool+isCanonicalRemoteName candidateName = candidateName `elem` canonicalRemoteNames++-- | Synthesizes the canonical 5-remote topology for a given account owner and project name.+--+-- Constructs RemoteSpec entries for:+-- 1. @"rad"@: Radicle decentralized peer-to-peer URN.+-- 2. @"github"@: GitHub commercial mirror.+-- 3. @"gitcode"@: GitCode regional mirror.+-- 4. @"framagit"@: Framagit community GitLab mirror.+-- 5. @"disroot"@: Disroot community Forgejo mirror.+canonicalRemoteSpecs :: Text -> Text -> [RemoteSpec]+canonicalRemoteSpecs owner pName =+  [ RemoteSpec "rad"      ("rad://" <> pName)+  , RemoteSpec "github"   ("git@github.com:" <> owner <> "/" <> pName <> ".git")+  , RemoteSpec "gitcode"  ("git@gitcode.com:" <> owner <> "/" <> pName <> ".git")+  , RemoteSpec "framagit" ("git@framagit.org:" <> owner <> "/" <> pName <> ".git")+  , RemoteSpec "disroot"  ("git@git.disroot.org:" <> owner <> "/" <> pName <> ".git")+  ]++-- | Synthesizes the default 5-remote topology using the standard owner (@NadiaYvette@).+defaultRemoteSpecsFor :: Text -> [RemoteSpec]+defaultRemoteSpecsFor = canonicalRemoteSpecs defaultOwnerName++--------------------------------------------------------------------------------+-- 4. Deterministic Parsing & Pure Evaluation+--------------------------------------------------------------------------------++-- | Pure parser transforming the output of @git remote -v@ into structured 'ConfiguredRemote' records.+--+-- Handles multiple push URLs and separate fetch/push specifications cleanly.+--+-- === Format Specification+--+-- Expected input lines follow standard Git porcelain format:+-- @<name>TAB<url>SPACE(fetch)@ or @<name>TAB<url>SPACE(push)@+parseGitRemoteOutput :: Text -> [ConfiguredRemote]+parseGitRemoteOutput raw =+  let rawLines = filter (not . T.null . T.strip) (T.lines raw)+      parsedEntries = mapMaybe parseLine rawLines+      grouped = foldl insertEntry Map.empty parsedEntries+  in map toConfigured (Map.toList grouped)+  where+    parseLine :: Text -> Maybe (Text, Text, Bool)+    parseLine line =+      let parts = T.words line+      in case parts of+        (rName : rUrl : rType : _) ->+          let isPush = "(push)" `T.isPrefixOf` rType+          in Just (rName, rUrl, isPush)+        _ -> Nothing++    insertEntry acc (rName, rUrl, isPush) =+      let (mFetch, pushes) = fromMaybe (Nothing, []) (Map.lookup rName acc)+          updated = if isPush+            then (mFetch, pushes ++ [rUrl | rUrl `notElem` pushes])+            else (Just rUrl, pushes)+      in Map.insert rName updated acc++    toConfigured (rName, (mFetch, pushes)) =+      ConfiguredRemote+        { crName     = rName+        , crFetchUrl = mFetch+        , crPushUrls = pushes+        }++-- | Pure evaluation routine assessing configured remotes against expected canonical specs.+--+-- Detects:+-- * Missing canonical remotes that must be added.+-- * Remotes exhibiting URL drift requiring reconciliation.+-- * Non-compliant or prohibited remotes (e.g. SourceHut) that must be removed.+-- * Unknown remotes configured outside the canonical topology.+evaluateRemoteTopology+  :: Text+  -- ^ Project name identifier.+  -> [RemoteSpec]+  -- ^ Expected canonical remote specifications.+  -> [ConfiguredRemote]+  -- ^ Physically configured remotes parsed from Git.+  -> RemoteTopologyReport+evaluateRemoteTopology pName expected configured =+  let configMap = Map.fromList [(crName r, r) | r <- configured]+      expectedNames = [remoteName r | r <- expected]++      -- 1. Evaluate expected canonical remotes+      expectedResults = map (evaluateExpected configMap) expected++      -- 2. Evaluate extra/unknown configured remotes+      extraConfigured = filter (\r -> crName r `notElem` expectedNames) configured+      extraResults = map evaluateExtra extraConfigured++      allResults = expectedResults ++ extraResults++      missingC = length [() | r <- allResults, case resStatus r of RemoteMissing -> True; _ -> False]+      driftC   = length [() | r <- allResults, case resStatus r of RemoteUrlDrift {} -> True; _ -> False]+      nonCompC = length [() | r <- allResults, case resStatus r of RemoteNonCompliant {} -> True; _ -> False]++      isCompliant = missingC == 0 && driftC == 0 && nonCompC == 0+  in RemoteTopologyReport+    { reportRepoDir           = ""+    , reportProject           = pName+    , reportRemotes           = allResults+    , reportCompliant         = isCompliant+    , remoteMissingCount      = missingC+    , remoteDriftCount        = driftC+    , remoteNonCompliantCount = nonCompC+    }+  where+    evaluateExpected configMap spec =+      let rName = remoteName spec+          expUrl = remoteUrl spec+      in case Map.lookup rName configMap of+        Nothing ->+          RemoteAuditResult rName (Just expUrl) Nothing RemoteMissing+        Just conf ->+          let actUrl = fromMaybe "" (crFetchUrl conf)+          in case classifyProvider actUrl of+            ProviderExcluded pName' reason ->+              RemoteAuditResult rName (Just expUrl) (Just actUrl) (RemoteNonCompliant pName' reason)+            ProviderAllowed _ ->+              if urlsMatch actUrl expUrl+                then RemoteAuditResult rName (Just expUrl) (Just actUrl) RemoteCompliant+                else RemoteAuditResult rName (Just expUrl) (Just actUrl) (RemoteUrlDrift actUrl expUrl)++    evaluateExtra conf =+      let rName = crName conf+          actUrl = fromMaybe "" (crFetchUrl conf)+      in case classifyProvider actUrl of+        ProviderExcluded pName' reason ->+          RemoteAuditResult rName Nothing (Just actUrl) (RemoteNonCompliant pName' reason)+        ProviderAllowed _ ->+          RemoteAuditResult rName Nothing (Just actUrl) RemoteUnknown++-- | Computes a deterministic 'RemoteSyncPlan' from a 'RemoteTopologyReport'.+--+-- Translates audit findings into concrete Git actions:+-- * 'RemoteMissing' -> 'ActionAddRemote'+-- * 'RemoteUrlDrift' -> 'ActionSetUrl'+-- * 'RemoteNonCompliant' -> 'ActionRemoveRemote'+computeRemoteSyncPlan :: RemoteTopologyReport -> RemoteSyncPlan+computeRemoteSyncPlan report =+  let actions = mapMaybe actionForResult (reportRemotes report)+      adds    = length [() | ActionAddRemote {} <- actions]+      updates = length [() | ActionSetUrl {} <- actions]+      removes = length [() | ActionRemoveRemote {} <- actions]+  in RemoteSyncPlan+    { planTargetRepo    = reportRepoDir report+    , planProject       = reportProject report+    , planRemoteActions = actions+    , planAddCount      = adds+    , planUpdateCount   = updates+    , planRemoveCount   = removes+    }+  where+    actionForResult res = case resStatus res of+      RemoteMissing ->+        case resExpectedUrl res of+          Just expUrl' -> Just (ActionAddRemote (resRemoteName res) expUrl')+          Nothing      -> Nothing+      RemoteUrlDrift _ expUrl' ->+        Just (ActionSetUrl (resRemoteName res) expUrl')+      RemoteNonCompliant _ _ ->+        Just (ActionRemoveRemote (resRemoteName res))+      RemoteCompliant ->+        Just (ActionRemoteNoOp (resRemoteName res))+      RemoteUnknown ->+        Nothing++-- | Formats a 'RemoteTopologyReport' into a clean ASCII diagnostic table for CLI rendering.+formatRemoteReport :: RemoteTopologyReport -> Text+formatRemoteReport report =+  let header =+        [ "=========================================================================================="+        , "  Gachi Multi-Remote Topology Audit: " <> reportProject report+        , "  Note [Multi-Remote Replication Topology and Provider Compliance] [ref:note-remote-topology]"+        , "=========================================================================================="+        , "  Remote Name   | Status        | Configured / Actual URL"+        , "----------------+---------------+---------------------------------------------------------"+        ]+      rows = map formatResultRow (reportRemotes report)+      summary =+        [ "----------------+---------------+---------------------------------------------------------"+        , "  Total Evaluated: " <> T.pack (show (length (reportRemotes report)))+          <> " | Missing: " <> T.pack (show (remoteMissingCount report))+          <> " | Drifted: " <> T.pack (show (remoteDriftCount report))+          <> " | Prohibited: " <> T.pack (show (remoteNonCompliantCount report))+        , "  Compliance: " <> (if reportCompliant report then "PASS (Topology Fully Aligned)" else "DRIFT DETECTED")+        , "=========================================================================================="+        ]+  in T.unlines (header ++ rows ++ summary)+  where+    formatResultRow res =+      let nameCol = T.justifyLeft 14 ' ' (resRemoteName res)+          statusCol = T.justifyLeft 13 ' ' (statusLabel (resStatus res))+          urlCol = fromMaybe "(none)" (resActualUrl res)+      in "  " <> nameCol <> " | " <> statusCol <> " | " <> urlCol++    statusLabel RemoteCompliant         = "COMPLIANT"+    statusLabel RemoteMissing           = "MISSING"+    statusLabel (RemoteUrlDrift {})     = "URL DRIFT"+    statusLabel (RemoteNonCompliant {}) = "PROHIBITED"+    statusLabel RemoteUnknown          = "CUSTOM"++-- | Formats a 'RemoteSyncPlan' into a clean ASCII summary table.+formatRemoteSyncPlan :: RemoteSyncPlan -> Text+formatRemoteSyncPlan plan =+  let header =+        [ "=========================================================================================="+        , "  Gachi Remote Synchronization Plan: " <> planProject plan+        , "=========================================================================================="+        ]+      rows = map formatActionRow (planRemoteActions plan)+      summary =+        [ "------------------------------------------------------------------------------------------"+        , "  Planned Actions: " <> T.pack (show (length (planRemoteActions plan)))+          <> " (Add: " <> T.pack (show (planAddCount plan))+          <> ", Update: " <> T.pack (show (planUpdateCount plan))+          <> ", Remove: " <> T.pack (show (planRemoveCount plan)) <> ")"+        , "=========================================================================================="+        ]+  in T.unlines (header ++ rows ++ summary)+  where+    formatActionRow (ActionAddRemote rName targetUrl) =+      "  [ADD REMOTE]    " <> rName <> " -> " <> targetUrl+    formatActionRow (ActionSetUrl rName targetUrl) =+      "  [UPDATE URL]    " <> rName <> " -> " <> targetUrl+    formatActionRow (ActionAddPushUrl rName targetUrl) =+      "  [ADD PUSH URL]  " <> rName <> " -> " <> targetUrl+    formatActionRow (ActionRemoveRemote rName) =+      "  [REMOVE REMOTE] " <> rName <> " (Purging prohibited provider)"+    formatActionRow (ActionRemoteNoOp rName) =+      "  [NO-OP]         " <> rName <> " (Already aligned)"++--------------------------------------------------------------------------------+-- 5. Live Filesystem IO Integration+--------------------------------------------------------------------------------++-- | Queries Git remotes physically configured in a local repository checkout.+--+-- Executes @git -C <repoRoot> remote -v@ and parses the resulting output.+queryGitRemotes :: FilePath -> IO (Either Text [ConfiguredRemote])+queryGitRemotes repoRoot = (do+  (exitCode, stdout, stderr) <- readProcessWithExitCode "git" ["-C", repoRoot, "remote", "-v"] ""+  case exitCode of+    ExitSuccess -> pure (Right (parseGitRemoteOutput (T.pack stdout)))+    ExitFailure code ->+      pure (Left ("Git remote query failed with exit code " <> T.pack (show code) <> ": " <> T.pack stderr))+  ) `catch` (\(e :: SomeException) ->+    pure (Left ("Exception querying git remotes: " <> T.pack (show e))))++-- | Audits the physical remote topology of a repository checkout against canonical expectations.+auditRemoteTopology :: FilePath -> Project -> IO RemoteTopologyReport+auditRemoteTopology repoRoot proj = do+  let pName = projectName proj+      expected = case projectRemotes proj of+        []  -> defaultRemoteSpecsFor pName+        rms -> filterCompliantRemotes rms+  res <- queryGitRemotes repoRoot+  case res of+    Left _ ->+      -- If git fails, report all expected as missing+      pure (evaluateRemoteTopology pName expected [])+        { reportRepoDir = repoRoot }+    Right configured ->+      pure (evaluateRemoteTopology pName expected configured)+        { reportRepoDir = repoRoot }++-- | Executes a 'RemoteSyncPlan' in either 'DryRun' or 'ApplySync' mode.+--+-- In 'DryRun' mode: emits descriptive action logs without mutating Git configuration.+-- In 'ApplySync' mode: invokes Git commands to add, update, or remove remotes.+executeRemoteSyncPlan :: SyncMode -> RemoteSyncPlan -> IO (Either Text [Text])+executeRemoteSyncPlan DryRun plan =+  pure (Right [formatActionLog a | a <- planRemoteActions plan])+  where+    formatActionLog (ActionAddRemote n u)    = "DRY RUN: would execute 'git remote add " <> n <> " " <> u <> "'"+    formatActionLog (ActionSetUrl n u)       = "DRY RUN: would execute 'git remote set-url " <> n <> " " <> u <> "'"+    formatActionLog (ActionAddPushUrl n u)   = "DRY RUN: would execute 'git remote set-url --add --push " <> n <> " " <> u <> "'"+    formatActionLog (ActionRemoveRemote n)   = "DRY RUN: would execute 'git remote remove " <> n <> "'"+    formatActionLog (ActionRemoteNoOp n)     = "DRY RUN: no-op for " <> n++executeRemoteSyncPlan ApplySync plan = do+  let repo = planTargetRepo plan+  results <- mapM (applyAction repo) (planRemoteActions plan)+  let errors = lefts results+  if null errors+    then pure (Right (rights results))+    else pure (Left (T.intercalate "; " errors))+  where+    -- Executes 'git remote add <name> <url>' in target repository+    applyAction repo (ActionAddRemote n u) = do+      (c, _, err) <- readProcessWithExitCode "git" ["-C", repo, "remote", "add", T.unpack n, T.unpack u] ""+      case c of+        ExitSuccess -> pure (Right ("Added remote '" <> n <> "' pointing to " <> u))+        ExitFailure code -> pure (Left ("Failed to add remote '" <> n <> "' (code " <> T.pack (show code) <> "): " <> T.pack err))++    -- Executes 'git remote set-url <name> <url>' to reconcile URL drift+    applyAction repo (ActionSetUrl n u) = do+      (c, _, err) <- readProcessWithExitCode "git" ["-C", repo, "remote", "set-url", T.unpack n, T.unpack u] ""+      case c of+        ExitSuccess -> pure (Right ("Updated URL for remote '" <> n <> "' to " <> u))+        ExitFailure code -> pure (Left ("Failed to update remote '" <> n <> "' (code " <> T.pack (show code) <> "): " <> T.pack err))++    -- Executes 'git remote set-url --add --push <name> <url>' for multi-push targets+    applyAction repo (ActionAddPushUrl n u) = do+      (c, _, err) <- readProcessWithExitCode "git" ["-C", repo, "remote", "set-url", "--add", "--push", T.unpack n, T.unpack u] ""+      case c of+        ExitSuccess -> pure (Right ("Added push URL for remote '" <> n <> "': " <> u))+        ExitFailure _code -> pure (Left ("Failed to add push URL for remote '" <> n <> "': " <> T.pack err))++    -- Executes 'git remote remove <name>' to purge prohibited or non-compliant providers+    applyAction repo (ActionRemoveRemote n) = do+      (c, _, err) <- readProcessWithExitCode "git" ["-C", repo, "remote", "remove", T.unpack n] ""+      case c of+        ExitSuccess -> pure (Right ("Removed prohibited remote '" <> n <> "'"))+        ExitFailure _code -> pure (Left ("Failed to remove remote '" <> n <> "': " <> T.pack err))++    -- No-op for already aligned remotes+    applyAction _ (ActionRemoteNoOp n) =+      pure (Right ("Remote '" <> n <> "' is already aligned."))++-- | High-level orchestrator auditing and synchronizing remote topology for a project.+--+-- First audits the local checkout using 'auditRemoteTopology', derives an atomic+-- 'RemoteSyncPlan' via 'computeRemoteSyncPlan', and executes the planned mutations+-- under the specified 'SyncMode' ('DryRun' simulation or 'ApplySync' execution).+syncRemoteTopology :: SyncMode -> FilePath -> Project -> IO (Either Text [Text])+syncRemoteTopology mode repoRoot proj = do+  report <- auditRemoteTopology repoRoot proj+  let plan = computeRemoteSyncPlan report+  executeRemoteSyncPlan mode plan++-- | Zero-configuration remote topology inspection routine with automatic project inference.+--+-- Inspects the local checkout directory for @project.dhall@. When present, hermetically+-- decodes the declarative project specification to extract configured remotes.+-- When absent, derives a default project model using the checkout directory's canonical+-- basename and standard ecosystem owner conventions (@NadiaYvette@).+--+-- === Operational Safety+--+-- This operation is strictly read-only and will never alter Git configuration or+-- files on disk.+auditRemoteTopologyQuick :: FilePath -> IO RemoteTopologyReport+auditRemoteTopologyQuick repoRoot = do+  absRoot <- canonicalizePath repoRoot+  let dhallPath = absRoot </> "project.dhall"+  hasDhall <- doesFileExist dhallPath+  proj <- if hasDhall+    then decodeProjectFile dhallPath `catch` (\(_ :: SomeException) -> do+      let inferredName = T.pack (takeFileName absRoot)+      pure (defaultProject inferredName (Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me")))+    else do+      let inferredName = T.pack (takeFileName absRoot)+      pure (defaultProject inferredName (Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me"))+  auditRemoteTopology absRoot proj++-- | High-level zero-configuration remote topology synchronization orchestrator.+--+-- Automatically resolves project metadata via 'auditRemoteTopologyQuick', computes the+-- declarative synchronization plan, and applies or simulates the required remote+-- adjustments according to 'SyncMode'.+--+-- Returns a tuple containing the evaluated 'RemoteTopologyReport' and the resulting+-- execution outcome (a list of descriptive log messages or an error string).+syncRemoteTopologyQuick+  :: SyncMode+  -> FilePath+  -> IO (RemoteTopologyReport, Either Text [Text])+syncRemoteTopologyQuick mode repoRoot = do+  absRoot <- canonicalizePath repoRoot+  report <- auditRemoteTopologyQuick absRoot+  let plan = computeRemoteSyncPlan report+  res <- executeRemoteSyncPlan mode plan+  pure (report, res)
+ src/Gachi/Sync/Sheaf.hs view
@@ -0,0 +1,932 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Gachi.Sync.Sheaf+-- Description: 1-D cellular sheaf cohomology and cross-repository coherence auditor+--+-- This module implements the 1-dimensional cellular sheaf model over the ecosystem+-- repositories (~/src/PLAN.md §1.A, Milestone 3.5).+--+-- = Architectural Context & Homological Foundations+--+-- In applied algebraic topology (Curry 2014, Ghrist 2014) and distributed systems+-- governance, consistency across heterogeneous multi-package repositories is formalized+-- as a 1-dimensional cellular sheaf \(\mathcal{F}\) over a cell complex \(G = (V, E)\).+--+-- == The 1-Dimensional Cellular Sheaf Model+--+-- 1. __0-Cells (Vertices \(v \in V\)):__+--    Represent individual repository checkouts across @~/src/*@ (e.g. @polytropos@,+--    @gargamelle@, @sarutahiko@, @gachi@, @gongju@), augmented by an authoritative+--    canonical reference vertex (@"canonical"@) defined by the Standardization Matrix.+--+-- 2. __1-Cells (Edges \(e = (u, v) \in E\)):__+--    Represent shared contract obligations between repositories along ten standard+--    dimensions: compiler pinning (GHC 9.14.1), Cabal specification (3.0/3.18),+--    licensing (EUPL-1.2), Git commit trailers, pre-commit verification hooks,+--    HLint rules, CI workflows, and agent advisory context.+--+-- 3. __Stalk Assignments:__+--    * Vertex Stalk \(\mathcal{F}(v)\): 'RepoStalk' containing observed configuration+--      attributes extracted from the filesystem.+--    * Edge Stalk \(\mathcal{F}(e)\): 'ContractValue' representing projected contract states.+--+-- 4. __Restriction Morphisms (\(\rho_{u \trianglelefteq e}\)):__+--    Project the full repository state into the boundary contract language.+--+-- 5. __Sheaf Coboundary Operator (\(\delta^0\)):__+--    Maps 0-cochains \(C^0\) to 1-cochains \(C^1\), measuring boundary discrepancies:+--    \[+--      \delta^0 : C^0(G; \mathcal{F}) \to C^1(G; \mathcal{F})+--    \]+--    \[+--      \delta^0(x)_e = (\rho_{u \trianglelefteq e}(x_u), \rho_{v \trianglelefteq e}(x_v))+--    \]+--+-- 6. __Cohomological Invariants:__+--    * __0-th Cohomology \(H^0(G; \mathcal{F}) = \ker \delta^0\):__+--      The space of harmonic global sections. When all boundary projections coincide,+--      \(\delta^0(x) = 0\), mathematically certifying total ecosystem equilibrium (0 drift).+--    * __1-st Cohomology \(H^1(G; \mathcal{F}) = \operatorname{coker} \delta^0\):__+--      When \(\delta^0(x) \neq 0\), the non-vanishing cochains localize exact boundary+--      obstructions, pinpointing drifting repositories and breached contracts.+--+-- See Note [Cellular Sheaf Coherence and Homological Obstruction Localization]+-- [ref:note-cellular-sheaf-coherence].+--+-- = Worked Example+--+-- >>> import qualified Data.Map.Strict as Map+-- >>> let complex = buildCanonicalSheafComplex ["gachi"]+-- >>> let stalk = canonicalRepoStalk { stalkRepoName = "gachi" }+-- >>> let c0 = Cochain0 (Map.fromList [("canonical", canonicalRepoStalk), ("gachi", stalk)])+-- >>> let (_, isHarmonic) = coboundary complex defaultEcosystemSheaf c0+-- >>> isHarmonic+-- True+module Gachi.Sync.Sheaf+  ( -- * Note Annotation+    -- $note_cellular_sheaf++    -- * Cellular Complex Types+    CellComplex(..)+  , emptyComplex+  , addVertex+  , addEdge+  , numVertices+  , numEdges+  , eulerCharacteristic+  , Orientation(..)+  , orientationSign+  , Cell(..)++    -- * Cellular Sheaf & Cochains+  , CellularSheaf(..)+  , symmetricSheaf+  , directionalSheaf+  , Cochain0(..)+  , Cochain1(..)+  , CohomologyResult(..)+  , SheafObstruction(..)++    -- * Sheaf Cohomology Operators+  , coboundary+  , isHarmonicCochain+  , computeCohomology+  , locateSheafObstructions++    -- * Ecosystem Domain Types+  , ContractDimension(..)+  , contractDimensionName+  , contractDimensionRemediation+  , SheafEdgeId(..)+  , ContractValue(..)+  , RepoStalk(..)+  , SheafReport(..)++    -- * Ecosystem Sheaf Constructors & Evaluators+  , canonicalRepoStalk+  , restrictRepoStalk+  , defaultEcosystemSheaf+  , buildCanonicalSheafComplex+  , buildPairwiseComplex+  , evaluateSheafCoherence+  , formatSheafReport++    -- * Live Filesystem Inspection+  , inspectRepoStalk+  , inspectRepoGhcVersion+  , inspectRepoCabalVersion+  , inspectRepoLicense+  , findRepositoryRoot+  , auditSheafTarget+  , auditEcosystemSheaf+  ) where++import Control.Exception (SomeException, catch)+import Data.Char (isDigit)+import Data.List (nub)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (mapMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory+  ( canonicalizePath+  , doesDirectoryExist+  , doesFileExist+  , getPermissions+  , executable+  , listDirectory+  )+import System.FilePath ((</>), takeFileName, takeExtension, takeDirectory)++import Gachi.Sync.Engine (discoverEcosystemRepositories)++-- $note_cellular_sheaf+-- Note [Cellular Sheaf Coherence and Homological Obstruction Localization]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+-- [tag:note-cellular-sheaf-coherence]+-- In distributed software systems engineering and applied algebraic topology+-- (Curry 2014, Ghrist 2014), cross-repository consistency is formalized as a+-- 1-dimensional cellular sheaf F over a cell complex G = (V, E):+--+-- 1. Topological Complex G = (V, E):+--    - 0-Cells (Vertices v ∈ V): Standalone repository checkouts across ~/src/*+--      (e.g., polytropos, gargamelle, sarutahiko, gachi, gongju) plus an+--      authoritative canonical reference vertex ("canonical").+--    - 1-Cells (Edges e = (u, v) ∈ E): Shared interface boundaries governing+--      toolchain pinning, license terms, git governance, linter guardrails,+--      and CI matrix workflows.+--+-- 2. Stalks and Restriction Morphisms:+--    - Stalk F(v) at vertex v: The local configuration state (RepoStalk)+--      extracted from disk at checkout v.+--    - Stalk F(e) on edge e: The projected contract value (ContractValue) along+--      dimension d.+--    - Restriction maps ρ_{u ⊴ e} : F(u) -> F(e) and ρ_{v ⊴ e} : F(v) -> F(e)+--      extract local configuration attributes into the contract boundary language.+--+-- 3. Sheaf Coboundary Operator δ⁰:+--    - Maps 0-cochains (vertex states) to 1-cochains (edge boundary discrepancies):+--        δ⁰ : C⁰(G; F) -> C¹(G; F)+--        δ⁰(x)_e = (ρ_{u ⊴ e}(x_u), ρ_{v ⊴ e}(x_v))+--+-- 4. Cohomological Invariants:+--    - 0-th Cohomology H⁰(G; F) = ker δ⁰:+--      Represents the space of global sections. When all boundary projections+--      coincide across all edges (isHarmonicCochain == True), δ⁰(x) vanishes.+--      This certifies that all repositories satisfy the ecosystem contracts in+--      total harmonic equilibrium (0 drift).+--    - 1-st Cohomology H¹(G; F) = coker δ⁰:+--      When δ⁰(x) fails to vanish, the non-zero cochains localize the exact+--      cohomological obstructions to global consistency, pinpointing the+--      drifting repository, the breached contract dimension, and the observed+--      mismatch without requiring whole-ecosystem compilation.++--------------------------------------------------------------------------------+-- 1. Generic 1-D Cell Complex Primitives+--------------------------------------------------------------------------------++-- | A finite 1-dimensional cell complex (graph complex) \(G = (V, E)\).+--+-- 0-cells represent repositories or configuration contexts, and 1-cells+-- represent shared contract interfaces connecting them.+data CellComplex vId eId = CellComplex+  { complexVertices :: ![vId]+    -- ^ Exhaustive list of 0-cells in the complex.+  , complexEdges    :: !(Map eId (vId, vId))+    -- ^ Map of 1-cells identifying @(source, target)@ endpoint incidence.+  } deriving stock (Eq, Show)++-- | Orientation of an incident 1-cell relative to a vertex 0-cell.+data Orientation+  = Forward   -- ^ Directed away from vertex (vertex is source: sign -1)+  | Backward  -- ^ Directed into vertex (vertex is target: sign +1)+  deriving stock (Eq, Ord, Show)++-- | Numeric homological sign for an orientation.+orientationSign :: Orientation -> Int+orientationSign Forward  = -1+orientationSign Backward = 1++-- | Graded cells of a 1-dimensional cell complex.+data Cell vId eId+  = Cell0 !vId  -- ^ 0-cell (vertex)+  | Cell1 !eId  -- ^ 1-cell (edge)+  deriving stock (Eq, Ord, Show)++-- | Empty cell complex with no vertices or edges.+emptyComplex :: CellComplex vId eId+emptyComplex = CellComplex+  { complexVertices = []+  , complexEdges    = Map.empty+  }++-- | Inserts a vertex into the cell complex if not already present.+addVertex :: (Eq vId) => vId -> CellComplex vId eId -> CellComplex vId eId+addVertex v complex+  | v `elem` complexVertices complex = complex+  | otherwise = complex { complexVertices = v : complexVertices complex }++-- | Inserts a directed 1-cell and ensures its endpoints are in the vertex set.+addEdge+  :: (Ord eId, Eq vId)+  => eId+  -> vId+  -> vId+  -> CellComplex vId eId+  -> CellComplex vId eId+addEdge e u w complex = complex+  { complexVertices = nub (u : w : complexVertices complex)+  , complexEdges    = Map.insert e (u, w) (complexEdges complex)+  }++-- | Total count of 0-cells in the complex.+numVertices :: CellComplex vId eId -> Int+numVertices = length . complexVertices++-- | Total count of 1-cells in the complex.+numEdges :: CellComplex vId eId -> Int+numEdges = Map.size . complexEdges++-- | Computes the topological Euler characteristic: \(\chi = |V| - |E|\).+eulerCharacteristic :: CellComplex vId eId -> Int+eulerCharacteristic c = numVertices c - numEdges c++--------------------------------------------------------------------------------+-- 2. Cellular Sheaf and Cochain Representations+--------------------------------------------------------------------------------++-- | A cellular sheaf \(\mathcal{F}\) over a cell complex \(G = (V, E)\).+--+-- Assigns stalks to 0-cells and 1-cells, with directional restriction+-- morphisms along cell face inclusions.+--+-- See Note [Cellular Sheaf Coherence and Homological Obstruction Localization]+-- [ref:note-cellular-sheaf-coherence].+data CellularSheaf vId eId stalk edgeStalk = CellularSheaf+  { restrictSrc :: eId -> stalk -> edgeStalk+    -- ^ Source restriction morphism \(\rho_{u \trianglelefteq e}\).+  , restrictTgt :: eId -> stalk -> edgeStalk+    -- ^ Target restriction morphism \(\rho_{v \trianglelefteq e}\).+  , liftSrc     :: eId -> edgeStalk -> stalk+    -- ^ Adjoint lift from edge stalk back to source vertex.+  , liftTgt     :: eId -> edgeStalk -> stalk+    -- ^ Adjoint lift from edge stalk back to target vertex.+  }++-- | Constructs a cellular sheaf where source and target restriction maps coincide.+symmetricSheaf+  :: (eId -> stalk -> edgeStalk)+  -> (eId -> edgeStalk -> stalk)+  -> CellularSheaf vId eId stalk edgeStalk+symmetricSheaf restr lift_ = CellularSheaf+  { restrictSrc = restr+  , restrictTgt = restr+  , liftSrc     = lift_+  , liftTgt     = lift_+  }++-- | Constructs a directional cellular sheaf with explicit source and target morphisms.+directionalSheaf+  :: (eId -> stalk -> edgeStalk)+  -> (eId -> stalk -> edgeStalk)+  -> (eId -> edgeStalk -> stalk)+  -> (eId -> edgeStalk -> stalk)+  -> CellularSheaf vId eId stalk edgeStalk+directionalSheaf = CellularSheaf++-- | 0-Cochain: an assignment of stalk elements across 0-cells (vertices).+newtype Cochain0 vId stalk = Cochain0+  { unCochain0 :: Map vId stalk+  } deriving stock (Eq, Show)+    deriving newtype (Functor)++-- | 1-Cochain: an assignment of edge stalk elements across 1-cells (edges).+newtype Cochain1 eId edgeStalk = Cochain1+  { unCochain1 :: Map eId edgeStalk+  } deriving stock (Eq, Show)+    deriving newtype (Functor)++-- | Result of cohomology computation at dimension 0 and 1.+data CohomologyResult eId edgeStalk+  = HarmonicSection+    -- ^ \(\ker \delta^0\): All boundary restrictions coincide; 0 drift.+  | ObstructionMap !(Map eId (edgeStalk, edgeStalk))+    -- ^ \(\operatorname{coker} \delta^0\): Non-zero boundary discrepancies.+  deriving stock (Eq, Show)++-- | Detailed diagnostic record for an interface obstruction along an edge.+data SheafObstruction vId eId edgeStalk = SheafObstruction+  { obstructionEdge    :: !eId+    -- ^ Identifier of the failing contract edge.+  , obstructionSource  :: !vId+    -- ^ Source vertex (e.g. @"canonical"@).+  , obstructionTarget  :: !vId+    -- ^ Target vertex (e.g. @"gongju"@).+  , sourceRestriction  :: !edgeStalk+    -- ^ Projected contract value from source.+  , targetRestriction  :: !edgeStalk+    -- ^ Projected contract value from target.+  , obstructionMessage :: !Text+    -- ^ Human-readable diagnostic explanation and remediation guidance.+  } deriving stock (Eq, Show)++--------------------------------------------------------------------------------+-- 3. Sheaf Coboundary Operator & Cohomology Invariants+--------------------------------------------------------------------------------++-- | Sheaf Coboundary Operator: \(\delta^0 : C^0(G; \mathcal{F}) \to C^1(G; \mathcal{F})\).+--+-- Evaluates the boundary discrepancy pair+-- \((\rho_{u \trianglelefteq e}(x_u), \rho_{v \trianglelefteq e}(x_v))\)+-- for every 1-cell \(e = (u, v)\) in the complex.+--+-- Returns the 1-cochain of boundary pairs and a boolean indicating whether the+-- 0-cochain is harmonic (i.e. all boundary restrictions match).+--+-- See Note [Cellular Sheaf Coherence and Homological Obstruction Localization]+-- [ref:note-cellular-sheaf-coherence].+coboundary+  :: forall vId eId stalk edgeStalk.+     (Ord vId, Eq edgeStalk)+  => CellComplex vId eId+  -> CellularSheaf vId eId stalk edgeStalk+  -> Cochain0 vId stalk+  -> (Cochain1 eId (edgeStalk, edgeStalk), Bool)+coboundary complex sheaf (Cochain0 vMap) =+  let edgeDiffs = Map.mapMaybeWithKey computeEdge (complexEdges complex)+      harmonic = all (uncurry (==)) edgeDiffs+  in (Cochain1 edgeDiffs, harmonic)+  where+    computeEdge eId (u, v) = case (Map.lookup u vMap, Map.lookup v vMap) of+      (Just stU, Just stV) ->+        Just (restrictSrc sheaf eId stU, restrictTgt sheaf eId stV)+      _ -> Nothing++-- | Predicate checking if a 0-cochain satisfies the sheaf gluing condition (zero coboundary).+isHarmonicCochain+  :: (Ord vId, Eq edgeStalk)+  => CellComplex vId eId+  -> CellularSheaf vId eId stalk edgeStalk+  -> Cochain0 vId stalk+  -> Bool+isHarmonicCochain complex sheaf = snd . coboundary complex sheaf++-- | Computes cohomology: returns 'HarmonicSection' if all boundary restrictions+-- agree, or 'ObstructionMap' detailing the non-vanishing cocycles.+computeCohomology+  :: (Ord vId, Eq edgeStalk)+  => CellComplex vId eId+  -> CellularSheaf vId eId stalk edgeStalk+  -> Cochain0 vId stalk+  -> CohomologyResult eId edgeStalk+computeCohomology complex sheaf c0 =+  let (Cochain1 diffs, harmonic) = coboundary complex sheaf c0+  in if harmonic+       then HarmonicSection+       else ObstructionMap (Map.filter (uncurry (/=)) diffs)++-- | Locates and extracts all failing interface boundaries where \(H^1\)+-- cohomological obstructions prevent global consensus.+locateSheafObstructions+  :: (Ord vId, Eq edgeStalk)+  => CellComplex vId eId+  -> CellularSheaf vId eId stalk edgeStalk+  -> Cochain0 vId stalk+  -> (eId -> vId -> vId -> edgeStalk -> edgeStalk -> Text)+  -> [SheafObstruction vId eId edgeStalk]+locateSheafObstructions complex sheaf (Cochain0 vMap) explainFn =+  [ SheafObstruction+      { obstructionEdge    = e+      , obstructionSource  = u+      , obstructionTarget  = v+      , sourceRestriction  = s+      , targetRestriction  = t+      , obstructionMessage = explainFn e u v s t+      }+  | (e, (u, v)) <- Map.toList (complexEdges complex)+  , Just stU <- [Map.lookup u vMap]+  , let s = restrictSrc sheaf e stU+  , Just stV <- [Map.lookup v vMap]+  , let t = restrictTgt sheaf e stV+  , s /= t+  ]++--------------------------------------------------------------------------------+-- 4. Ecosystem Standardization Dimensions & Domain Stalks+--------------------------------------------------------------------------------++-- | Standardization contract dimensions evaluated across repository boundaries.+data ContractDimension+  = ContractGhcVersion       -- ^ GHC compiler version pinning (GHC 9.14.1)+  | ContractCabalVersion     -- ^ Cabal specification version (3.0 / 3.18)+  | ContractLicense          -- ^ Permissive open source license (EUPL-1.2)+  | ContractGitCommitHook    -- ^ Git commit-msg hook for AI trailers (.githooks/commit-msg)+  | ContractGitPreCommitHook -- ^ Git pre-commit static verification hook (.githooks/pre-commit)+  | ContractGitMessage       -- ^ Git commit message template (.gitmessage)+  | ContractHLintConfig      -- ^ HLint static analysis configuration (.hlint.yaml)+  | ContractCIWorkflow       -- ^ Continuous integration matrix (.github/workflows/ci.yml)+  | ContractAgentsAdvisory   -- ^ AI agent guidance pointer (AGENTS.md)+  | ContractVerifyRunner     -- ^ Turnkey physical verification script (bin/<name>-verify)+  deriving stock (Eq, Ord, Show, Enum, Bounded)++-- | Human-readable name for a contract dimension.+contractDimensionName :: ContractDimension -> Text+contractDimensionName = \case+  ContractGhcVersion       -> "GHC Compiler Pinning"+  ContractCabalVersion     -> "Cabal Spec Version"+  ContractLicense          -> "Software License (EUPL-1.2)"+  ContractGitCommitHook    -> "Git Commit-Msg Hook"+  ContractGitPreCommitHook -> "Git Pre-Commit Hook"+  ContractGitMessage       -> "Git Message Template"+  ContractHLintConfig      -> "HLint Guardrails"+  ContractCIWorkflow       -> "CI Workflow Matrix"+  ContractAgentsAdvisory   -> "Agent Advisory Context"+  ContractVerifyRunner     -> "Physical Verify Runner"++-- | Remediation guidance string for a contract dimension breach.+contractDimensionRemediation :: ContractDimension -> Text+contractDimensionRemediation = \case+  ContractGhcVersion       -> "Update cabal.project / .cabal to target GHC 9.14.1."+  ContractCabalVersion     -> "Update cabal manifests to cabal-version: 3.0 or 3.18."+  ContractLicense          -> "Add EUPL-1.2 LICENSE and update cabal license field."+  ContractGitCommitHook    -> "Install .githooks/commit-msg via 'gachi sync'."+  ContractGitPreCommitHook -> "Install .githooks/pre-commit via 'gachi sync'."+  ContractGitMessage       -> "Install .gitmessage via 'gachi sync' and set commit.template."+  ContractHLintConfig      -> "Install canonical .hlint.yaml via 'gachi sync'."+  ContractCIWorkflow       -> "Generate forge CI workflows via 'gachi sync'."+  ContractAgentsAdvisory   -> "Install AGENTS.md and .agents/ rules via 'gachi sync'."+  ContractVerifyRunner     -> "Scaffold bin/<project>-verify executable via 'gachi init/sync'."++-- | Edge identifier in the ecosystem sheaf complex.+data SheafEdgeId = SheafEdgeId+  { edgeSourceRepo :: !Text+    -- ^ Source repository name (e.g. @"canonical"@).+  , edgeTargetRepo :: !Text+    -- ^ Target repository name (e.g. @"gongju"@).+  , edgeDimension  :: !ContractDimension+    -- ^ Evaluated contract dimension.+  } deriving stock (Eq, Ord, Show)++-- | Projected contract value on an edge stalk.+data ContractValue+  = ContractText !(Maybe Text)  -- ^ Textual attribute (e.g. Just "9.14.1")+  | ContractBool !Bool          -- ^ Binary conformance status (True / False)+  deriving stock (Eq, Ord, Show)++-- | Observed repository configuration stalk extracted from a checkout.+data RepoStalk = RepoStalk+  { stalkRepoName        :: !Text+    -- ^ Canonical repository identifier.+  , stalkRepoPath        :: !FilePath+    -- ^ Absolute path to repository root.+  , stalkGhcVersion      :: !(Maybe Text)+    -- ^ Detected GHC compiler pinning.+  , stalkCabalVersion    :: !(Maybe Text)+    -- ^ Detected Cabal specification version.+  , stalkLicense         :: !(Maybe Text)+    -- ^ Detected license stanza or file.+  , stalkHasCommitMsg    :: !Bool+    -- ^ Presence and validity of .githooks/commit-msg.+  , stalkHasPreCommit    :: !Bool+    -- ^ Presence and validity of .githooks/pre-commit.+  , stalkHasGitMessage   :: !Bool+    -- ^ Presence of .gitmessage commit template.+  , stalkHasHLint        :: !Bool+    -- ^ Presence of .hlint.yaml.+  , stalkHasCIWorkflow   :: !Bool+    -- ^ Presence of CI configuration (.github/workflows/ci.yml or .gitlab-ci.yml).+  , stalkHasAgentsMd     :: !Bool+    -- ^ Presence of AGENTS.md orientation pointer.+  , stalkHasVerifyRunner :: !Bool+    -- ^ Presence of executable bin/*-verify runner.+  } deriving stock (Eq, Show)++-- | Canonical reference stalk representing the ideal ecosystem contract.+canonicalRepoStalk :: RepoStalk+canonicalRepoStalk = RepoStalk+  { stalkRepoName        = "canonical"+  , stalkRepoPath        = "/"+  , stalkGhcVersion      = Just "9.14.1"+  , stalkCabalVersion    = Just "3.0"+  , stalkLicense         = Just "EUPL-1.2"+  , stalkHasCommitMsg    = True+  , stalkHasPreCommit    = True+  , stalkHasGitMessage   = True+  , stalkHasHLint        = True+  , stalkHasCIWorkflow   = True+  , stalkHasAgentsMd     = True+  , stalkHasVerifyRunner = True+  }++-- | Restriction morphism: projects a 'RepoStalk' onto a 'ContractValue' along a dimension.+restrictRepoStalk :: ContractDimension -> RepoStalk -> ContractValue+restrictRepoStalk dim stalk = case dim of+  ContractGhcVersion       -> ContractText (stalkGhcVersion stalk)+  ContractCabalVersion     -> ContractText (normalizeCabalVer (stalkCabalVersion stalk))+  ContractLicense          -> ContractText (normalizeLicense (stalkLicense stalk))+  ContractGitCommitHook    -> ContractBool (stalkHasCommitMsg stalk)+  ContractGitPreCommitHook -> ContractBool (stalkHasPreCommit stalk)+  ContractGitMessage       -> ContractBool (stalkHasGitMessage stalk)+  ContractHLintConfig      -> ContractBool (stalkHasHLint stalk)+  ContractCIWorkflow       -> ContractBool (stalkHasCIWorkflow stalk)+  ContractAgentsAdvisory   -> ContractBool (stalkHasAgentsMd stalk)+  ContractVerifyRunner     -> ContractBool (stalkHasVerifyRunner stalk)+  where+    -- Normalize Cabal versions: 3.18, 3.12, 3.0 all treated as valid 3.x specifications+    normalizeCabalVer (Just v)+      | "3." `T.isPrefixOf` T.strip v = Just "3.0"+      | otherwise = Just (T.strip v)+    normalizeCabalVer Nothing = Nothing++    -- Normalize EUPL licenses: EUPL-1.2, EUPL-1.1 treated as canonical EUPL+    normalizeLicense (Just lic)+      | "EUPL" `T.isInfixOf` T.toUpper lic = Just "EUPL-1.2"+      | otherwise = Just (T.strip lic)+    normalizeLicense Nothing = Nothing++-- | Default cellular sheaf connecting repositories to the canonical reference.+defaultEcosystemSheaf+  :: CellularSheaf Text SheafEdgeId RepoStalk ContractValue+defaultEcosystemSheaf = CellularSheaf+  { restrictSrc = restrictRepoStalk . edgeDimension+  , restrictTgt = restrictRepoStalk . edgeDimension+  , liftSrc     = \_ _ -> canonicalRepoStalk+  , liftTgt     = \_ _ -> canonicalRepoStalk+  }++--------------------------------------------------------------------------------+-- 5. Complex Construction & Sheaf Coherence Evaluation+--------------------------------------------------------------------------------++-- | All ten contract dimensions evaluated in the ecosystem complex.+allContractDimensions :: [ContractDimension]+allContractDimensions = [minBound .. maxBound]++-- | Constructs a star cell complex centered at @"canonical"@ connecting every+-- client repository along all ten contract dimensions.+buildCanonicalSheafComplex :: [Text] -> CellComplex Text SheafEdgeId+buildCanonicalSheafComplex clientRepos =+  let v0 = "canonical"+      allVerts = v0 : clientRepos+      edgeList =+        [ ( SheafEdgeId v0 repo dim, (v0, repo) )+        | repo <- clientRepos+        , dim  <- allContractDimensions+        ]+  in CellComplex+      { complexVertices = allVerts+      , complexEdges    = Map.fromList edgeList+      }++-- | Constructs a pairwise cell complex connecting adjacent client repositories.+buildPairwiseComplex :: [Text] -> CellComplex Text SheafEdgeId+buildPairwiseComplex repos =+  let pairs = zip repos (drop 1 repos)+      edgeList =+        [ ( SheafEdgeId u v dim, (u, v) )+        | (u, v) <- pairs+        , dim    <- allContractDimensions+        ]+  in CellComplex+      { complexVertices = repos+      , complexEdges    = Map.fromList edgeList+      }++-- | Comprehensive diagnostic summary of cellular sheaf coherence.+data SheafReport = SheafReport+  { reportVertices          :: ![Text]+    -- ^ All 0-cells in the complex.+  , reportEdgeCount         :: !Int+    -- ^ Total 1-cells evaluated across all contract dimensions.+  , reportEulerChar         :: !Int+    -- ^ Euler characteristic \(\chi = |V| - |E|\).+  , reportHarmonicEdges     :: !Int+    -- ^ Count of edges satisfying boundary agreement.+  , reportDriftEdges        :: !Int+    -- ^ Count of edges exhibiting boundary discrepancies.+  , reportCoherencePct      :: !Double+    -- ^ Percentage of edges in harmonic resonance (0.0 to 100.0).+  , reportIsHarmonic        :: !Bool+    -- ^ True if \(H^0\) is complete and \(H^1 = \emptyset\) (0 obstructions).+  , reportObstructions      :: ![SheafObstruction Text SheafEdgeId ContractValue]+    -- ^ Localized cohomological obstructions.+  } deriving stock (Eq, Show)++-- | Evaluates cellular sheaf coherence over a cell complex and 0-cochain.+evaluateSheafCoherence+  :: CellComplex Text SheafEdgeId+  -> Cochain0 Text RepoStalk+  -> SheafReport+evaluateSheafCoherence complex c0 =+  let (Cochain1 diffs, harmonic) = coboundary complex defaultEcosystemSheaf c0+      totalEdges = Map.size diffs+      driftCount = Map.size (Map.filter (uncurry (/=)) diffs)+      harmonicCount = totalEdges - driftCount+      pct = if totalEdges == 0 then 100.0 else (fromIntegral harmonicCount / fromIntegral totalEdges) * 100.0+      obstructions = locateSheafObstructions complex defaultEcosystemSheaf c0 formatObstructionMsg+  in SheafReport+      { reportVertices      = complexVertices complex+      , reportEdgeCount     = totalEdges+      , reportEulerChar     = eulerCharacteristic complex+      , reportHarmonicEdges = harmonicCount+      , reportDriftEdges    = driftCount+      , reportCoherencePct  = pct+      , reportIsHarmonic    = harmonic+      , reportObstructions  = obstructions+      }+  where+    formatObstructionMsg e u v s t =+      "Boundary obstruction on edge (" <> u <> " ⊵ " <> v <> ", "+      <> contractDimensionName (edgeDimension e) <> "): expected "+      <> formatContractVal s <> ", observed " <> formatContractVal t <> ". "+      <> contractDimensionRemediation (edgeDimension e)++    formatContractVal (ContractText (Just txt)) = "'" <> txt <> "'"+    formatContractVal (ContractText Nothing)    = "Nothing"+    formatContractVal (ContractBool b)          = T.show b++-- | Formats a 'SheafReport' into an aligned terminal diagnostic ASCII report.+formatSheafReport :: SheafReport -> Text+formatSheafReport rep = T.unlines $+  [ "================================================================================"+  , " Gachi Cellular Sheaf Coherence Report (~/src/PLAN.md §1.A, Milestone 3.5)"+  , "================================================================================"+  , " Complex Topology:   1-D Cell Complex G = (V, E)"+  , " 0-Cells (Vertices): " <> T.show (length (reportVertices rep))+      <> " " <> T.pack (show (reportVertices rep))+  , " 1-Cells (Edges):    " <> T.show (reportEdgeCount rep)+      <> " evaluated across " <> T.show (length allContractDimensions) <> " contract dimensions"+  , " Euler Char (\x03c7):     " <> T.show (reportEulerChar rep)+  , "--------------------------------------------------------------------------------"+  , " Cohomological Invariants:"+  , " [+] 0-th Cohomology H^0 (Harmonic Equilibrium): ker \x03b4\x2070"+  , "     Status: " <> T.show (reportHarmonicEdges rep) <> " / " <> T.show (reportEdgeCount rep)+      <> " edges in harmonic resonance (" <> formatPct (reportCoherencePct rep) <> "% coherent)"+  ]+  ++ (if reportIsHarmonic rep+        then [ "     System State: HARMONIC EQUILIBRIUM (zero boundary obstruction)" ]+        else [ "     System State: DRIFT DETECTED" ])+  +++  [ " [-] 1-st Cohomology H^1 (Boundary Obstructions): coker \x03b4\x2070"+  , "     Status: " <> T.show (reportDriftEdges rep) <> " boundary obstructions localized"+  ]+  ++ (if null (reportObstructions rep)+        then [ "--------------------------------------------------------------------------------"+             , " [+] All repositories satisfy ecosystem contracts in total harmonic equilibrium."+             , "================================================================================"+             ]+        else+          [ "--------------------------------------------------------------------------------"+          , " Localized Cohomological Obstructions (H^1):"+          , "--------------------------------------------------------------------------------"+          ]+          ++ concatMap formatObstructionCard (reportObstructions rep)+          +++          [ "================================================================================" ]+     )+  where+    formatPct val =+      let whole = truncate val :: Int+          dec = truncate (val * 10) `mod` 10 :: Int+      in T.show whole <> "." <> T.show dec++    formatObstructionCard obs =+      let dim = edgeDimension (obstructionEdge obs)+      in [ " Edge: " <> obstructionSource obs <> " \x22b5 " <> obstructionTarget obs+             <> " (" <> contractDimensionName dim <> ")"+         , "   Expected:    " <> formatVal (sourceRestriction obs)+         , "   Observed:    " <> formatVal (targetRestriction obs)+         , "   Remediation: " <> contractDimensionRemediation dim+         , ""+         ]++    formatVal (ContractText (Just txt)) = "'" <> txt <> "'"+    formatVal (ContractText Nothing)    = "Missing / Not Specified"+    formatVal (ContractBool True)       = "Compliant (True)"+    formatVal (ContractBool False)      = "Missing / Non-Compliant (False)"++--------------------------------------------------------------------------------+-- 6. Live Filesystem Inspection Routines+--------------------------------------------------------------------------------++-- | Traverses upward from a given directory to locate the authoritative+-- workspace or repository root containing @.git@, @cabal.project@, or @project.dhall@.+findRepositoryRoot :: FilePath -> IO FilePath+findRepositoryRoot startPath = do+  absPath <- canonicalizePath startPath+  let searchUp p = do+        hasGit <- doesDirectoryExist (p </> ".git")+        hasProj <- doesFileExist (p </> "cabal.project")+        hasDhall <- doesFileExist (p </> "project.dhall")+        if hasGit || hasProj || hasDhall+          then pure p+          else do+            let parent = takeDirectory p+            if parent == p+              then pure absPath+              else searchUp parent+  searchUp absPath++-- | Inspects a repository directory and extracts its 'RepoStalk'.+inspectRepoStalk :: FilePath -> IO RepoStalk+inspectRepoStalk rawPath = do+  repoPath <- findRepositoryRoot rawPath+  let repoName = T.pack (takeFileName repoPath)+  ghcVer <- inspectRepoGhcVersion repoPath+  cabalVer <- inspectRepoCabalVersion repoPath+  lic <- inspectRepoLicense repoPath++  hasCommitMsg <- doesFileExist (repoPath </> ".githooks" </> "commit-msg")+  hasPreCommit <- doesFileExist (repoPath </> ".githooks" </> "pre-commit")+  hasGitMsg <- doesFileExist (repoPath </> ".gitmessage")+  hasHLint <- doesFileExist (repoPath </> ".hlint.yaml")++  hasGhCi <- doesFileExist (repoPath </> ".github" </> "workflows" </> "ci.yml")+  hasGlCi <- doesFileExist (repoPath </> ".gitlab-ci.yml")+  let hasCI = hasGhCi || hasGlCi++  hasAgents <- doesFileExist (repoPath </> "AGENTS.md")++  hasRunner <- checkVerifyRunner repoPath repoName++  pure RepoStalk+    { stalkRepoName        = repoName+    , stalkRepoPath        = repoPath+    , stalkGhcVersion      = ghcVer+    , stalkCabalVersion    = cabalVer+    , stalkLicense         = lic+    , stalkHasCommitMsg    = hasCommitMsg+    , stalkHasPreCommit    = hasPreCommit+    , stalkHasGitMessage   = hasGitMsg+    , stalkHasHLint        = hasHLint+    , stalkHasCIWorkflow   = hasCI+    , stalkHasAgentsMd     = hasAgents+    , stalkHasVerifyRunner = hasRunner+    }++-- | Inspects GHC compiler version pinning from @cabal.project@ or @*.cabal@.+inspectRepoGhcVersion :: FilePath -> IO (Maybe Text)+inspectRepoGhcVersion repoPath = do+  let projPath = repoPath </> "cabal.project"+  hasProj <- doesFileExist projPath+  if hasProj+    then do+      txt <- TIO.readFile projPath `catch` (\(_ :: SomeException) -> pure "")+      case extractGhcFromText txt of+        Just v  -> pure (Just v)+        Nothing -> inspectFromCabal+    else inspectFromCabal+  where+    extractGhcFromText txt =+      let ls = T.lines txt+          findCompiler l =+            let s = T.strip l+            in if "with-compiler:" `T.isPrefixOf` s+                 then let rest = T.strip (T.drop 14 s)+                      in if "ghc-" `T.isPrefixOf` rest+                           then Just (T.drop 4 rest)+                           else Just rest+                 else Nothing+      in case mapMaybe findCompiler ls of+           (v:_) -> Just v+           []    -> Nothing++    inspectFromCabal = do+      cabalFiles <- findCabalFiles repoPath+      results <- mapM readGhcFromCabal cabalFiles+      let allVers = concat results+      pure (if "9.14.1" `elem` allVers+              then Just "9.14.1"+              else case allVers of { (v:_) -> Just v; [] -> Nothing })++    readGhcFromCabal path = do+      txt <- TIO.readFile path `catch` (\(_ :: SomeException) -> pure "")+      let ls = T.lines txt+          findGhcLine l =+            let s = T.strip l+            in if "GHC ==" `T.isInfixOf` s+                 then let afterEq = T.strip (snd (T.breakOnEnd "==" s))+                          v = T.takeWhile (\c -> isDigit c || c == '.') afterEq+                      in if T.null v then Nothing else Just v+                 else Nothing+      pure (mapMaybe findGhcLine ls)++-- | Inspects Cabal specification version from @*.cabal@ files.+inspectRepoCabalVersion :: FilePath -> IO (Maybe Text)+inspectRepoCabalVersion repoPath = do+  cabalFiles <- findCabalFiles repoPath+  case cabalFiles of+    [] -> pure Nothing+    (c:_) -> do+      txt <- TIO.readFile c `catch` (\(_ :: SomeException) -> pure "")+      let ls = T.lines txt+          findCabalVer l =+            let s = T.strip l+            in if "cabal-version:" `T.isPrefixOf` s+                 then Just (T.strip (T.drop 14 s))+                 else Nothing+      pure (case mapMaybe findCabalVer ls of { (v:_) -> Just v; [] -> Nothing })++-- | Inspects license stanza from @*.cabal@ or root @LICENSE@ file.+inspectRepoLicense :: FilePath -> IO (Maybe Text)+inspectRepoLicense repoPath = do+  cabalFiles <- findCabalFiles repoPath+  cabalLic <- case cabalFiles of+    (c:_) -> do+      txt <- TIO.readFile c `catch` (\(_ :: SomeException) -> pure "")+      let ls = T.lines txt+          findLic l =+            let s = T.strip l+            in if "license:" `T.isPrefixOf` s+                 then Just (T.strip (T.drop 8 s))+                 else Nothing+      pure (case mapMaybe findLic ls of { (v:_) -> Just v; [] -> Nothing })+    [] -> pure Nothing++  case cabalLic of+    Just lic -> pure (Just lic)+    Nothing  -> do+      let licPath = repoPath </> "LICENSE"+      hasLic <- doesFileExist licPath+      if hasLic+        then do+          txt <- TIO.readFile licPath `catch` (\(_ :: SomeException) -> pure "")+          if "European Union Public Licence" `T.isInfixOf` txt || "EUPL" `T.isInfixOf` txt+            then pure (Just "EUPL-1.2")+            else pure (Just "LICENSE-FOUND")+        else pure Nothing++-- | Checks for an executable verification runner under @bin/@.+checkVerifyRunner :: FilePath -> Text -> IO Bool+checkVerifyRunner repoPath repoName = do+  let explicitRunner = repoPath </> "bin" </> (T.unpack repoName ++ "-verify")+  hasExplicit <- doesFileExist explicitRunner+  if hasExplicit+    then do+      perms <- getPermissions explicitRunner+      pure (executable perms)+    else do+      let binDir = repoPath </> "bin"+      hasBin <- doesDirectoryExist binDir+      if hasBin+        then do+          files <- listDirectory binDir+          let verifyScripts = filter (\f -> "-verify" `T.isSuffixOf` T.pack f) files+          case verifyScripts of+            [] -> pure False+            (v:_) -> do+              perms <- getPermissions (binDir </> v)+              pure (executable perms)+        else pure False++-- | Finds all @.cabal@ files within a repository directory or @packages/*/@.+findCabalFiles :: FilePath -> IO [FilePath]+findCabalFiles repoPath = do+  rootEntries <- listDirectory repoPath `catch` (\(_ :: SomeException) -> pure [])+  let rootCabals = [ repoPath </> f | f <- rootEntries, takeExtension f == ".cabal" ]+  if not (null rootCabals)+    then pure rootCabals+    else do+      let pkgsDir = repoPath </> "packages"+      hasPkgs <- doesDirectoryExist pkgsDir+      if hasPkgs+        then do+          pkgEntries <- listDirectory pkgsDir `catch` (\(_ :: SomeException) -> pure [])+          nestedCabals <- mapM (findPkgCabal pkgsDir) pkgEntries+          pure (concat nestedCabals)+        else pure []+  where+    findPkgCabal baseDir pkgName = do+      let dir = baseDir </> pkgName+      isDir <- doesDirectoryExist dir+      if isDir+        then do+          entries <- listDirectory dir `catch` (\(_ :: SomeException) -> pure [])+          pure [ dir </> f | f <- entries, takeExtension f == ".cabal" ]+        else pure []++-- | Audits a single target repository using the cellular sheaf model.+auditSheafTarget :: FilePath -> IO SheafReport+auditSheafTarget targetDir = do+  stalk <- inspectRepoStalk targetDir+  let clientName = stalkRepoName stalk+      complex = buildCanonicalSheafComplex [clientName]+      c0 = Cochain0 (Map.fromList [("canonical", canonicalRepoStalk), (clientName, stalk)])+  pure (evaluateSheafCoherence complex c0)++-- | Audits all discovered ecosystem repositories within a parent directory.+auditEcosystemSheaf :: FilePath -> IO SheafReport+auditEcosystemSheaf parentDir = do+  repoPaths <- discoverEcosystemRepositories parentDir+  stalks <- mapM inspectRepoStalk repoPaths+  let clientNames = map stalkRepoName stalks+      complex = buildCanonicalSheafComplex clientNames+      stalkPairs = ("canonical", canonicalRepoStalk) : zip clientNames stalks+      c0 = Cochain0 (Map.fromList stalkPairs)+  pure (evaluateSheafCoherence complex c0)
+ src/Gachi/Sync/Types.hs view
@@ -0,0 +1,430 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}++-- | Module: Gachi.Sync.Types+-- Description: Core types, models, and data structures for repository standardization+--+-- This module defines the foundational types modeling the ecosystem-wide+-- Standardization Matrix (~/src/PLAN.md §1.A), asset categories across the+-- nine canonical dimensions, asset audit outcomes, and synchronization plans.+--+-- = Architectural Context & Invariants+--+-- In accordance with Note [Ecosystem Standardization Invariants]+-- [ref:note-ecosystem-standardization], all repositories across ~/src/*+-- adhere to shared structural conventions. This module formalizes those+-- conventions as strongly-typed Haskell records suitable for static analysis,+-- diagnostic reporting, and automated configuration propagation.+--+-- == The Nine Standardization Dimensions+--+-- In accordance with ~/src/PLAN.md §1.A, the ecosystem formalizes nine core+-- standardization dimensions to maintain ergonomic parity across all checkouts:+--+-- 1. __Dimension 1 (Workspace Layout):__+--    Multi-package hierarchy (@packages/*/src@, @packages/*/test@, @.cabal@),+--    central documentation corpus under @docs/@ (@docs/INDEX.md@, @docs/notes/@),+--    and the operational planning pair (@PLAN.md@ and @STATE.md@).+--+-- 2. __Dimension 2 (Code Formatters):__+--    Deterministic code formatting profiles across Haskell (@fourmolu.yaml@),+--    Rust (@rustfmt.toml@), and C/C++ (@.clang-format@). Eliminates whitespace+--    churn and enforces AST-preserving layout.+--+-- 3. __Dimension 3 (Linters & Guardrails):__+--    Static analysis rules enforcing zero-hint cleanliness (@.hlint.yaml@)+--    and modern GHC idioms such as @OverloadedRecordDot@.+--+-- 4. __Dimension 4 (Build Conventions):__+--    Hermetic toolchain pinning (@cabal.project@) targeting GHC 9.14.1 and+--    Cabal 3.18, with developer override templates (@cabal.project.local.example@).+--+-- 5. __Dimension 5 (Git Governance):__+--    Linux kernel RFC 2822 commit message trailers (@.gitmessage@), Tagref+--    cross-reference configuration (@tagref.yml@), and pre-commit hooks.+--+-- 6. __Dimension 6 (Remote Topology):__+--    Multi-forge distribution topology separating Radicle peer-to-peer from+--    commercial and community mirrors (@docs/MAINTAINER_GUIDE.md@).+--+-- 7. __Dimension 7 (Tiered CI Workflows):__+--    Asymmetric forge resource allocation: heavyweight physical verification on+--    GitHub Actions versus lightweight static checks on community GitLab runners.+--+-- 8. __Dimension 8 (Polyglot Invariants):__+--    ABI stability, FFI boundary pinning, and cross-language integration test runners.+--+-- 9. __Dimension 9 (Agent Advisory Layer):__+--    Context-budgeted agent instructions (@AGENTS.md@, ~4 KB limit), compatibility+--    symlinks (@CLAUDE.md@), and modular rules in @.agents/rules/@.+--+-- 10. __Physical Verification Harnesses:__+--     Turnkey shell scripts in @bin/@ enforcing the five physical gates across+--     every checkout (@verify@, @tagref@, @linkcheck@, @cloc@, @generate-tags@).+--+-- == Audit and Synchronization Lifecycle+--+-- The types defined here establish a two-phase verification and synchronization cycle:+--+-- * __Inspection Phase:__+--   The target workspace is audited without mutations ('Gachi.Sync.Audit.auditRepository').+--   Each asset is tested for presence, content equality against 'assetGenerator',+--   and executable bit validity, yielding an 'AssetAuditResult'.+--+-- * __Remediation Phase:__+--   Diagnostic reports ('AuditReport') guide interactive or automated synchronization+--   ('gachi sync') to resolve drift and install missing required infrastructure assets.+--+-- = When to Use This Module+--+-- Import this module when working with:+-- * The 9 standardization dimensions ('AssetCategory').+-- * Asset definitions and generators ('StandardAsset').+-- * The complete multi-dimension matrix ('StandardizationMatrix').+-- * Audit results and diagnostic reports ('AssetAuditStatus', 'AuditReport').+--+-- = Worked Example+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> import Gachi.Sync.Types+-- >>> let auth = Author "Nadia Yvette Chambers" "nadia.yvette.chambers@ik.me"+-- >>> let proj = defaultProject "gongju" auth+-- >>> let asset = StandardAsset "bin/:name:-verify" CategoryVerification True True "Runner" (const "")+-- >>> resolveAssetPath proj asset+-- "bin/gongju-verify"+-- >>> categoryName CategoryFormatter+-- "Code Formatters"+module Gachi.Sync.Types+  ( -- * Standardization Dimensions & Categories+    AssetCategory(..)+  , categoryName+  , categoryDescription++    -- * Standard Asset Models+  , StandardAsset(..)+  , resolveAssetPath++    -- * Standardization Matrix+  , StandardizationMatrix(..)++    -- * Audit Results & Statuses+  , AssetAuditStatus(..)+  , AssetAuditResult(..)+  , isCompliant+  , isDrift+  , isMissing++    -- * Diagnostic Reporting+  , AuditReport(..)++    -- * Operational Synchronization Modes+  , SyncMode(..)+  ) where++import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import Gachi.Schema.Project (Project, projectName)++--------------------------------------------------------------------------------+-- 1. Standardization Dimensions & Categories+--------------------------------------------------------------------------------++-- | The nine foundational standardization dimensions governing ecosystem repositories.+--+-- Defined in ~/src/PLAN.md §1.A to ensure uniform developer ergonomics,+-- reproducible builds, and automated AI pair programming context across all checkouts.+data AssetCategory+  = CategoryLayout+    -- ^ Dimension 1: Workspace and directory hierarchy (packages/, docs/, bin/, PLAN.md, STATE.md).+  | CategoryFormatter+    -- ^ Dimension 2: Code formatting profiles (fourmolu.yaml, rustfmt.toml, .clang-format).+  | CategoryLinter+    -- ^ Dimension 3: Static analysis guardrails (.hlint.yaml).+  | CategoryBuild+    -- ^ Dimension 4: Build system conventions (cabal.project, cabal.project.local.example).+  | CategoryGitGovernance+    -- ^ Dimension 5: Git governance, RFC 2822 commit templates, and hooks (.githooks/).+  | CategoryRemoteTopology+    -- ^ Dimension 6: Multi-forge replication remotes (rad, github, gitcode, framagit, disroot).+  | CategoryCIWorkflow+    -- ^ Dimension 7: Forge-tiered CI resource allocation (.github/workflows/, .gitlab-ci.yml).+  | CategoryPolyglot+    -- ^ Dimension 8: Multi-language FFI boundary invariants and cross-language tests.+  | CategoryAgentAdvisory+    -- ^ Dimension 9: Agent orientation pointers (AGENTS.md), symlinks, rules, and skills.+  | CategoryVerification+    -- ^ Physical verification harnesses and non-negotiable verification gates (bin/*-verify).+  deriving stock (Eq, Show, Ord, Enum, Bounded, Generic)++-- | Human-readable short name for an 'AssetCategory'.+--+-- Used in diagnostic audit tables and status report banners.+categoryName :: AssetCategory -> Text+categoryName CategoryLayout         = "Workspace Layout"+categoryName CategoryFormatter      = "Code Formatters"+categoryName CategoryLinter         = "Linters & Guardrails"+categoryName CategoryBuild          = "Build Conventions"+categoryName CategoryGitGovernance  = "Git Governance"+categoryName CategoryRemoteTopology = "Remote Topology"+categoryName CategoryCIWorkflow     = "Tiered CI Workflows"+categoryName CategoryPolyglot       = "Polyglot Invariants"+categoryName CategoryAgentAdvisory  = "Agent Advisory Layer"+categoryName CategoryVerification   = "Physical Verification"++-- | Detailed synopsis of what an 'AssetCategory' governs.+--+-- Explains the architectural responsibilities and scope of each dimension.+categoryDescription :: AssetCategory -> Text+categoryDescription CategoryLayout =+  "Multi-package directory hierarchy, central docs/ corpus, and two-file planning loop."+categoryDescription CategoryFormatter =+  "Deterministic code formatting profiles across Haskell (Fourmolu), Rust, and C/C++."+categoryDescription CategoryLinter =+  "Static analysis rules enforcing zero-hint clean compiler and linter policies."+categoryDescription CategoryBuild =+  "Hermetic toolchain pinning (GHC 9.14.1 / Cabal 3.18) and local override patterns."+categoryDescription CategoryGitGovernance =+  "RFC 2822 commit message trailers (Assisted-by:) and pre-commit gate hooks."+categoryDescription CategoryRemoteTopology =+  "Multi-forge mirroring separating Radicle p2p from commercial and community forges."+categoryDescription CategoryCIWorkflow =+  "Asymmetric forge resource allocation: heavy GitHub Actions vs lightweight community CI."+categoryDescription CategoryPolyglot =+  "ABI stability, FFI boundary pinning, and cross-language integration test runners."+categoryDescription CategoryAgentAdvisory =+  "Context-budgeted AGENTS.md (~4 KB pointer), compatibility symlinks, and modular rules."+categoryDescription CategoryVerification =+  "Turnkey verification scripts enforcing the 5 physical gates across every repository."++--------------------------------------------------------------------------------+-- 2. Standard Asset Models+--------------------------------------------------------------------------------++-- | Declarative specification for an individual standardized asset in the ecosystem.+--+-- Each asset defines its relative file path, category dimension, permission requirements,+-- and a pure generator synthesizing its canonical content from a 'Project' specification.+data StandardAsset = StandardAsset+  { assetPath        :: !FilePath+    -- ^ Relative file path within the workspace root (e.g. @".hlint.yaml"@ or @"bin/:name:-verify"@).+  , assetCategory    :: !AssetCategory+    -- ^ Standardization dimension governing this asset.+  , assetRequired    :: !Bool+    -- ^ Whether absence of this file constitutes an audit violation.+  , assetExecutable  :: !Bool+    -- ^ Whether POSIX executable permissions (@+x@) must be applied.+  , assetDescription :: !Text+    -- ^ Human-readable explanation of asset purpose and architectural role.+  , assetGenerator   :: !(Project -> Text)+    -- ^ Pure generator synthesizing canonical file content for the given project.+  } deriving stock (Generic)++instance Show StandardAsset where+  showsPrec d a = showParen (d > 10) $+    showString "StandardAsset { assetPath = " . shows (assetPath a)+    . showString ", assetCategory = " . shows (assetCategory a)+    . showString ", assetRequired = " . shows (assetRequired a)+    . showString ", assetExecutable = " . shows (assetExecutable a)+    . showString ", assetDescription = " . shows (assetDescription a)+    . showString " }"++instance Eq StandardAsset where+  a == b = assetPath a == assetPath b+        && assetCategory a == assetCategory b+        && assetRequired a == assetRequired b+        && assetExecutable a == assetExecutable b++-- | Resolves the concrete filesystem relative path of an asset for a target project.+--+-- Replaces template tokens such as @":name:"@ and @":project:"@ with the project's+-- declared 'projectName'. For static paths (e.g. @".hlint.yaml"@), returns the+-- path unmodified.+--+-- === Worked Example+--+-- >>> import Gachi.Schema.Project (defaultProject, Author(..))+-- >>> let proj = defaultProject "gongju" (Author "Nadia" "nadia@example.com")+-- >>> let asset = StandardAsset "bin/:name:-verify" CategoryVerification True True "Verify script" (const "")+-- >>> resolveAssetPath proj asset+-- "bin/gongju-verify"+resolveAssetPath :: Project -> StandardAsset -> FilePath+resolveAssetPath proj asset =+  let pName = T.unpack (projectName proj)+      raw = assetPath asset+      subst [] = []+      subst str@(c:cs)+        | take 6 str == ":name:"    = pName ++ subst (drop 6 str)+        | take 9 str == ":project:" = pName ++ subst (drop 9 str)+        | otherwise                 = c : subst cs+  in subst raw++--------------------------------------------------------------------------------+-- 3. Standardization Matrix+--------------------------------------------------------------------------------++-- | Composite Standardization Matrix aggregating all registered standard assets.+--+-- Encapsulates the entire ecosystem contract: which dimensions are active,+-- which assets belong to each dimension, and how their contents are generated.+--+-- === Structural Invariants & Properties+--+-- 1. __Dimension Congruence:__ Every declared dimension in 'matrixDimensions' is+--    represented by at least one asset in 'matrixAssets'.+-- 2. __Unique Target Paths:__ Within a concrete project instantiation, no two+--    assets in 'matrixAssets' resolve to the same relative file path.+-- 3. __Pure Hermetic Generation:__ Every asset's 'assetGenerator' is a pure, total+--    function evaluated strictly from the immutable 'Project' record.+-- 4. __Static Analysis Friendly:__ The matrix can be audited, filtered by category,+--    queried by path, or serialized without accessing ambient filesystem state.+data StandardizationMatrix = StandardizationMatrix+  { matrixAssets     :: ![StandardAsset]+    -- ^ Complete list of registered standard asset specifications.+  , matrixDimensions :: ![AssetCategory]+    -- ^ Active dimensions enforced by this matrix instance.+  } deriving stock (Generic)++--------------------------------------------------------------------------------+-- 4. Audit Results & Statuses+--------------------------------------------------------------------------------++-- | Outcome of auditing an individual asset against the physical filesystem.+--+-- The audit engine classifies every asset into exactly one of these five statuses:+--+-- 1. 'StatusCompliant':+--    File exists on disk, content matches expected generator output, and executable+--    bits conform to 'assetExecutable'.+--+-- 2. 'StatusMissing':+--    File is completely absent from the target directory on disk.+--+-- 3. 'StatusContentDrift':+--    File exists, but its text diverges from canonical generator output.+--    Contains actual character count and expected character count.+--+-- 4. 'StatusPermissionDrift':+--    File exists and content matches, but its executable bit differs from+--    expected (e.g. executable script lacking @+x@ permissions).+--+-- 5. 'StatusIOError':+--    Filesystem I/O exception occurred while attempting to inspect the file.+data AssetAuditStatus+  = StatusCompliant+    -- ^ File exists, textual content matches canonical generator, and permissions are valid.+  | StatusMissing+    -- ^ File is missing on disk.+  | StatusContentDrift !Int !Int+    -- ^ File exists but content diverges (actual character length vs expected length).+  | StatusPermissionDrift !Bool !Bool+    -- ^ File exists and matches content, but executable bits differ (actual vs expected).+  | StatusIOError !Text+    -- ^ Filesystem I/O exception occurred while attempting to inspect the file.+  deriving stock (Eq, Show, Generic)++-- | Helper checking if an audit status represents full compliance.+--+-- Returns 'True' exclusively for 'StatusCompliant'.+isCompliant :: AssetAuditStatus -> Bool+isCompliant StatusCompliant = True+isCompliant _               = False++-- | Helper checking if an audit status represents content or permission drift.+--+-- Returns 'True' for 'StatusContentDrift' and 'StatusPermissionDrift'.+isDrift :: AssetAuditStatus -> Bool+isDrift (StatusContentDrift _ _)    = True+isDrift (StatusPermissionDrift _ _) = True+isDrift _                           = False++-- | Helper checking if an audit status represents a missing file.+--+-- Returns 'True' exclusively for 'StatusMissing'.+isMissing :: AssetAuditStatus -> Bool+isMissing StatusMissing = True+isMissing _             = False++-- | Paired audit result combining an asset specification with its observed status.+--+-- Represents the atomic finding for a single asset evaluated by the audit engine.+-- Pairs the structural contract ('resultAsset') with the empirical status observed+-- on disk ('resultStatus').+--+-- === Worked Example+--+-- >>> import Gachi.Sync.Types+-- >>> let asset = StandardAsset ".hlint.yaml" CategoryLinter True False "HLint" (const "")+-- >>> let res = AssetAuditResult asset StatusCompliant+-- >>> isCompliant (resultStatus res)+-- === Invariants & Verification Semantics+--+-- * An asset result is immutable once constructed.+-- * Predicates like 'isCompliant', 'isDrift', and 'isMissing' project directly+--   from 'resultStatus'.+-- * Audit reports aggregate lists of 'AssetAuditResult' to render tabular diagnostics.+data AssetAuditResult = AssetAuditResult+  { resultAsset  :: !StandardAsset+    -- ^ The standardized asset specification audited.+  , resultStatus :: !AssetAuditStatus+    -- ^ Observed compliance or discrepancy status on disk.+  } deriving stock (Eq, Show, Generic)++--------------------------------------------------------------------------------+-- 5. Diagnostic Reporting+--------------------------------------------------------------------------------++-- | Comprehensive diagnostic summary of a repository audit across the matrix.+--+-- Summarizes aggregate compliance statistics, lists all individual asset audit+-- outcomes, and computes the overall compliance percentage.+--+-- === Diagnostic Metrics & Mathematical Guarantees+--+-- 1. __Exact Asset Partitioning:__+--    The evaluated outcomes in 'reportResults' partition the universe of standard+--    assets:+--    \[+--      \text{length}(\text{reportResults}) = \text{reportTotalAssets}+--    \]+-- 2. __Compliance Ratio Calculation:__+--    The compliance percentage is bounded in \([0.0, 100.0]\):+--    \[+--      \text{reportCompliancePct} = \frac{\text{reportCompliantCount}}{\text{reportTotalAssets}} \times 100.0+--    \]+-- 3. __Observational Safety:__+--    Audits are read-only and non-destructive. Computing an 'AuditReport' guarantees+--    zero filesystem mutations, zero file deletions, and zero timestamp alterations.+data AuditReport = AuditReport+  { reportTargetDir      :: !FilePath+    -- ^ Target repository filesystem path audited.+  , reportProjectName    :: !Text+    -- ^ Name of the audited project specification.+  , reportResults        :: ![AssetAuditResult]+    -- ^ Detailed per-asset audit outcomes.+  , reportTotalAssets    :: !Int+    -- ^ Total number of matrix assets evaluated.+  , reportCompliantCount :: !Int+    -- ^ Number of assets achieving full compliance.+  , reportDriftCount     :: !Int+    -- ^ Number of existing assets with content or permission drift.+  , reportMissingCount   :: !Int+    -- ^ Number of required assets missing from the filesystem.+  , reportCompliancePct  :: !Double+    -- ^ Overall compliance percentage (0.0% to 100.0%).+  } deriving stock (Eq, Show, Generic)++--------------------------------------------------------------------------------+-- 5. Operational Synchronization Modes+--------------------------------------------------------------------------------++-- | Operational mode controlling filesystem mutations during synchronization.+--+-- Governs whether synchronization plans are simulated as read-only dry runs or+-- executed as atomic mutations on disk.+data SyncMode+  = DryRun+    -- ^ Inspect discrepancies and compute the 'SyncPlan', but perform no disk mutations.+  | ApplySync+    -- ^ Apply all planned changes: create missing files, update content, and fix permissions.+  deriving stock (Eq, Show, Ord, Enum, Bounded, Generic)
+ test/Main.hs view
@@ -0,0 +1,772 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Module: Main+-- Description: Comprehensive test suite for gachi-sync+--+-- Validates:+-- 1. Canonical Standardization Matrix completeness and category queries.+-- 2. Pure catalog template generators and multi-language formatting stanzas.+-- 3. Path token substitution (:name:, :project:).+-- 4. Audit engine accuracy: clean compliance, content drift detection, missing file reporting,+--    and permission drift detection on physical workspaces.+-- 5. Diagnostic ASCII report formatting.+-- 6. Forge-tiered CI matrix generation, asymmetric runner allocation, and classification (Milestone 3.3).+module Main (main) where++import Control.Exception (bracket)+import qualified Data.Map.Strict as Map+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory+  ( createDirectoryIfMissing+  , doesFileExist+  , executable+  , getPermissions+  , removeDirectoryRecursive+  , setPermissions+  , setOwnerExecutable+  )+import System.Exit (ExitCode(..))+import System.FilePath ((</>), takeFileName)+import System.Process (readProcessWithExitCode)+import Test.Hspec++import Gachi.Schema.Project+  ( Author(..)+  , Project+  , RemoteSpec(..)+  , defaultProject+  , remoteName+  , remoteUrl+  )+import Gachi.Scaffold+  ( ScaffoldMode(..)+  , ScaffoldResult(..)+  , generateAndWriteScaffold+  )+import Gachi.Sync++-- | Test project specification for synthesis and auditing.+testProject :: Project+testProject = defaultProject "audit-test" (Author "Test Runner" "test@example.com")++-- | Helper to create a temporary test directory and clean it up after testing.+withTempDir :: (FilePath -> IO a) -> IO a+withTempDir = bracket createTemp cleanupTemp+  where+    createTemp = do+      let dir = "dist-newstyle" </> "test-audit-workspace"+      createDirectoryIfMissing True dir+      pure dir+    cleanupTemp = removeDirectoryRecursive++-- | Helper creating an isolated Git repository in a temporary directory for live remote testing.+withGitTempDir :: (FilePath -> IO a) -> IO a+withGitTempDir action = withTempDir $ \baseDir -> do+  let repoDir = baseDir </> "git-remote-test"+  createDirectoryIfMissing True repoDir+  (code, _, err) <- readProcessWithExitCode "git" ["-C", repoDir, "init"] ""+  if code == ExitSuccess+    then action repoDir+    else error ("Failed to initialize test git repo: " ++ err)++main :: IO ()+main = hspec $ do+  describe "Gachi.Sync.Matrix (Standardization Matrix)" $ do+    it "registers all 10 canonical dimensions" $ do+      length canonicalDimensions `shouldBe` 10+      matrixDimensions canonicalMatrix `shouldBe` canonicalDimensions++    it "registers at least 20 canonical assets" $ do+      length canonicalAssets `shouldSatisfy` (>= 20)+      length (matrixAssets canonicalMatrix) `shouldSatisfy` (>= 20)++    it "assigns non-empty paths and descriptions to all canonical assets" $ do+      mapM_ (\a -> do+        assetPath a `shouldNotBe` ""+        assetDescription a `shouldNotBe` ""+        ) canonicalAssets++    it "filters assets correctly by category" $ do+      let formatters = assetsForCategory CategoryFormatter canonicalMatrix+      map assetPath formatters `shouldContain` ["fourmolu.yaml"]+      map assetPath formatters `shouldContain` ["rustfmt.toml"]+      map assetPath formatters `shouldContain` [".clang-format"]++      let linters = assetsForCategory CategoryLinter canonicalMatrix+      map assetPath linters `shouldBe` [".hlint.yaml"]++      let builds = assetsForCategory CategoryBuild canonicalMatrix+      map assetPath builds `shouldContain` ["cabal.project"]++      let gitGov = assetsForCategory CategoryGitGovernance canonicalMatrix+      map assetPath gitGov `shouldContain` [".gitmessage"]+      map assetPath gitGov `shouldContain` [".githooks/commit-msg"]++      let agents = assetsForCategory CategoryAgentAdvisory canonicalMatrix+      map assetPath agents `shouldContain` ["AGENTS.md"]+      map assetPath agents `shouldContain` [".agents/rules/commit-attribution.md"]++    it "identifies executable verification scripts and git hooks" $ do+      let execs = executableAssets canonicalMatrix+      map assetPath execs `shouldContain` [".githooks/commit-msg"]+      map assetPath execs `shouldContain` [".githooks/pre-commit"]+      map assetPath execs `shouldContain` ["bin/:name:-verify"]+      map assetPath execs `shouldContain` ["bin/:name:-tagref"]++    it "counts assets per category accurately" $ do+      let counts = countByCategory canonicalMatrix+      length counts `shouldBe` 10+      all (\(_, c) -> c > 0) counts `shouldBe` True++    it "finds assets by relative path" $ do+      let maybeHlint = findAssetByPath ".hlint.yaml" canonicalMatrix+      fmap assetCategory maybeHlint `shouldBe` Just CategoryLinter++      let missing = findAssetByPath "nonexistent.file" canonicalMatrix+      missing `shouldBe` Nothing++  describe "Gachi.Sync.Types (Path Resolution)" $ do+    it "leaves static asset paths unchanged" $ do+      let asset = StandardAsset ".hlint.yaml" CategoryLinter True False "HLint" (const "")+      resolveAssetPath testProject asset `shouldBe` ".hlint.yaml"++    it "substitutes :name: tokens with project name" $ do+      let asset = StandardAsset "bin/:name:-verify" CategoryVerification True True "Verify" (const "")+      resolveAssetPath testProject asset `shouldBe` "bin/audit-test-verify"++    it "substitutes :project: tokens with project name" $ do+      let asset = StandardAsset "docs/:project:-notes.md" CategoryLayout True False "Notes" (const "")+      resolveAssetPath testProject asset `shouldBe` "docs/audit-test-notes.md"++  describe "Gachi.Sync.Catalog (Asset Generators)" $ do+    it "synthesizes valid Fourmolu configuration" $ do+      fourmoluTemplate `shouldSatisfy` (\t -> "indentation: 2" `T.isInfixOf` t)+      fourmoluTemplate `shouldSatisfy` (\t -> "fixities:" `T.isInfixOf` t)++    it "synthesizes valid rustfmt configuration" $ do+      rustfmtTemplate `shouldSatisfy` (\t -> "edition = \"2021\"" `T.isInfixOf` t)++    it "synthesizes valid .clang-format configuration" $ do+      clangFormatTemplate `shouldSatisfy` (\t -> "BasedOnStyle: LLVM" `T.isInfixOf` t)++    it "synthesizes high-resource GitHub Actions workflow" $ do+      let ci = githubCiTemplate testProject+      ci `shouldSatisfy` (\t -> "./bin/audit-test-verify" `T.isInfixOf` t)+      ci `shouldSatisfy` (\t -> "ubuntu-latest" `T.isInfixOf` t)++    it "synthesizes lightweight community GitLab CI workflow" $ do+      let ci = gitlabCiTemplate testProject+      ci `shouldSatisfy` (\t -> "./bin/audit-test-tagref" `T.isInfixOf` t)++    it "synthesizes modular agent advisory rules" $ do+      commitAttributionRuleTemplate `shouldSatisfy` (\t -> "Assisted-by:" `T.isInfixOf` t)+      codeNavigationRuleTemplate `shouldSatisfy` (\t -> "`./tags` symbol jump" `T.isInfixOf` t)+      compilerVerificationRuleTemplate `shouldSatisfy` (\t -> "Zero warnings tolerated" `T.isInfixOf` t)+      documentationPrecedenceRuleTemplate `shouldSatisfy` (\t -> "Precedence rule" `T.isInfixOf` t)++  describe "Gachi.Sync.Audit (Repository Audit Engine)" $ do+    it "evaluates clean synthesized workspace with high compliance" $ withTempDir $ \tmpDir -> do+      -- Materialize clean workspace+      res <- generateAndWriteScaffold tmpDir OverwriteExisting testProject+      writtenFiles res `shouldSatisfy` (not . null)+      skippedFiles res `shouldBe` []++      -- Also ensure Fourmolu config is created if needed by matrix+      TIO.writeFile (tmpDir </> "fourmolu.yaml") fourmoluTemplate++      -- Run audit+      report <- auditRepository tmpDir testProject canonicalMatrix+      reportProjectName report `shouldBe` "audit-test"+      reportTotalAssets report `shouldSatisfy` (>= 20)+      reportCompliantCount report `shouldSatisfy` (>= 14)+      reportCompliancePct report `shouldSatisfy` (>= 70.0)++    it "detects content drift when a file is modified" $ withTempDir $ \tmpDir -> do+      -- Materialize clean workspace+      _ <- generateAndWriteScaffold tmpDir OverwriteExisting testProject+      -- Deliberately mutate .hlint.yaml+      TIO.writeFile (tmpDir </> ".hlint.yaml") "# Divergent HLint configuration\n"++      report <- auditRepository tmpDir testProject canonicalMatrix+      reportDriftCount report `shouldSatisfy` (>= 1)+      let drifts = filterDriftResults report+      map (assetPath . resultAsset) drifts `shouldContain` [".hlint.yaml"]++    it "detects missing assets when a required file is absent" $ withTempDir $ \tmpDir -> do+      -- In an empty directory, all required assets should be missing+      report <- auditRepository tmpDir testProject canonicalMatrix+      reportMissingCount report `shouldSatisfy` (>= 15)+      reportCompliantCount report `shouldBe` 0+      let missings = filterMissingResults report+      map (assetPath . resultAsset) missings `shouldContain` [".hlint.yaml"]+      map (assetPath . resultAsset) missings `shouldContain` [".gitmessage"]++    it "detects permission drift on executable scripts" $ withTempDir $ \tmpDir -> do+      -- Materialize clean workspace+      _ <- generateAndWriteScaffold tmpDir OverwriteExisting testProject+      -- Strip executable bit from .githooks/commit-msg+      let hookPath = tmpDir </> ".githooks" </> "commit-msg"+      perms <- getPermissions hookPath+      setPermissions hookPath (setOwnerExecutable False perms)++      report <- auditRepository tmpDir testProject canonicalMatrix+      let permDrifts = filter (\r -> case resultStatus r of { StatusPermissionDrift _ _ -> True; _ -> False }) (reportResults report)+      map (assetPath . resultAsset) permDrifts `shouldContain` [".githooks/commit-msg"]++    it "renders ASCII diagnostic reports with summary and tables" $ withTempDir $ \tmpDir -> do+      report <- auditRepository tmpDir testProject canonicalMatrix+      let formatted = formatAuditReport report+      formatted `shouldSatisfy` (\t -> "Gachi Standardization Matrix Audit Report" `T.isInfixOf` t)+      formatted `shouldSatisfy` (\t -> "Total Assets:" `T.isInfixOf` t)+      formatted `shouldSatisfy` (\t -> "Missing Required Assets:" `T.isInfixOf` t)++      let str = formatAuditReportString report+      str `shouldSatisfy` (\s -> length s > 100)++  describe "Gachi.Sync.Hash (Differential Cryptographic Hashing)" $ do+    it "computes standard FIPS 180-2 SHA-256 test vectors" $ do+      -- NIST Empty String Vector+      sha256Text "" `shouldBe` "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"+      -- NIST 3-character Vector+      sha256Text "abc" `shouldBe` "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"+      -- NIST Pangram Vector+      sha256Text "The quick brown fox jumps over the lazy dog"+        `shouldBe` "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"++    it "computes consistent hashes across text and file representations" $ withTempDir $ \tmpDir -> do+      let sampleText = "Author: Nadia Yvette Chambers\nLicense: EUPL-1.2\n"+          filePath = tmpDir </> "test-hash.txt"+      TIO.writeFile filePath sampleText+      fileHash <- sha256File filePath+      let textHash = sha256Text sampleText+      fileHash `shouldBe` textHash++  describe "Gachi.Sync.Engine (Harmonic Chattering Synchronization)" $ do+    it "computes a sync plan proposing creation of all assets in an empty workspace" $ withTempDir $ \tmpDir -> do+      plan <- computeSyncPlan tmpDir testProject canonicalMatrix+      planProjectName plan `shouldBe` "audit-test"+      planCreatedCount plan `shouldSatisfy` (>= 20)+      planUpdatedCount plan `shouldBe` 0+      planChmodCount plan `shouldBe` 0+      isPlanClean plan `shouldBe` False++    it "executes sync plan in DryRun mode without creating files on disk" $ withTempDir $ \tmpDir -> do+      plan <- computeSyncPlan tmpDir testProject canonicalMatrix+      res <- executeSyncPlan DryRun plan+      resultMode res `shouldBe` DryRun+      resultSuccess res `shouldBe` True+      length (resultCreated res) `shouldSatisfy` (>= 20)+      -- Assert disk files remain untouched+      doesFileExist (tmpDir </> ".hlint.yaml") `shouldReturn` False+      doesFileExist (tmpDir </> ".gitmessage") `shouldReturn` False++    it "executes sync plan in ApplySync mode and creates files on disk" $ withTempDir $ \tmpDir -> do+      plan <- computeSyncPlan tmpDir testProject canonicalMatrix+      res <- executeSyncPlan ApplySync plan+      resultMode res `shouldBe` ApplySync+      resultSuccess res `shouldBe` True+      length (resultCreated res) `shouldSatisfy` (>= 20)+      -- Assert disk files exist+      doesFileExist (tmpDir </> ".hlint.yaml") `shouldReturn` True+      doesFileExist (tmpDir </> ".gitmessage") `shouldReturn` True+      -- Assert executable bit+      let hookPath = tmpDir </> ".githooks" </> "commit-msg"+      perms <- getPermissions hookPath+      executable perms `shouldBe` True++    it "satisfies the mathematical idempotency guarantee: f(f(x)) = f(x)" $ withTempDir $ \tmpDir -> do+      -- 1. First synchronization pass: applies all creations+      res1 <- syncRepository ApplySync tmpDir testProject canonicalMatrix+      resultSuccess res1 `shouldBe` True+      length (resultCreated res1) `shouldSatisfy` (>= 20)++      -- 2. Second synchronization pass: must produce zero modifications+      res2 <- syncRepository ApplySync tmpDir testProject canonicalMatrix+      resultSuccess res2 `shouldBe` True+      length (resultCreated res2) `shouldBe` 0+      length (resultUpdated res2) `shouldBe` 0+      length (resultChmodded res2) `shouldBe` 0+      length (resultUnchanged res2) `shouldSatisfy` (>= 20)+      length (resultErrors res2) `shouldBe` 0++      -- 3. Verify that re-computing the plan yields isPlanClean == True+      plan2 <- computeSyncPlan tmpDir testProject canonicalMatrix+      isPlanClean plan2 `shouldBe` True++    it "detects and repairs content drift and permission drift non-destructively" $ withTempDir $ \tmpDir -> do+      -- 1. Initial synchronization+      _ <- syncRepository ApplySync tmpDir testProject canonicalMatrix++      -- 2. Mutate .hlint.yaml (content drift)+      let hlintPath = tmpDir </> ".hlint.yaml"+      TIO.writeFile hlintPath "# Corrupted or drifted HLint file\n"++      -- 3. Strip executable bit from .githooks/commit-msg (permission drift)+      let hookPath = tmpDir </> ".githooks" </> "commit-msg"+      hookPerms <- getPermissions hookPath+      setPermissions hookPath (setOwnerExecutable False hookPerms)++      -- 4. Compute plan and verify drift detection+      driftPlan <- computeSyncPlan tmpDir testProject canonicalMatrix+      planUpdatedCount driftPlan `shouldBe` 1+      planChmodCount driftPlan `shouldBe` 1+      isPlanClean driftPlan `shouldBe` False++      -- 5. Synchronize with ApplySync to repair drift+      repairRes <- syncRepository ApplySync tmpDir testProject canonicalMatrix+      resultSuccess repairRes `shouldBe` True+      resultUpdated repairRes `shouldBe` [".hlint.yaml"]+      resultChmodded repairRes `shouldBe` [".githooks/commit-msg"]++      -- 6. Verify restored state is clean+      postPlan <- computeSyncPlan tmpDir testProject canonicalMatrix+      isPlanClean postPlan `shouldBe` True+      permsAfter <- getPermissions hookPath+      executable permsAfter `shouldBe` True++    it "discovers sibling ecosystem repositories" $ withTempDir $ \tmpDir -> do+      -- Create repository A (has .git)+      let repoA = tmpDir </> "repo-alpha"+      createDirectoryIfMissing True (repoA </> ".git")++      -- Create repository B (has project.dhall)+      let repoB = tmpDir </> "repo-beta"+      createDirectoryIfMissing True repoB+      TIO.writeFile (repoB </> "project.dhall") "-- test dhall"++      -- Create non-repository directory+      let plainDir = tmpDir </> "plain-notes"+      createDirectoryIfMissing True plainDir++      discovered <- discoverEcosystemRepositories tmpDir+      length discovered `shouldBe` 2+      map takeFileName discovered `shouldMatchList` ["repo-alpha", "repo-beta"]++    it "renders formatted diagnostic sync plan and sync result tables" $ withTempDir $ \tmpDir -> do+      plan <- computeSyncPlan tmpDir testProject canonicalMatrix+      let planText = formatSyncPlan plan+      planText `shouldSatisfy` (\t -> "Gachi Synchronization Plan" `T.isInfixOf` t)+      planText `shouldSatisfy` (\t -> "Actions Planned:" `T.isInfixOf` t)++      res <- executeSyncPlan DryRun plan+      let resText = formatSyncResult res+      resText `shouldSatisfy` (\t -> "Gachi Synchronization Execution Summary" `T.isInfixOf` t)+      resText `shouldSatisfy` (\t -> "DRY RUN" `T.isInfixOf` t)++  describe "Gachi.Sync.CI (Forge-Tiered CI Matrix Synthesis)" $ do+    it "classifies forge types from standard workflow paths" $ do+      classifyForge ".github/workflows/ci.yml" `shouldBe` Just ForgeGitHub+      classifyForge ".github/workflows/verify.yaml" `shouldBe` Just ForgeGitHub+      classifyForge ".gitlab-ci.yml" `shouldBe` Just ForgeGitLab+      classifyForge ".woodpecker.yml" `shouldBe` Just ForgeWoodpecker+      classifyForge ".woodpecker/verify.yml" `shouldBe` Just ForgeWoodpecker+      classifyForge ".forgejo/workflows/ci.yml" `shouldBe` Just ForgeForgejo+      classifyForge ".radicle/ci.yaml" `shouldBe` Just ForgeRadicle+      classifyForge "docs/INDEX.md" `shouldBe` Nothing++    it "classifies resource tiers accurately from paths and specifications" $ do+      classifyTier ".github/workflows/ci.yml" `shouldBe` Just TierHighResource+      classifyTier ".gitlab-ci.yml" `shouldBe` Just TierLightweightStatic+      classifyTier ".woodpecker.yml" `shouldBe` Just TierLightweightStatic+      classifyTier ".forgejo/workflows/ci.yml" `shouldBe` Just TierHighResource+      classifyTier ".radicle/ci.yaml" `shouldBe` Just TierLightweightStatic+      classifyTier "docs/INDEX.md" `shouldBe` Nothing++      isHighResource defaultGitHubSpec `shouldBe` True+      isLightweight defaultGitHubSpec `shouldBe` False++      isLightweight defaultGitLabSpec `shouldBe` True+      isHighResource defaultGitLabSpec `shouldBe` False++      isLightweight defaultWoodpeckerSpec `shouldBe` True+      isHighResource defaultForgejoSpec `shouldBe` True+      isLightweight defaultRadicleSpec `shouldBe` True++    it "synthesizes complete high-resource GitHub Actions workflows" $ do+      let wf = generateGitHubWorkflow defaultGitHubSpec testProject+      wf `shouldSatisfy` ("actions/checkout@v4" `T.isInfixOf`)+      wf `shouldSatisfy` ("actions/cache@v4" `T.isInfixOf`)+      wf `shouldSatisfy` ("haskell-actions/setup@v2" `T.isInfixOf`)+      wf `shouldSatisfy` ("ghc-version: '9.14.1'" `T.isInfixOf`)+      wf `shouldSatisfy` ("cabal-version: '3.18.1.0'" `T.isInfixOf`)+      wf `shouldSatisfy` ("cabal build all --enable-tests" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-cloc --check" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-verify" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-tagref" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-linkcheck" `T.isInfixOf`)+      wf `shouldSatisfy` ("sudo apt-get install -y bubblewrap sqlite3 z3" `T.isInfixOf`)+      wf `shouldSatisfy` ("kernel.apparmor_restrict_unprivileged_userns=0" `T.isInfixOf`)+      wf `shouldSatisfy` ("url.\"https://github.com/\".insteadOf" `T.isInfixOf`)++    it "synthesizes lightweight community GitLab CI workflows without compiler overhead" $ do+      let wf = generateGitLabWorkflow defaultGitLabSpec testProject+      wf `shouldSatisfy` ("image: debian:bookworm-slim" `T.isInfixOf`)+      wf `shouldSatisfy` ("stages:" `T.isInfixOf`)+      wf `shouldSatisfy` ("tagref-check:" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-tagref" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-linkcheck" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-cloc --check" `T.isInfixOf`)+      -- Crucial invariant: No heavy GHC build steps in lightweight community CI+      wf `shouldNotSatisfy` ("cabal build" `T.isInfixOf`)+      wf `shouldNotSatisfy` ("ghc-version" `T.isInfixOf`)++    it "synthesizes lightweight Woodpecker CI pipelines" $ do+      let wf = generateWoodpeckerWorkflow defaultWoodpeckerSpec testProject+      wf `shouldSatisfy` ("pipeline:" `T.isInfixOf`)+      wf `shouldSatisfy` ("image: debian:bookworm-slim" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-tagref" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-linkcheck" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-cloc --check" `T.isInfixOf`)+      wf `shouldNotSatisfy` ("cabal build" `T.isInfixOf`)++    it "synthesizes Radicle P2P local verification broker configurations" $ do+      let wf = generateRadicleWorkflow defaultRadicleSpec testProject+      wf `shouldSatisfy` ("version: 1" `T.isInfixOf`)+      wf `shouldSatisfy` ("pipelines:" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-tagref" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-linkcheck" `T.isInfixOf`)+      wf `shouldSatisfy` ("./bin/audit-test-cloc --check" `T.isInfixOf`)++    it "converts CI specifications into standard matrix assets in CategoryCIWorkflow" $ do+      let assets = synthesizeCIAssets canonicalForgeSpecs+      length assets `shouldBe` 3+      all (\a -> assetCategory a == CategoryCIWorkflow) assets `shouldBe` True+      map assetPath assets `shouldMatchList`+        [ ".github/workflows/ci.yml"+        , ".gitlab-ci.yml"+        , ".woodpecker.yml"+        ]+      map assetRequired assets `shouldMatchList` [True, True, False]+      any assetExecutable assets `shouldBe` False++    it "extracts CI workflow assets from canonicalMatrix via ciWorkflowAssets" $ do+      let ciAssets = ciWorkflowAssets canonicalMatrix+      length ciAssets `shouldSatisfy` (>= 2)+      map assetPath ciAssets `shouldContain` [".github/workflows/ci.yml", ".gitlab-ci.yml"]+      all (\a -> assetCategory a == CategoryCIWorkflow) ciAssets `shouldBe` True++    it "supports high-resource GitLab and lightweight GitHub overrides via forgeDefaultSpec" $ do+      let highGitLab = forgeDefaultSpec ForgeGitLab TierHighResource+      ciTier highGitLab `shouldBe` TierHighResource+      ciEnableTests highGitLab `shouldBe` True+      ciContainerImage highGitLab `shouldBe` "haskell:9.14"++      let lightGH = forgeDefaultSpec ForgeGitHub TierLightweightStatic+      ciTier lightGH `shouldBe` TierLightweightStatic+      ciEnableTests lightGH `shouldBe` False++  describe "Gachi.Sync.Remote (Multi-Remote Topology & Provider Compliance)" $ do+    it "classifies approved and excluded hosting providers with detailed rationales" $ do+      classifyProvider "git@github.com:NadiaYvette/gachi.git"+        `shouldBe` ProviderAllowed "GitHub"+      classifyProvider "https://github.com/NadiaYvette/gachi.git"+        `shouldBe` ProviderAllowed "GitHub"+      classifyProvider "git@gitcode.com:NadiaYvette/gachi.git"+        `shouldBe` ProviderAllowed "GitCode"+      classifyProvider "git@framagit.org:NadiaYvette/gachi.git"+        `shouldBe` ProviderAllowed "Framagit (GitLab CE)"+      classifyProvider "git@git.disroot.org:NadiaYvette/gachi.git"+        `shouldBe` ProviderAllowed "Disroot (Forgejo)"+      classifyProvider "rad://z4V1sNQGXxPZ5wUQkjSoaDguKA8mt"+        `shouldBe` ProviderAllowed "Radicle P2P"++      -- Verify SourceHut exclusion+      case classifyProvider "git@git.sr.ht:~user/gachi" of+        ProviderExcluded pName reason -> do+          pName `shouldBe` "SourceHut (git.sr.ht)"+          reason `shouldSatisfy` ("AI" `T.isInfixOf`)+        _ -> expectationFailure "Expected SourceHut to be excluded"++      -- Verify plaintext HTTP prohibition+      case classifyProvider "http://github.com/NadiaYvette/gachi.git" of+        ProviderExcluded pName reason -> do+          pName `shouldBe` "Insecure Plaintext HTTP"+          reason `shouldSatisfy` ("Plaintext" `T.isInfixOf`)+        _ -> expectationFailure "Expected plaintext HTTP to be excluded"++      -- Predicate checks+      isProviderCompliant "git@github.com:NadiaYvette/gachi.git" `shouldBe` True+      isProviderCompliant "rad://z4V1sNQGXxPZ5wUQkjSoaDguKA8mt" `shouldBe` True+      isProviderCompliant "git@git.sr.ht:~user/gachi" `shouldBe` False+      isProviderCompliant "http://github.com/NadiaYvette/gachi.git" `shouldBe` False++    it "filters compliant and non-compliant remote specifications" $ do+      let rCompliant = RemoteSpec "github" "git@github.com:NadiaYvette/gachi.git"+          rRadicle   = RemoteSpec "rad" "rad://gachi"+          rExcluded  = RemoteSpec "srht" "git@git.sr.ht:~user/gachi"+          allSpecs   = [rCompliant, rRadicle, rExcluded]++      filterCompliantRemotes allSpecs `shouldBe` [rCompliant, rRadicle]+      let nonComp = filterNonCompliantRemotes allSpecs+      length nonComp `shouldBe` 1+      case nonComp of+        ((r, _):_) -> remoteName r `shouldBe` "srht"+        []         -> expectationFailure "Expected non-compliant remote"++    it "normalizes URLs and evaluates semantic equivalence" $ do+      normalizeRemoteUrl "git@github.com:NadiaYvette/gachi.git"+        `shouldBe` "git@github.com:nadiayvette/gachi"+      normalizeRemoteUrl "https://github.com/NadiaYvette/gachi/"+        `shouldBe` "https://github.com/nadiayvette/gachi"++      urlsMatch "git@github.com:NadiaYvette/gachi.git" "https://github.com/NadiaYvette/gachi"+        `shouldBe` True+      urlsMatch "git@github.com:NadiaYvette/gachi.git" "git@gitcode.com:NadiaYvette/gachi.git"+        `shouldBe` False++    it "synthesizes canonical 5-forge replication specs" $ do+      canonicalRemoteNames `shouldBe` ["rad", "github", "gitcode", "framagit", "disroot"]+      all isCanonicalRemoteName canonicalRemoteNames `shouldBe` True+      isCanonicalRemoteName "origin" `shouldBe` False+      isCanonicalRemoteName "upstream" `shouldBe` False++      let specs = defaultRemoteSpecsFor "gachi"+      length specs `shouldBe` 5+      map remoteName specs `shouldBe` ["rad", "github", "gitcode", "framagit", "disroot"]+      map remoteUrl specs `shouldBe`+        [ "rad://gachi"+        , "git@github.com:NadiaYvette/gachi.git"+        , "git@gitcode.com:NadiaYvette/gachi.git"+        , "git@framagit.org:NadiaYvette/gachi.git"+        , "git@git.disroot.org:NadiaYvette/gachi.git"+        ]++    it "parses git remote -v porcelain output into ConfiguredRemote records" $ do+      let rawOutput = T.unlines+            [ "github\tgit@github.com:NadiaYvette/gachi.git (fetch)"+            , "github\tgit@github.com:NadiaYvette/gachi.git (push)"+            , "rad\trad://gachi (fetch)"+            , "rad\trad://gachi (push)"+            , "all\tgit@github.com:NadiaYvette/gachi.git (push)"+            , "all\tgit@gitcode.com:NadiaYvette/gachi.git (push)"+            ]+          configured = parseGitRemoteOutput rawOutput+      length configured `shouldBe` 3+      let mGithub = lookup "github" [(crName r, r) | r <- configured]+      case mGithub of+        Just gh -> do+          crFetchUrl gh `shouldBe` Just "git@github.com:NadiaYvette/gachi.git"+          crPushUrls gh `shouldBe` ["git@github.com:NadiaYvette/gachi.git"]+        Nothing -> expectationFailure "Expected 'github' remote parsed"++      let mAll = lookup "all" [(crName r, r) | r <- configured]+      case mAll of+        Just allR -> do+          crFetchUrl allR `shouldBe` Nothing+          length (crPushUrls allR) `shouldBe` 2+        Nothing -> expectationFailure "Expected 'all' remote parsed"++    it "evaluates topology reports detecting clean alignment, missing remotes, drift, and prohibited providers" $ do+      let specs = defaultRemoteSpecsFor "gachi"++      -- 1. Empty configuration -> 5 missing remotes+      let reportEmpty = evaluateRemoteTopology "gachi" specs []+      reportCompliant reportEmpty `shouldBe` False+      remoteMissingCount reportEmpty `shouldBe` 5+      remoteDriftCount reportEmpty `shouldBe` 0+      remoteNonCompliantCount reportEmpty `shouldBe` 0++      -- 2. Fully aligned configuration -> compliant+      let alignedConfig =+            [ ConfiguredRemote "rad" (Just "rad://gachi") ["rad://gachi"]+            , ConfiguredRemote "github" (Just "git@github.com:NadiaYvette/gachi.git") ["git@github.com:NadiaYvette/gachi.git"]+            , ConfiguredRemote "gitcode" (Just "git@gitcode.com:NadiaYvette/gachi.git") ["git@gitcode.com:NadiaYvette/gachi.git"]+            , ConfiguredRemote "framagit" (Just "git@framagit.org:NadiaYvette/gachi.git") ["git@framagit.org:NadiaYvette/gachi.git"]+            , ConfiguredRemote "disroot" (Just "git@git.disroot.org:NadiaYvette/gachi.git") ["git@git.disroot.org:NadiaYvette/gachi.git"]+            ]+          reportAligned = evaluateRemoteTopology "gachi" specs alignedConfig+      reportCompliant reportAligned `shouldBe` True+      remoteMissingCount reportAligned `shouldBe` 0++      -- 3. Drifted URL+      let driftedConfig =+            [ ConfiguredRemote "rad" (Just "rad://gachi") ["rad://gachi"]+            , ConfiguredRemote "github" (Just "git@github.com:OtherOrg/gachi.git") ["git@github.com:OtherOrg/gachi.git"]+            , ConfiguredRemote "gitcode" (Just "git@gitcode.com:NadiaYvette/gachi.git") ["git@gitcode.com:NadiaYvette/gachi.git"]+            , ConfiguredRemote "framagit" (Just "git@framagit.org:NadiaYvette/gachi.git") ["git@framagit.org:NadiaYvette/gachi.git"]+            , ConfiguredRemote "disroot" (Just "git@git.disroot.org:NadiaYvette/gachi.git") ["git@git.disroot.org:NadiaYvette/gachi.git"]+            ]+          reportDrift = evaluateRemoteTopology "gachi" specs driftedConfig+      reportCompliant reportDrift `shouldBe` False+      remoteDriftCount reportDrift `shouldBe` 1++      -- 4. Prohibited provider+      let prohibitedConfig =+            alignedConfig ++ [ConfiguredRemote "srht" (Just "git@git.sr.ht:~user/gachi") []]+          reportProhibited = evaluateRemoteTopology "gachi" specs prohibitedConfig+      reportCompliant reportProhibited `shouldBe` False+      remoteNonCompliantCount reportProhibited `shouldBe` 1++    it "computes deterministic RemoteSyncPlan and formats ASCII report tables" $ do+      let specs = defaultRemoteSpecsFor "gachi"+          mixedConfig =+            [ ConfiguredRemote "github" (Just "git@github.com:OtherOrg/gachi.git") []+            , ConfiguredRemote "srht" (Just "git@git.sr.ht:~user/gachi") []+            ]+          report = evaluateRemoteTopology "gachi" specs mixedConfig+          plan = computeRemoteSyncPlan report++      planAddCount plan `shouldBe` 4    -- rad, gitcode, framagit, disroot+      planUpdateCount plan `shouldBe` 1 -- github drift+      planRemoveCount plan `shouldBe` 1 -- srht prohibited++      let reportTable = formatRemoteReport report+      reportTable `shouldSatisfy` ("Gachi Multi-Remote Topology Audit" `T.isInfixOf`)+      reportTable `shouldSatisfy` ("ref:note-remote-topology" `T.isInfixOf`)++      let planTable = formatRemoteSyncPlan plan+      planTable `shouldSatisfy` ("[ADD REMOTE]" `T.isInfixOf`)+      planTable `shouldSatisfy` ("[UPDATE URL]" `T.isInfixOf`)+      planTable `shouldSatisfy` ("[REMOVE REMOTE]" `T.isInfixOf`)++    it "audits and executes synchronization in a live Git repository" $ do+      withGitTempDir $ \repoDir -> do+        let pName = T.pack (takeFileName repoDir)+            proj = defaultProject pName (Author "Nadia" "nadia@example.com")++        -- Initial audit on fresh repo has 5 missing remotes+        initialReport <- auditRemoteTopology repoDir proj+        remoteMissingCount initialReport `shouldBe` 5++        -- Dry run produces planned logs without altering git config+        let initialPlan = computeRemoteSyncPlan initialReport+        dryRes <- executeRemoteSyncPlan DryRun initialPlan+        case dryRes of+          Left err -> expectationFailure (T.unpack err)+          Right logs -> length logs `shouldBe` 5++        postDryRemotes <- queryGitRemotes repoDir+        postDryRemotes `shouldBe` Right []++        -- Apply synchronization to add all canonical remotes+        applyRes <- executeRemoteSyncPlan ApplySync initialPlan+        case applyRes of+          Left err -> expectationFailure (T.unpack err)+          Right logs -> length logs `shouldBe` 5++        -- Verify all remotes configured in Git+        configured <- queryGitRemotes repoDir+        case configured of+          Left err -> expectationFailure (T.unpack err)+          Right rms -> do+            length rms `shouldBe` 5+            map crName rms `shouldMatchList` ["rad", "github", "gitcode", "framagit", "disroot"]++        -- Secondary audit confirms full compliance+        finalReport <- auditRemoteTopology repoDir proj+        reportCompliant finalReport `shouldBe` True+        remoteMissingCount finalReport `shouldBe` 0+        remoteDriftCount finalReport `shouldBe` 0+        remoteNonCompliantCount finalReport `shouldBe` 0++  describe "Gachi.Sync.Sheaf (1-D Cellular Sheaf Coherence & Obstruction Localization)" $ do+    it "constructs canonical star complex and computes topological Euler characteristic" $ do+      let complex = buildCanonicalSheafComplex ["gachi", "polytropos"]+      numVertices complex `shouldBe` 3 -- canonical, gachi, polytropos+      numEdges complex `shouldBe` 20    -- 2 clients * 10 dimensions+      eulerCharacteristic complex `shouldBe` -17 -- 3 - 20 = -17++    it "evaluates coboundary delta^0 on harmonic cochain confirming 0-th cohomology H^0 equilibrium" $ do+      let complex = buildCanonicalSheafComplex ["gachi", "polytropos"]+          stalkGachi = canonicalRepoStalk { stalkRepoName = "gachi" }+          stalkPoly  = canonicalRepoStalk { stalkRepoName = "polytropos" }+          c0 = Cochain0 $ Map.fromList+            [ ("canonical", canonicalRepoStalk)+            , ("gachi", stalkGachi)+            , ("polytropos", stalkPoly)+            ]+          (Cochain1 diffs, isHarmonic) = coboundary complex defaultEcosystemSheaf c0++      isHarmonic `shouldBe` True+      Map.size diffs `shouldBe` 20+      all (uncurry (==)) (Map.elems diffs) `shouldBe` True++      let report = evaluateSheafCoherence complex c0+      reportIsHarmonic report `shouldBe` True+      reportHarmonicEdges report `shouldBe` 20+      reportDriftEdges report `shouldBe` 0+      reportCoherencePct report `shouldBe` 100.0+      reportObstructions report `shouldBe` []++    it "localizes 1-st cohomology H^1 boundary obstructions on drifting repository configuration" $ do+      let complex = buildCanonicalSheafComplex ["gongju"]+          driftStalk = canonicalRepoStalk+            { stalkRepoName        = "gongju"+            , stalkGhcVersion      = Just "9.12.1"  -- Drift: expected 9.14.1+            , stalkHasCommitMsg    = False          -- Drift: missing hook+            , stalkHasVerifyRunner = False          -- Drift: missing runner+            }+          c0 = Cochain0 $ Map.fromList+            [ ("canonical", canonicalRepoStalk)+            , ("gongju", driftStalk)+            ]+          report = evaluateSheafCoherence complex c0++      reportIsHarmonic report `shouldBe` False+      reportHarmonicEdges report `shouldBe` 7+      reportDriftEdges report `shouldBe` 3+      reportCoherencePct report `shouldBe` 70.0++      let obs = reportObstructions report+      length obs `shouldBe` 3+      let driftedDims = map (edgeDimension . obstructionEdge) obs+      driftedDims `shouldMatchList`+        [ ContractGhcVersion+        , ContractGitCommitHook+        , ContractVerifyRunner+        ]++    it "verifies restriction projections for all 10 contract dimensions" $ do+      let stalk = canonicalRepoStalk+      restrictRepoStalk ContractGhcVersion stalk `shouldBe` ContractText (Just "9.14.1")+      restrictRepoStalk ContractCabalVersion stalk `shouldBe` ContractText (Just "3.0")+      restrictRepoStalk ContractLicense stalk `shouldBe` ContractText (Just "EUPL-1.2")+      restrictRepoStalk ContractGitCommitHook stalk `shouldBe` ContractBool True+      restrictRepoStalk ContractGitPreCommitHook stalk `shouldBe` ContractBool True+      restrictRepoStalk ContractGitMessage stalk `shouldBe` ContractBool True+      restrictRepoStalk ContractHLintConfig stalk `shouldBe` ContractBool True+      restrictRepoStalk ContractCIWorkflow stalk `shouldBe` ContractBool True+      restrictRepoStalk ContractAgentsAdvisory stalk `shouldBe` ContractBool True+      restrictRepoStalk ContractVerifyRunner stalk `shouldBe` ContractBool True++    it "inspects live repository checkout and confirms harmonic coherence against canonical contracts" $ do+      stalk <- inspectRepoStalk "."+      stalkGhcVersion stalk `shouldBe` Just "9.14.1"+      stalkLicense stalk `shouldBe` Just "EUPL-1.2"+      stalkHasCommitMsg stalk `shouldBe` True+      stalkHasPreCommit stalk `shouldBe` True+      stalkHasGitMessage stalk `shouldBe` True+      stalkHasHLint stalk `shouldBe` True+      stalkHasCIWorkflow stalk `shouldBe` True+      stalkHasAgentsMd stalk `shouldBe` True+      stalkHasVerifyRunner stalk `shouldBe` True++      report <- auditSheafTarget "."+      reportIsHarmonic report `shouldBe` True+      reportHarmonicEdges report `shouldBe` 10+      reportDriftEdges report `shouldBe` 0+      reportCoherencePct report `shouldBe` 100.0++    it "formats aligned ASCII diagnostic reports for both harmonic and obstructed states" $ do+      let complex = buildCanonicalSheafComplex ["gongju"]+          driftStalk = canonicalRepoStalk+            { stalkRepoName   = "gongju"+            , stalkGhcVersion = Just "9.12.1"+            }+          c0 = Cochain0 $ Map.fromList+            [ ("canonical", canonicalRepoStalk)+            , ("gongju", driftStalk)+            ]+          report = evaluateSheafCoherence complex c0+          formatted = formatSheafReport report++      formatted `shouldSatisfy` ("Gachi Cellular Sheaf Coherence Report" `T.isInfixOf`)+      formatted `shouldSatisfy` ("0-th Cohomology H^0 (Harmonic Equilibrium)" `T.isInfixOf`)+      formatted `shouldSatisfy` ("1-st Cohomology H^1 (Boundary Obstructions)" `T.isInfixOf`)+      formatted `shouldSatisfy` ("Edge: canonical" `T.isInfixOf`)+      formatted `shouldSatisfy` ("Update cabal.project / .cabal to target GHC 9.14.1" `T.isInfixOf`)+      case reportObstructions report of+        (obs:_) -> obstructionMessage obs `shouldSatisfy` ("Boundary obstruction on edge" `T.isInfixOf`)+        []      -> expectationFailure "Expected at least one obstruction"