diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog for `recollections`
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to the
+[Haskell Package Versioning Policy](https://pvp.haskell.org/).
+
+## 0.1.0.0 - 2026-07-26
+
+Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2026 IC Rainbow
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1.  Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+
+2.  Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+
+3.  Neither the name of the copyright holder nor the names of its contributors
+    may be used to endorse or promote products derived from this software
+    without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+# recollections
+
+The splicing module needs these extensions and imports:
+
+```haskell
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DerivingVia #-}
+
+import GHC.Generics (Generically1(..))
+```
+
+Given an enumeration of things like
+
+```haskell
+data Things
+  = This
+  | That
+  | Something
+  | Else
+  | Entirely
+  deriving (Eq, Ord, Show, Enum, Bounded, Generic)
+```
+
+We can derive a record that will behave like a homogenous zip-list of static size:
+
+```haskell
+mkCollection ''Things
+
+-- data Collection a = Collection
+--   { this, that, something, else', entirely :: a
+--   }
+--   deriving stock (Eq, Show, Generic1, Functor, Foldable, Traversable)
+--   deriving Applicative via (Generically1 Collection)
+
+-- Fill the record with its indices
+mkIndices ''Things
+```
+
+## Kmettoverse-lite
+
+```haskell
+-- Distributive
+mkDistribute ''Things
+-- collect is 
+
+-- Representable
+mkIndex ''Things
+mkTabulate ''Things
+```
+
+## VS
+
+- Vanilla monomorphic records: don't have functor/foldable/traversable/applicative goodies.
+- Hand-rolling the functor: be my guest, hand-roll the field enum too, while you're at it.
+- `Map Things a`: All fields are mandatory, all keys are known -> lookups are total and O(1). The optional fields are recoverable, with `Collection (Maybe a)`.
+- `Things -> a`: The collection can be stored and loaded.
+
+## Gotchas
+
+All the constructors of the tag type must be nullary — anything else is a
+compile-time error, since a partial `Collection` would make `index` partial too.
+
+The generated names (`Collection`, `indices`, `distribute`, `index`,
+`tabulate`) are fixed, so a tag named e.g. `Index` will clash with them.
diff --git a/recollections.cabal b/recollections.cabal
new file mode 100644
--- /dev/null
+++ b/recollections.cabal
@@ -0,0 +1,68 @@
+cabal-version: 2.2
+
+-- This file has been generated from package.yaml by hpack version 0.39.6.
+--
+-- see: https://github.com/sol/hpack
+
+name:           recollections
+version:        0.1.0.0
+synopsis:       Fixed-size representable (Zippy Applicative) collections.
+category:       Data Structures
+author:         IC Rainbow
+maintainer:     aenor.realm@gmail.com
+copyright:      2026 IC Rainbow
+license:        BSD-3-Clause
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://gitlab.com/dpwiz/recollections
+
+library
+  exposed-modules:
+      Data.Recollections.TH
+  other-modules:
+      Paths_recollections
+  autogen-modules:
+      Paths_recollections
+  hs-source-dirs:
+      src
+  default-extensions:
+      StrictData
+      LambdaCase
+      ApplicativeDo
+      BlockArguments
+      DerivingStrategies
+      DerivingVia
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
+  build-depends:
+      base >=4.18 && <5
+    , template-haskell >=2.20
+  default-language: GHC2021
+
+test-suite recollections-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_recollections
+  autogen-modules:
+      Paths_recollections
+  hs-source-dirs:
+      test
+  default-extensions:
+      StrictData
+      LambdaCase
+      ApplicativeDo
+      BlockArguments
+      DerivingStrategies
+      DerivingVia
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.18 && <5
+    , recollections
+    , template-haskell >=2.20
+  default-language: GHC2021
diff --git a/src/Data/Recollections/TH.hs b/src/Data/Recollections/TH.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Recollections/TH.hs
@@ -0,0 +1,211 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
+
+module Data.Recollections.TH
+  ( mkCollection
+  , mkIndices
+
+  , mkDistribute
+
+  , mkIndex
+  , mkTabulate
+  ) where
+
+import Data.Char
+import Data.Foldable
+import Data.Traversable
+import GHC.Generics (Generic, Generic1, Generically1)
+import Language.Haskell.TH
+
+mkCollection :: Name -> Q [Dec]
+mkCollection tags = do
+  let nothingsBanger = bang noSourceUnpackedness noSourceStrictness
+  let collectionName = mkName "Collection"
+
+  names <- tagNames tags
+  let
+    fields = do
+      name <- names
+      pure $ varBangType (mkName $ fieldNameOf name) $ (bangType nothingsBanger) (varT (mkName "a"))
+  let constr = recC collectionName fields
+  let
+    derivs =
+      [ derivClause (Just StockStrategy)
+          [ conT ''Show
+          , conT ''Eq
+          , conT ''Generic
+          , conT ''Generic1
+          , conT ''Functor
+          , conT ''Foldable
+          , conT ''Traversable
+          ]
+      , derivClause (Just $ ViaStrategy $ ConT ''Generically1 `AppT` ConT collectionName)
+        [ conT ''Applicative
+        ]
+      ]
+  pure <$> dataD mempty collectionName [collectionTyVar] Nothing [constr] derivs
+
+-- | The @a@ binder of @data Collection a@.
+--
+-- @template-haskell-2.21@ (GHC 9.8) changed the binder flag of 'dataD'
+-- from @()@ to 'BndrVis'.
+#if MIN_VERSION_template_haskell(2,21,0)
+collectionTyVar :: TyVarBndrVis
+collectionTyVar = PlainTV (mkName "a") BndrReq
+#else
+collectionTyVar :: TyVarBndrUnit
+collectionTyVar = PlainTV (mkName "a") ()
+#endif
+
+mkIndices :: Name -> Q [Dec]
+mkIndices tags = do
+  let collectionName = mkName "Collection"
+  let indicesName = mkName "indices"
+  names <- tagNames tags
+  sig <- sigD indicesName $ conT collectionName `appT` conT tags
+  let body = foldl' appE (conE collectionName) (map (conE . mkName) names)
+  fun <- funD indicesName [ clause [] (normalB body) [] ]
+  pure [sig, fun]
+
+-- * Distributive
+
+{- |
+@
+distribute f = Collection
+  { this      = this      <$> f
+  , that      = that      <$> f
+  , something = something <$> f
+  , else'     = else'     <$> f
+  , entirely  = entirely  <$> f
+  }
+@
+-}
+mkDistribute :: Name -> Q [Dec]
+mkDistribute tags = do
+  let collectionName = mkName "Collection"
+  let distributeName = mkName "distribute"
+  names <- tagNames tags
+  let f = mkName "f"
+  let a = mkName "a"
+  {- The binder must not shadow any field selector the body refers to by
+     'mkName' (those are resolved lexically, by occurrence name). Field names
+     always start with a lowercase letter, so a leading underscore is safe;
+     plain @f@ would break for a tag named @F@. -}
+  arg <- newName "_f"
+  sig <- sigD distributeName $
+    forallT [] (cxt [conT ''Functor `appT` varT f]) $
+      appT (appT arrowT (varT f `appT` (conT collectionName `appT` (varT a)))) $
+        (conT collectionName `appT` (varT f `appT` varT a))
+  let
+    body =
+      foldl' appE (conE collectionName) do
+        name <- names
+        let fieldSelector = varE . mkName $ fieldNameOf name
+        pure $ appE (varE 'fmap) fieldSelector `appE` varE arg
+  fun <- funD distributeName
+    [ clause [varP arg] (normalB body) []
+    ]
+  inl <- inlineP distributeName
+  pure [sig, fun, inl]
+
+-- * Representable
+
+{- |
+@
+index c = \case This -> this c; That -> that c; …
+@
+-}
+mkIndex :: Name -> Q [Dec]
+mkIndex tags = do
+  let collectionName = mkName "Collection"
+  let indexName = mkName "index"
+  names <- tagNames tags
+  let a = mkName "a"
+  sig <- sigD indexName $
+    appT (appT arrowT (conT collectionName `appT` varT a)) $
+      appT (appT arrowT (conT tags)) (varT a)
+  bounds <- for names \name ->
+    (,) name <$> newName (fieldNameOf name)
+  let
+    body = lamCaseE do
+      (name, bound) <- bounds
+      pure $ match (conP (mkName name) []) (normalB $ varE bound) []
+
+  fun <- funD indexName
+    [ clause
+        [ conP collectionName $ map (varP . snd) bounds
+        ]
+        (normalB body)
+        []
+    ]
+  inl <- inlineP indexName
+  pure [sig, fun, inl]
+
+{- |
+@
+tabulate k = Collection (k This) (k That) (k Something) (k Else) (k Entirely)
+@
+-}
+mkTabulate :: Name -> Q [Dec]
+mkTabulate tags = do
+  let collectionName = mkName "Collection"
+  let tabulateName = mkName "tabulate"
+  names <- tagNames tags
+  let a = mkName "a"
+  sig <- sigD tabulateName $
+    appT (appT arrowT (appT (appT arrowT (conT tags)) (varT a))) $
+      conT collectionName `appT` varT a
+  f <- newName "_f"
+  let
+    body =
+      foldl' appE (conE collectionName) do
+        name <- names
+        pure $ appE (varE f) $ conE (mkName name)
+  fun <- funD tabulateName
+    [ clause [varP f] (normalB body) []
+    ]
+  inl <- inlineP tabulateName
+  pure [sig, fun, inl]
+
+-- * Utils
+
+tagNames :: Name -> Q [String]
+tagNames tags =
+ reify tags >>= \case
+    TyConI (DataD _ _ _ _ constructors _) ->
+      foldrM (flip extractTags) [] constructors
+    _ ->
+      fail "Expected a type constructor name"
+
+extractTags :: [String] -> Con -> Q [String]
+extractTags acc = \case
+  NormalC name [] ->
+    pure $ nameBase name : acc
+  huh ->
+    -- Skipping would produce a 'Collection' with fewer fields than there are
+    -- tags, making the generated 'index' silently non-exhaustive.
+    fail $ "Expected a nullary constructor, got: " <> show huh
+
+-- | Lowercase the leading character and dodge reserved words.
+fieldNameOf :: String -> String
+fieldNameOf = \case
+  [] -> []
+  n : ame -> legalizeVariableName $ toLower n : ame
+
+legalizeVariableName :: String -> String
+legalizeVariableName name
+  | name `elem` illegal = name ++ "'"
+  | otherwise = name
+
+-- | Haskell 2010 reserved words, which cannot be used as record field names.
+illegal :: [String]
+illegal =
+  [ "case", "class", "data", "default", "deriving", "do"
+  , "else", "foreign", "if", "import", "in"
+  , "infix", "infixl", "infixr", "instance"
+  , "let", "module", "newtype", "of"
+  , "then", "type", "where", "_"
+  ]
+
+inlineP :: Name -> Q Dec
+inlineP name = pragInlD name Inline FunLike AllPhases
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_GHC -ddump-splices #-}
+
+module Main (main) where
+
+import Data.Recollections.TH
+import GHC.Generics
+
+data Things
+  = This
+  | That
+  | Something
+  | Else
+  | Entirely
+  deriving (Eq, Ord, Show, Enum, Bounded, Generic)
+
+mkCollection ''Things
+mkIndices ''Things
+mkDistribute ''Things
+mkIndex ''Things
+mkTabulate ''Things
+
+-- data Collection a = Collection
+--   { this, that, something, else', entirely :: a
+--   }
+--   deriving stock (Eq, Show, Generic1, Functor, Foldable, Traversable)
+--   deriving Applicative via (Generically1 Collection)
+
+main :: IO ()
+main = do
+  putStrLn $ "Applicative: " <>  show (pure @Collection ())
+  putStrLn ""
+  putStrLn $ "indices: " <>  show indices
+  putStrLn ""
+  putStrLn $ "indices, numeric: " <>  show (fmap fromEnum indices)
+  putStrLn ""
+  putStrLn $ "Representable.index: " <>  show ([ (t, t == index indices t) | t <- [minBound .. maxBound] ])
+  putStrLn ""
+  putStrLn $ "Representable.tabulate show: " <>  show (tabulate show)
+  putStrLn ""
+  putStrLn $ "Ditributive.distribute: " <>  show (distribute [indices])
+  putStrLn ""
+  putStrLn $ "Ditributive.collect: " <>  show (distribute . fmap (\thing -> pure $ thing == This) $ indices)
