diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -38,15 +38,15 @@
 source file. It produces its result and the files it made, each with the step
 that made it.
 
-* `step name outputs x` names the `FilePather` `x`, which makes the files
+* `track name outputs x` names the `FilePather` `x`, which makes the files
   `outputs` gives for its input. Before `x` runs, the step is checked against
-  the trail: a step whose name is already in the trail (`RepeatsStep`), or
-  that would make a file already in the trail (`ReproducesInput`), is
+  the trail: a step whose name is already in the trail (`CycleRepeatsStep`),
+  or that would make a file already in the trail (`CycleReproducesInput`), is
   stopped with a `Cycle`, and `x` does not run.
-* `step' name x` names a `FilePather` that returns the files it made, for a
+* `track' name x` names a `FilePather` that returns the files it made, for a
   step whose outputs are known only after it runs. It is checked after it
   runs.
-* `stepWith Repeatable` allows a step to appear more than once in a trail.
+* `trackWith Repeatable` allows a step to appear more than once in a trail.
 * `whenPath`, `whenTrail`, `onPath`, `askTrail` and `untracked` choose
   files, and run `FilePather` values that make no files.
 * `displayCycle` describes the chain of a `Cycle`:
@@ -64,6 +64,27 @@
 `MonadWriter` and `Contravariant` instances. `<>` runs both steps and keeps
 the outputs of both; `<|>` and `catchError` alternate in `f`; and a `Cycle`
 is never caught or discarded by any of them.
+
+## Optics
+
+Every data type has a name for each of its fields and constructors: a class
+whose method is an optic, of the kind that each data type gives it (the
+classy optics recipe of
+[claude-haskell](https://github.com/sirius-beta/claude-haskell)). Each name
+is in its own module under `System.FilePather.Name`, and each data type also
+has its own name (`step`, `trail`, `cycle`, `filePather`, `tracked`). A data
+type that holds another inherits its names, so `stepName` reaches the name of
+the step in a `Step`, a `ReproducesInput`, or a `Cycle`:
+
+```haskell
+view (cloneLens stepName) (Step "pdf page 1" "a.pdf")        -- a lens
+preview stepName (CycleRepeatsStep (RepeatsStep s t))         -- an affine traversal
+view (cloneLens pathFunction) (asks length :: FilePather' Int) "notes.md"
+```
+
+Read one value with `view (cloneLens n)`, at most one with `preview n`, and
+set with `set` and `over`. `System.FilePather` exports `cycle`, the name of
+`Cycle`, so hide `Prelude.cycle` where both are used.
 
 ## Development
 
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,42 @@
 # Changelog
 
+## 0.8.0.0
+
+* Replace the optics of every data type with the classy optics recipe of
+  claude-haskell: each data type has a class, `TLike`, whose method is its
+  own name (`step`, `trail`, `repetition`, `cycle`, `filePather`,
+  `tracked`), each field and constructor has a name, a class in its own
+  module under System.FilePather.Name (`stepName`, `stepInput`, `steps`,
+  `once`, `repeatable`, `file`, `pathFunction`), and each data type has a
+  manifest (`lawNamesT`), a synonym and function of inheritance (`InheritT`,
+  `inheritT`) and, where it holds another data type, a check of inheritance
+  (`inheritsT`)
+* Remove the GetX, HasX, ReviewX and AsX classes, the Field and Injection
+  instances of Step, Repetition and Cycle, and the Wrapped and Rewrapped
+  instances of FilePather, Tracked and Trail; read the function of a
+  FilePather with `view (cloneLens pathFunction)`
+* Remove the lenses `cycleStep` and `cycleTrail`, and the prisms `_Once`,
+  `_Repeatable`, `_ReproducesInput` and `_RepeatsStep`: use `step`, `trail`
+  (affine traversals of a Cycle), `once`, `repeatable`, `reproducesInput` and
+  `repeatsStep`
+* Split System.FilePather.Trail into System.FilePather.Step,
+  System.FilePather.Trail, System.FilePather.Repetition,
+  System.FilePather.ReproducesInput, System.FilePather.RepeatsStep and
+  System.FilePather.Cycle (checkStep and displayCycle)
+* The constructors of Cycle each hold a product type of their own:
+  `CycleReproducesInput (ReproducesInput file step trail)` and
+  `CycleRepeatsStep (RepeatsStep step trail)`
+* Rename step, stepWith, step' and stepWith' to track, trackWith, track' and
+  trackWith', because `step` is the name of Step
+* System.FilePather exports the name `cycle`, which has the same name as
+  Prelude.cycle
+* Remove the dependency on either-n
+* Build with -Werror=incomplete-patterns, and without
+  -Wredundant-constraints
+* Add hedgehog tests of the classy optics: generators in the shape of each
+  manifest, the coverage of each sum, the manifest laws, and the laws of the
+  kind of every instance of every name
+
 ## 0.7.0.0
 
 * Start again, replacing filepather 0.6, in the document-build repository
diff --git a/filepather.cabal b/filepather.cabal
--- a/filepather.cabal
+++ b/filepather.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.4
 name:               filepather
-version:            0.7.0.0
+version:            0.8.0.0
 license:            BSD-3-Clause
 license-file:       LICENCE
 author:             Tony Morris <ʇǝu˙sıɹɹoɯʇ@ןןǝʞsɐɥ> <dibblego>
@@ -55,7 +55,6 @@
                     , deepseq           >= 1.4.8.1   && < 2
                     , directory         >= 1.3.8.5   && < 2
                     , distributive      >= 0.6.2.1   && < 1
-                    , either-n          >= 0.1       && < 0.2
                     , exitcode          >= 0.3.0.1   && < 0.4
                     , filepath          >= 1.4.301.0 && < 1.6
                     , lens              >= 5.2.1     && < 6
@@ -68,9 +67,8 @@
   ghc-options:
                     -Wall
                     -Wcompat
-                    -Wincomplete-record-updates
                     -Wincomplete-uni-patterns
-                    -Wredundant-constraints
+                    -Werror=incomplete-patterns
 
   hs-source-dirs:
                     src
@@ -78,7 +76,19 @@
   exposed-modules:
                     System.FilePather
                     System.FilePather.Combinators
+                    System.FilePather.Cycle
                     System.FilePather.FilePather
+                    System.FilePather.Name.File
+                    System.FilePather.Name.Once
+                    System.FilePather.Name.PathFunction
+                    System.FilePather.Name.Repeatable
+                    System.FilePather.Name.StepInput
+                    System.FilePather.Name.StepName
+                    System.FilePather.Name.Steps
+                    System.FilePather.RepeatsStep
+                    System.FilePather.Repetition
+                    System.FilePather.ReproducesInput
+                    System.FilePather.Step
                     System.FilePather.Trail
                     System.FilePather.Tracked
 
@@ -93,6 +103,7 @@
                     FilePatherTests
                     Gens
                     Laws
+                    OpticsTests
                     TrackedTests
 
   default-language:
diff --git a/src/System/FilePather.hs b/src/System/FilePather.hs
--- a/src/System/FilePather.hs
+++ b/src/System/FilePather.hs
@@ -5,18 +5,51 @@
 
 See "System.FilePather.FilePather" for the 'FilePather' type,
 "System.FilePather.Combinators" for combining them,
-"System.FilePather.Tracked" for 'Tracked' build steps, and
-"System.FilePather.Trail" for the trails of built files and the cycles that
-stop a chain of build steps.
+"System.FilePather.Tracked" for 'Tracked' build steps,
+"System.FilePather.Trail" for the trails of built files, and
+"System.FilePather.Cycle" for the cycles that stop a chain of build steps.
+
+Every data type has a name for each of its fields and constructors: a class
+whose method is an optic (see the modules under "System.FilePather.Name").
+Read a field with @'Control.Lens.view' ('Control.Lens.cloneLens' n)@, and
+a constructor with 'Control.Lens.preview' and 'Control.Lens.review'.
+
+This module also exports the class method 'cycle', the name of 'Cycle', which
+has the same name as 'Prelude.cycle'.
 -}
 module System.FilePather (
   module System.FilePather.Combinators,
+  module System.FilePather.Cycle,
   module System.FilePather.FilePather,
+  module System.FilePather.Name.File,
+  module System.FilePather.Name.Once,
+  module System.FilePather.Name.PathFunction,
+  module System.FilePather.Name.Repeatable,
+  module System.FilePather.Name.StepInput,
+  module System.FilePather.Name.StepName,
+  module System.FilePather.Name.Steps,
+  module System.FilePather.RepeatsStep,
+  module System.FilePather.Repetition,
+  module System.FilePather.ReproducesInput,
+  module System.FilePather.Step,
   module System.FilePather.Trail,
   module System.FilePather.Tracked,
 ) where
 
 import System.FilePather.Combinators
+import System.FilePather.Cycle
 import System.FilePather.FilePather
+import System.FilePather.Name.File
+import System.FilePather.Name.Once
+import System.FilePather.Name.PathFunction
+import System.FilePather.Name.Repeatable
+import System.FilePather.Name.StepInput
+import System.FilePather.Name.StepName
+import System.FilePather.Name.Steps
+import System.FilePather.RepeatsStep
+import System.FilePather.Repetition
+import System.FilePather.ReproducesInput
+import System.FilePather.Step
 import System.FilePather.Tracked
 import System.FilePather.Trail
+import Prelude hiding (cycle)
diff --git a/src/System/FilePather/Combinators.hs b/src/System/FilePather/Combinators.hs
--- a/src/System/FilePather/Combinators.hs
+++ b/src/System/FilePather/Combinators.hs
@@ -36,7 +36,7 @@
 ) where
 
 import Control.Applicative (Alternative (empty))
-import Control.Lens (over, view, _Wrapped)
+import Control.Lens (cloneLens, over, view)
 import Control.Monad.IO.Class (MonadIO (liftIO))
 import Control.Monad.Reader (local)
 import Control.Monad.Trans.Class (lift)
@@ -46,16 +46,18 @@
 import System.Directory (doesDirectoryExist, listDirectory)
 import System.FilePath (equalFilePath, isExtensionOf, normalise, splitDirectories, (</>))
 import System.FilePather.FilePather (FilePather (..))
+import System.FilePather.Name.PathFunction (PathFunctionLike (..))
 import Witherable (wither)
 
 {- $setup
 >>> import Control.Applicative ((<|>))
->>> import Control.Lens (view, _Wrapped)
+>>> import Control.Lens (cloneLens, view)
 >>> import Control.Monad.Reader (ask, asks)
 >>> import Control.Monad.Trans.Maybe (runMaybeT)
 >>> import Data.Functor.Identity (Identity (..))
 >>> import System.FilePath (replaceExtension)
 >>> import System.FilePather.FilePather (FilePather (..))
+>>> import System.FilePather.Name.PathFunction (pathFunction)
 -}
 
 infixr 1 ~>
@@ -67,7 +69,7 @@
 This is composition in the Kleisli category of @f@, at 'FilePath'. It is
 associative, with 'Control.Monad.Reader.ask' as the identity.
 
->>> runIdentity (view _Wrapped (asks (`replaceExtension` "pdf") ~> asks (++ ".png")) "notes.md")
+>>> runIdentity (view (cloneLens pathFunction) (asks (`replaceExtension` "pdf") ~> asks (++ ".png")) "notes.md")
 "notes.pdf.png"
 
 @since 0.7.0.0
@@ -79,7 +81,7 @@
 
 {- | Run a 'FilePather', then run another on each file path that it produces.
 
->>> view _Wrapped (FilePather (\p -> [[p ++ "-0.png", p ++ "-1.png"]]) ~>> asks length) "a.pdf"
+>>> view (cloneLens pathFunction) (FilePather (\p -> [[p ++ "-0.png", p ++ "-1.png"]]) ~>> asks length) "a.pdf"
 [[11,11]]
 
 @since 0.7.0.0
@@ -92,27 +94,27 @@
 {- | Run a 'FilePather' on the given file path, instead of the one being
 operated on.
 
->>> runIdentity (view _Wrapped (onFile "other.md" ask) "notes.md")
+>>> runIdentity (view (cloneLens pathFunction) (onFile "other.md" ask) "notes.md")
 "other.md"
 
 @since 0.7.0.0
 -}
 onFile :: FilePath -> FilePather f a -> FilePather f a
 onFile p =
-  over _Wrapped (\g -> const (g p))
+  over pathFunction (\g -> const (g p))
 {-# INLINE onFile #-}
 
 {- | Change the type constructor of a 'FilePather' with a natural
 transformation.
 
->>> view _Wrapped (hoistFilePather (Just . runIdentity) ask) "notes.md"
+>>> view (cloneLens pathFunction) (hoistFilePather (Just . runIdentity) ask) "notes.md"
 Just "notes.md"
 
 @since 0.7.0.0
 -}
 hoistFilePather :: (forall x. f x -> g x) -> FilePather f a -> FilePather g a
 hoistFilePather nat =
-  over _Wrapped (nat .)
+  over pathFunction (nat .)
 {-# INLINE hoistFilePather #-}
 
 {- | A 'FilePather' that applies to some file paths and not others.
@@ -127,30 +129,30 @@
 
 {- | Apply a 'FilePather' only to file paths that satisfy a predicate.
 
->>> view _Wrapped (whenFile (hasExtension ["md"]) ask) "notes.md" :: Maybe FilePath
+>>> view (cloneLens pathFunction) (whenFile (hasExtension ["md"]) ask) "notes.md" :: Maybe FilePath
 Just "notes.md"
 
->>> view _Wrapped (whenFile (hasExtension ["md"]) ask) "notes.txt" :: Maybe FilePath
+>>> view (cloneLens pathFunction) (whenFile (hasExtension ["md"]) ask) "notes.txt" :: Maybe FilePath
 Nothing
 
 @since 0.7.0.0
 -}
 whenFile :: (Alternative f) => (FilePath -> Bool) -> FilePather f a -> FilePather f a
 whenFile p =
-  over _Wrapped (\g x -> bool empty (g x) (p x))
+  over pathFunction (\g x -> bool empty (g x) (p x))
 {-# INLINE whenFile #-}
 
 {- | A 'Rule' that applies a 'FilePather' to file paths that satisfy a
 predicate.
 
 >>> let r = rule (isFile "special.md") (pure "special") <|> rule (hasExtension ["md"]) (pure "markdown") :: Rule Identity String
->>> runIdentity (runMaybeT (view _Wrapped r "special.md"))
+>>> runIdentity (runMaybeT (view (cloneLens pathFunction) r "special.md"))
 Just "special"
 
->>> runIdentity (runMaybeT (view _Wrapped r "notes.md"))
+>>> runIdentity (runMaybeT (view (cloneLens pathFunction) r "notes.md"))
 Just "markdown"
 
->>> runIdentity (runMaybeT (view _Wrapped r "notes.txt"))
+>>> runIdentity (runMaybeT (view (cloneLens pathFunction) r "notes.txt"))
 Nothing
 
 @since 0.7.0.0
@@ -166,23 +168,23 @@
 skipped.
 
 >>> let files = FilePather (\d -> Identity [d ++ "/a.md", d ++ "/b.txt", d ++ "/c.md"])
->>> runIdentity (view _Wrapped (everyFile files (rule (hasExtension ["md"]) (asks length))) "src")
+>>> runIdentity (view (cloneLens pathFunction) (everyFile files (rule (hasExtension ["md"]) (asks length))) "src")
 [("src/a.md",8),("src/c.md",8)]
 
 @since 0.7.0.0
 -}
 everyFile :: (Monad f) => FilePather f [FilePath] -> Rule f a -> FilePather f [(FilePath, a)]
 everyFile files r =
-  files >>= lift . wither (\p -> runMaybeT (fmap (p,) (view _Wrapped r p)))
+  files >>= lift . wither (\p -> runMaybeT (fmap (p,) (view (cloneLens pathFunction) r p)))
 {-# INLINE everyFile #-}
 
 {- | Whether a file path has one of the given extensions. An extension may be
 given with or without the leading dot.
 
->>> hasExtension ["fodt", ".odt"] "src/cv.odt"
+>>> hasExtension ["fodt", ".odt"] "src/report.odt"
 True
 
->>> hasExtension ["fodt", "odt"] "src/cv.pdf"
+>>> hasExtension ["fodt", "odt"] "src/report.pdf"
 False
 
 @since 0.7.0.0
@@ -194,7 +196,7 @@
 
 {- | Whether two file paths are equal, after normalisation.
 
->>> isFile "src/cv.fodt" "./src//cv.fodt"
+>>> isFile "src/report.fodt" "./src//report.fodt"
 True
 
 @since 0.7.0.0
@@ -206,13 +208,13 @@
 
 {- | Whether a file path is within a directory, after normalisation.
 
->>> isWithin "src" "src/a/cv.fodt"
+>>> isWithin "src" "src/a/report.fodt"
 True
 
->>> isWithin "src/a" "src/b/cv.fodt"
+>>> isWithin "src/a" "src/b/report.fodt"
 False
 
->>> isWithin "." "src/cv.fodt"
+>>> isWithin "." "src/report.fodt"
 True
 
 @since 0.7.0.0
@@ -226,7 +228,7 @@
 {- | The files in a directory and its subdirectories, in sorted order. The
 file paths are the directory joined with the path of the file relative to it,
 then normalised, so that the files of @"."@ are relative paths, such as
-@"src/cv.fodt"@. Symbolic links are followed.
+@"src/report.fodt"@. Symbolic links are followed.
 
 @since 0.7.0.0
 -}
diff --git a/src/System/FilePather/Cycle.hs b/src/System/FilePather/Cycle.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Cycle.hs
@@ -0,0 +1,275 @@
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | Why a chain of build steps is stopped.
+
+A 'Cycle' is a sum: its constructors are named 'reproducesInput' and
+'repeatsStep', the default names of the types they hold, and it inherits the
+names of both. The names it reaches through both constructors ('step',
+'trail', 'stepName', 'stepInput' and 'steps') are affine traversals.
+-}
+module System.FilePather.Cycle (
+  -- * Type
+  Cycle (..),
+
+  -- * Cycles
+  checkStep,
+  displayCycle,
+
+  -- * Optics
+  CycleLike (..),
+  lawNamesCycle,
+  InheritCycle,
+  inheritCycle,
+  inheritsCycle,
+) where
+
+import Control.DeepSeq (NFData)
+import Control.Lens (Choice, Optic, cloneLens, each, prism', review, toListOf, view)
+import Data.Data (Data)
+import Data.Functor.Const (Const)
+import Data.List (find, intercalate)
+import Data.Proxy (Proxy (..))
+import GHC.Generics (Generic)
+import System.FilePath (normalise)
+import System.FilePather.Name.File (FileLike (..))
+import System.FilePather.Name.StepInput (StepInputLike (..))
+import System.FilePather.Name.StepName (StepNameLike (..))
+import System.FilePather.Name.Steps (StepsLike (..))
+import System.FilePather.RepeatsStep (RepeatsStep (..), RepeatsStepLike (..), inheritRepeatsStep)
+import System.FilePather.Repetition (Repetition (..))
+import System.FilePather.ReproducesInput (ReproducesInput (..), ReproducesInputLike (..), inheritReproducesInput)
+import System.FilePather.Step (Step (..), StepLike (..))
+import System.FilePather.Trail (Trail (..), TrailLike (..), trailFiles)
+import Prelude hiding (cycle)
+
+{- $setup
+>>> import Control.Lens (preview, review)
+-}
+
+{- | Why a chain of build steps is stopped, with the step that is stopped and
+the trail of its input file.
+
+>>> review repeatsStep (RepeatsStep (Step "pdf page 1" "a_page1.pdf") (Trail [])) :: Cycle
+CycleRepeatsStep (RepeatsStep (Step "pdf page 1" "a_page1.pdf") (Trail []))
+
+>>> preview step (CycleRepeatsStep (RepeatsStep (Step "pdf page 1" "a_page1.pdf") (Trail [])))
+Just (Step "pdf page 1" "a_page1.pdf")
+
+@since 0.7.0.0
+-}
+data Cycle
+  = -- | The step would make a file that is already in the chain: its input
+    -- file, or an input file of a step in the trail ('reproducesInput').
+    CycleReproducesInput ReproducesInput
+  | -- | The step is already in the trail ('repeatsStep').
+    CycleRepeatsStep RepeatsStep
+  deriving stock (Eq, Ord, Show, Generic, Data)
+  deriving anyclass (NFData)
+
+{- | The class of 'Cycle'.
+
+@since 0.8.0.0
+-}
+class CycleLike p f x x' where
+  cycle ::
+    Optic p f x x' Cycle Cycle
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ Cycle) => CycleLike p f Cycle x' where
+  cycle =
+    id
+  {-# INLINE cycle #-}
+
+{- | Prism: 'reproducesInput' is the default name of 'CycleReproducesInput'.
+
+@since 0.8.0.0
+-}
+instance (Choice p, Applicative f, x' ~ Cycle) => ReproducesInputLike p f Cycle x' where
+  reproducesInput =
+    prism' CycleReproducesInput (\case CycleReproducesInput r -> Just r; CycleRepeatsStep _ -> Nothing)
+  {-# INLINE reproducesInput #-}
+
+{- | Prism: 'repeatsStep' is the default name of 'CycleRepeatsStep'.
+
+@since 0.8.0.0
+-}
+instance (Choice p, Applicative f, x' ~ Cycle) => RepeatsStepLike p f Cycle x' where
+  repeatsStep =
+    prism' CycleRepeatsStep (\case CycleRepeatsStep r -> Just r; CycleReproducesInput _ -> Nothing)
+  {-# INLINE repeatsStep #-}
+
+{- | Affine traversal: through 'CycleReproducesInput', a prism, then
+'ReproducesInput''s 'file', a lens.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Applicative f, x' ~ Cycle) => FileLike p f Cycle x' where
+  file =
+    reproducesInput . file
+  {-# INLINE file #-}
+
+{- | Affine traversal: 'step' is reached through both constructors, once each.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Applicative f, x' ~ Cycle) => StepLike p f Cycle x' where
+  step f = \case
+    CycleReproducesInput r -> CycleReproducesInput <$> step f r
+    CycleRepeatsStep r -> CycleRepeatsStep <$> step f r
+  {-# INLINE step #-}
+
+{- | Affine traversal: 'trail' is reached through both constructors, once
+each.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Applicative f, x' ~ Cycle) => TrailLike p f Cycle x' where
+  trail f = \case
+    CycleReproducesInput r -> CycleReproducesInput <$> trail f r
+    CycleRepeatsStep r -> CycleRepeatsStep <$> trail f r
+  {-# INLINE trail #-}
+
+{- | Affine traversal: 'stepName' is reached through both constructors, once
+each.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Applicative f, x' ~ Cycle) => StepNameLike p f Cycle x' where
+  stepName f = \case
+    CycleReproducesInput r -> CycleReproducesInput <$> stepName f r
+    CycleRepeatsStep r -> CycleRepeatsStep <$> stepName f r
+  {-# INLINE stepName #-}
+
+{- | Affine traversal: 'stepInput' is reached through both constructors, once
+each.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Applicative f, x' ~ Cycle) => StepInputLike p f Cycle x' where
+  stepInput f = \case
+    CycleReproducesInput r -> CycleReproducesInput <$> stepInput f r
+    CycleRepeatsStep r -> CycleRepeatsStep <$> stepInput f r
+  {-# INLINE stepInput #-}
+
+{- | Affine traversal: 'steps' is reached through both constructors, once
+each.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Applicative f, x' ~ Cycle) => StepsLike p f Cycle x' where
+  steps f = \case
+    CycleReproducesInput r -> CycleReproducesInput <$> steps f r
+    CycleRepeatsStep r -> CycleRepeatsStep <$> steps f r
+  {-# INLINE steps #-}
+
+{- | Rebuild a value using only the name of each constructor.
+
+Laws:
+
+> lawNamesCycle == id
+
+@since 0.8.0.0
+-}
+lawNamesCycle :: Cycle -> Cycle
+lawNamesCycle = \case
+  CycleReproducesInput r -> review reproducesInput r
+  CycleRepeatsStep r -> review repeatsStep r
+
+{- | The classes that a data type holding 'Cycle' inherits.
+
+@since 0.8.0.0
+-}
+type InheritCycle s =
+  ( CycleLike (->) (Const ()) s s
+  , ReproducesInputLike (->) (Const ()) s s
+  , RepeatsStepLike (->) (Const ()) s s
+  , FileLike (->) (Const ()) s s
+  , StepLike (->) (Const ()) s s
+  , TrailLike (->) (Const ()) s s
+  , StepNameLike (->) (Const ()) s s
+  , StepInputLike (->) (Const ()) s s
+  , StepsLike (->) (Const ()) s s
+  )
+
+{- | Check, where it is applied, that @s@ inherits every name of 'Cycle'.
+
+@since 0.8.0.0
+-}
+inheritCycle :: (InheritCycle s) => proxy s -> ()
+inheritCycle _ =
+  ()
+
+{- | 'Cycle' inherits every name of 'ReproducesInput' and of 'RepeatsStep'.
+
+@since 0.8.0.0
+-}
+inheritsCycle :: [()]
+inheritsCycle =
+  let p = Proxy :: Proxy Cycle
+   in [inheritReproducesInput p, inheritRepeatsStep p]
+
+{- | Check a step, with the files it makes, against the trail of its input
+file. A step that is already in the trail, and may appear 'Once', is a
+'CycleRepeatsStep'. Otherwise, a step that makes its input file, or the input
+file of a step in the trail, is a 'CycleReproducesInput'. File paths are
+compared after normalisation.
+
+>>> let t = Trail [Step "markdown to pdf" "src/a.md"]
+>>> checkStep Once (Step "pdf page 1" "public/a.pdf") ["public/a_page1.pdf"] t
+Right ()
+
+>>> checkStep Once (Step "markdown to pdf" "public/a.pdf") ["public/b.pdf"] t
+Left (CycleRepeatsStep (RepeatsStep (Step "markdown to pdf" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"])))
+
+>>> checkStep Repeatable (Step "markdown to pdf" "public/a.pdf") ["public/b.pdf"] t
+Right ()
+
+>>> checkStep Once (Step "pdf to markdown" "public/a.pdf") ["./src/a.md"] t
+Left (CycleReproducesInput (ReproducesInput "./src/a.md" (Step "pdf to markdown" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"])))
+
+@since 0.7.0.0
+-}
+checkStep :: Repetition -> Step -> [FilePath] -> Trail -> Either Cycle ()
+checkStep r s os t =
+  let repeated =
+        case r of
+          Once -> view (cloneLens stepName) s `elem` toListOf (each . stepName) t
+          Repeatable -> False
+      files = fmap normalise (view (cloneLens stepInput) s : toListOf trailFiles t)
+   in if repeated
+        then Left (review repeatsStep (RepeatsStep s t))
+        else maybe (Right ()) (\o -> Left (review reproducesInput (ReproducesInput o s t))) (find ((`elem` files) . normalise) os)
+
+{- | A description of a cycle, for people to read: the chain of files and
+steps from the source file.
+
+>>> putStrLn (displayCycle (CycleRepeatsStep (RepeatsStep (Step "pdf page 1" "public/a_page1.pdf") (Trail [Step "pdf page 1" "public/a.pdf", Step "markdown to pdf" "src/a.md"]))))
+src/a.md → markdown to pdf → public/a.pdf → pdf page 1 → public/a_page1.pdf → pdf page 1 again
+
+>>> putStrLn (displayCycle (CycleReproducesInput (ReproducesInput "src/a.md" (Step "pdf to markdown" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"]))))
+src/a.md → markdown to pdf → public/a.pdf → pdf to markdown → src/a.md, which is already in the chain
+
+@since 0.7.0.0
+-}
+displayCycle :: Cycle -> String
+displayCycle c =
+  let chain t = concatMap (\s -> [view (cloneLens stepInput) s, view (cloneLens stepName) s]) (reverse (view (cloneLens steps) t))
+      arrows = intercalate " → "
+   in case c of
+        CycleRepeatsStep r ->
+          arrows (chain (view (cloneLens trail) r) <> [view (cloneLens stepInput) r, view (cloneLens stepName) r <> " again"])
+        CycleReproducesInput r ->
+          arrows (chain (view (cloneLens trail) r) <> [view (cloneLens stepInput) r, view (cloneLens stepName) r, view (cloneLens file) r]) <> ", which is already in the chain"
diff --git a/src/System/FilePather/FilePather.hs b/src/System/FilePather/FilePather.hs
--- a/src/System/FilePather/FilePather.hs
+++ b/src/System/FilePather/FilePather.hs
@@ -1,5 +1,7 @@
+{-# LANGUAGE ConstraintKinds #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE FunctionalDependencies #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
@@ -24,7 +26,8 @@
 newtype FilePather f a = FilePather (FilePath -> f a)
 @
 
-A 'FilePather' is isomorphic to @ReaderT FilePath f a@ (see
+A 'FilePather' is a product with one field, named 'pathFunction'. It is
+isomorphic to @ReaderT FilePath f a@ (see
 'filePatherReaderT'), and has the same instances. In particular, when @f@ is a
 'Monad', so is @FilePather f@, and 'Control.Monad.Reader.ask' returns the file path being operated
 on.
@@ -45,13 +48,15 @@
   FilePatherExitcode,
   FilePatherExitcode',
 
-  -- * Optics
+  -- * Isomorphisms
   filePatherReaderT,
-  GetFilePather (..),
-  HasFilePather (..),
-  ReviewFilePather (..),
-  AsFilePather (..),
 
+  -- * Optics
+  FilePatherLike (..),
+  lawNamesFilePather,
+  InheritFilePather,
+  inheritFilePather,
+
   -- * Fusion workers
   mapFilePather,
   mapMaybeFilePather,
@@ -60,7 +65,7 @@
 import Control.Applicative (Alternative)
 import Control.DeepSeq (NFData)
 import Control.Exitcode (Exitcode)
-import Control.Lens (Getter, Iso, Lens', Prism', Review, Rewrapped, Wrapped (..), iso, lens, prism', review, unto, view, _Wrapped)
+import Control.Lens (Iso, Optic, cloneLens, iso, view)
 import Control.Monad (MonadPlus)
 import Control.Monad.Cont.Class (MonadCont)
 import Control.Monad.Error.Class (MonadError)
@@ -79,6 +84,7 @@
 import Data.Functor.Alt (Alt (..))
 import Data.Functor.Apply (Apply)
 import Data.Functor.Bind (Bind (..))
+import Data.Functor.Const (Const)
 import Data.Functor.Contravariant (Contravariant)
 import Data.Functor.Contravariant.Divisible (Decidable, Divisible)
 import Data.Functor.Identity (Identity)
@@ -87,10 +93,11 @@
 import Data.Kind (Type)
 import Data.List.NonEmpty (NonEmpty)
 import GHC.Generics (Generic, Generic1)
+import System.FilePather.Name.PathFunction (PathFunctionLike (..))
 import Witherable (Filterable (..))
 
 {- $setup
->>> import Control.Lens (view, _Wrapped)
+>>> import Control.Lens (cloneLens, view)
 >>> import Control.Monad.Reader (ask, asks)
 >>> import Data.Functor.Identity (Identity (..))
 >>> import System.FilePath (takeExtension)
@@ -105,13 +112,13 @@
 
 ==== __Examples__
 
->>> runIdentity (view _Wrapped (asks takeExtension :: FilePather' String) "notes.md")
+>>> runIdentity (view (cloneLens pathFunction) (asks takeExtension :: FilePather' String) "notes.md")
 ".md"
 
->>> view _Wrapped (FilePather (\p -> [p, p ++ ".bak"])) "notes.md"
+>>> view (cloneLens pathFunction) (FilePather (\p -> [p, p ++ ".bak"])) "notes.md"
 ["notes.md","notes.md.bak"]
 
->>> runIdentity (view _Wrapped ((,) <$> ask <*> asks length :: FilePather' (FilePath, Int)) "notes.md")
+>>> runIdentity (view (cloneLens pathFunction) ((,) <$> ask <*> asks length :: FilePather' (FilePath, Int)) "notes.md")
 ("notes.md",8)
 
 @since 0.7.0.0
@@ -189,7 +196,7 @@
 {- | Map over the result of a 'FilePather'. This is 'fmap', and is the
 function that the fusion rules match.
 
->>> runIdentity (view _Wrapped (mapFilePather length ask) "notes.md")
+>>> runIdentity (view (cloneLens pathFunction) (mapFilePather length ask) "notes.md")
 8
 
 @since 0.7.0.0
@@ -202,7 +209,7 @@
 {- | Filter and map over the result of a 'FilePather'. This is 'mapMaybe', and
 is the function that the fusion rules match.
 
->>> view _Wrapped (mapMaybeFilePather (\x -> if x > 1 then Just (x * 10) else Nothing) (FilePather (\p -> [1, length p]))) "abc"
+>>> view (cloneLens pathFunction) (mapMaybeFilePather (\x -> if x > 1 then Just (x * 10) else Nothing) (FilePather (\p -> [1, length p]))) "abc"
 [30]
 
 @since 0.7.0.0
@@ -233,7 +240,7 @@
 -- | @since 0.7.0.0
 instance (Bind f) => Bind (FilePather f) where
   FilePather g >>- k =
-    FilePather (\p -> g p >>- \a -> view _Wrapped (k a) p)
+    FilePather (\p -> g p >>- \a -> view (cloneLens pathFunction) (k a) p)
   {-# INLINE (>>-) #-}
 
 -- | @since 0.7.0.0
@@ -251,7 +258,7 @@
 -- | @since 0.7.0.0
 instance (Distributive f) => Distributive (FilePather f) where
   distribute w =
-    FilePather (\p -> distribute (fmap (\x -> view _Wrapped x p) w))
+    FilePather (\p -> distribute (fmap (\x -> view (cloneLens pathFunction) x p) w))
   {-# INLINE distribute #-}
 
 {- | The representation pairs the file path with the representation of @f@.
@@ -267,16 +274,6 @@
     index (g p) r
   {-# INLINE index #-}
 
--- | @since 0.7.0.0
-instance Wrapped (FilePather f a) where
-  type Unwrapped (FilePather f a) = FilePath -> f a
-  _Wrapped' =
-    iso (\(FilePather g) -> g) FilePather
-  {-# INLINE _Wrapped' #-}
-
--- | @since 0.7.0.0
-instance (t ~ FilePather g b) => Rewrapped (FilePather f a) t
-
 {- | The isomorphism between 'FilePather' and 'ReaderT'.
 
 >>> runIdentity (runReaderT (view filePatherReaderT (asks length)) "notes.md")
@@ -289,69 +286,62 @@
   iso (\(FilePather g) -> ReaderT g) (\(ReaderT g) -> FilePather g)
 {-# INLINE filePatherReaderT #-}
 
-{- | Values that can be viewed as a 'FilePather'.
+{- | The class of 'FilePather', with a pair of type variables for each of
+its two parameters.
 
->>> runIdentity (view (getFilePather . _Wrapped) (asks length :: FilePather' Int) "notes.md")
+>>> runIdentity (view (cloneLens (filePather . pathFunction)) (asks length :: FilePather' Int) "notes.md")
 8
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-class GetFilePather s f a | s -> f a where
-  getFilePather :: Getter s (FilePather f a)
+class FilePatherLike p f x x' g g' a a' | x -> g a where
+  filePather ::
+    Optic p f x x' (FilePather g a) (FilePather g' a')
 
--- | @since 0.7.0.0
-instance GetFilePather (FilePather f a) f a where
-  getFilePather =
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ FilePather g' a') => FilePatherLike p f (FilePather g a) x' g g' a a' where
+  filePather =
     id
-  {-# INLINE getFilePather #-}
+  {-# INLINE filePather #-}
 
-{- | Values with a lens to a 'FilePather'.
+{- | Lens
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-class (GetFilePather s f a) => HasFilePather s f a | s -> f a where
-  {-# MINIMAL setFilePather #-}
-  setFilePather :: FilePather f a -> s -> s
+instance (p ~ (->), Functor f, x' ~ FilePather g' a') => PathFunctionLike p f (FilePather g a) x' g g' a a' where
+  pathFunction f (FilePather k) =
+    fmap FilePather (f k)
+  {-# INLINE pathFunction #-}
 
-  filePather :: Lens' s (FilePather f a)
-  filePather =
-    lens (view getFilePather) (flip setFilePather)
-  {-# INLINE filePather #-}
+{- | Rebuild a value using only the name of each field.
 
--- | @since 0.7.0.0
-instance HasFilePather (FilePather f a) f a where
-  setFilePather =
-    const
-  {-# INLINE setFilePather #-}
+Laws, for every file path @p@:
 
-{- | Values that can be constructed from a 'FilePather'.
+> view (cloneLens pathFunction) (lawNamesFilePather x) p == view (cloneLens pathFunction) x p
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-class ReviewFilePather t f a | t -> f a where
-  reviewFilePather :: Review t (FilePather f a)
-
--- | @since 0.7.0.0
-instance ReviewFilePather (FilePather f a) f a where
-  reviewFilePather =
-    unto id
-  {-# INLINE reviewFilePather #-}
+lawNamesFilePather :: FilePather g a -> FilePather g a
+lawNamesFilePather s =
+  FilePather (view (cloneLens pathFunction) s)
 
-{- | Values with a prism to a 'FilePather'.
+{- | The classes that a data type holding 'FilePather' inherits.
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-class (ReviewFilePather t f a) => AsFilePather t f a | t -> f a where
-  {-# MINIMAL matchFilePather #-}
-  matchFilePather :: t -> Maybe (FilePather f a)
+type InheritFilePather s g a =
+  ( FilePatherLike (->) (Const ()) s s g g a a
+  , PathFunctionLike (->) (Const ()) s s g g a a
+  )
 
-  _FilePather :: Prism' t (FilePather f a)
-  _FilePather =
-    prism' (review reviewFilePather) matchFilePather
-  {-# INLINE _FilePather #-}
+{- | Check, where it is applied, that @s@ inherits every name of
+'FilePather'.
 
--- | @since 0.7.0.0
-instance AsFilePather (FilePather f a) f a where
-  matchFilePather =
-    Just
-  {-# INLINE matchFilePather #-}
+@since 0.8.0.0
+-}
+inheritFilePather :: (InheritFilePather s g a) => proxy s -> ()
+inheritFilePather _ =
+  ()
diff --git a/src/System/FilePather/Name/File.hs b/src/System/FilePather/Name/File.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Name/File.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | The name @file@, targeting 'FilePath': a file that something is about, such as the file that a step would make again.
+
+Any value of the target type has the name, as the identity.
+
+@since 0.8.0.0
+-}
+module System.FilePather.Name.File (
+  FileLike (..),
+) where
+
+import Control.Lens (Optic)
+
+{- | A field or a constructor named @file@.
+
+@since 0.8.0.0
+-}
+class FileLike p f x x' where
+  file ::
+    Optic p f x x' FilePath FilePath
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ FilePath) => FileLike p f FilePath x' where
+  file =
+    id
+  {-# INLINE file #-}
diff --git a/src/System/FilePather/Name/Once.hs b/src/System/FilePather/Name/Once.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Name/Once.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | The name @once@, targeting @()@: the constructor of a step that may appear once in a trail.
+
+Any value of the target type has the name, as the identity.
+
+@since 0.8.0.0
+-}
+module System.FilePather.Name.Once (
+  OnceLike (..),
+) where
+
+import Control.Lens (Optic)
+
+{- | A field or a constructor named @once@.
+
+@since 0.8.0.0
+-}
+class OnceLike p f x x' where
+  once ::
+    Optic p f x x' () ()
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ ()) => OnceLike p f () x' where
+  once =
+    id
+  {-# INLINE once #-}
diff --git a/src/System/FilePather/Name/PathFunction.hs b/src/System/FilePather/Name/PathFunction.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Name/PathFunction.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | The name @pathFunction@, targeting @FilePath -> g a@: the function of a 'System.FilePather.FilePather.FilePather', from the file path being operated on.
+
+Any value of the target type has the name, as the identity.
+
+@since 0.8.0.0
+-}
+module System.FilePather.Name.PathFunction (
+  PathFunctionLike (..),
+) where
+
+import Control.Lens (Optic)
+
+{- | A field or a constructor named @pathFunction@.
+
+@since 0.8.0.0
+-}
+class PathFunctionLike p f x x' g g' a a' | x -> g a where
+  pathFunction ::
+    Optic p f x x' (FilePath -> g a) (FilePath -> g' a')
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ (FilePath -> g' a')) => PathFunctionLike p f (FilePath -> g a) x' g g' a a' where
+  pathFunction =
+    id
+  {-# INLINE pathFunction #-}
diff --git a/src/System/FilePather/Name/Repeatable.hs b/src/System/FilePather/Name/Repeatable.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Name/Repeatable.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | The name @repeatable@, targeting @()@: the constructor of a step that may appear any number of times in a trail.
+
+Any value of the target type has the name, as the identity.
+
+@since 0.8.0.0
+-}
+module System.FilePather.Name.Repeatable (
+  RepeatableLike (..),
+) where
+
+import Control.Lens (Optic)
+
+{- | A field or a constructor named @repeatable@.
+
+@since 0.8.0.0
+-}
+class RepeatableLike p f x x' where
+  repeatable ::
+    Optic p f x x' () ()
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ ()) => RepeatableLike p f () x' where
+  repeatable =
+    id
+  {-# INLINE repeatable #-}
diff --git a/src/System/FilePather/Name/StepInput.hs b/src/System/FilePather/Name/StepInput.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Name/StepInput.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | The name @stepInput@, targeting 'FilePath': the input file of a step.
+
+Any value of the target type has the name, as the identity.
+
+@since 0.8.0.0
+-}
+module System.FilePather.Name.StepInput (
+  StepInputLike (..),
+) where
+
+import Control.Lens (Optic)
+
+{- | A field or a constructor named @stepInput@.
+
+@since 0.8.0.0
+-}
+class StepInputLike p f x x' where
+  stepInput ::
+    Optic p f x x' FilePath FilePath
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ FilePath) => StepInputLike p f FilePath x' where
+  stepInput =
+    id
+  {-# INLINE stepInput #-}
diff --git a/src/System/FilePather/Name/StepName.hs b/src/System/FilePather/Name/StepName.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Name/StepName.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | The name @stepName@, targeting 'String': the name of a step, such as @"pdf page 1"@.
+
+Any value of the target type has the name, as the identity.
+
+@since 0.8.0.0
+-}
+module System.FilePather.Name.StepName (
+  StepNameLike (..),
+) where
+
+import Control.Lens (Optic)
+
+{- | A field or a constructor named @stepName@.
+
+@since 0.8.0.0
+-}
+class StepNameLike p f x x' where
+  stepName ::
+    Optic p f x x' String String
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ String) => StepNameLike p f String x' where
+  stepName =
+    id
+  {-# INLINE stepName #-}
diff --git a/src/System/FilePather/Name/Steps.hs b/src/System/FilePather/Name/Steps.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Name/Steps.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | The name @steps@, targeting @['System.FilePather.Step.Step']@: the steps of a trail, nearest first.
+
+Any value of the target type has the name, as the identity.
+
+@since 0.8.0.0
+-}
+module System.FilePather.Name.Steps (
+  StepsLike (..),
+) where
+
+import Control.Lens (Optic)
+import System.FilePather.Step (Step)
+
+{- | A field or a constructor named @steps@.
+
+@since 0.8.0.0
+-}
+class StepsLike p f x x' where
+  steps ::
+    Optic p f x x' [Step] [Step]
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ [Step]) => StepsLike p f [Step] x' where
+  steps =
+    id
+  {-# INLINE steps #-}
diff --git a/src/System/FilePather/RepeatsStep.hs b/src/System/FilePather/RepeatsStep.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/RepeatsStep.hs
@@ -0,0 +1,154 @@
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | A step that is already in its trail.
+
+A 'RepeatsStep' is a product: its fields are named 'step' and 'trail'. It
+holds a 'Step' and a 'Trail', so it inherits their names.
+-}
+module System.FilePather.RepeatsStep (
+  -- * Type
+  RepeatsStep (..),
+
+  -- * Optics
+  RepeatsStepLike (..),
+  lawNamesRepeatsStep,
+  InheritRepeatsStep,
+  inheritRepeatsStep,
+  inheritsRepeatsStep,
+) where
+
+import Control.DeepSeq (NFData)
+import Control.Lens (Optic, cloneLens, view)
+import Data.Data (Data)
+import Data.Functor.Const (Const)
+import Data.Proxy (Proxy (..))
+import GHC.Generics (Generic)
+import System.FilePather.Name.StepInput (StepInputLike (..))
+import System.FilePather.Name.StepName (StepNameLike (..))
+import System.FilePather.Name.Steps (StepsLike (..))
+import System.FilePather.Step (Step, StepLike (..), inheritStep)
+import System.FilePather.Trail (Trail, TrailLike (..), inheritTrail)
+
+{- | A step is already in the trail of its input file: the step ('step'), and
+the trail ('trail').
+
+@since 0.8.0.0
+-}
+data RepeatsStep
+  = RepeatsStep Step Trail
+  deriving stock (Eq, Ord, Show, Generic, Data)
+  deriving anyclass (NFData)
+
+{- | The class of 'RepeatsStep'.
+
+@since 0.8.0.0
+-}
+class RepeatsStepLike p f x x' where
+  repeatsStep ::
+    Optic p f x x' RepeatsStep RepeatsStep
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ RepeatsStep) => RepeatsStepLike p f RepeatsStep x' where
+  repeatsStep =
+    id
+  {-# INLINE repeatsStep #-}
+
+{- | Lens: 'step' is the default name of the field of type 'Step'.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ RepeatsStep) => StepLike p f RepeatsStep x' where
+  step f (RepeatsStep s t) =
+    fmap (`RepeatsStep` t) (f s)
+  {-# INLINE step #-}
+
+{- | Lens: 'trail' is the default name of the field of type 'Trail'.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ RepeatsStep) => TrailLike p f RepeatsStep x' where
+  trail f (RepeatsStep s t) =
+    fmap (RepeatsStep s) (f t)
+  {-# INLINE trail #-}
+
+{- | Lens: through 'step', a lens, then 'Step''s 'stepName', a lens.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ RepeatsStep) => StepNameLike p f RepeatsStep x' where
+  stepName =
+    step . stepName
+  {-# INLINE stepName #-}
+
+{- | Lens: through 'step', a lens, then 'Step''s 'stepInput', a lens.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ RepeatsStep) => StepInputLike p f RepeatsStep x' where
+  stepInput =
+    step . stepInput
+  {-# INLINE stepInput #-}
+
+{- | Lens: through 'trail', a lens, then 'Trail''s 'steps', a lens.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ RepeatsStep) => StepsLike p f RepeatsStep x' where
+  steps =
+    trail . steps
+  {-# INLINE steps #-}
+
+{- | Rebuild a value using only the name of each field.
+
+Laws:
+
+> lawNamesRepeatsStep == id
+
+@since 0.8.0.0
+-}
+lawNamesRepeatsStep :: RepeatsStep -> RepeatsStep
+lawNamesRepeatsStep s =
+  RepeatsStep (view (cloneLens step) s) (view (cloneLens trail) s)
+
+{- | The classes that a data type holding 'RepeatsStep' inherits.
+
+@since 0.8.0.0
+-}
+type InheritRepeatsStep s =
+  ( RepeatsStepLike (->) (Const ()) s s
+  , StepLike (->) (Const ()) s s
+  , TrailLike (->) (Const ()) s s
+  , StepNameLike (->) (Const ()) s s
+  , StepInputLike (->) (Const ()) s s
+  , StepsLike (->) (Const ()) s s
+  )
+
+{- | Check, where it is applied, that @s@ inherits every name of
+'RepeatsStep'.
+
+@since 0.8.0.0
+-}
+inheritRepeatsStep :: (InheritRepeatsStep s) => proxy s -> ()
+inheritRepeatsStep _ =
+  ()
+
+{- | 'RepeatsStep' inherits every name of 'Step' and of 'Trail'.
+
+@since 0.8.0.0
+-}
+inheritsRepeatsStep :: [()]
+inheritsRepeatsStep =
+  let p = Proxy :: Proxy RepeatsStep
+   in [inheritStep p, inheritTrail p]
diff --git a/src/System/FilePather/Repetition.hs b/src/System/FilePather/Repetition.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Repetition.hs
@@ -0,0 +1,114 @@
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | Whether a step may appear more than once in a trail.
+
+A 'Repetition' is a sum: its constructors are named 'once' and 'repeatable'.
+-}
+module System.FilePather.Repetition (
+  -- * Type
+  Repetition (..),
+
+  -- * Optics
+  RepetitionLike (..),
+  lawNamesRepetition,
+  InheritRepetition,
+  inheritRepetition,
+) where
+
+import Control.DeepSeq (NFData)
+import Control.Lens (Choice, Optic, prism', review)
+import Data.Data (Data)
+import Data.Functor.Const (Const)
+import GHC.Generics (Generic)
+import System.FilePather.Name.Once (OnceLike (..))
+import System.FilePather.Name.Repeatable (RepeatableLike (..))
+
+{- | Whether a step may appear more than once in a trail. A step that may
+not is stopped with 'System.FilePather.Cycle.CycleRepeatsStep' when it is
+already in the trail.
+
+@since 0.7.0.0
+-}
+data Repetition
+  = -- | The step may appear once in a trail ('once').
+    Once
+  | -- | The step may appear any number of times in a trail ('repeatable').
+    Repeatable
+  deriving stock (Eq, Ord, Show, Generic, Data)
+  deriving anyclass (NFData)
+
+{- | The class of 'Repetition'.
+
+@since 0.8.0.0
+-}
+class RepetitionLike p f x x' where
+  repetition ::
+    Optic p f x x' Repetition Repetition
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ Repetition) => RepetitionLike p f Repetition x' where
+  repetition =
+    id
+  {-# INLINE repetition #-}
+
+{- | Prism
+
+@since 0.8.0.0
+-}
+instance (Choice p, Applicative f, x' ~ Repetition) => OnceLike p f Repetition x' where
+  once =
+    prism' (\() -> Once) (\case Once -> Just (); Repeatable -> Nothing)
+  {-# INLINE once #-}
+
+{- | Prism
+
+@since 0.8.0.0
+-}
+instance (Choice p, Applicative f, x' ~ Repetition) => RepeatableLike p f Repetition x' where
+  repeatable =
+    prism' (\() -> Repeatable) (\case Repeatable -> Just (); Once -> Nothing)
+  {-# INLINE repeatable #-}
+
+{- | Rebuild a value using only the name of each constructor.
+
+Laws:
+
+> lawNamesRepetition == id
+
+@since 0.8.0.0
+-}
+lawNamesRepetition :: Repetition -> Repetition
+lawNamesRepetition = \case
+  Once -> review once ()
+  Repeatable -> review repeatable ()
+
+{- | The classes that a data type holding 'Repetition' inherits.
+
+@since 0.8.0.0
+-}
+type InheritRepetition s =
+  ( RepetitionLike (->) (Const ()) s s
+  , OnceLike (->) (Const ()) s s
+  , RepeatableLike (->) (Const ()) s s
+  )
+
+{- | Check, where it is applied, that @s@ inherits every name of 'Repetition'.
+
+@since 0.8.0.0
+-}
+inheritRepetition :: (InheritRepetition s) => proxy s -> ()
+inheritRepetition _ =
+  ()
diff --git a/src/System/FilePather/ReproducesInput.hs b/src/System/FilePather/ReproducesInput.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/ReproducesInput.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | A step that would make a file that is already in its chain.
+
+A 'ReproducesInput' is a product: its fields are named 'file', 'step' and
+'trail'. It holds a 'Step' and a 'Trail', so it inherits their names.
+-}
+module System.FilePather.ReproducesInput (
+  -- * Type
+  ReproducesInput (..),
+
+  -- * Optics
+  ReproducesInputLike (..),
+  lawNamesReproducesInput,
+  InheritReproducesInput,
+  inheritReproducesInput,
+  inheritsReproducesInput,
+) where
+
+import Control.DeepSeq (NFData)
+import Control.Lens (Optic, cloneLens, view)
+import Data.Data (Data)
+import Data.Functor.Const (Const)
+import Data.Proxy (Proxy (..))
+import GHC.Generics (Generic)
+import System.FilePather.Name.File (FileLike (..))
+import System.FilePather.Name.StepInput (StepInputLike (..))
+import System.FilePather.Name.StepName (StepNameLike (..))
+import System.FilePather.Name.Steps (StepsLike (..))
+import System.FilePather.Step (Step, StepLike (..), inheritStep)
+import System.FilePather.Trail (Trail, TrailLike (..), inheritTrail)
+
+{- | A step would make a file that is already in its chain: the file
+('file'), which is its input file or the input file of a step in the trail,
+the step ('step'), and the trail of its input file ('trail').
+
+@since 0.8.0.0
+-}
+data ReproducesInput
+  = ReproducesInput FilePath Step Trail
+  deriving stock (Eq, Ord, Show, Generic, Data)
+  deriving anyclass (NFData)
+
+{- | The class of 'ReproducesInput'.
+
+@since 0.8.0.0
+-}
+class ReproducesInputLike p f x x' where
+  reproducesInput ::
+    Optic p f x x' ReproducesInput ReproducesInput
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ ReproducesInput) => ReproducesInputLike p f ReproducesInput x' where
+  reproducesInput =
+    id
+  {-# INLINE reproducesInput #-}
+
+{- | Lens
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ ReproducesInput) => FileLike p f ReproducesInput x' where
+  file f (ReproducesInput o s t) =
+    fmap (\o' -> ReproducesInput o' s t) (f o)
+  {-# INLINE file #-}
+
+{- | Lens: 'step' is the default name of the field of type 'Step'.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ ReproducesInput) => StepLike p f ReproducesInput x' where
+  step f (ReproducesInput o s t) =
+    fmap (\s' -> ReproducesInput o s' t) (f s)
+  {-# INLINE step #-}
+
+{- | Lens: 'trail' is the default name of the field of type 'Trail'.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ ReproducesInput) => TrailLike p f ReproducesInput x' where
+  trail f (ReproducesInput o s t) =
+    fmap (ReproducesInput o s) (f t)
+  {-# INLINE trail #-}
+
+{- | Lens: through 'step', a lens, then 'Step''s 'stepName', a lens.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ ReproducesInput) => StepNameLike p f ReproducesInput x' where
+  stepName =
+    step . stepName
+  {-# INLINE stepName #-}
+
+{- | Lens: through 'step', a lens, then 'Step''s 'stepInput', a lens.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ ReproducesInput) => StepInputLike p f ReproducesInput x' where
+  stepInput =
+    step . stepInput
+  {-# INLINE stepInput #-}
+
+{- | Lens: through 'trail', a lens, then 'Trail''s 'steps', a lens.
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ ReproducesInput) => StepsLike p f ReproducesInput x' where
+  steps =
+    trail . steps
+  {-# INLINE steps #-}
+
+{- | Rebuild a value using only the name of each field.
+
+Laws:
+
+> lawNamesReproducesInput == id
+
+@since 0.8.0.0
+-}
+lawNamesReproducesInput :: ReproducesInput -> ReproducesInput
+lawNamesReproducesInput s =
+  ReproducesInput (view (cloneLens file) s) (view (cloneLens step) s) (view (cloneLens trail) s)
+
+{- | The classes that a data type holding 'ReproducesInput' inherits.
+
+@since 0.8.0.0
+-}
+type InheritReproducesInput s =
+  ( ReproducesInputLike (->) (Const ()) s s
+  , FileLike (->) (Const ()) s s
+  , StepLike (->) (Const ()) s s
+  , TrailLike (->) (Const ()) s s
+  , StepNameLike (->) (Const ()) s s
+  , StepInputLike (->) (Const ()) s s
+  , StepsLike (->) (Const ()) s s
+  )
+
+{- | Check, where it is applied, that @s@ inherits every name of
+'ReproducesInput'.
+
+@since 0.8.0.0
+-}
+inheritReproducesInput :: (InheritReproducesInput s) => proxy s -> ()
+inheritReproducesInput _ =
+  ()
+
+{- | 'ReproducesInput' inherits every name of 'Step' and of 'Trail'.
+
+@since 0.8.0.0
+-}
+inheritsReproducesInput :: [()]
+inheritsReproducesInput =
+  let p = Proxy :: Proxy ReproducesInput
+   in [inheritStep p, inheritTrail p]
diff --git a/src/System/FilePather/Step.hs b/src/System/FilePather/Step.hs
new file mode 100644
--- /dev/null
+++ b/src/System/FilePather/Step.hs
@@ -0,0 +1,118 @@
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | One application of a named build step to a file.
+
+A 'Step' is a product: its fields are named 'stepName' and 'stepInput'.
+-}
+module System.FilePather.Step (
+  -- * Type
+  Step (..),
+
+  -- * Optics
+  StepLike (..),
+  lawNamesStep,
+  InheritStep,
+  inheritStep,
+) where
+
+import Control.DeepSeq (NFData)
+import Control.Lens (Optic, cloneLens, view)
+import Data.Data (Data)
+import Data.Functor.Const (Const)
+import GHC.Generics (Generic)
+import System.FilePather.Name.StepInput (StepInputLike (..))
+import System.FilePather.Name.StepName (StepNameLike (..))
+
+{- $setup
+>>> import Control.Lens (cloneLens, view)
+-}
+
+{- | One application of a named build step to a file: the name of the step
+('stepName'), such as @"pdf page 1"@, and its input file ('stepInput').
+
+>>> view (cloneLens stepName) (Step "pdf page 1" "public/a.pdf")
+"pdf page 1"
+
+>>> view (cloneLens stepInput) (Step "pdf page 1" "public/a.pdf")
+"public/a.pdf"
+
+@since 0.7.0.0
+-}
+data Step
+  = Step String FilePath
+  deriving stock (Eq, Ord, Show, Generic, Data)
+  deriving anyclass (NFData)
+
+{- | The class of 'Step'.
+
+@since 0.8.0.0
+-}
+class StepLike p f x x' where
+  step ::
+    Optic p f x x' Step Step
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ Step) => StepLike p f Step x' where
+  step =
+    id
+  {-# INLINE step #-}
+
+{- | Lens
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ Step) => StepNameLike p f Step x' where
+  stepName f (Step n i) =
+    fmap (`Step` i) (f n)
+  {-# INLINE stepName #-}
+
+{- | Lens
+
+@since 0.8.0.0
+-}
+instance (p ~ (->), Functor f, x' ~ Step) => StepInputLike p f Step x' where
+  stepInput f (Step n i) =
+    fmap (Step n) (f i)
+  {-# INLINE stepInput #-}
+
+{- | Rebuild a value using only the name of each field.
+
+Laws:
+
+> lawNamesStep == id
+
+@since 0.8.0.0
+-}
+lawNamesStep :: Step -> Step
+lawNamesStep s =
+  Step (view (cloneLens stepName) s) (view (cloneLens stepInput) s)
+
+{- | The classes that a data type holding 'Step' inherits.
+
+@since 0.8.0.0
+-}
+type InheritStep s =
+  ( StepLike (->) (Const ()) s s
+  , StepNameLike (->) (Const ()) s s
+  , StepInputLike (->) (Const ()) s s
+  )
+
+{- | Check, where it is applied, that @s@ inherits every name of 'Step'.
+
+@since 0.8.0.0
+-}
+inheritStep :: (InheritStep s) => proxy s -> ()
+inheritStep _ =
+  ()
diff --git a/src/System/FilePather/Tracked.hs b/src/System/FilePather/Tracked.hs
--- a/src/System/FilePather/Tracked.hs
+++ b/src/System/FilePather/Tracked.hs
@@ -1,5 +1,7 @@
+{-# LANGUAGE ConstraintKinds #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE FunctionalDependencies #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
@@ -28,8 +30,8 @@
 
 A 'Tracked' value is run on a file path, with the 'Trail' of that file: the
 steps that built it. It produces its result, and the files it made, each with
-the 'Step' that made it. Make one from a 'FilePather' with 'step', which
-names the step and checks it against the trail before it runs, or 'step'',
+the 'Step' that made it. Make one from a 'FilePather' with 'track', which
+names the step and checks it against the trail before it runs, or 'track'',
 which checks after it runs, for steps whose outputs are known only then.
 
 'Tracked' is separate from 'FilePather', so that 'FilePather' keeps all of
@@ -45,10 +47,10 @@
   Tracked',
 
   -- * Steps
-  step,
-  stepWith,
-  step',
-  stepWith',
+  track,
+  trackWith,
+  track',
+  trackWith',
   untracked,
 
   -- * Combinators
@@ -58,13 +60,16 @@
   whenTrail,
   hoistTracked,
 
-  -- * Optics
+  -- * Isomorphisms
   trackedFunction,
-  GetTracked (..),
-  HasTracked (..),
-  ReviewTracked (..),
-  AsTracked (..),
 
+  -- * Optics
+  TrackedLike (..),
+  lawNamesTracked,
+  InheritTracked,
+  inheritTracked,
+  inheritsTracked,
+
   -- * Fusion workers
   mapTracked,
   mapMaybeTracked,
@@ -72,7 +77,7 @@
 
 import Control.Applicative (Alternative (..))
 import Control.DeepSeq (NFData)
-import Control.Lens (Getter, Iso, Lens', Prism', Review, Rewrapped, Wrapped (..), from, iso, lens, over, prism', review, unto, view, _Wrapped)
+import Control.Lens (Iso, Optic, cloneLens, from, iso, over, view)
 import Control.Monad (MonadPlus)
 import Control.Monad.Error.Class (MonadError (..))
 import Control.Monad.IO.Class (MonadIO (..))
@@ -88,13 +93,19 @@
 import Data.Functor.Alt (Alt (..))
 import Data.Functor.Apply (Apply (..))
 import Data.Functor.Bind (Bind (..))
+import Data.Functor.Const (Const)
 import Data.Functor.Contravariant (Contravariant (..))
 import Data.Functor.Identity (Identity)
 import Data.Functor.Plus (Plus (..))
 import Data.Kind (Type)
+import Data.Proxy (Proxy (..))
 import GHC.Generics (Generic)
-import System.FilePather.FilePather (FilePather (..))
-import System.FilePather.Trail (Cycle, Repetition (..), Step (..), Trail, checkStep)
+import System.FilePather.Cycle (Cycle, checkStep)
+import System.FilePather.FilePather (FilePather (..), FilePatherLike (..), inheritFilePather)
+import System.FilePather.Name.PathFunction (PathFunctionLike (..))
+import System.FilePather.Repetition (Repetition (..))
+import System.FilePather.Step (Step (..))
+import System.FilePather.Trail (Trail)
 import Witherable (Filterable (..))
 
 {- $setup
@@ -102,7 +113,8 @@
 >>> import Control.Monad.Reader (asks)
 >>> import Data.Functor.Identity (Identity (..))
 >>> import System.FilePath (replaceExtension)
->>> import System.FilePather.Trail
+>>> import System.FilePather.Step (Step (..))
+>>> import System.FilePather.Trail (Trail (..))
 >>> let run x t p = runIdentity (view trackedFunction x t p)
 -}
 
@@ -116,12 +128,12 @@
 
 ==== __Examples__
 
->>> let pdf = step "markdown to pdf" (\p -> [replaceExtension p "pdf"]) (asks length) :: Tracked Identity Int
+>>> let pdf = track "markdown to pdf" (\p -> [replaceExtension p "pdf"]) (asks length) :: Tracked Identity Int
 >>> run pdf (Trail []) "src/a.md"
 Right ([("src/a.pdf",Step "markdown to pdf" "src/a.md")],8)
 
 >>> run pdf (Trail [Step "markdown to pdf" "src/b.md"]) "src/a.md"
-Left (RepeatsStep (Step "markdown to pdf" "src/a.md") (Trail [Step "markdown to pdf" "src/b.md"]))
+Left (CycleRepeatsStep (RepeatsStep (Step "markdown to pdf" "src/a.md") (Trail [Step "markdown to pdf" "src/b.md"])))
 
 @since 0.7.0.0
 -}
@@ -154,10 +166,10 @@
 {-# INLINE trackedFunction #-}
 
 -- | Make a 'Tracked' value from its function.
-tracked :: (Trail -> FilePath -> f (Either Cycle (Outputs, a))) -> Tracked f a
-tracked =
+fromTrackedFunction :: (Trail -> FilePath -> f (Either Cycle (Outputs, a))) -> Tracked f a
+fromTrackedFunction =
   view (from trackedFunction)
-{-# INLINE tracked #-}
+{-# INLINE fromTrackedFunction #-}
 
 -- | Run a 'Tracked' value as its function.
 runTracked :: Tracked f a -> Trail -> FilePath -> f (Either Cycle (Outputs, a))
@@ -168,7 +180,7 @@
 -- | Bind for 'Tracked': the outputs of both are kept, and a 'Cycle' stops.
 bindTracked :: (Monad f) => Tracked f a -> (a -> Tracked f b) -> Tracked f b
 bindTracked m k =
-  tracked $ \t p ->
+  fromTrackedFunction $ \t p ->
     runTracked m t p
       >>= either
         (pure . Left)
@@ -231,7 +243,7 @@
 -- | @since 0.7.0.0
 instance (Monad f) => Applicative (Tracked f) where
   pure a =
-    tracked (\_ _ -> pure (Right ([], a)))
+    fromTrackedFunction (\_ _ -> pure (Right ([], a)))
   {-# INLINE pure #-}
   (<*>) =
     (<.>)
@@ -262,13 +274,13 @@
 -}
 instance (Alt f) => Alt (Tracked f) where
   x <!> y =
-    tracked (\t p -> runTracked x t p <!> runTracked y t p)
+    fromTrackedFunction (\t p -> runTracked x t p <!> runTracked y t p)
   {-# INLINE (<!>) #-}
 
 -- | @since 0.7.0.0
 instance (Plus f) => Plus (Tracked f) where
   zero =
-    tracked (\_ _ -> zero)
+    fromTrackedFunction (\_ _ -> zero)
   {-# INLINE zero #-}
 
 {- | As for 'Alt', a 'Cycle' is never caught.
@@ -277,10 +289,10 @@
 -}
 instance (Alternative f, Monad f) => Alternative (Tracked f) where
   empty =
-    tracked (\_ _ -> empty)
+    fromTrackedFunction (\_ _ -> empty)
   {-# INLINE empty #-}
   x <|> y =
-    tracked (\t p -> runTracked x t p <|> runTracked y t p)
+    fromTrackedFunction (\t p -> runTracked x t p <|> runTracked y t p)
   {-# INLINE (<|>) #-}
 
 -- | @since 0.7.0.0
@@ -315,7 +327,7 @@
 -}
 instance MonadTrans Tracked where
   lift m =
-    tracked (\_ _ -> fmap (\a -> Right ([], a)) m)
+    fromTrackedFunction (\_ _ -> fmap (\a -> Right ([], a)) m)
   {-# INLINE lift #-}
 
 -- | @since 0.7.0.0
@@ -337,10 +349,10 @@
 -}
 instance (Monad f) => MonadReader FilePath (Tracked f) where
   ask =
-    tracked (\_ p -> pure (Right ([], p)))
+    fromTrackedFunction (\_ p -> pure (Right ([], p)))
   {-# INLINE ask #-}
   local g x =
-    tracked (\t -> runTracked x t . g)
+    fromTrackedFunction (\t -> runTracked x t . g)
   {-# INLINE local #-}
 
 {- | The state of @f@.
@@ -362,7 +374,7 @@
     lift . throwError
   {-# INLINE throwError #-}
   catchError x h =
-    tracked (\t p -> catchError (runTracked x t p) (\e -> runTracked (h e) t p))
+    fromTrackedFunction (\t p -> catchError (runTracked x t p) (\e -> runTracked (h e) t p))
   {-# INLINE catchError #-}
 
 {- | The output of @f@, which is separate from the files that steps make.
@@ -377,10 +389,10 @@
     lift . tell
   {-# INLINE tell #-}
   listen x =
-    tracked (\t p -> fmap (\(r, w) -> fmap (fmap (,w)) r) (listen (runTracked x t p)))
+    fromTrackedFunction (\t p -> fmap (\(r, w) -> fmap (fmap (,w)) r) (listen (runTracked x t p)))
   {-# INLINE listen #-}
   pass x =
-    tracked $ \t p ->
+    fromTrackedFunction $ \t p ->
       pass
         ( fmap
             ( either
@@ -397,59 +409,49 @@
     over trackedFunction (\h t p -> contramap (fmap (fmap g)) (h t p))
   {-# INLINE contramap #-}
 
--- | @since 0.7.0.0
-instance Wrapped (Tracked f a) where
-  type Unwrapped (Tracked f a) = FilePather (ReaderT Trail (ExceptT Cycle f)) (Outputs, a)
-  _Wrapped' =
-    iso (\(Tracked x) -> x) Tracked
-  {-# INLINE _Wrapped' #-}
-
--- | @since 0.7.0.0
-instance (t ~ Tracked g b) => Rewrapped (Tracked f a) t
-
 {- | A named step that makes the files given by a function of its input file,
-and may appear 'Once' in a trail. See 'stepWith'.
+and may appear 'Once' in a trail. See 'trackWith'.
 
 @since 0.7.0.0
 -}
-step :: (Applicative f) => String -> (FilePath -> [FilePath]) -> FilePather f a -> Tracked f a
-step =
-  stepWith Once
-{-# INLINE step #-}
+track :: (Applicative f) => String -> (FilePath -> [FilePath]) -> FilePather f a -> Tracked f a
+track =
+  trackWith Once
+{-# INLINE track #-}
 
 {- | A named step that makes the files given by a function of its input file.
 The step is checked against the trail of its input file with 'checkStep'
 before it runs: if it is stopped, the 'FilePather' is not run.
 
->>> let page1 = stepWith Once "pdf page 1" (\p -> [p <> ".page1.pdf"]) (pure ()) :: Tracked Identity ()
+>>> let page1 = trackWith Once "pdf page 1" (\p -> [p <> ".page1.pdf"]) (pure ()) :: Tracked Identity ()
 >>> run page1 (Trail [Step "markdown to pdf" "a.md"]) "a.pdf"
 Right ([("a.pdf.page1.pdf",Step "pdf page 1" "a.pdf")],())
 
 >>> run page1 (Trail [Step "pdf page 1" "a.pdf"]) "a.pdf.page1.pdf"
-Left (RepeatsStep (Step "pdf page 1" "a.pdf.page1.pdf") (Trail [Step "pdf page 1" "a.pdf"]))
+Left (CycleRepeatsStep (RepeatsStep (Step "pdf page 1" "a.pdf.page1.pdf") (Trail [Step "pdf page 1" "a.pdf"])))
 
 @since 0.7.0.0
 -}
-stepWith :: (Applicative f) => Repetition -> String -> (FilePath -> [FilePath]) -> FilePather f a -> Tracked f a
-stepWith r n outs x =
-  tracked $ \t p ->
+trackWith :: (Applicative f) => Repetition -> String -> (FilePath -> [FilePath]) -> FilePather f a -> Tracked f a
+trackWith r n outs x =
+  fromTrackedFunction $ \t p ->
     let s = Step n p
         os = outs p
      in either
           (pure . Left)
-          (\() -> fmap (\a -> Right (fmap (,s) os, a)) (view _Wrapped x p))
+          (\() -> fmap (\a -> Right (fmap (,s) os, a)) (view (cloneLens pathFunction) x p))
           (checkStep r s os t)
-{-# INLINE stepWith #-}
+{-# INLINE trackWith #-}
 
 {- | A named step that returns the files it made, and may appear 'Once' in a
-trail. See 'stepWith''.
+trail. See 'trackWith''.
 
 @since 0.7.0.0
 -}
-step' :: (Functor f) => String -> FilePather f ([FilePath], a) -> Tracked f a
-step' =
-  stepWith' Once
-{-# INLINE step' #-}
+track' :: (Functor f) => String -> FilePather f ([FilePath], a) -> Tracked f a
+track' =
+  trackWith' Once
+{-# INLINE track' #-}
 
 {- | A named step that returns the files it made, for a step whose outputs are
 known only after it runs, such as an image for each page of a PDF file. The
@@ -457,18 +459,18 @@
 runs, so it cannot be stopped before it runs, but its outputs are not passed
 on.
 
->>> let pages = stepWith' Once "pdf pages" (asks (\p -> ([p <> "-0.png", p <> "-1.png"], ()))) :: Tracked Identity ()
+>>> let pages = trackWith' Once "pdf pages" (asks (\p -> ([p <> "-0.png", p <> "-1.png"], ()))) :: Tracked Identity ()
 >>> run pages (Trail []) "a.pdf"
 Right ([("a.pdf-0.png",Step "pdf pages" "a.pdf"),("a.pdf-1.png",Step "pdf pages" "a.pdf")],())
 
 @since 0.7.0.0
 -}
-stepWith' :: (Functor f) => Repetition -> String -> FilePather f ([FilePath], a) -> Tracked f a
-stepWith' r n x =
-  tracked $ \t p ->
+trackWith' :: (Functor f) => Repetition -> String -> FilePather f ([FilePath], a) -> Tracked f a
+trackWith' r n x =
+  fromTrackedFunction $ \t p ->
     let s = Step n p
-     in fmap (\(os, a) -> fmap (\() -> (fmap (,s) os, a)) (checkStep r s os t)) (view _Wrapped x p)
-{-# INLINE stepWith' #-}
+     in fmap (\(os, a) -> fmap (\() -> (fmap (,s) os, a)) (checkStep r s os t)) (view (cloneLens pathFunction) x p)
+{-# INLINE trackWith' #-}
 
 {- | A 'FilePather' that makes no files, such as one that gets information
 about a file, or a condition.
@@ -480,7 +482,7 @@
 -}
 untracked :: (Functor f) => FilePather f a -> Tracked f a
 untracked x =
-  tracked (\_ p -> fmap (\a -> Right ([], a)) (view _Wrapped x p))
+  fromTrackedFunction (\_ p -> fmap (\a -> Right ([], a)) (view (cloneLens pathFunction) x p))
 {-# INLINE untracked #-}
 
 {- | The trail of the file being operated on.
@@ -492,7 +494,7 @@
 -}
 askTrail :: (Applicative f) => Tracked f Trail
 askTrail =
-  tracked (\t _ -> pure (Right ([], t)))
+  fromTrackedFunction (\t _ -> pure (Right ([], t)))
 {-# INLINE askTrail #-}
 
 {- | Run on the given file path, instead of the one being operated on, with
@@ -502,13 +504,13 @@
 -}
 onPath :: FilePath -> Tracked f a -> Tracked f a
 onPath q x =
-  tracked (\t _ -> runTracked x t q)
+  fromTrackedFunction (\t _ -> runTracked x t q)
 {-# INLINE onPath #-}
 
 {- | Run only on file paths that satisfy a predicate. On other file paths, the
 result is 'mempty' and no files are made.
 
->>> let md = whenPath (\p -> p == "a.md") (step "copy" (\p -> [p <> ".bak"]) (pure ())) :: Tracked Identity ()
+>>> let md = whenPath (\p -> p == "a.md") (track "copy" (\p -> [p <> ".bak"]) (pure ())) :: Tracked Identity ()
 >>> run md (Trail []) "a.md"
 Right ([("a.md.bak",Step "copy" "a.md")],())
 
@@ -519,7 +521,7 @@
 -}
 whenPath :: (Applicative f, Monoid a) => (FilePath -> Bool) -> Tracked f a -> Tracked f a
 whenPath f x =
-  tracked (\t p -> bool (pure (Right ([], mempty))) (runTracked x t p) (f p))
+  fromTrackedFunction (\t p -> bool (pure (Right ([], mempty))) (runTracked x t p) (f p))
 {-# INLINE whenPath #-}
 
 {- | Run only on files whose trail satisfies a predicate, such as files made
@@ -530,7 +532,7 @@
 -}
 whenTrail :: (Applicative f, Monoid a) => (Trail -> Bool) -> Tracked f a -> Tracked f a
 whenTrail f x =
-  tracked (\t p -> bool (pure (Right ([], mempty))) (runTracked x t p) (f t))
+  fromTrackedFunction (\t p -> bool (pure (Right ([], mempty))) (runTracked x t p) (f t))
 {-# INLINE whenTrail #-}
 
 {- | Change the type constructor of a 'Tracked' value with a natural
@@ -543,54 +545,97 @@
   over trackedFunction (\h t -> nat . h t)
 {-# INLINE hoistTracked #-}
 
--- | @since 0.7.0.0
-class GetTracked s f a | s -> f a where
-  getTracked :: Getter s (Tracked f a)
+{- | The class of 'Tracked', with a pair of type variables for each of its
+two parameters.
 
--- | @since 0.7.0.0
-instance GetTracked (Tracked f a) f a where
-  getTracked =
+@since 0.8.0.0
+-}
+class TrackedLike p f x x' g g' a a' | x -> g a where
+  tracked ::
+    Optic p f x x' (Tracked g a) (Tracked g' a')
+
+{- | Identity
+
+@since 0.8.0.0
+-}
+instance (x' ~ Tracked g' a') => TrackedLike p f (Tracked g a) x' g g' a a' where
+  tracked =
     id
-  {-# INLINE getTracked #-}
+  {-# INLINE tracked #-}
 
--- | @since 0.7.0.0
-class (GetTracked s f a) => HasTracked s f a | s -> f a where
-  {-# MINIMAL setTracked #-}
-  setTracked :: Tracked f a -> s -> s
+{- | Lens: 'filePather' is the default name of the field, of type
+'FilePather'. It changes the type constructor and the result of the
+'Tracked' value, which occur only in the field.
 
-  trackedLens :: Lens' s (Tracked f a)
-  trackedLens =
-    lens (view getTracked) (flip setTracked)
-  {-# INLINE trackedLens #-}
+@since 0.8.0.0
+-}
+instance
+  ( p ~ (->)
+  , Functor f
+  , x' ~ Tracked h' b'
+  , g' ~ ReaderT Trail (ExceptT Cycle h')
+  , a' ~ (Outputs, b')
+  ) =>
+  FilePatherLike p f (Tracked h b) x' (ReaderT Trail (ExceptT Cycle h)) g' (Outputs, b) a'
+  where
+  filePather f (Tracked x) =
+    fmap Tracked (f x)
+  {-# INLINE filePather #-}
 
--- | @since 0.7.0.0
-instance HasTracked (Tracked f a) f a where
-  setTracked =
-    const
-  {-# INLINE setTracked #-}
+{- | Lens: through 'filePather', a lens, then 'FilePather''s 'pathFunction',
+a lens.
 
--- | @since 0.7.0.0
-class ReviewTracked t f a | t -> f a where
-  reviewTracked :: Review t (Tracked f a)
+@since 0.8.0.0
+-}
+instance
+  ( p ~ (->)
+  , Functor f
+  , x' ~ Tracked h' b'
+  , g' ~ ReaderT Trail (ExceptT Cycle h')
+  , a' ~ (Outputs, b')
+  ) =>
+  PathFunctionLike p f (Tracked h b) x' (ReaderT Trail (ExceptT Cycle h)) g' (Outputs, b) a'
+  where
+  pathFunction =
+    filePather . pathFunction
+  {-# INLINE pathFunction #-}
 
--- | @since 0.7.0.0
-instance ReviewTracked (Tracked f a) f a where
-  reviewTracked =
-    unto id
-  {-# INLINE reviewTracked #-}
+{- | Rebuild a value using only the name of each field.
 
--- | @since 0.7.0.0
-class (ReviewTracked t f a) => AsTracked t f a | t -> f a where
-  {-# MINIMAL matchTracked #-}
-  matchTracked :: t -> Maybe (Tracked f a)
+Laws, for every trail @t@ and file path @p@:
 
-  _Tracked :: Prism' t (Tracked f a)
-  _Tracked =
-    prism' (review reviewTracked) matchTracked
-  {-# INLINE _Tracked #-}
+> view trackedFunction (lawNamesTracked x) t p == view trackedFunction x t p
 
--- | @since 0.7.0.0
-instance AsTracked (Tracked f a) f a where
-  matchTracked =
-    Just
-  {-# INLINE matchTracked #-}
+@since 0.8.0.0
+-}
+lawNamesTracked :: Tracked g a -> Tracked g a
+lawNamesTracked s =
+  Tracked (view (cloneLens filePather) s)
+
+{- | The classes that a data type holding 'Tracked' inherits.
+
+@since 0.8.0.0
+-}
+type InheritTracked s g a =
+  ( TrackedLike (->) (Const ()) s s g g a a
+  , FilePatherLike (->) (Const ()) s s (ReaderT Trail (ExceptT Cycle g)) (ReaderT Trail (ExceptT Cycle g)) (Outputs, a) (Outputs, a)
+  , PathFunctionLike (->) (Const ()) s s (ReaderT Trail (ExceptT Cycle g)) (ReaderT Trail (ExceptT Cycle g)) (Outputs, a) (Outputs, a)
+  )
+
+{- | Check, where it is applied, that @s@ inherits every name of 'Tracked'.
+
+@since 0.8.0.0
+-}
+inheritTracked :: (InheritTracked s g a) => proxy s -> ()
+inheritTracked _ =
+  ()
+
+{- | 'Tracked' inherits every name of 'FilePather'. Its type constructor is
+set to 'Identity', and its result to @()@.
+
+@since 0.8.0.0
+-}
+inheritsTracked :: [()]
+inheritsTracked =
+  let p = Proxy :: Proxy (Tracked Identity ())
+   in [inheritFilePather p]
diff --git a/src/System/FilePather/Trail.hs b/src/System/FilePather/Trail.hs
--- a/src/System/FilePather/Trail.hs
+++ b/src/System/FilePather/Trail.hs
@@ -1,171 +1,52 @@
-{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE ConstraintKinds #-}
 {-# LANGUAGE DeriveDataTypeable #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE TypeOperators #-}
 {-# OPTIONS_GHC -Wall #-}
 
-{- | How a file came to be built, and why a chain of build steps is stopped.
+{- | The steps that built a file.
 
-A 'Step' is one application of a named build step to a file. A 'Trail' is the
-steps that built a file, from the file back to its source file. A 'Cycle' is
-the reason a chain of steps is stopped: a step would make a file that is
-already in the chain, or a step is already in the chain.
+A 'Trail' is a product with one field, named 'steps'.
 -}
 module System.FilePather.Trail (
-  -- * Steps
-  Step (..),
-  stepName,
-  stepInput,
-  GetStep (..),
-  HasStep (..),
-  ReviewStep (..),
-  AsStep (..),
-
-  -- * Trails
+  -- * Type
   Trail (..),
+
+  -- * Traversals
   trailFiles,
   lastStep,
-  GetTrail (..),
-  HasTrail (..),
-  ReviewTrail (..),
-  AsTrail (..),
 
-  -- * Repetition
-  Repetition (..),
-  _Once,
-  _Repeatable,
-  GetRepetition (..),
-  HasRepetition (..),
-  ReviewRepetition (..),
-  AsRepetition (..),
-
-  -- * Cycles
-  Cycle (..),
-  _ReproducesInput,
-  _RepeatsStep,
-  cycleStep,
-  cycleTrail,
-  checkStep,
-  displayCycle,
-  GetCycle (..),
-  HasCycle (..),
-  ReviewCycle (..),
-  AsCycle (..),
+  -- * Optics
+  TrailLike (..),
+  lawNamesTrail,
+  InheritTrail,
+  inheritTrail,
 ) where
 
 import Control.DeepSeq (NFData)
-import Control.Lens (AsEmpty (..), Cons (..), Each (..), Field1, Field2, Getter, Index, IxValue, Ixed (..), Lens', Prism', Reversing (..), Review, Rewrapped, Snoc (..), Traversal', Wrapped (..), iso, lens, nearly, prism, prism', review, toListOf, unsnoc, unto, view, _1, _2, _Wrapped, _head)
+import Control.Lens (AsEmpty (..), Cons (..), Each (..), Index, IxValue, Ixed (..), Optic, Reversing (..), Snoc (..), Traversal', cloneLens, nearly, prism, unsnoc, view, _head)
 import Data.Data (Data)
-import Data.Lens.Injection (Injection1 (..), Injection2 (..))
-import Data.List (find, intercalate)
+import Data.Functor.Const (Const)
 import GHC.Generics (Generic)
-import System.FilePath (normalise)
+import System.FilePather.Name.StepInput (StepInputLike (..))
+import System.FilePather.Name.Steps (StepsLike (..))
+import System.FilePather.Step (Step)
 
 {- $setup
->>> import Control.Lens ((^.), (^..), (^?), (#))
+>>> import Control.Lens ((^..), (^?))
+>>> import System.FilePather.Step (Step (..))
+>>> import System.FilePather.Name.StepName (stepName)
 -}
 
-{- | One application of a named build step to a file: the name of the step,
-such as @"pdf page 1"@, and its input file.
-
->>> Step "pdf page 1" "public/a.pdf" ^. stepName
-"pdf page 1"
-
-@since 0.7.0.0
--}
-data Step
-  = Step String FilePath
-  deriving stock (Eq, Ord, Show, Generic, Data)
-  deriving anyclass (NFData)
-
--- | @since 0.7.0.0
-instance Field1 Step Step String String
-
--- | @since 0.7.0.0
-instance Field2 Step Step FilePath FilePath
-
-{- | The name of a step.
-
-@since 0.7.0.0
--}
-stepName :: Lens' Step String
-stepName =
-  _1
-{-# INLINE stepName #-}
-
-{- | The input file of a step.
-
->>> Step "pdf page 1" "public/a.pdf" ^. stepInput
-"public/a.pdf"
-
-@since 0.7.0.0
--}
-stepInput :: Lens' Step FilePath
-stepInput =
-  _2
-{-# INLINE stepInput #-}
-
--- | @since 0.7.0.0
-class GetStep s where
-  getStep :: Getter s Step
-
--- | @since 0.7.0.0
-instance GetStep Step where
-  getStep =
-    id
-  {-# INLINE getStep #-}
-
--- | @since 0.7.0.0
-class (GetStep s) => HasStep s where
-  {-# MINIMAL setStep #-}
-  setStep :: Step -> s -> s
-
-  stepLens :: Lens' s Step
-  stepLens =
-    lens (view getStep) (flip setStep)
-  {-# INLINE stepLens #-}
-
--- | @since 0.7.0.0
-instance HasStep Step where
-  setStep =
-    const
-  {-# INLINE setStep #-}
-
--- | @since 0.7.0.0
-class ReviewStep t where
-  reviewStep :: Review t Step
-
--- | @since 0.7.0.0
-instance ReviewStep Step where
-  reviewStep =
-    unto id
-  {-# INLINE reviewStep #-}
-
--- | @since 0.7.0.0
-class (ReviewStep t) => AsStep t where
-  {-# MINIMAL matchStep #-}
-  matchStep :: t -> Maybe Step
-
-  _Step :: Prism' t Step
-  _Step =
-    prism' (review reviewStep) matchStep
-  {-# INLINE _Step #-}
-
--- | @since 0.7.0.0
-instance AsStep Step where
-  matchStep =
-    Just
-  {-# INLINE matchStep #-}
-
-{- | The steps that built a file, nearest first: the step that made the file,
-then the step that made its input, and so on, back to the step whose input
-is a source file. A source file has the empty trail.
+{- | The steps that built a file ('steps'), nearest first: the step that made
+the file, then the step that made its input, and so on, back to the step
+whose input is a source file. A source file has the empty trail.
 
 @since 0.7.0.0
 -}
@@ -175,16 +56,6 @@
   deriving newtype (NFData, Semigroup, Monoid)
 
 -- | @since 0.7.0.0
-instance Wrapped Trail where
-  type Unwrapped Trail = [Step]
-  _Wrapped' =
-    iso (\(Trail x) -> x) Trail
-  {-# INLINE _Wrapped' #-}
-
--- | @since 0.7.0.0
-instance (t ~ Trail) => Rewrapped Trail t
-
--- | @since 0.7.0.0
 instance Cons Trail Trail Step Step where
   _Cons =
     prism
@@ -213,7 +84,7 @@
 -- | @since 0.7.0.0
 instance Each Trail Trail Step Step where
   each =
-    _Wrapped . traverse
+    steps . traverse
   {-# INLINE each #-}
 
 -- | @since 0.7.0.0
@@ -225,7 +96,7 @@
 -- | @since 0.7.0.0
 instance Ixed Trail where
   ix i =
-    _Wrapped . ix i
+    steps . ix i
   {-# INLINE ix #-}
 
 -- | @since 0.7.0.0
@@ -259,317 +130,60 @@
 -}
 lastStep :: Traversal' Trail Step
 lastStep =
-  _Wrapped . _head
+  steps . _head
 {-# INLINE lastStep #-}
 
--- | @since 0.7.0.0
-class GetTrail s where
-  getTrail :: Getter s Trail
-
--- | @since 0.7.0.0
-instance GetTrail Trail where
-  getTrail =
-    id
-  {-# INLINE getTrail #-}
-
--- | @since 0.7.0.0
-class (GetTrail s) => HasTrail s where
-  {-# MINIMAL setTrail #-}
-  setTrail :: Trail -> s -> s
-
-  trail :: Lens' s Trail
-  trail =
-    lens (view getTrail) (flip setTrail)
-  {-# INLINE trail #-}
-
--- | @since 0.7.0.0
-instance HasTrail Trail where
-  setTrail =
-    const
-  {-# INLINE setTrail #-}
-
--- | @since 0.7.0.0
-class ReviewTrail t where
-  reviewTrail :: Review t Trail
-
--- | @since 0.7.0.0
-instance ReviewTrail Trail where
-  reviewTrail =
-    unto id
-  {-# INLINE reviewTrail #-}
-
--- | @since 0.7.0.0
-class (ReviewTrail t) => AsTrail t where
-  {-# MINIMAL matchTrail #-}
-  matchTrail :: t -> Maybe Trail
-
-  _Trail :: Prism' t Trail
-  _Trail =
-    prism' (review reviewTrail) matchTrail
-  {-# INLINE _Trail #-}
-
--- | @since 0.7.0.0
-instance AsTrail Trail where
-  matchTrail =
-    Just
-  {-# INLINE matchTrail #-}
-
-{- | Whether a step may appear more than once in a trail. A step that may
-not is stopped with 'RepeatsStep' when it is already in the trail.
+{- | The class of 'Trail'.
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-data Repetition
-  = -- | The step may appear once in a trail.
-    Once
-  | -- | The step may appear any number of times in a trail.
-    Repeatable
-  deriving stock (Eq, Ord, Show, Generic, Data)
-  deriving anyclass (NFData)
-
--- | @since 0.7.0.0
-instance Injection1 Repetition Repetition () ()
-
--- | @since 0.7.0.0
-instance Injection2 Repetition Repetition () ()
-
--- | @since 0.7.0.0
-_Once :: Prism' Repetition ()
-_Once =
-  _I1
-{-# INLINE _Once #-}
-
--- | @since 0.7.0.0
-_Repeatable :: Prism' Repetition ()
-_Repeatable =
-  _I2
-{-# INLINE _Repeatable #-}
-
--- | @since 0.7.0.0
-class GetRepetition s where
-  getRepetition :: Getter s Repetition
-
--- | @since 0.7.0.0
-instance GetRepetition Repetition where
-  getRepetition =
-    id
-  {-# INLINE getRepetition #-}
-
--- | @since 0.7.0.0
-class (GetRepetition s) => HasRepetition s where
-  {-# MINIMAL setRepetition #-}
-  setRepetition :: Repetition -> s -> s
-
-  repetition :: Lens' s Repetition
-  repetition =
-    lens (view getRepetition) (flip setRepetition)
-  {-# INLINE repetition #-}
-
--- | @since 0.7.0.0
-instance HasRepetition Repetition where
-  setRepetition =
-    const
-  {-# INLINE setRepetition #-}
-
--- | @since 0.7.0.0
-class ReviewRepetition t where
-  reviewRepetition :: Review t Repetition
-
--- | @since 0.7.0.0
-instance ReviewRepetition Repetition where
-  reviewRepetition =
-    unto id
-  {-# INLINE reviewRepetition #-}
-
--- | @since 0.7.0.0
-class (ReviewRepetition t) => AsRepetition t where
-  {-# MINIMAL matchRepetition #-}
-  matchRepetition :: t -> Maybe Repetition
-
-  _Repetition :: Prism' t Repetition
-  _Repetition =
-    prism' (review reviewRepetition) matchRepetition
-  {-# INLINE _Repetition #-}
-
--- | @since 0.7.0.0
-instance AsRepetition Repetition where
-  matchRepetition =
-    Just
-  {-# INLINE matchRepetition #-}
+class TrailLike p f x x' where
+  trail ::
+    Optic p f x x' Trail Trail
 
-{- | Why a chain of build steps is stopped, with the step that is stopped and
-the trail of its input file.
+{- | Identity
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-data Cycle
-  = -- | The step would make a file that is already in the chain: its input
-    -- file, or an input file of a step in the trail.
-    ReproducesInput FilePath Step Trail
-  | -- | The step is already in the trail.
-    RepeatsStep Step Trail
-  deriving stock (Eq, Ord, Show, Generic, Data)
-  deriving anyclass (NFData)
-
--- | @since 0.7.0.0
-instance Injection1 Cycle Cycle (FilePath, Step, Trail) (FilePath, Step, Trail)
-
--- | @since 0.7.0.0
-instance Injection2 Cycle Cycle (Step, Trail) (Step, Trail)
+instance (x' ~ Trail) => TrailLike p f Trail x' where
+  trail =
+    id
+  {-# INLINE trail #-}
 
-{- | >>> _RepeatsStep # (Step "pdf page 1" "a_page1.pdf", Trail [])
-RepeatsStep (Step "pdf page 1" "a_page1.pdf") (Trail [])
+{- | Lens
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-_ReproducesInput :: Prism' Cycle (FilePath, Step, Trail)
-_ReproducesInput =
-  _I1
-{-# INLINE _ReproducesInput #-}
-
--- | @since 0.7.0.0
-_RepeatsStep :: Prism' Cycle (Step, Trail)
-_RepeatsStep =
-  _I2
-{-# INLINE _RepeatsStep #-}
+instance (p ~ (->), Functor f, x' ~ Trail) => StepsLike p f Trail x' where
+  steps f (Trail x) =
+    fmap Trail (f x)
+  {-# INLINE steps #-}
 
-{- | The step that is stopped.
+{- | Rebuild a value using only the name of each field.
 
-@since 0.7.0.0
--}
-cycleStep :: Lens' Cycle Step
-cycleStep =
-  lens
-    ( \case
-        ReproducesInput _ s _ -> s
-        RepeatsStep s _ -> s
-    )
-    ( \c s ->
-        case c of
-          ReproducesInput o _ t -> ReproducesInput o s t
-          RepeatsStep _ t -> RepeatsStep s t
-    )
-{-# INLINE cycleStep #-}
+Laws:
 
-{- | The trail of the input file of the step that is stopped.
+> lawNamesTrail == id
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-cycleTrail :: Lens' Cycle Trail
-cycleTrail =
-  lens
-    ( \case
-        ReproducesInput _ _ t -> t
-        RepeatsStep _ t -> t
-    )
-    ( \c t ->
-        case c of
-          ReproducesInput o s _ -> ReproducesInput o s t
-          RepeatsStep s _ -> RepeatsStep s t
-    )
-{-# INLINE cycleTrail #-}
-
-{- | Check a step, with the files it makes, against the trail of its input
-file. A step that is already in the trail, and may appear 'Once', is a
-'RepeatsStep'. Otherwise, a step that makes its input file, or the input file
-of a step in the trail, is a 'ReproducesInput'. File paths are compared after
-normalisation.
-
->>> let t = Trail [Step "markdown to pdf" "src/a.md"]
->>> checkStep Once (Step "pdf page 1" "public/a.pdf") ["public/a_page1.pdf"] t
-Right ()
-
->>> checkStep Once (Step "markdown to pdf" "public/a.pdf") ["public/b.pdf"] t
-Left (RepeatsStep (Step "markdown to pdf" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"]))
-
->>> checkStep Repeatable (Step "markdown to pdf" "public/a.pdf") ["public/b.pdf"] t
-Right ()
+lawNamesTrail :: Trail -> Trail
+lawNamesTrail s =
+  Trail (view (cloneLens steps) s)
 
->>> checkStep Once (Step "pdf to markdown" "public/a.pdf") ["./src/a.md"] t
-Left (ReproducesInput "./src/a.md" (Step "pdf to markdown" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"]))
+{- | The classes that a data type holding 'Trail' inherits.
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-checkStep :: Repetition -> Step -> [FilePath] -> Trail -> Either Cycle ()
-checkStep r s@(Step n i) os t =
-  let repeated =
-        case r of
-          Once -> n `elem` toListOf (each . stepName) t
-          Repeatable -> False
-      files = fmap normalise (i : toListOf trailFiles t)
-   in if repeated
-        then Left (RepeatsStep s t)
-        else maybe (Right ()) (\o -> Left (ReproducesInput o s t)) (find ((`elem` files) . normalise) os)
-
-{- | A description of a cycle, for people to read: the chain of files and
-steps from the source file.
-
->>> putStrLn (displayCycle (RepeatsStep (Step "pdf page 1" "public/a_page1.pdf") (Trail [Step "pdf page 1" "public/a.pdf", Step "markdown to pdf" "src/a.md"])))
-src/a.md → markdown to pdf → public/a.pdf → pdf page 1 → public/a_page1.pdf → pdf page 1 again
+type InheritTrail s =
+  ( TrailLike (->) (Const ()) s s
+  , StepsLike (->) (Const ()) s s
+  )
 
->>> putStrLn (displayCycle (ReproducesInput "src/a.md" (Step "pdf to markdown" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"])))
-src/a.md → markdown to pdf → public/a.pdf → pdf to markdown → src/a.md, which is already in the chain
+{- | Check, where it is applied, that @s@ inherits every name of 'Trail'.
 
-@since 0.7.0.0
+@since 0.8.0.0
 -}
-displayCycle :: Cycle -> String
-displayCycle c =
-  let chain (Trail ss) = concatMap (\(Step n i) -> [i, n]) (reverse ss)
-      arrows = intercalate " → "
-   in case c of
-        RepeatsStep (Step n i) t ->
-          arrows (chain t <> [i, n <> " again"])
-        ReproducesInput o (Step n i) t ->
-          arrows (chain t <> [i, n, o]) <> ", which is already in the chain"
-
--- | @since 0.7.0.0
-class GetCycle s where
-  getCycle :: Getter s Cycle
-
--- | @since 0.7.0.0
-instance GetCycle Cycle where
-  getCycle =
-    id
-  {-# INLINE getCycle #-}
-
--- | @since 0.7.0.0
-class (GetCycle s) => HasCycle s where
-  {-# MINIMAL setCycle #-}
-  setCycle :: Cycle -> s -> s
-
-  cycleLens :: Lens' s Cycle
-  cycleLens =
-    lens (view getCycle) (flip setCycle)
-  {-# INLINE cycleLens #-}
-
--- | @since 0.7.0.0
-instance HasCycle Cycle where
-  setCycle =
-    const
-  {-# INLINE setCycle #-}
-
--- | @since 0.7.0.0
-class ReviewCycle t where
-  reviewCycle :: Review t Cycle
-
--- | @since 0.7.0.0
-instance ReviewCycle Cycle where
-  reviewCycle =
-    unto id
-  {-# INLINE reviewCycle #-}
-
--- | @since 0.7.0.0
-class (ReviewCycle t) => AsCycle t where
-  {-# MINIMAL matchCycle #-}
-  matchCycle :: t -> Maybe Cycle
-
-  _Cycle :: Prism' t Cycle
-  _Cycle =
-    prism' (review reviewCycle) matchCycle
-  {-# INLINE _Cycle #-}
-
--- | @since 0.7.0.0
-instance AsCycle Cycle where
-  matchCycle =
-    Just
-  {-# INLINE matchCycle #-}
+inheritTrail :: (InheritTrail s) => proxy s -> ()
+inheritTrail _ =
+  ()
diff --git a/test/FilePatherTests.hs b/test/FilePatherTests.hs
--- a/test/FilePatherTests.hs
+++ b/test/FilePatherTests.hs
@@ -21,7 +21,7 @@
 ) where
 
 import Control.Applicative (Alternative (..), Const (..))
-import Control.Lens (view, _Wrapped)
+import Control.Lens (cloneLens, view)
 import Control.Monad ((>=>))
 import Control.Monad.Fix (fix, mfix)
 import Control.Monad.Reader (ask, asks, local)
@@ -49,7 +49,7 @@
 -- | Apply a 'FilePather' to a file path.
 at :: FilePather f a -> FilePath -> f a
 at =
-  view _Wrapped
+  view (cloneLens pathFunction)
 
 -- | A Kleisli arrow from 'Int' to 'FilePather' @[]@.
 genKleisli :: Gen (Fn (Int, Int) [Int])
diff --git a/test/Gens.hs b/test/Gens.hs
--- a/test/Gens.hs
+++ b/test/Gens.hs
@@ -13,14 +13,24 @@
   genFilePatherMaybe,
   toFilePather,
   toFilePatherFn,
+
+  -- * Data types
+  genName,
+  genStep,
+  genTrail,
+  genRepetition,
+  genReproducesInput,
+  genRepeatsStep,
+  genCycle,
 ) where
 
+import Control.Lens (review)
 import Data.Char (ord)
 import Hedgehog (Gen)
 import Hedgehog.Function (Fn, apply, fn)
 import qualified Hedgehog.Gen as Gen
 import qualified Hedgehog.Range as Range
-import System.FilePather (FilePather (..))
+import System.FilePather
 
 genInt :: Gen Int
 genInt =
@@ -38,7 +48,7 @@
 pathKey =
   foldr (\c k -> (k * 31 + ord c) `mod` 1000003) 7
 
--- | A file path, such as @src/a/cv.fodt@.
+-- | A file path, such as @src/a/report.fodt@.
 genPath :: Gen FilePath
 genPath =
   let part = Gen.string (Range.linear 1 6) Gen.alphaNum
@@ -81,3 +91,38 @@
 toFilePatherFn :: (Functor f) => Fn Int (f (Fn Int Int)) -> FilePather f (Int -> Int)
 toFilePatherFn f =
   FilePather (fmap apply . apply f . pathKey)
+
+-- | The name of a step.
+genName :: Gen String
+genName =
+  Gen.element ["prepare", "pdf page 1", "libreoffice pdf", "image png", "book"]
+
+{- | A 'Step': a product, so its constructor applied to a generator of each
+field, in the shape of its manifest.
+-}
+genStep :: Gen Step
+genStep =
+  Step <$> genName <*> genPath
+
+genTrail :: Gen Trail
+genTrail =
+  Trail <$> Gen.list (Range.linear 0 3) genStep
+
+{- | A 'Repetition': a sum, so one alternative for each constructor, through
+its name.
+-}
+genRepetition :: Gen Repetition
+genRepetition =
+  Gen.choice [pure (review once ()), pure (review repeatable ())]
+
+genReproducesInput :: Gen ReproducesInput
+genReproducesInput =
+  ReproducesInput <$> genPath <*> genStep <*> genTrail
+
+genRepeatsStep :: Gen RepeatsStep
+genRepeatsStep =
+  RepeatsStep <$> genStep <*> genTrail
+
+genCycle :: Gen Cycle
+genCycle =
+  Gen.choice [review reproducesInput <$> genReproducesInput, review repeatsStep <$> genRepeatsStep]
diff --git a/test/Laws.hs b/test/Laws.hs
--- a/test/Laws.hs
+++ b/test/Laws.hs
@@ -1,19 +1,60 @@
 {-# LANGUAGE RankNTypes #-}
 {-# OPTIONS_GHC -Wall #-}
 
--- | Laws of lenses and prisms.
+{- | Laws of the kinds of optics that the instances of names have: identity,
+lens, prism and affine traversal, and the law of a manifest.
+
+Each law reads the optic through 'cloneLens' for a lens, 'clonePrism' for a
+prism and 'cloneTraversal' for an affine traversal, so a test fails to
+compile where an instance is of a weaker kind than its comment says.
+-}
 module Laws (
   Laws,
+  manifestLaw,
+  identityLaws,
   lensLaws,
   prismLaws,
+  affineLaws,
 ) where
 
-import Control.Lens (ALens', APrism', cloneLens, clonePrism, preview, review, set, view)
+import Control.Lens (ALens', APrism', ATraversal', cloneLens, clonePrism, cloneTraversal, preview, review, set, toListOf, traverseOf, view)
+import Data.Char (ord)
+import Data.Functor.Compose (Compose (..))
+import Data.Functor.Identity (Identity (..))
 import Data.String (fromString)
 import Hedgehog
+import Hedgehog.Function (fn, forAllFn)
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
 
 type Laws = [(PropertyName, Property)]
 
+-- | The law of a manifest: it rebuilds every value as it was.
+manifestLaw :: (Eq s, Show s) => String -> (s -> s) -> Gen s -> Laws
+manifestLaw name m genS =
+  [
+    ( fromString (name <> " == id")
+    , property $
+        do
+          s <- forAll genS
+          m s === s
+    )
+  ]
+
+-- | The laws of an identity: viewing gives the value, and setting replaces it.
+identityLaws :: (Eq s, Show s) => String -> ALens' s s -> Gen s -> Laws
+identityLaws name o genS =
+  [
+    ( fromString (name <> " identity")
+    , property $
+        do
+          s <- forAll genS
+          t <- forAll genS
+          view (cloneLens o) s === s
+          set (cloneLens o) t s === t
+    )
+  ]
+
 -- | The lens laws: view-set, set-view and set-set.
 lensLaws :: (Eq s, Show s, Eq a, Show a) => String -> ALens' s a -> Gen s -> Gen a -> Laws
 lensLaws name l genS genA =
@@ -61,3 +102,41 @@
           maybe s (review (clonePrism p)) (preview (clonePrism p) s) === s
     )
   ]
+
+{- | The laws of an affine traversal: identity, composition, and at most one
+target. The functions of the composition law are generated with hedgehog-fn,
+over a key that each target is mapped to by its 'show', so that targets
+without an instance of 'Hedgehog.Function.Arg', such as 'String', are
+covered.
+-}
+affineLaws :: (Eq s, Show s, Eq a, Show a) => String -> ATraversal' s a -> Gen s -> Gen a -> Laws
+affineLaws name o genS genA =
+  [
+    ( fromString (name <> " traverse pure")
+    , property $
+        do
+          s <- forAll genS
+          traverseOf (cloneTraversal o) Identity s === Identity s
+    )
+  ,
+    ( fromString (name <> " traverse composition")
+    , property $
+        do
+          s <- forAll genS
+          f <- (. key) <$> forAllFn (fn (Gen.maybe genA))
+          g <- (. key) <$> forAllFn (fn (Gen.list (Range.linear 0 3) genA))
+          fmap (traverseOf (cloneTraversal o) f) (traverseOf (cloneTraversal o) g s)
+            === getCompose (traverseOf (cloneTraversal o) (Compose . fmap f . g) s)
+    )
+  ,
+    ( fromString (name <> " at most one target")
+    , property $
+        do
+          s <- forAll genS
+          assert (length (toListOf (cloneTraversal o) s) <= 1)
+    )
+  ]
+ where
+  key :: (Show a) => a -> Int
+  key =
+    foldr (\c k -> (k * 31 + ord c) `mod` 1000003) 7 . show
diff --git a/test/OpticsTests.hs b/test/OpticsTests.hs
new file mode 100644
--- /dev/null
+++ b/test/OpticsTests.hs
@@ -0,0 +1,312 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -Wall #-}
+
+{- | The laws of the classy optics of every data type: the coverage of each
+sum by its generator, the manifest of each data type, and the laws of the
+kind of every instance of every name, including the identity instances of
+names and the inherited instances.
+
+'FilePather' and 'Tracked' hold functions, so they are observed by applying
+them to a generated file path, and trail.
+-}
+module OpticsTests (
+  opticsTests,
+) where
+
+import Control.Lens (cloneLens, from, has, set, view)
+import Control.Monad.Reader (ReaderT, runReaderT)
+import Control.Monad.Trans.Except (ExceptT, runExceptT)
+import Gens
+import Hedgehog
+import Hedgehog.Function (Fn, apply, fn)
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+import Laws
+import System.FilePather
+import Prelude hiding (cycle)
+
+opticsTests :: Laws
+opticsTests =
+  [ ("cover Repetition", prop_coverRepetition)
+  , ("cover Cycle", prop_coverCycle)
+  , ("inherits", prop_inherits)
+  ]
+    -- Identity instances of names, for their target types
+    <> identityLaws "stepName String" stepName genName
+    <> identityLaws "stepInput FilePath" stepInput genPath
+    <> identityLaws "steps [Step]" steps (Gen.list (Range.linear 0 3) genStep)
+    <> identityLaws "once ()" once (pure ())
+    <> identityLaws "repeatable ()" repeatable (pure ())
+    <> identityLaws "file FilePath" file genPath
+    <> [("pathFunction (FilePath -> f a) identity", prop_identityPathFunction)]
+    -- Step
+    <> manifestLaw "lawNamesStep" lawNamesStep genStep
+    <> identityLaws "step Step" step genStep
+    <> lensLaws "stepName Step" stepName genStep genName
+    <> lensLaws "stepInput Step" stepInput genStep genPath
+    -- Trail
+    <> manifestLaw "lawNamesTrail" lawNamesTrail genTrail
+    <> identityLaws "trail Trail" trail genTrail
+    <> lensLaws "steps Trail" steps genTrail (Gen.list (Range.linear 0 3) genStep)
+    -- Repetition
+    <> manifestLaw "lawNamesRepetition" lawNamesRepetition genRepetition
+    <> identityLaws "repetition Repetition" repetition genRepetition
+    <> prismLaws "once Repetition" once genRepetition (pure ())
+    <> prismLaws "repeatable Repetition" repeatable genRepetition (pure ())
+    -- ReproducesInput
+    <> manifestLaw "lawNamesReproducesInput" lawNamesReproducesInput genReproducesInput
+    <> identityLaws "reproducesInput ReproducesInput" reproducesInput genReproducesInput
+    <> lensLaws "file ReproducesInput" file genReproducesInput genPath
+    <> lensLaws "step ReproducesInput" step genReproducesInput genStep
+    <> lensLaws "trail ReproducesInput" trail genReproducesInput genTrail
+    <> lensLaws "stepName ReproducesInput" stepName genReproducesInput genName
+    <> lensLaws "stepInput ReproducesInput" stepInput genReproducesInput genPath
+    <> lensLaws "steps ReproducesInput" steps genReproducesInput (Gen.list (Range.linear 0 3) genStep)
+    -- RepeatsStep
+    <> manifestLaw "lawNamesRepeatsStep" lawNamesRepeatsStep genRepeatsStep
+    <> identityLaws "repeatsStep RepeatsStep" repeatsStep genRepeatsStep
+    <> lensLaws "step RepeatsStep" step genRepeatsStep genStep
+    <> lensLaws "trail RepeatsStep" trail genRepeatsStep genTrail
+    <> lensLaws "stepName RepeatsStep" stepName genRepeatsStep genName
+    <> lensLaws "stepInput RepeatsStep" stepInput genRepeatsStep genPath
+    <> lensLaws "steps RepeatsStep" steps genRepeatsStep (Gen.list (Range.linear 0 3) genStep)
+    -- Cycle
+    <> manifestLaw "lawNamesCycle" lawNamesCycle genCycle
+    <> identityLaws "cycle Cycle" cycle genCycle
+    <> prismLaws "reproducesInput Cycle" reproducesInput genCycle genReproducesInput
+    <> prismLaws "repeatsStep Cycle" repeatsStep genCycle genRepeatsStep
+    <> affineLaws "file Cycle" file genCycle genPath
+    <> affineLaws "step Cycle" step genCycle genStep
+    <> affineLaws "trail Cycle" trail genCycle genTrail
+    <> affineLaws "stepName Cycle" stepName genCycle genName
+    <> affineLaws "stepInput Cycle" stepInput genCycle genPath
+    <> affineLaws "steps Cycle" steps genCycle (Gen.list (Range.linear 0 3) genStep)
+    -- FilePather
+    <> [ ("lawNamesFilePather == id", prop_lawNamesFilePather)
+       , ("filePather FilePather identity", prop_identityFilePather)
+       , ("pathFunction FilePather view . set", prop_pathFunctionViewSet)
+       , ("pathFunction FilePather set . view", prop_pathFunctionSetView)
+       , ("pathFunction FilePather set . set", prop_pathFunctionSetSet)
+       , -- Tracked
+         ("lawNamesTracked == id", prop_lawNamesTracked)
+       , ("tracked Tracked identity", prop_identityTracked)
+       , ("filePather Tracked view . set", prop_filePatherTrackedViewSet)
+       , ("filePather Tracked set . view", prop_filePatherTrackedSetView)
+       , ("filePather Tracked set . set", prop_filePatherTrackedSetSet)
+       , ("pathFunction Tracked view . set", prop_pathFunctionTrackedViewSet)
+       , ("pathFunction Tracked set . view", prop_pathFunctionTrackedSetView)
+       , ("pathFunction Tracked set . set", prop_pathFunctionTrackedSetSet)
+       ]
+
+prop_coverRepetition :: Property
+prop_coverRepetition =
+  property $
+    do
+      x <- forAll genRepetition
+      cover 10 "once" (has once x)
+      cover 10 "repeatable" (has repeatable x)
+
+prop_coverCycle :: Property
+prop_coverCycle =
+  property $
+    do
+      x <- forAll genCycle
+      cover 10 "reproducesInput" (has reproducesInput x)
+      cover 10 "repeatsStep" (has repeatsStep x)
+
+-- | The checks of inheritance compile, so each is @()@.
+prop_inherits :: Property
+prop_inherits =
+  withTests 1 . property $
+    concat [inheritsReproducesInput, inheritsRepeatsStep, inheritsCycle, inheritsTracked] === replicate 7 ()
+
+-- FilePather, observed at a file path
+
+genFilePather :: Gen (Fn Int [Int])
+genFilePather =
+  genFilePatherList
+
+-- | A function from file paths, to set with 'pathFunction'.
+genPathFunction :: Gen (Fn Int [Int])
+genPathFunction =
+  fn genIntList
+
+pathFunctionOf :: Fn Int [Int] -> FilePath -> [Int]
+pathFunctionOf f =
+  apply f . pathKey
+
+at :: FilePather f a -> FilePath -> f a
+at =
+  view (cloneLens pathFunction)
+
+prop_identityPathFunction :: Property
+prop_identityPathFunction =
+  property $
+    do
+      g <- pathFunctionOf <$> forAll genPathFunction
+      h <- pathFunctionOf <$> forAll genPathFunction
+      p <- forAll genPath
+      view (cloneLens pathFunction) g p === g p
+      set pathFunction h g p === h p
+
+prop_lawNamesFilePather :: Property
+prop_lawNamesFilePather =
+  property $
+    do
+      x <- toFilePather <$> forAll genFilePather
+      p <- forAll genPath
+      at (lawNamesFilePather x) p === at x p
+
+prop_identityFilePather :: Property
+prop_identityFilePather =
+  property $
+    do
+      x <- toFilePather <$> forAll genFilePather
+      y <- toFilePather <$> forAll genFilePather
+      p <- forAll genPath
+      at (view (cloneLens filePather) x) p === at x p
+      at (set filePather y x) p === at y p
+
+prop_pathFunctionViewSet :: Property
+prop_pathFunctionViewSet =
+  property $
+    do
+      x <- toFilePather <$> forAll genFilePather
+      g <- pathFunctionOf <$> forAll genPathFunction
+      p <- forAll genPath
+      view (cloneLens pathFunction) (set pathFunction g x) p === g p
+
+prop_pathFunctionSetView :: Property
+prop_pathFunctionSetView =
+  property $
+    do
+      x <- toFilePather <$> forAll genFilePather
+      p <- forAll genPath
+      at (set pathFunction (view (cloneLens pathFunction) x) x) p === at x p
+
+prop_pathFunctionSetSet :: Property
+prop_pathFunctionSetSet =
+  property $
+    do
+      x <- toFilePather <$> forAll genFilePather
+      g <- pathFunctionOf <$> forAll genPathFunction
+      h <- pathFunctionOf <$> forAll genPathFunction
+      p <- forAll genPath
+      at (set pathFunction h (set pathFunction g x)) p === at (set pathFunction h x) p
+
+-- Tracked, observed at a trail and a file path
+
+type Result a = Either Cycle (Outputs, a)
+
+genResult :: Gen (Result Int)
+genResult =
+  Gen.frequency
+    [ (1, Left <$> genCycle)
+    , (4, fmap Right ((,) <$> Gen.list (Range.linear 0 2) ((,) <$> genPath <*> genStep) <*> genInt))
+    ]
+
+genTracked :: Gen (Fn Int [Result Int])
+genTracked =
+  fn (Gen.list (Range.linear 0 2) genResult)
+
+toTracked :: Fn Int [Result Int] -> Tracked [] Int
+toTracked f =
+  view (from trackedFunction) (\t p -> apply f (pathKey (p <> show t)))
+
+run :: Tracked f a -> Trail -> FilePath -> f (Result a)
+run =
+  view trackedFunction
+
+prop_lawNamesTracked :: Property
+prop_lawNamesTracked =
+  property $
+    do
+      x <- toTracked <$> forAll genTracked
+      t <- forAll genTrail
+      p <- forAll genPath
+      run (lawNamesTracked x) t p === run x t p
+
+prop_identityTracked :: Property
+prop_identityTracked =
+  property $
+    do
+      x <- toTracked <$> forAll genTracked
+      y <- toTracked <$> forAll genTracked
+      t <- forAll genTrail
+      p <- forAll genPath
+      run (view (cloneLens tracked) x) t p === run x t p
+      run (set tracked y x) t p === run y t p
+
+-- | The type constructor of the 'FilePather' of a 'Tracked' value in @[]@.
+type Inner = ReaderT Trail (ExceptT Cycle [])
+
+runInner :: Inner a -> Trail -> [Either Cycle a]
+runInner r t =
+  runExceptT (runReaderT r t)
+
+{- | The 'FilePather' of a generated 'Tracked' value, to set with
+'filePather'.
+-}
+genTrackedFilePather :: PropertyT IO (FilePather Inner (Outputs, Int))
+genTrackedFilePather =
+  view (cloneLens filePather) . toTracked <$> forAll genTracked
+
+prop_filePatherTrackedViewSet :: Property
+prop_filePatherTrackedViewSet =
+  property $
+    do
+      x <- toTracked <$> forAll genTracked
+      y <- genTrackedFilePather
+      t <- forAll genTrail
+      p <- forAll genPath
+      runInner (at (view (cloneLens filePather) (set filePather y x)) p) t === runInner (at y p) t
+
+prop_filePatherTrackedSetView :: Property
+prop_filePatherTrackedSetView =
+  property $
+    do
+      x <- toTracked <$> forAll genTracked
+      t <- forAll genTrail
+      p <- forAll genPath
+      run (set filePather (view (cloneLens filePather) x) x) t p === run x t p
+
+prop_filePatherTrackedSetSet :: Property
+prop_filePatherTrackedSetSet =
+  property $
+    do
+      x <- toTracked <$> forAll genTracked
+      y <- genTrackedFilePather
+      z <- genTrackedFilePather
+      t <- forAll genTrail
+      p <- forAll genPath
+      run (set filePather z (set filePather y x)) t p === run (set filePather z x) t p
+
+prop_pathFunctionTrackedViewSet :: Property
+prop_pathFunctionTrackedViewSet =
+  property $
+    do
+      x <- toTracked <$> forAll genTracked
+      y <- at <$> genTrackedFilePather
+      t <- forAll genTrail
+      p <- forAll genPath
+      runInner (view (cloneLens pathFunction) (set pathFunction y x) p) t === runInner (y p) t
+
+prop_pathFunctionTrackedSetView :: Property
+prop_pathFunctionTrackedSetView =
+  property $
+    do
+      x <- toTracked <$> forAll genTracked
+      t <- forAll genTrail
+      p <- forAll genPath
+      run (set pathFunction (view (cloneLens pathFunction) x) x) t p === run x t p
+
+prop_pathFunctionTrackedSetSet :: Property
+prop_pathFunctionTrackedSetSet =
+  property $
+    do
+      x <- toTracked <$> forAll genTracked
+      y <- at <$> genTrackedFilePather
+      z <- at <$> genTrackedFilePather
+      t <- forAll genTrail
+      p <- forAll genPath
+      run (set pathFunction z (set pathFunction y x)) t p === run (set pathFunction z x) t p
diff --git a/test/TrackedTests.hs b/test/TrackedTests.hs
--- a/test/TrackedTests.hs
+++ b/test/TrackedTests.hs
@@ -2,7 +2,7 @@
 {-# OPTIONS_GHC -Wall #-}
 
 {- | Laws of the instances of 'Tracked', of the steps that check a trail, and
-of the optics of trails and cycles.
+of the 'Cons' and 'Snoc' instances of trails.
 
 A 'Tracked' value is a function of a trail and a file path, so it is observed
 by applying it to a generated trail and file path. Functions are generated
@@ -27,12 +27,12 @@
 import Data.Functor.Contravariant (contramap)
 import Data.Functor.Identity (Identity (..))
 import Data.Functor.Plus (zero)
-import Gens (genFnInt, genInt, genIntList, genPath, pathKey)
+import Gens (genCycle, genFnInt, genInt, genIntList, genPath, genStep, genTrail, pathKey)
 import Hedgehog
 import Hedgehog.Function (Fn, apply, fn)
 import qualified Hedgehog.Gen as Gen
 import qualified Hedgehog.Range as Range
-import Laws (Laws, lensLaws, prismLaws)
+import Laws (Laws, prismLaws)
 import System.FilePather
 import Witherable (mapMaybe)
 
@@ -54,25 +54,6 @@
 key t p =
   pathKey (p <> show t)
 
-genName :: Gen String
-genName =
-  Gen.element ["prepare", "pdf page 1", "libreoffice pdf", "image png", "book"]
-
-genStep :: Gen Step
-genStep =
-  Step <$> genName <*> genPath
-
-genTrail :: Gen Trail
-genTrail =
-  Trail <$> Gen.list (Range.linear 0 3) genStep
-
-genCycle :: Gen Cycle
-genCycle =
-  Gen.choice
-    [ ReproducesInput <$> genPath <*> genStep <*> genTrail
-    , RepeatsStep <$> genStep <*> genTrail
-    ]
-
 genOutputs :: Gen Outputs
 genOutputs =
   Gen.list (Range.linear 0 2) ((,) <$> genPath <*> genStep)
@@ -157,11 +138,11 @@
   , ("Contravariant identity", prop_contravariantIdentity)
   , ("<|> propagates a cycle", prop_altCycle)
   , ("<> propagates a cycle, and stops", prop_semigroupCycle)
-  , ("step records its outputs", prop_stepOutputs)
-  , ("step stops a repeated step before running", prop_stepRepeats)
-  , ("step stops a reproduced input before running", prop_stepReproduces)
-  , ("stepWith Repeatable allows a repeated step", prop_stepRepeatable)
-  , ("step' stops after running", prop_step'Stops)
+  , ("track records its outputs", prop_stepOutputs)
+  , ("track stops a repeated step before running", prop_stepRepeats)
+  , ("track stops a reproduced input before running", prop_stepReproduces)
+  , ("trackWith Repeatable allows a repeated step", prop_stepRepeatable)
+  , ("track' stops after running", prop_step'Stops)
   , ("whenPath const False", prop_whenPathFalse)
   , ("whenTrail const True", prop_whenTrailTrue)
   , ("onPath", prop_onPath)
@@ -169,14 +150,6 @@
   , ("untracked", prop_untracked)
   , ("hoistTracked id", prop_hoistIdentity)
   ]
-    <> lensLaws "stepName" stepName genStep genName
-    <> lensLaws "stepInput" stepInput genStep genPath
-    <> lensLaws "cycleStep" cycleStep genCycle genStep
-    <> lensLaws "cycleTrail" cycleTrail genCycle genTrail
-    <> prismLaws "_ReproducesInput" _ReproducesInput genCycle ((,,) <$> genPath <*> genStep <*> genTrail)
-    <> prismLaws "_RepeatsStep" _RepeatsStep genCycle ((,) <$> genStep <*> genTrail)
-    <> prismLaws "_Once" _Once (Gen.element [Once, Repeatable]) (pure ())
-    <> prismLaws "_Repeatable" _Repeatable (Gen.element [Once, Repeatable]) (pure ())
     <> prismLaws "Trail _Cons" _Cons genTrail ((,) <$> genStep <*> genTrail)
     <> prismLaws "Trail _Snoc" _Snoc genTrail ((,) <$> genTrail <*> genStep)
 
@@ -494,7 +467,7 @@
   property $
     do
       p <- forAll genPath
-      runWriter (at (step "copy" (\q -> [q <> ".bak"]) ran) (Trail []) p) === (Right ([(p <> ".bak", Step "copy" p)], ()), ["ran"])
+      runWriter (at (track "copy" (\q -> [q <> ".bak"]) ran) (Trail []) p) === (Right ([(p <> ".bak", Step "copy" p)], ()), ["ran"])
 
 prop_stepRepeats :: Property
 prop_stepRepeats =
@@ -504,7 +477,7 @@
       q <- forAll genPath
       p <- forAll genPath
       let t' = Trail [Step "copy" q] <> t
-      runWriter (at (step "copy" (\r -> [r <> ".bak"]) ran) t' p) === (Left (RepeatsStep (Step "copy" p) t'), [])
+      runWriter (at (track "copy" (\r -> [r <> ".bak"]) ran) t' p) === (Left (CycleRepeatsStep (RepeatsStep (Step "copy" p) t')), [])
 
 prop_stepReproduces :: Property
 prop_stepReproduces =
@@ -513,7 +486,7 @@
       q <- forAll genPath
       p <- forAll genPath
       let t = Trail [Step "prepare" q]
-      runWriter (at (step "unprepare" (const [q]) ran) t p) === (Left (ReproducesInput q (Step "unprepare" p) t), [])
+      runWriter (at (track "unprepare" (const [q]) ran) t p) === (Left (CycleReproducesInput (ReproducesInput q (Step "unprepare" p) t)), [])
 
 prop_stepRepeatable :: Property
 prop_stepRepeatable =
@@ -522,7 +495,7 @@
       q <- forAll genPath
       p <- forAll genPath
       let t = Trail [Step "copy" q]
-      fmap fst (fst (runWriter (at (stepWith Repeatable "copy" (\r -> [r <> ".bak"]) ran) t p))) === Right [(p <> ".bak", Step "copy" p)]
+      fmap fst (fst (runWriter (at (trackWith Repeatable "copy" (\r -> [r <> ".bak"]) ran) t p))) === Right [(p <> ".bak", Step "copy" p)]
 
 prop_step'Stops :: Property
 prop_step'Stops =
@@ -531,7 +504,7 @@
       q <- forAll genPath
       p <- forAll genPath
       let t = Trail [Step "prepare" q]
-      runWriter (at (step' "unprepare" (([q], ()) <$ ran)) t p) === (Left (ReproducesInput q (Step "unprepare" p) t), ["ran"])
+      runWriter (at (track' "unprepare" (([q], ()) <$ ran)) t p) === (Left (CycleReproducesInput (ReproducesInput q (Step "unprepare" p) t)), ["ran"])
 
 prop_whenPathFalse :: Property
 prop_whenPathFalse =
diff --git a/test/doctest_tests.hs b/test/doctest_tests.hs
--- a/test/doctest_tests.hs
+++ b/test/doctest_tests.hs
@@ -16,10 +16,15 @@
             , f
             ]
       )
-      [ "src/System/FilePather/Combinators.hs"
+      [ "src/System/FilePather.hs"
+      , "src/System/FilePather/Combinators.hs"
+      , "src/System/FilePather/Cycle.hs"
       , "src/System/FilePather/FilePather.hs"
-      , "src/System/FilePather/Trail.hs"
+      , "src/System/FilePather/RepeatsStep.hs"
+      , "src/System/FilePather/Repetition.hs"
+      , "src/System/FilePather/ReproducesInput.hs"
+      , "src/System/FilePather/Step.hs"
       , "src/System/FilePather/Tracked.hs"
-      , "src/System/FilePather.hs"
+      , "src/System/FilePather/Trail.hs"
       ]
   unless (all (== ExitSuccess) results) exitFailure
diff --git a/test/hedgehog_tests.hs b/test/hedgehog_tests.hs
--- a/test/hedgehog_tests.hs
+++ b/test/hedgehog_tests.hs
@@ -3,6 +3,7 @@
 import Control.Monad (unless)
 import FilePatherTests (filePatherTests)
 import Hedgehog (Group (..), checkParallel)
+import OpticsTests (opticsTests)
 import System.Exit (exitFailure)
 import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
 import TrackedTests (trackedTests)
@@ -18,6 +19,7 @@
         checkParallel
         [ Group "FilePather" filePatherTests
         , Group "Tracked" trackedTests
+        , Group "Optics" opticsTests
         ]
 
   unless result exitFailure
