packages feed

gachi-schema-0.1.0.0: src/Gachi/Schema/Project.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

-- | Module: Gachi.Schema.Project
-- Description: Strongly-typed nominal records and open row representations for projects
--
-- Defines foundational schema types for declarative workspace and package configurations:
-- 'Author', 'PackageSpec', 'RemoteSpec', and 'Project'.
--
-- All types provide both nominal representations deriving 'FromDhall' / 'ToDhall' via
-- 'GenericDhall' and isomorphic open row representations ('ProjectRec', 'AuthorRec', etc.)
-- backed by Polytropos flat 'Record' storage.
--
-- = Architectural Overview & Rationale
--
-- In Gachi's infrastructure model, all repository scaffolding, configuration synthesis,
-- and project retrofitting operate on declarative, hermetic schemas encoded in the
-- Dhall configuration language. Rather than relying on fragile untyped string substitution
-- (such as Jinja, Mustache, or sed expressions), Gachi models repository metadata as
-- strongly-typed algebraic records.
--
-- == Duality: Nominal Records and Open Rows
--
-- This module implements a deliberate structural duality:
--
-- 1. __Nominal Representations ('Project', 'Author', etc.):__
--    Provide standard Haskell data types with 'Generic' deriving. These types interface
--    seamlessly with Dhall via 'Polytropos.Dhall.GenericDhall' through @DerivingVia@,
--    enabling automatic decoder ('FromDhall') and encoder ('ToDhall') synthesis.
--
-- 2. __Open Row Isomorphisms ('ProjectRec', 'AuthorRec', etc.):__
--    Provide isomorphic open record types backed by Polytropos contiguous
--    'Polytropos.Core.RowRep' storage. In the open row representation, field values reside in
--    contiguous 'Data.Primitive.SmallArray' slots, permitting dynamic field extension,
--    narrowing, and sub-row projection without intermediate data type declarations.
--
-- == Ecosystem Invariants & Licensing
--
-- All project skeletons generated or verified by Gachi adhere to core ecosystem policies:
--
-- * __Licensing Discipline:__ By default, all ecosystem assets are licensed under
--   the European Union Public Licence (EUPL-1.2) or Apache-2.0. The EUPL is legally
--   interoperable across 22 European languages and compatible with GPLv2/v3 while protecting
--   upstream authors against SaaS-loophole proprietary enclosure.
-- * __Toolchain Pinning:__ Projects explicitly pin GHC 9.14.1 and Cabal 3.18, enabling
--   reproducible, bit-identical build artifacts across heterogeneous development workstations
--   and automated CI matrices.
-- * __Modular Multi-Package DAGs:__ Large systems are decomposed into fine-grained packages
--   (e.g., @*-core@, @*-schema@, @*-cli@) with explicit module exposure lists, maximizing
--   compilation parallelism and enforcing clean architectural boundaries.
--
-- = When to Use This Module
--
-- * Reading, validating, or writing Dhall project manifests (@project.dhall@).
-- * Generating multi-package workspace structures in @gachi init@.
-- * Converting between nominal Haskell configurations and open row representations.
--
-- = Worked Example
--
-- >>> import Gachi.Schema.Project
-- >>> let auth = Author { name = "Nadia Yvette Chambers", email = "nadia.yvette.chambers@ik.me" }
-- >>> let proj = defaultProject "gongju" auth
-- >>> projectName proj
-- "gongju"
-- >>> projectLicense proj
-- "EUPL-1.2"
module Gachi.Schema.Project
  ( -- * Nominal Schema Data Types
    Author(..)
  , PackageSpec(..)
  , RemoteSpec(..)
  , Project(..)

    -- * Field Projection Aliases
  , authorName
  , authorEmail
  , packageName
  , packageSynopsis
  , packageExposes
  , remoteName
  , remoteUrl
  , projectName
  , projectDescription
  , projectLicense
  , projectAuthor
  , projectGhcVersion
  , projectCabalVersion
  , projectPackages
  , projectRemotes

    -- * Open Row Types
  , AuthorRow
  , AuthorRec
  , toAuthorRec
  , fromAuthorRec

  , PackageRow
  , PackageRec
  , toPackageRec
  , fromPackageRec

  , RemoteRow
  , RemoteRec
  , toRemoteRec
  , fromRemoteRec

  , ProjectRow
  , ProjectRec
  , toProjectRec
  , fromProjectRec

    -- * Defaults & Canonical Invariants
  , defaultProject
  , defaultPackage
  , canonicalLicense
  , canonicalGhcVersion
  , canonicalCabalVersion
  ) where

import Data.Text (Text)
import Data.Functor.Identity (Identity(..))
import Dhall (FromDhall, ToDhall)
import GHC.Generics (Generic)
import Polytropos
  ( Pair(..)
  , Record
  )
import Polytropos.Core
  ( RowRep(..)
  , fieldAt
  , noInlineUnsafeCo
  , productRep
  )
import Polytropos.Dhall (GenericDhall(..))

-- | Author and maintainer metadata for a project.
data Author = Author
  { name  :: !Text
    -- ^ Full personal or organizational author name.
  , email :: !Text
    -- ^ Contact email address.
  } deriving stock (Generic, Show, Eq)
    deriving (FromDhall, ToDhall) via (GenericDhall Author)

-- | Specification for an individual Cabal package within a multi-package workspace.
data PackageSpec = PackageSpec
  { name     :: !Text
    -- ^ Package name (e.g., @"gachi-core"@).
  , synopsis :: !Text
    -- ^ Short one-line synopsis of package purpose.
  , exposes  :: ![Text]
    -- ^ List of exposed top-level modules.
  } deriving stock (Generic, Show, Eq)
    deriving (FromDhall, ToDhall) via (GenericDhall PackageSpec)

-- | Remote Git repository configuration for multi-forge mirroring.
data RemoteSpec = RemoteSpec
  { name :: !Text
    -- ^ Remote identifier (e.g., @"github"@, @"gitlab"@, @"radicle"@).
  , url  :: !Text
    -- ^ Canonical fetch/push URL.
  } deriving stock (Generic, Show, Eq)
    deriving (FromDhall, ToDhall) via (GenericDhall RemoteSpec)

-- | Comprehensive declarative specification for a project workspace.
--
-- See Note [Dhall Parameterized Skeleton Templating] [ref:note-dhall-skeleton-templating].
data Project = Project
  { name         :: !Text
    -- ^ Canonical project name.
  , description  :: !Text
    -- ^ Human-readable project description and mission.
  , license      :: !Text
    -- ^ SPDX license identifier (defaults to @"EUPL-1.2"@).
  , author       :: !Author
    -- ^ Primary maintainer or author identity.
  , ghcVersion   :: !Text
    -- ^ Pinned GHC compiler release (e.g., @"9.14.1"@).
  , cabalVersion :: !Text
    -- ^ Pinned Cabal specification release (e.g., @"3.18"@).
  , packages     :: ![PackageSpec]
    -- ^ Modular package hierarchy constituting this workspace.
  , remotes      :: ![RemoteSpec]
    -- ^ Multi-forge remotes configured for replication.
  } deriving stock (Generic, Show, Eq)
    deriving (FromDhall, ToDhall) via (GenericDhall Project)

--------------------------------------------------------------------------------
-- Field Projection Aliases (disambiguating DuplicateRecordFields when needed)
--------------------------------------------------------------------------------

-- | Disambiguated projection selector for 'Author' name.
--
-- Time complexity: @O(1)@ direct pattern match.
authorName :: Author -> Text
authorName (Author n _) = n

-- | Disambiguated projection selector for 'Author' email.
--
-- Time complexity: @O(1)@ direct pattern match.
authorEmail :: Author -> Text
authorEmail (Author _ e) = e

-- | Disambiguated projection selector for 'PackageSpec' name.
--
-- Time complexity: @O(1)@ direct pattern match.
packageName :: PackageSpec -> Text
packageName (PackageSpec n _ _) = n

-- | Disambiguated projection selector for 'PackageSpec' synopsis.
--
-- Time complexity: @O(1)@ direct pattern match.
packageSynopsis :: PackageSpec -> Text
packageSynopsis (PackageSpec _ s _) = s

-- | Disambiguated projection selector for 'PackageSpec' exposed modules.
--
-- Time complexity: @O(1)@ direct pattern match.
packageExposes :: PackageSpec -> [Text]
packageExposes (PackageSpec _ _ ex) = ex

-- | Disambiguated projection selector for 'RemoteSpec' name.
--
-- Time complexity: @O(1)@ direct pattern match.
remoteName :: RemoteSpec -> Text
remoteName (RemoteSpec n _) = n

-- | Disambiguated projection selector for 'RemoteSpec' URL.
--
-- Time complexity: @O(1)@ direct pattern match.
remoteUrl :: RemoteSpec -> Text
remoteUrl (RemoteSpec _ u) = u

-- | Disambiguated projection selector for 'Project' name.
--
-- Time complexity: @O(1)@ direct pattern match.
projectName :: Project -> Text
projectName (Project n _ _ _ _ _ _ _) = n

-- | Disambiguated projection selector for 'Project' description.
--
-- Time complexity: @O(1)@ direct pattern match.
projectDescription :: Project -> Text
projectDescription (Project _ d _ _ _ _ _ _) = d

-- | Disambiguated projection selector for 'Project' license.
--
-- Time complexity: @O(1)@ direct pattern match.
projectLicense :: Project -> Text
projectLicense (Project _ _ l _ _ _ _ _) = l

-- | Disambiguated projection selector for 'Project' author.
--
-- Time complexity: @O(1)@ direct pattern match.
projectAuthor :: Project -> Author
projectAuthor (Project _ _ _ a _ _ _ _) = a

-- | Disambiguated projection selector for 'Project' GHC version.
--
-- Time complexity: @O(1)@ direct pattern match.
projectGhcVersion :: Project -> Text
projectGhcVersion (Project _ _ _ _ g _ _ _) = g

-- | Disambiguated projection selector for 'Project' Cabal version.
--
-- Time complexity: @O(1)@ direct pattern match.
projectCabalVersion :: Project -> Text
projectCabalVersion (Project _ _ _ _ _ c _ _) = c

-- | Disambiguated projection selector for 'Project' packages.
--
-- Time complexity: @O(1)@ direct pattern match.
projectPackages :: Project -> [PackageSpec]
projectPackages (Project _ _ _ _ _ _ pkgs _) = pkgs

-- | Disambiguated projection selector for 'Project' remotes.
--
-- Time complexity: @O(1)@ direct pattern match.
projectRemotes :: Project -> [RemoteSpec]
projectRemotes (Project _ _ _ _ _ _ _ rms) = rms

--------------------------------------------------------------------------------
-- Open Row Representations (Polytropos Flat Memory Records)
--------------------------------------------------------------------------------

-- | Heterogeneous type-level row for 'Author'.
type AuthorRow = '["name" ':= Text, "email" ':= Text]

-- | Open record type for 'Author'.
type AuthorRec = Record AuthorRow

-- | Convert nominal 'Author' into open 'AuthorRec'.
--
-- Packs fields into a contiguous 2-element 'Data.Primitive.SmallArray' in row order:
-- Slot 0 (@"name"@) and Slot 1 (@"email"@).
--
-- Time complexity: @O(1)@.
toAuthorRec :: Author -> AuthorRec
toAuthorRec (Author n e) =
  RowRep (productRep [Identity (noInlineUnsafeCo n), Identity (noInlineUnsafeCo e)])

-- | Convert open 'AuthorRec' into nominal 'Author'.
--
-- Dereferences contiguous array slots via 'fieldAt' with zero intermediate heap allocation.
--
-- Time complexity: @O(1)@.
fromAuthorRec :: AuthorRec -> Author
fromAuthorRec (RowRep rep) = Author
  { name  = let Identity n = fieldAt 0 rep in n
  , email = let Identity e = fieldAt 1 rep in e
  }

-- | Heterogeneous type-level row for 'PackageSpec'.
type PackageRow = '["name" ':= Text, "synopsis" ':= Text, "exposes" ':= [Text]]

-- | Open record type for 'PackageSpec'.
type PackageRec = Record PackageRow

-- | Convert nominal 'PackageSpec' into open 'PackageRec'.
--
-- Packs fields into a contiguous 3-element 'Data.Primitive.SmallArray' in row order:
-- Slot 0 (@"name"@), Slot 1 (@"synopsis"@), and Slot 2 (@"exposes"@).
--
-- Time complexity: @O(1)@.
toPackageRec :: PackageSpec -> PackageRec
toPackageRec (PackageSpec n s ex) =
  RowRep (productRep [Identity (noInlineUnsafeCo n), Identity (noInlineUnsafeCo s), Identity (noInlineUnsafeCo ex)])

-- | Convert open 'PackageRec' into nominal 'PackageSpec'.
--
-- Dereferences contiguous array slots via 'fieldAt' with zero intermediate heap allocation.
--
-- Time complexity: @O(1)@.
fromPackageRec :: PackageRec -> PackageSpec
fromPackageRec (RowRep rep) = PackageSpec
  { name     = let Identity n  = fieldAt 0 rep in n
  , synopsis = let Identity s  = fieldAt 1 rep in s
  , exposes  = let Identity ex = fieldAt 2 rep in ex
  }

-- | Heterogeneous type-level row for 'RemoteSpec'.
type RemoteRow = '["name" ':= Text, "url" ':= Text]

-- | Open record type for 'RemoteSpec'.
type RemoteRec = Record RemoteRow

-- | Convert nominal 'RemoteSpec' into open 'RemoteRec'.
--
-- Packs fields into a contiguous 2-element 'Data.Primitive.SmallArray' in row order:
-- Slot 0 (@"name"@) and Slot 1 (@"url"@).
--
-- Time complexity: @O(1)@.
toRemoteRec :: RemoteSpec -> RemoteRec
toRemoteRec (RemoteSpec n u) =
  RowRep (productRep [Identity (noInlineUnsafeCo n), Identity (noInlineUnsafeCo u)])

-- | Convert open 'RemoteRec' into nominal 'RemoteSpec'.
--
-- Dereferences contiguous array slots via 'fieldAt' with zero intermediate heap allocation.
--
-- Time complexity: @O(1)@.
fromRemoteRec :: RemoteRec -> RemoteSpec
fromRemoteRec (RowRep rep) = RemoteSpec
  { name = let Identity n = fieldAt 0 rep in n
  , url  = let Identity u = fieldAt 1 rep in u
  }

-- | Heterogeneous type-level row for 'Project'.
type ProjectRow =
  '[ "name" ':= Text
   , "description" ':= Text
   , "license" ':= Text
   , "author" ':= Author
   , "ghcVersion" ':= Text
   , "cabalVersion" ':= Text
   , "packages" ':= [PackageSpec]
   , "remotes" ':= [RemoteSpec]
   ]

-- | Open record type for 'Project'.
type ProjectRec = Record ProjectRow

-- | Convert nominal 'Project' into open 'ProjectRec'.
--
-- Packs all 8 project fields into a single contiguous 'Data.Primitive.SmallArray' in row order:
-- Slot 0 (@"name"@), Slot 1 (@"description"@), Slot 2 (@"license"@), Slot 3 (@"author"@),
-- Slot 4 (@"ghcVersion"@), Slot 5 (@"cabalVersion"@), Slot 6 (@"packages"@), and Slot 7 (@"remotes"@).
--
-- Time complexity: @O(1)@.
toProjectRec :: Project -> ProjectRec
toProjectRec (Project n d l a g c pkgs rms) =
  RowRep (productRep
    [ Identity (noInlineUnsafeCo n)
    , Identity (noInlineUnsafeCo d)
    , Identity (noInlineUnsafeCo l)
    , Identity (noInlineUnsafeCo a)
    , Identity (noInlineUnsafeCo g)
    , Identity (noInlineUnsafeCo c)
    , Identity (noInlineUnsafeCo pkgs)
    , Identity (noInlineUnsafeCo rms)
    ])

-- | Convert open 'ProjectRec' into nominal 'Project'.
--
-- Unpacks the flat contiguous array into the nominal 'Project' record in $O(1)$ time
-- with zero intermediate tree allocations.
--
-- Time complexity: @O(1)@.
fromProjectRec :: ProjectRec -> Project
fromProjectRec (RowRep rep) = Project
  { name         = let Identity n    = fieldAt 0 rep in n
  , description  = let Identity d    = fieldAt 1 rep in d
  , license      = let Identity l    = fieldAt 2 rep in l
  , author       = let Identity a    = fieldAt 3 rep in a
  , ghcVersion   = let Identity g    = fieldAt 4 rep in g
  , cabalVersion = let Identity c    = fieldAt 5 rep in c
  , packages     = let Identity pkgs = fieldAt 6 rep in pkgs
  , remotes      = let Identity rms  = fieldAt 7 rep in rms
  }

--------------------------------------------------------------------------------
-- Canonical Ecosystem Standards & Turnkey Defaults
--------------------------------------------------------------------------------

-- | Canonical default license per ecosystem policy: EUPL-1.2.
canonicalLicense :: Text
canonicalLicense = "EUPL-1.2"

-- | Canonical default GHC version: 9.14.1.
canonicalGhcVersion :: Text
canonicalGhcVersion = "9.14.1"

-- | Canonical default Cabal specification version: 3.18.
canonicalCabalVersion :: Text
canonicalCabalVersion = "3.18"

-- | Construct a default 'PackageSpec' with canonical conventions.
defaultPackage :: Text -> PackageSpec
defaultPackage pkgName = PackageSpec
  { name     = pkgName
  , synopsis = "Foundational package for " <> pkgName
  , exposes  = []
  }

-- | Construct a turnkey 'Project' with canonical defaults.
--
-- Automatically configures EUPL-1.2 license, GHC 9.14.1, Cabal 3.18, and a primary package.
defaultProject :: Text -> Author -> Project
defaultProject projName auth = Project
  { name         = projName
  , description  = "Ecosystem workspace for " <> projName
  , license      = canonicalLicense
  , author       = auth
  , ghcVersion   = canonicalGhcVersion
  , cabalVersion = canonicalCabalVersion
  , packages     = [defaultPackage projName]
  , remotes      = []
  }