# Audit Implementation Review — `audit-fixes` branch **Reviewer:** Independent post-implementation review **Date:** 2026-04-10 **Subject:** All uncommitted changes on the `audit-fixes` branch (working tree only — there are no commits ahead of `main` yet) **Source of work being reviewed:** `audit.md` (independent code audit dated 2026-04-09) **Method:** `git diff main` over every modified file, full read of new files, and a successful `cabal build` ("Up to date") to confirm no compile-breaking refactor was introduced. This document answers two questions: 1. For each change introduced on `audit-fixes`, what audit finding (if any) does it address, and is the fix correct? 2. What regressions or net-negatives did the branch introduce? The headline answer is: **the branch makes the codebase materially better and addresses the great majority of the audit's CRITICAL and HIGH findings correctly, but it has three real concerns.** First, the new `build/Patterns.hs` module — introduced specifically to close the H-1.10.1 "directory-form essays missed by Authors.hs" finding — is only adopted by three of the five modules that should consume it; `Stats.hs` and `Site.hs` still hold private patterns, so the same class of bug is partially perpetuated on the stats page. Second, an unrelated content file (`content/essays/modern_idolatry.md`) was moved out of the repo root into `content/essays/` instead of into `content/drafts/essays/`, which means it will publish to the live site on the next non-dev build despite its `status: "Draft"` frontmatter — this is an *introduced* risk that the audit did not warn about. Third, several "consolidation" refactors actually introduced new duplication (`percentDecode` is now byte-identical in two modules; `escAttr` is locally redefined in `Catalog.hs` and `Transclusion.hs` despite the new `Utils.escapeHtml`), and one fix in `Stats.hs` is annotated with a misleading "fixed" comment for code that was not actually changed. The build compiles cleanly with the audit-fixes diff applied, so no refactor introduced an unbound name or type error. --- ## 1. Executive summary ### 1.1 Audit findings status The audit identified ten CRITICAL/HIGH findings, ~22 MEDIUM, ~36 LOW, and a handful of NIT items. The branch's coverage: | Severity | Total | Fixed correctly | Partial / risk | Not addressed | |----------|-------|-----------------|----------------|---------------| | CRITICAL | 1 | 1 | 0 | 0 | | HIGH | 10 | 9 | 1 | 0 | | MEDIUM | ~22 | ~14 | ~3 | ~5 | | LOW | ~36 | ~12 | ~4 | ~20 | | NIT | ~10 | ~3 | 0 | ~7 | (Counts are approximate where the audit aggregates multiple sub-findings under one ID.) Every CRITICAL and every HIGH except one is addressed. The single HIGH that is partially addressed is **H-1.10.1** (directory-form essays omitted by `Authors.hs`): Authors.hs is fixed, but the new canonical `Patterns.hs` module was not adopted by `Stats.hs`, so the writing-statistics page still under-counts the same essays. ### 1.2 Top-level verdict per area | Area | Files touched | Net assessment | Headline reason | |------|---------------|----------------|-----------------| | Haskell core (`build/*.hs`) | 13 modified, 1 new, 1 deleted | **Net positive, with caveats** | Most fixes correct. Stats.hs blaze rewrite is high-quality but exceeds audit scope and skips Patterns.hs adoption. New `percentDecode` duplication. One misleading "fixed" comment. | | Pandoc filters (`build/Filters/*.hs`) | 9 modified | **Net positive** | All HIGH/MEDIUM filter findings addressed correctly. Sidenote rewrite is the highest-risk change but verified. New local `escAttr` in Transclusion partially undercuts the consolidation goal. | | JavaScript (`static/js/*.js`) | 9 modified, 2 new | **Net positive** | All scoped HIGH/MEDIUM findings addressed. Silent removal of TOC auto-collapse-on-scroll is an unannounced UX change. | | CSS (`static/css/*.css`) | 5 modified | **Net positive** | Three HIGH a11y findings closed. Several declared design tokens (`--transition-medium`, `--bp-*`) are dead — defined but not consumed. | | Templates | 3 modified | **Net positive** | KaTeX bootstrap externalized; `type="button"` added; new `utils.js` wired in correctly. | | Tools / Makefile / config | 11 modified | **Net positive** | Deploy ordering fixed; `.env.example` documents the new vars; Python imports hardened; `download-model.sh` gains checksum verification. README expanded from one line to a usable document. | | Content (essay move + new content) | 1 deleted, 1 new dir + figures, several untracked | **Mixed** | BCI essay rewrite is solid; figures and citations all resolve. **`content/essays/modern_idolatry.md` will accidentally publish** because it's matched by `publishedEssays`. | ### 1.3 The three concerns to take action on 1. **Partial adoption of `build/Patterns.hs`.** `Stats.hs` (line 747 and 901) and `Site.hs` (`publishedEssays`/`draftEssays`) still maintain their own essay patterns instead of importing from `Patterns`. The audit's L-1.1.2 finding said "extract one canonical `Patterns.hs`" — it was extracted, but two of five candidate consumers still bypass it. The H-1.10.1 fix (directory-form essays in author indexes) is therefore incomplete on the build/stats page. **Recommendation:** Switch the two `loadAll` calls in `Stats.hs:747,901` to use `Patterns.essayPattern`, and replace `Site.hs`'s `publishedEssays` with `Patterns.essayPattern`. 2. **`content/essays/modern_idolatry.md` will publish on the next build.** The Hakyll publication gate is **path-based** (`content/essays/**/*.md` is matched by `publishedEssays` in `build/Site.hs:23-24`), not frontmatter-based. The file's `status: "Draft"` frontmatter is metadata only — it does not exclude the file from the build. **Recommendation:** Move to `content/drafts/essays/modern_idolatry.md` (the directory does not yet exist on disk and will need to be created) before any non-dev build. 3. **Misleading "fixed" comment in `Stats.hs` for L-1.3.4.** A new comment claims that the Backlinks-decode round-trip was eliminated, but the underlying `Aeson.decodeStrict (TE.encodeUtf8 (T.pack rawBL))` code is byte-identical to `main`. **Recommendation:** Remove the comment or actually load the JSON as `ByteString` from a custom compiler. The rest of this document walks through the changes file-by-file, in five sections. --- ## 2. Haskell core build modules ### 2.1 New: `build/Patterns.hs` A clean new module that exports `essayPattern`, `blogPattern`, `poetryPattern`, `fictionPattern`, `musicPattern`, `standalonePagesPattern`, and three aggregations (`allWritings`, `allContent`, `authorIndexable`, `tagIndexable`). `essayPattern` correctly includes both `content/essays/*.md` and `content/essays/*/index.md`. `poetryPattern` correctly excludes collection `index.md` files via `complement`. `authorIndexable` and `tagIndexable` apply `hasNoVersion` so the "links" version produced by `Backlinks.hs` is not double-indexed. - **Audit findings addressed:** L-1.1.2 (pattern centralization). Indirectly enables H-1.10.1 to be closed in modules that adopt it. - **Verdict:** The module is the right abstraction and is cleanly implemented. The problem is partial adoption (see 2.4 below). ### 2.2 Deleted: `build/Metadata.hs` The empty placeholder module is deleted; the cabal `other-modules` entry is removed in the same diff. Confirmed via `cabal build` that no remaining file imports it (the 30+ grep hits for "Metadata" all reference Hakyll's `Hakyll.Core.Metadata` module/type, not the deleted local one). - **Audit findings addressed:** Hygiene line item. - **Verdict:** Clean deletion. ### 2.3 `build/Authors.hs` Drops the local `authorLinksField` (relocated to `Contexts.hs`), uses `Patterns.authorIndexable` in place of the hand-rolled `allContent`, delegates `slugify`/`nameOf` to `Utils.authorSlugify`/`authorNameOf`, and adds `abstractField` to the per-item context. - **Audit findings addressed:** **H-1.10.1** (directory-form essays now indexed on author pages), **L-1.10.2** (slugify deduplicated), **L-1.1.2** (shares the new canonical pattern). - **Risks:** Adding `abstractField` to the author item context is a minor template contract change — author-page templates that previously had no `$abstract$` will now receive one. Worth a quick template spot-check. - **Verdict:** Net positive. Closes the highest-impact author-page bug correctly. ### 2.4 `build/Stats.hs` (the largest single diff: ~720 lines) This is the most ambitious change in the branch. It rewrites the HTML-generating helpers to use **`blaze-html`** throughout, introduces a defense-in-depth `isSafeUrl`/`safeHref`/`link`/`pageLink` URL allowlist, replaces the naive `stripHtmlTags` with a small state machine (handling tag bodies, comments, CDATA, quoted attribute values), adds `pathIsSymbolicLink` skipping to `walkDir`, switches `countLinesDir` to strict `TIO.readFile`, replaces a partial `s !! (length s div 2)` `median` with a total pattern match, and aliases `renderStatsTags = renderTagsSection` to collapse the duplicate. Two new cabal dependencies (`blaze-html`, `blaze-markup`) accompany this change. - **Audit findings addressed:** **M-1.3.1** (naive `stripHtmlTags`), **M-1.3.2** (no symlink protection), **L-1.3.3** (protocol-relative URL allowlist hole), **L-1.3.5** (duplicate tag rendering function), **L-1.3.6** (lazy `readFile`). - **Real concerns:** 1. **`Stats.hs` did NOT adopt `Patterns.hs`.** Lines 747 and 901 still call `loadAll ("content/essays/*.md" .&&. hasNoVersion)`, which means the writing-statistics page still under-counts directory-form essays. Verified by direct grep on the current file. This perpetuates the exact class of bug that **H-1.10.1** was meant to close. 2. **L-1.3.4 has a misleading "fixed" comment.** A new comment describes "decoding directly from the encoded UTF-8 bytes [to] avoid the previous String → Text → ByteString round-trip", but the underlying code is byte-identical to `main` (`Aeson.decodeStrict (TE.encodeUtf8 (T.pack rawBL))`). Either the comment should be removed or the JSON should be loaded as `ByteString` from a custom compiler. 3. **Scope creep.** The audit only asked for a smarter `stripHtmlTags`. The blaze rewrite is a substantial quality improvement (HTML escaping is now structural rather than manual) and it does subsume several other findings, but it triples the line count of the affected functions and adds two cabal dependencies. This is a defensible call but it expanded the review surface considerably and any regression in the rendered `/build/` and `/stats/` pages will live in this code. 4. **Cosmetic dedup of tag function.** `renderStatsTags = renderTagsSection` is two names pointing to the same body — the surface area is unchanged. The audit recommended deleting one caller, not aliasing them. - **Other observations:** The new heatmap CSS classes (`.hm0`...`.hm4`, `.hm-lbl`) were moved out of an inline `