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 +80/−0
- src/Gachi/Sync.hs +122/−0
- src/Gachi/Sync/Audit.hs +389/−0
- src/Gachi/Sync/CI.hs +810/−0
- src/Gachi/Sync/Catalog.hs +355/−0
- src/Gachi/Sync/Engine.hs +643/−0
- src/Gachi/Sync/Hash.hs +298/−0
- src/Gachi/Sync/Matrix.hs +597/−0
- src/Gachi/Sync/Remote.hs +826/−0
- src/Gachi/Sync/Sheaf.hs +932/−0
- src/Gachi/Sync/Types.hs +430/−0
- test/Main.hs +772/−0
+ 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"