darcsweb - reviews/claude-readability.md

[root] / reviews / claude-readability.md
# Readability Review — darcsweb

## Summary

Overall the project reads well for a small Scotty-based web app: modules are
split by responsibility, the domain types live in one place, and the security-
sensitive helpers are isolated in `Clone.hs`/`Darcs.hs`. The biggest sources
of friction for a first-time reader are (1) deeply nested `if … then … else`
chains in `Main.hs` (`serveStatic`, `withRepo`, `serveClone`, `getRepoBlob`)
where guards or early-return helpers would flatten the control flow
dramatically, (2) long `renderXxx` functions in `Html.hs` that interleave
string concatenation, branching and escaping in one big `T.concat` block,
and (3) a sprinkling of partial functions (`head`, `init`, `last`, `read`,
`!!`) that force the reader to reason about totality. The Coq-extracted
modules in `gen/` are, as expected, painful to read but correctly marked
un-editable at the Haskell level.

## Confusing constructs

### `app/Main.hs`

- **`app/Main.hs:74`** — `foldl (flip id) defaultOpts o` is cute but opaque.
  Current:
  ```haskell
  (o, _, [])   -> return (foldl (flip id) defaultOpts o)
  ```
  Plain rewrite:
  ```haskell
  (updates, _, []) -> return (foldr ($) defaultOpts updates)
  -- or, even more explicit:
  -- return (foldl (\opts f -> f opts) defaultOpts updates)
  ```
  `foldr ($)` is the standard idiom for "apply each endomorphism in turn";
  `flip id` hides the fact that the fold is just function application.

- **`app/Main.hs:105-120`** — The two branches of `if optDaemon opts` duplicate
  almost the entire startup (logging, building the config, calling
  `runServer`). A reader has to diff the two branches visually to see that
  only the logger and the `withSyslog` wrapper differ. Extract:
  ```haskell
  let startup logf wrap = wrap $ do
        logf $ "Starting on " ++ bind ++ ":" ++ show port
        logf $ "Serving repositories from: " ++ absRepoDir
        runServer (mkConfig port bind absRepoDir title absStaticDir logf)
  if optDaemon opts
    then do
      logStdout $ "Daemonizing (bind " ++ bind ++ ":" ++ show port ++ ")"
      hFlush stdout
      daemonize (withSyslog "darcsweb" [LogPID] Daemon (startup logSyslog id))
    else startup logStdout id
  ```

- **`app/Main.hs:346-380`** — `serveStatic` is a 35-line cascade of five
  nested `if … then … else` blocks, each with a duplicated 404 tail. Refactor
  to a helper that returns `Either NotFound FilePath` and a single responder:
  ```haskell
  serveStatic :: DarcsWebConfig -> ActionM ()
  serveStatic cfg = do
      req <- request
      mFile <- liftIO (resolveStatic cfg (pathInfo req))
      case mFile of
        Nothing    -> respond404 "Not found."
        Just path  -> do
          setHeader "Content-Type" (mimeType (takeExtension path))
          setHeader "Cache-Control" "public, max-age=86400, immutable"
          file path

  respond404 :: Text -> ActionM ()
  respond404 msg = do
      status status404
      html $ TL.fromStrict $ render404 msg
  ```
  `respond404` alone replaces six copy-pasted 2-line blocks across this
  file (lines 242, 292, 306, 320, 333, 341, 358, 368, 375, 389, 396,
  405, 438). This is probably the single biggest readability win in the
  project.

- **`app/Main.hs:354-356`** — Three separate `any` predicates on the same
  list, two of which overlap (`s == "."` and `T.isPrefixOf "."`).
  Current:
  ```haskell
  if null subSegments
     || any (\s -> s == ".." || s == "." || T.null s) subSegments
     || any (T.isPrefixOf ".") subSegments
  ```
  Plain rewrite:
  ```haskell
  let isUnsafe s = T.null s || s == ".." || T.isPrefixOf "." s
  if null subSegments || any isUnsafe subSegments
  ```
  (`T.isPrefixOf "."` already covers `s == "."`.)

- **`app/Main.hs:385-406`** — `withRepo` mixes name validation, repo
  existence check, canonicalization, and jail check in one big guard-and-
  nested-if. The guard on line 387-388 packs three predicates with awkward
  operator precedence. Rewrite the guard as a named helper:
  ```haskell
  isBadName :: Text -> Bool
  isBadName n =
         T.any (`elem` ("/\\" :: String)) n
      || T.isInfixOf ".." n
      || T.isPrefixOf "." n
  ```
  then `withRepo` reads `| isBadName name = respond404 "Invalid repository name."`.

- **`app/Main.hs:413-414`** — `findRepo` reimplements `Data.List.find`:
  ```haskell
  findRepo name = foldr (\r acc -> if riName r == name then Just r else acc) Nothing
  ```
  Plain rewrite:
  ```haskell
  import Data.List (find)
  findRepo name = find ((== name) . riName)
  ```

- **`app/Main.hs:449-454`** — Point-free section hides intent:
  ```haskell
  decodeHdr = (>>= eitherToMaybe . TE.decodeUtf8') . flip lookup hdrs
  ```
  Plain rewrite:
  ```haskell
  decodeHdr :: BS.ByteString -> Maybe Text
  decodeHdr name = do
      raw <- lookup name hdrs
      either (const Nothing) Just (TE.decodeUtf8' raw)
  ```
  One fewer combinator, one extra type signature, much clearer causal chain.

### `src/DarcsWeb/Clone.hs`

- **`src/DarcsWeb/Clone.hs:117-119`** — The list comprehension returns a
  list of `Text`s and then `T.concat`s, which re-walks the bytestring:
  ```haskell
  encodePathSegment t =
      T.concat [ encodeByte b | b <- BS.unpack (TE.encodeUtf8 t) ]
  ```
  Plain and equivalent:
  ```haskell
  encodePathSegment t =
      T.concat (map encodeByte (BS.unpack (TE.encodeUtf8 t)))
  ```
  A trivial change, but it removes the one list-comprehension from this
  module (the rest is `map`/`filter`/`case`), eliminating a stylistic bump.

- **`src/DarcsWeb/Clone.hs:122-124`** — The `Word8 -> Text` branch uses
  `toEnum (fromIntegral b)` which is only safe because we already filtered
  to unreserved ASCII. A reader has to verify that. A more obviously safe
  form: `T.singleton (BC.head (BS.singleton b))` is worse; the clearer
  fix is a comment or a named helper:
  ```haskell
  -- safe: isUnreserved b implies b is an ASCII byte
  word8ToChar :: Word8 -> Char
  word8ToChar = toEnum . fromIntegral
  ```
  and use `word8ToChar b` in both `isUnreserved` (which also does the same
  conversion) and the `T.singleton` branch.

- **`src/DarcsWeb/Clone.hs:125-129`** — `isUnreserved` is defined inside
  `encodeByte`'s `where` and does the `toEnum (fromIntegral b)` dance
  again. Extract once (see previous bullet).

### `src/DarcsWeb/Darcs.hs`

- **`src/DarcsWeb/Darcs.hs:69-96`** — `checkRepo` has a ladder of three
  nested `if … then … else` and duplicates the `RepoInfo` record
  construction between the two `Just`/`Nothing` cases (only two fields
  differ). Rewrite as a single early-exit style:
  ```haskell
  checkRepo baseDir name = do
      let fullPath = baseDir </> name
      isDir  <- doesDirectoryExist fullPath
      isRepo <- if isDir then isDarcsRepo fullPath else pure False
      if not isRepo
        then pure []
        else do
          desc  <- readRepoDescription fullPath
          mInfo <- getBasicRepoInfo fullPath
          let (lastDate, count) = maybe ("", 0) id mInfo
          pure [ RepoInfo
                 { riName        = T.pack name
                 , riPath        = fullPath
                 , riDescription = desc
                 , riLastChange  = lastDate
                 , riPatchCount  = count
                 } ]
  ```
  (`fromMaybe ("", 0) mInfo` is even shorter.) The record literal appears
  once, which means future fields can't drift between the branches.

- **`src/DarcsWeb/Darcs.hs:176-181`** — `jailCheck` re-implements trailing-slash
  logic that already lives in `PathPure.add_trailing_slash`:
  ```haskell
  let jailSlash = if null jail then "/"
                  else if last jail == '/' then jail else jail ++ "/"
  return (jailSlash `isPrefixOf` canonical || canonical == init jailSlash)
  ```
  Uses **partial** `last` and **partial** `init`. Both are safe only
  because of the `null` guard and the fact that the string ends in `/`,
  which forces the reader to trace the condition twice. Plain rewrite:
  ```haskell
  jailCheck jail path = do
      canonical <- canonicalizePath path
      let jailSlash = PathPure.add_trailing_slash jail
          jailNoSlash = dropSuffix "/" jailSlash
      pure (jailSlash `isPrefixOf` canonical || canonical == jailNoSlash)
    where
      dropSuffix suf s
        | suf `isSuffixOf` s = take (length s - length suf) s
        | otherwise          = s
  ```
  Now there are no partial functions and the "is it the jail itself, or
  inside the jail?" logic is a one-liner predicate each.

- **`src/DarcsWeb/Darcs.hs:141-144`, `252-272`** — Two `extractPatch*`
  functions differ only in whether they render the diff. Collapse into
  a parameterised helper:
  ```haskell
  data DiffMode = WithDiff | ListingOnly

  extractPatch :: RepoPatch p => DiffMode -> PatchInfoAnd p wA wB -> PatchSummary
  extractPatch mode piap =
      let pinfo = info piap
          (diffText, summaryText) = case hopefullyM piap of
            Just p  -> ( case mode of
                           WithDiff    -> T.pack (renderString (showPatch ForDisplay p))
                           ListingOnly -> ""
                       , T.pack (renderString (summary p)) )
            Nothing -> (if mode == WithDiff then "(patch content unavailable)" else "", "")
      in patchInfoToSummary pinfo diffText summaryText
  ```
  Two callers, one function. If that feels over-engineered, at minimum
  factor out the shared `case hopefullyM piap of Just _ -> … ; Nothing -> …`
  idiom into a helper so the two definitions visually pair up.

### `src/DarcsWeb/Html.hs`

- **`src/DarcsWeb/Html.hs:274`** — `maybe (psName ps) id (psTagName ps)` is
  `fromMaybe (psName ps) (psTagName ps)`. Preferred:
  ```haskell
  import Data.Maybe (fromMaybe)
  fromMaybe (psName ps) (psTagName ps)
  ```

- **`src/DarcsWeb/Html.hs:298-308`** — `buildCrumbs` is a recursive helper
  that threads an accumulator. A reader has to follow two state updates
  per recursion. A `scanl`/`zip`/`map` version reads as "build prefix paths,
  then render each":
  ```haskell
  renderTreeBreadcrumb repoName subPath =
      let parts   = if T.null subPath then [] else T.splitOn "/" subPath
          paths   = drop 1 (T.scanl joinSeg "" parts)
          crumbs  = zipWith crumb paths parts
          crumb href label =
            " / <a href=\"/repo/" <> esc repoName <> "/tree/" <> esc href <> "\">"
            <> esc label <> "</a>"
          joinSeg acc p = if T.null acc then p else acc <> "/" <> p
      in "<div class=\"tree-path\">"
         <> "<a href=\"/repo/" <> esc repoName <> "/tree\">[root]</a>"
         <> T.concat crumbs
         <> "</div>\n"
  ```
  (Note: `T.scanl` works on chars, so use `Data.List.scanl` with the list.
  The point stands: "paths, labels, zip, render" is easier to skim than
  mutually recursive accumulator code.) The same helper is **duplicated**
  in `renderBlobBreadcrumb` (line 367-381) with only the final `fileName`
  line differing — extract once.

- **`src/DarcsWeb/Html.hs:431-445`** — `relativeDate` is a ladder of six
  `if … then … else if …` branches. Haskell's `|` guards read much
  better for this pattern:
  ```haskell
  relativeDate now dateStr =
      case parseDarcsDate (T.unpack dateStr) of
        Nothing -> dateStr
        Just t  ->
          let secs = floor (diffUTCTime now t) :: Int
          in pickBucket secs
    where
      pickBucket s
        | s < 0        = formatAbsolute dateStr
        | s < 60       = pluralize s "second"
        | s < 3600     = pluralize (s `div` 60) "minute"
        | s < 86400    = pluralize (s `div` 3600) "hour"
        | s < 604800   = pluralize (s `div` 86400) "day"
        | s < 2419200  = pluralize (s `div` 604800) "week"
        | otherwise    = formatAbsolute dateStr
  ```
  Bonus: name the magic numbers:
  ```haskell
  minute, hour, day, week, month :: Int
  minute = 60; hour = 60 * minute; day = 24 * hour
  week = 7 * day; month = 4 * week
  ```

- **`src/DarcsWeb/Html.hs:419-425`** — `highlightDiff`'s three-branch cascade
  is fine, but each branch builds the same `<span class="diff-X">…</span>\n`
  shape. A data-driven version reads more symmetrically:
  ```haskell
  highlightDiff = T.concat . map highlightLine . T.lines
    where
      highlightLine l = case classify l of
        Just cls -> "<span class=\"" <> cls <> "\">" <> esc l <> "</span>\n"
        Nothing  -> esc l <> "\n"
      classify l
        | T.isPrefixOf "+"  l = Just "diff-add"
        | T.isPrefixOf "-"  l = Just "diff-del"
        | T.isPrefixOf "@@" l = Just "diff-hunk"
        | otherwise           = Nothing
  ```

- **`src/DarcsWeb/Html.hs:452-458`** — `formatAbsolute` uses string slicing
  with `take`/`drop` offsets. It works but is tedious to audit. A splitAt
  pipeline reads more naturally:
  ```haskell
  formatAbsolute d
    | T.length d >= 14 =
        let (y, r1)  = T.splitAt 4 d
            (mo, r2) = T.splitAt 2 r1
            (dy, r3) = T.splitAt 2 r2
            (h,  r4) = T.splitAt 2 r3
            (mi, r5) = T.splitAt 2 r4
            (se, _)  = T.splitAt 2 r5
        in y <> "-" <> mo <> "-" <> dy <> " " <> h <> ":" <> mi <> ":" <> se
    | otherwise = d
  ```
  (And stay in `Text`, avoiding the `T.unpack`/`T.pack` round-trip.)

- **`src/DarcsWeb/Html.hs:367-381`** — `renderBlobBreadcrumb` uses partial
  `init` and `last` on `T.splitOn "/" subPath`. If `subPath` ever arrived
  as `""`, both fail. Route guard should ensure it can't, but "readability
  first" still prefers a total version:
  ```haskell
  case T.splitOn "/" subPath of
    []          -> ""   -- impossible, but total
    [fileName]  -> renderWithFile [] fileName
    parts       -> renderWithFile (init parts) (last parts)
  ```

## Naming

- **`app/Main.hs:50-53`** — `Opts` is used only for parsed CLI flags. Rename
  to `CliOpts` / `Flags` / `CliArgs` to avoid collision with
  `Web.Scotty.Options` which is imported on the same file.

- **`app/Main.hs:96-100`** — Local bindings use abbreviated `cfgMap`-
  extracted names: `port`, `bind`, `repoDir`, `title`, `staticDir`.
  Good. But the record later uses `cfgPort`, `cfgBind`, `cfgRepoDir`,
  `cfgTitle`, `cfgStaticDir` and the `mkConfig` parameter list on line 122
  repeats the positional order. If one day the record gets a new field,
  `mkConfig`'s 6-argument positional call site is a silent refactor hazard.
  Prefer record syntax at the call site:
  ```haskell
  let cfg = DarcsWebConfig { cfgPort = port, cfgBind = bind, ... cfgLog = logf }
  ```
  and delete `mkConfig` entirely.

- **`app/Main.hs:113,116`** — `logf` locally and `cfgLog` on the record.
  Pick one (`logf` throughout is fine).

- **`app/Main.hs:451`** — `hdrs` vs. `requestHeaders`. Project is generally
  consistent on full names — `hdrs` is the one outlier.

- **`src/DarcsWeb/Clone.hs:37,40,41`** — `mProto`, `mHost`, `name`, then
  locally `proto`, `host`. Fine, but `host`/`mHost` shadowing is confusing:
  rename the local to `finalHost` or `hostOrDefault`.

- **`src/DarcsWeb/Clone.hs:63,64`** — `raw` / `h` / `h` / `p` single-letter
  locals. `h` for both "host-like string" in `validRegName` and "name"
  inside the where clause is reused. Consider `host`, `port`.

- **`src/DarcsWeb/Darcs.hs:107`** — `getBasicRepoInfo` returns
  `Maybe (Text, Int)` — an anonymous tuple. A record (`data RepoStats =
  RepoStats { rsLastChange :: !Text, rsPatchCount :: !Int }`) would
  document what `fst`/`snd` mean at the call site (line 90).

- **`src/DarcsWeb/Darcs.hs:253-255,263-265`** — `extractPatchListing` vs.
  `extractPatchFull`. "Listing" vs. "Full" is fine; better names could be
  `summariseWithoutDiff` / `summariseWithDiff`, or see the `DiffMode`
  refactor above.

- **`src/DarcsWeb/Html.hs:405-409`** — `navLink'` has a trailing prime
  with no un-primed counterpart. Drop the prime: `navLink`.

- **`src/DarcsWeb/Html.hs:84`** — `"num"` CSS class is fine, but `T.pack
  (show (riPatchCount ri))` appears three times in this module with
  slightly different spelling. A one-liner helper `tshow = T.pack . show`
  would cut noise.

## Structure

- **`app/Main.hs:217-342`** — `app` is ~125 lines of inline route
  definitions. Each route repeats the shape `pathParam …; withRepo cfg
  name $ \repoPath -> do …`. Consider:
  1. Extract each handler (`handleSummary`, `handleShortlog`, `handleLog`,
     `handleTags`, `handleTree`, `handleBlob`, `handlePatch`) to a named
     top-level function with a type signature. `app` then becomes a
     one-screen route table.
  2. Collapse the common "lookup optional value + 404-or-render" pattern
     (lines 290-295, 304-309, 318-323, 331-337) into a helper:
     ```haskell
     renderOr404 :: Text -> Maybe a -> (a -> Text) -> ActionM ()
     renderOr404 msg mVal render = case mVal of
       Nothing -> respond404 msg
       Just x  -> html (TL.fromStrict (render x))
     ```

- **`app/Main.hs:6-35`** — The import list for Main spans 30 lines and
  mixes `Web.Scotty`, `Network.Wai`, POSIX, FFI, `Data.Text.*`, `System.*`.
  Grouping already helps; adding a blank line between `System.Posix.*`
  and `Foreign.*` would signal "FFI section" and a comment
  `-- Daemonization (POSIX + FFI)` on line 28 would frame why the FFI is
  there.

- **`app/Main.hs:169-176`** — `openDevNull` uses the raw `2` for `O_RDWR`.
  Either import from `System.Posix.IO` (`OpenMode(..)`) or at least name
  the constant: `let o_RDWR = 2 in …`. Also, a comment already explains
  *why* (unix-package compat); move it to a Haddock on `openDevNull`
  itself so it sticks to the function.

- **`src/DarcsWeb/Html.hs`** — At 468 lines, this module mixes:
  - page chrome (`renderPage`, `repoBreadcrumb`, `repoNavBar`, `navLink'`)
  - list renderers (`renderRepoList`, `renderRepoTable`, `renderRepoRow`,
    `renderShortLogTable`/`Row`, `renderTagList`/`Row`)
  - entity pages (`renderRepoSummary`, `renderPatchDetail`, `renderTree`,
    `renderBlob`, `render404`)
  - text utilities (`esc`, `highlightDiff`, `shortAuthor`, `formatSize`,
    `formatAbsolute`, `relativeDate`, `parseDarcsDate`, `pluralize`)

  Splitting into `DarcsWeb.Html.Page`, `DarcsWeb.Html.Repo`,
  `DarcsWeb.Html.Patch`, `DarcsWeb.Html.Format` would let a reader
  navigate by responsibility. If a split is too invasive, at minimum
  add banner comments (`-- * Page chrome`, `-- * Log views`, …) so
  the TOC is visible when scrolling.

- **`src/DarcsWeb/Darcs.hs:106-148`** — `getBasicRepoInfo`,
  `getRepoPatches`, `getRepoPatch`, `getRepoTags` all share the exact
  `try $ withRepositoryLocation YesUseCache repoPath $ RepoJob $ \r -> …`
  scaffolding plus a `case … of Left _ -> empty; Right v -> v` unwrap.
  Extract:
  ```haskell
  withRepoSafe :: a -> FilePath
               -> (forall p wR. RepoPatch p => Repository … -> IO a)
               -> IO a
  withRepoSafe fallback path job = do
      r <- try $ withRepositoryLocation YesUseCache path (RepoJob job)
      pure $ case r of Left (_ :: SomeException) -> fallback; Right v -> v
  ```
  The four call sites shrink to 3 lines each and the exception-handling
  policy lives in one place.

- **`src/DarcsWeb/Config.hs:33`** — `trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace`
  is a classic but surprising one-liner for non-Haskellers. `Data.Text`
  has `T.strip` for `Text`; for `String` the equivalent one-liner is
  acceptable but a `where` binding with a short comment would help
  first-time readers.

## Comments & docs

- **`app/Main.hs:1-2`** — No module-level Haddock describing what Main does.
  Three lines would help:
  ```haskell
  -- | darcsweb executable: parses CLI flags, loads config, optionally
  --   daemonizes via a double-fork, and starts a Scotty server that
  --   serves repository pages and read-only clone files.
  ```

- **`app/Main.hs:150-159`** — `daemonize` has a one-line comment but no
  explanation of *why* the double-fork is needed (SysV-style disown from
  controlling terminal). One line would save the next maintainer a
  Stack Overflow detour:
  ```haskell
  -- | Double-fork: first fork + setsid() detaches from the controlling
  --   terminal; second fork ensures we are not a session leader, so we
  --   can never re-acquire a TTY.
  ```

- **`app/Main.hs:408-411`** — The Haddock says "uses formally verified
  implementation from Coq/Rocq" — good pointer, but a link (or mention
  of `verified/Path.v`) would make the trail explicit.

- **`src/DarcsWeb/Clone.hs:3-31`** — Excellent Haddock on `buildCloneUrl`.
  `sanitizeHost`, `validAuthority`, `validBracketed`, `validRegName` would
  benefit from matching `@since`/spec-style annotations noting RFC 3986
  sections they partially implement.

- **`src/DarcsWeb/Darcs.hs:174-181`** — `jailCheck` has a comment but no
  Haddock. Since this function is a critical security boundary (reused
  in three places), it should be documented properly:
  ```haskell
  -- | Return 'True' iff 'path', after symlink resolution, is either the
  --   'jail' directory itself or a strict descendant of it. Used to
  --   prevent path traversal through symlinks.
  ```

- **`src/DarcsWeb/Html.hs:298-308,367-381`** — No Haddock on breadcrumb
  helpers. Since the two are near-duplicates, docs should either explain
  the difference or (preferable) factor them.

- **`test/Properties/Clone.hs`**, **`test/Properties/Csp.hs`**,
  **`test/Properties/Html.hs`** — Tests are consistently formatted and
  readable. The `run`/`quickCheckWithResult` boilerplate is duplicated
  across all three — worth extracting to a shared `Properties.Runner`
  module with one `runProp :: Testable p => String -> p -> IO Bool`.

## Small wins

Ordered by impact:

1. **`respond404` helper in `app/Main.hs`.** Kills ~13 duplicated 2-line
   blocks (`app/Main.hs:242-243, 292-293, 306-307, 320-321, 333-334, 341-342,
   358-359, 368-369, 374-375, 389-390, 396-397, 405-406, 438-440`).
2. **Replace `findRepo` body with `find`** (`app/Main.hs:414`).
3. **Replace `maybe X id` with `fromMaybe X`** (`src/DarcsWeb/Html.hs:274`).
4. **Replace `foldl (flip id)` with `foldr ($)`** (`app/Main.hs:74`).
5. **Drop `mkConfig`; use record syntax inline** (`app/Main.hs:113, 119, 122`).
6. **Top-level type signatures on inner `where` helpers** —
   `app/Main.hs:453` (`eitherToMaybe`), `src/DarcsWeb/Html.hs:444`
   (`pluralize`). They're short but the types aren't obvious.
7. **Guards instead of nested `if/else`** — `src/DarcsWeb/Html.hs:431-445`,
   `app/Main.hs:354-359`, `src/DarcsWeb/Darcs.hs:196-213`.
8. **Blank-line-grouped imports** with `-- Scotty`, `-- WAI`,
   `-- POSIX / FFI`, `-- Local modules` section comments
   (`app/Main.hs:6-43`).
9. **`tshow = T.pack . show`** in `src/DarcsWeb/Html.hs` — removes 6
   noisy repetitions (`Html.hs:84, 349, 350, 351, 445`).
10. **Rename `navLink'` to `navLink`** (`src/DarcsWeb/Html.hs:405`) — no
    un-primed sibling, the tick carries no information.
11. **`darcsDir` is only used twice in Main** (`app/Main.hs:47, 423`). Fine
    to keep, but add the same constant in `src/DarcsWeb/Darcs.hs` rather
    than the bare string `"_darcs"` (appears at lines 60, 100, 209).
    Either push `darcsDir` into `Types.hs` or introduce
    `DarcsWeb.Internal.Constants`.
12. **`Data.List (isPrefixOf)`** is imported in `app/Main.hs:35` and used
    once (line 367). Paired with the similar import in `Darcs.hs:23` and
    the verified `PathPure.add_trailing_slash` call, a single
    `pathIsInside :: FilePath -> FilePath -> Bool` helper (in `PathPure`
    or in a thin wrapper) would be clearer and testable.

## Extracted code note

Files `gen/CspPure.hs`, `gen/HtmlPure.hs`, `gen/PathPure.hs` are Coq
extraction output. They exhibit the usual extraction accents:

- **`gen/CspPure.hs:9-16`** — Hand-rolled `fst`/`snd` shadowing `Prelude`.
  Extraction-generated; harmless because `{-# OPTIONS_GHC -w #-}` silences
  the shadow warning. **Un-editable at source**. If the Coq side can
  map its tuple projections to the standard ones (via
  `Extract Constant fst => "Prelude.fst".`), the extracted file would
  read better. Same applies to `map` on line 18-22.
- **`gen/CspPure.hs:29-31`**, **`gen/HtmlPure.hs:18-35`**,
  **`gen/PathPure.hs:19-21, 44-46, 79-89`** — Every character comparison
  is annotated with
  `((Prelude.==) :: Prelude.Char -> Prelude.Char -> Prelude.Bool)`. This
  is an extraction artefact (Coq's decidable equality realisation). A
  Coq-side `Extract Inductive bool => "Prelude.Bool" …` and
  `Extract Inlined Constant Char.eqb => "(Prelude.==)"` would remove the
  inline type ascriptions and drop ~30% of the visual noise. **Un-editable
  at source**.
- **`gen/HtmlPure.hs:9-10`**, **`gen/HtmlPure.hs:13-14`** — Identity lambdas
  `(\x -> x) '"'` are obfuscated literals. Extraction-generated; in Coq,
  defining `Definition dquote := "034"%char` (or simply inlining the
  literal) and marking it `Extract Constant dquote => "'\"'"` would
  produce readable Haskell. **Un-editable at source**.
- **`gen/PathPure.hs:70-90`** — `is_safe_segment` is a four-deep nested
  `(Prelude.&&)` tower. This is the natural result of extracting a
  conjunction of `bool` predicates. If the Coq definition switched to a
  `forallb` over a list of unary predicates (or used `&&&`-style local
  notation), the extract would read better. **Un-editable at source**.
- **`gen/PathPure.hs:1-3`**, **`gen/HtmlPure.hs:1-3`**,
  **`gen/CspPure.hs:1-3`** — All three extracts import `Data.Bits` and
  `Data.Char` unconditionally, though none reference them. Extraction
  artefact. **Un-editable at source**; can only be suppressed via
  `-Wno-unused-imports` (already implicit under `-w`).

**Wrapper-side improvements (editable):** the thin Haskell wrappers in
`src/DarcsWeb/Html.hs:414`, `src/DarcsWeb/Darcs.hs:186, 191`,
`app/Main.hs:411`, `app/Main.hs:195` are all one-liners that call through
to the extracted `HtmlPure` / `PathPure` / `CspPure` module. They are the
right place to add Haddock with a link to the corresponding `.v` proof
file (e.g. `-- See @verified/Html.v@ for the proof that this is
HTML-injection safe.`) so the reader immediately knows where to look for
the semantic guarantee instead of having to pattern-match the mangled
extracted source.