- gallery.js: math/score focus overlays are keyboard-activatable
(role=button, tabindex, Enter/Space) and focus return on close lands
on a focusable trigger (AUDIT §5.7)
- annotations.js: marks are focusable; Enter/Space pins the tooltip
with focus moved to its Delete button, Escape dismisses — the delete
affordance is finally reachable without a mouse (§5.7)
- transclude.js: nested transclusions resolve (depth-capped at 3, with
ancestor-chain cycle rejection rendering the existing error style);
collapse.js reinit is idempotent via data-collapse-bound (§5.7)
- copy.js excludes the button label from code-less <pre> copies;
score-reader.js stops rewriting plain loads to ?p=1; search-filters
treats non-numeric threshold input as inactive instead of a
match-everything >=0 filter; selection-popup no longer re-summons
the toolbar while typing capitals in the annotation picker (§5.8)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- import-photo.sh deletes the copied JPEG when EXIF stripping fails, so
the auto-commit can never publish GPS/serial metadata (AUDIT §4.11)
- pre-commit-marks hook: tab-aware path parsing, probes the staged blob
rather than the working tree (§4.11)
- preset-signing-passphrase uses printf; stamp-build-time writes via
temp + os.replace; archive.py passes -- to pdftotext and verifies the
vendored monolith binary against its recorded sha256 (mismatch is
fatal, consistent with the tool's integrity contract); extract-exif
./-prefixes relative paths (§4.11)
- blog-post.html: id="similar-links"/"backlinks" each appear once;
rendered output unchanged (§6.4)
- site.webmanifest: start_url/scope/description added, maskable icon
purpose restored alongside any (§9.3)
- Frontmatter cleanup: scaffold comments out of scaling_outage,
dangling null confidence-history keys removed (populated ones kept),
dead modified: key dropped from colophon (§6.4)
- canto31.jpg: 4.0 MB -> 1.9 MB (2400px, q80, grayscale — the source
is a monochrome Doré engraving, so single-channel is colorimetrically
lossless); webp sidecar regenerated (§6.4, prior-audit §6.1)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Empty/all-comments manifest.yaml is the empty archive, not a fatal
parse error (AUDIT §3.11)
- Backlinks normaliseUrl strips index.html like SimilarLinks, so links
to canonical directory URLs invert again; Stats normUrl updated in
lockstep (§3.12)
- PDF viewer file= query value percent-encoded (hand-rolled RFC 3986
encoder; network-uri is not a dependency) (§3.13)
- Photography feed thumbnails embed for flat singles and series
children, not just directory entries (§3.14)
- Marks trust is Maybe Int: missing confidence/evidence collapses the
figure to the bare frame as documented, instead of a literal
"0 TRUST"; result-shape glyph centers when no score (§3.15)
- Unknown catalog categories fold into one Other bucket; medians take
the mean of middle elements; protocol-relative URLs excluded from
backlinks; @string/@comment/@preamble skipped in BibTeX parsing;
watch-staleness of the once-per-process archive reads documented;
stale comments fixed (§3.16, §3.9)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SourceRefs trigger whitelist aligned to the /source/ serving
whitelist (drops content/, yaml-source/, broad static//tools//data
prefixes; adds .bib); existsCached no longer memoizes non-existence,
so files created under make watch are picked up (§2.5, §2.16)
- fill/stroke hex replacement is boundary-aware: #000080 and 8-digit
RGBA forms can no longer be corrupted into currentColor80 (§2.12)
- Wikilinks/Transclusion/EmbedPdf skip fenced code blocks (shared
CommonMark fence tracker), and wikilinks additionally skip inline
code spans — the syntax-documentation essay now renders its own
examples literally while live wikilinks still convert (verified both
ways in output) (§2.13)
- domainIcon matches the extracted host by label suffix instead of
substring-of-URL; extractHost also strips userinfo (§2.14)
- webpSrc escaped in srcset; internal PDF links no longer double-
classified; Smallcaps/Archive header-skip now holds at every nesting
depth via protect/restore walks (§2.17)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Library page no longer hard-depends on content/library.md; deleting
it degrades to no intro block (AUDIT §2.8)
- primaryPortalOf accepts scalar comma-form tags via getTags, matching
the tag system (§2.9)
- allContent gains me/ and memento-mori/ so their outgoing links join
the backlinks graph; photography exclusion now documented (§2.10)
- Paginated tag pages partition AND sort by the same revision-aware
display date — cross-page order is monotone again (§2.11)
- New stripPrefixRoute replaces gsubRoute at 17 call sites: prefix-only
stripping, no mid-path mangling; route inventory verified identical
(§2.15)
- random-pages uses canonical patterns (collection poems randomizable);
pattern literals replaced with Patterns imports; duplicate local
poetry patterns deleted; flat/collection poetry rules merged (§2.17)
- noResult instead of empty-list/fail for tagLinksField, dotsField,
abstract/description/summary/bibliography/further-reading, plus the
confidence-trend, overall-score, has-score, has-movements, and
movement-audio fields — no more empty wrappers or [ERROR] log noise
for legitimately-absent values (§2.17)
- tagItemCtx composes siteCtx, so monograms render on tag pages (§2.17)
- readingTime ceilings (399 words -> 2 min); authorSlugify comment
fixed to match behavior, code untouched for URL stability; stale
portal-count comments corrected (§2.17)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'skip if outputs newer than every HTML' check could never fire:
stamp-build-time.py rewrites every page's footer AFTER embed.py runs,
so the comparison was always false and the full MiniLM paragraph pass
(and model load) ran on every build (AUDIT §4.3). Replaced with the
same content-hash cache the page pass already had — generalized
load/save_vec_cache, keyed by sha256 of the input text, invalidated on
model/revision/dim change. A no-change rerun now does no model loads:
measured 97s cold -> 4.8s warm.
Also strips section.footnotes from extraction: the new no-JS fallback
duplicates each sidenote's text at document end, which would double
footnotes in search results and skew page similarity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The ~33 KB traced logo moves from an inlined-per-page partial to
/logo-sprite.svg referenced with <use> — cached once instead of
shipped on every page (homepage HTML: 46 KB -> 13 KB). CSS custom
properties cascade into the use shadow tree, so the two-tone cutout
is unchanged (AUDIT §9.1)
- favicon.ico regenerated at 16/32/48 from the 512px master: 71 KB ->
15 KB; modern browsers take the SVG anyway, the .ico is the legacy
fallback (§9.2)
- link-icons/internal.svg restored to the simple 4 KB path: it renders
at 0.7-1.6 rem through a CSS mask, where the 33 KB traced detail
cannot resolve (§9.2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.gitignore has declared content/drafts/ local-only working notes since
the rule was added, but four drafts were already tracked — ignore rules
don't untrack, so make build's auto-commit kept staging and deploy kept
pushing them (AUDIT §6.3). Untracked with --cached; the files remain on
disk and still build in dev. Also moved inclusionist-manifesto.md into
drafts/essays/ where the draft rule actually matches it (§6.1), and
un-shadowed the tracked .env.example from the credential patterns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The filter consumes every Pandoc Note, so the "standard Pandoc-
generated section.footnotes" its doc claimed as the no-JS fallback
never existed — below 1500px with JS disabled, footnote content was
simply invisible (AUDIT §2.3). The filter now collects consumed notes
and appends the section itself: letter labels, jump targets for the
in-text refs (which now point at the visible fallback item), and
doc-backlink returns. sidenotes.js pairs ref/note by element id and
preventDefaults clicks, so behavior with JS is unchanged.
Verified in output: per-page item count matches inline sidenote count;
refs target #fn-<label>; backlinks target #snref-<label>.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- js: page-script paths are site-root-relative, not content-relative
(AUDIT §7.1)
- directory-form standalone pages need a dedicated Site.hs rule; flat
content/<page>.md is the generic form (§7.2)
- portal table: add the missing Photography row (§7.3)
- document the implemented-but-undocumented summary:, revised:, and
keywords: fields, including a Revision dates section (§7.4)
- default citation style is Chicago Notes Bibliography, not
Author-Date; hover previews come from popups.js, not the deleted
citations.js (§7.5)
- history: entries may be authored in any order (sorted at build
time); examples reordered newest-first (§3.5)
- README: make watch runs Hakyll's live-reload preview server (§7.5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- semantic-search.js: generation token prevents stale results from
rendering over newer queries; in-flight dedup on the index fetch;
index/meta size consistency check fails loudly instead of NaN
ranking (AUDIT §5.5)
- lightbox.js: triggers keyboard-activatable (role=button, tabindex,
Enter/Space); Tab trapped inside the aria-modal overlay, modeled on
gallery.js (§5.6)
- nav.js: portal toggle persists via guarded safeStorage so
storage-blocked contexts can't kill the toggle (§5.7)
- popups.js: provider url() throws (malformed percent-encoding) are
treated as no-popup; future dates render nothing instead of
"N days ago" (§5.7)
- search.js: missing PagefindUI degrades to a console warning instead
of aborting the whole handler (§5.7)
- citations.js: deleted — dead code superseded by popups.js (§5.7)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- archive.py: PROVENANCE.json / archive-index.json / archive-state.json
now written atomically (tmp + os.replace) — a truncated integrity
record is the one thing this tool must never produce (AUDIT §4.4);
manifest entries validated as mappings up front (§4.7); refresh
rejects provenance with a missing/empty artifact key instead of
crashing on IsADirectoryError (§4.7); wayback save URL quotes
unsafe characters (§4.7)
- download-leaflet.sh: existing files are re-verified before being
skipped, and downloads land in a .part temp moved into place only
after checksum verification — a failed verification can no longer
leave a bad file that the next run silently accepts (§4.5)
- download-model.sh, convert-images.sh: same temp-then-move pattern so
interrupted downloads/conversions never persist at final paths (§4.6)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Backlinks: handle Plain blocks (tight list items) and DefinitionList
in link extraction — links in ordinary bullet lists were invisible to
the backlinks system (AUDIT §3.3)
- Sidenotes: render note bodies with a KaTeX writer so footnote math
reaches the client-side KaTeX pass instead of degrading to italics
(§2.4)
- Archive: join manifest to provenance on normalised URLs like every
other comparison in the system — an equivalent-form URL edit silently
unpublished the page while links kept pointing at it (§3.6)
- Photography: flat singles get their basename as slug and root-level
asset paths in map.json (§3.7); geo-precision now fails closed — an
unrecognised value (typo'd "hidden") suppresses the pin instead of
publishing rounded coordinates (§3.8)
- Stability: age is measured first-commit -> today, not the commit
span, so quiet time stabilises a piece as documented (§3.4);
history: entries are sorted newest-first by date regardless of
authored order (§3.5); pinned pages format last-reviewed like the
git branch (§3.10)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nav, the home portal grid, and the library have linked both URLs since
the portals were added, but no rule generated either index — confirmed
404s in production (AUDIT §2.1). Both rules mirror the essays index;
fiction renders an empty list until content exists.
sectionOwnedTopLevelTags now lists every namespace owning a
<name>/index.html route, not just photography — Hakyll silently
overwrites on duplicate routes, so an essay tagged e.g. "music" would
have clobbered a real section landing (AUDIT §2.2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- embed.py: pin nomic's auto_map modeling repo via code_revision —
revision= alone left nomic-bert-2048 unpinned under
trust_remote_code (AUDIT §1.3; verified loadable with
HF_HUB_OFFLINE=1). Catch BadZipFile/EOFError when loading the page
cache so a half-written npz is discarded, not fatal (§4.2), and
unlink the tmp file on a failed save (§4.1)
- nginx: collapse the CSP to one physical line — nginx has no line
continuation in quoted strings, so the old value embedded literal
backslash+LF bytes, illegal in HTTP/2 (§8.1). Add the externals the
site actually uses: KaTeX webfonts + onnxruntime wasm via jsdelivr,
and the popup provider APIs popups.js documents (§8.2)
- Makefile: pathspec-limit the auto-commit to content/ so pre-staged
unrelated work is no longer swept into auto: commits (§8.3)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- score-reader template: load utils.js before theme.js — without
lnUtils.safeStorage the saved theme/text-size never restored on
score pages (AUDIT §5.1)
- search-filters: expand trailing-slash pathnames to .../index.html
before the epistemicMeta lookup; clean-URL pages were silently
bypassing every active filter (AUDIT §5.2)
- viz: treat cappuccino as a dark theme so charts stop rendering
near-black marks on a dark brown background (AUDIT §5.3)
- collapse: namespace section-collapsed keys by pathname (Pandoc
auto-slugs recur across essays) and go through safeStorage like the
rest of the site (AUDIT §5.4)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ArchiveIndex: guard rawIndex/rawState with doesFileExist so a fresh
clone (gitignored data/ JSONs absent) degrades to empty instead of
crashing — the behavior the module doc already promised (AUDIT §1.2)
- Commonplace: decode YAML via encodeUtf8, not Char8.pack, which
truncates codepoints above 0x7F (AUDIT §3.2)
- Stats: DayOfWeek is ISO-numbered (Mon=1..Sun=7); dowOf and weekStart
assumed Mon=0..Sun=6, clipping every Sunday cell outside the heatmap
viewBox and starting weeks on Sunday (AUDIT §3.1)
- Site: epistemicEntry now honors the proved/proven confidence sentinel
like Contexts.overallScoreField (AUDIT §2.6)
- Contexts: affiliationField returns noResult instead of an empty list,
so essays without affiliation no longer render an empty meta row
(AUDIT §2.7)
Verified: full site build passes; proved page gets score=100 in
epistemic-meta.json; empty .meta-affiliation gone; heatmap rows
y=22..94 all inside the 104-high viewBox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pages (similar-links.json, build-only) move to nomic-embed-text-v1.5
(768d) with an on-disk npz cache; paragraphs (browser semantic search)
stay on all-MiniLM-L6-v2 (384d), so the client contract is unchanged.
WRITING.md search row updated accordingly. einops added for nomic's
remote modeling code; cache gitignored with a trailing glob so
interrupted-write debris is covered too.
Known follow-ups (AUDIT-2026-06-09.md §1.3, §4): pin the
nomic-bert-2048 remote code, catch BadZipFile in cache loads, fix the
staleness check defeated by stamp-build-time ordering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New inline logo-mark.svg partial in the nav (two-tone cutout via
--logo-ink/--logo-bg), regenerated favicon set + web-app manifest icons
from the new mark, 1200x630 og-image wired into head.html.
Known follow-ups (AUDIT-2026-06-09.md §9): the traced SVG is ~33 KB
inlined per page, favicon.ico carries 128/256px entries, and the
webmanifest dropped its maskable purpose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pinned distributive 0.6.2.1 conflicted with the pacman package db
(comonad-5.0.10 built against 0.6.3), making a fresh solve impossible —
same failure mode as the 2026-05-07 audit's aeson pin. Regenerated via
tools/refreeze.sh; cabal build --dry-run now resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Patch-level bumps to five transitive dependencies (attoparsec-aeson,
http2, prettyprinter-ansi-terminal, semialign, zlib). index-state is
unchanged; refreezes against the same hackage snapshot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
build/Now.hs renders the .now-stamp-relative phrase ("3 days ago") at
build time against the build machine's clock; a page served days later
from cache or a CDN would then read stale. now.js recomputes the
phrase in the browser from the <time datetime> attribute (an
unambiguous YYYY-MM-DD) against the visitor's clock, with bucket
thresholds that mirror Now.hs:relativeTime exactly so the no-JS
fallback and the recomputed value agree.
* static/js/now.js — the recomputation script.
* templates/default.html includes it via $if(now)$ so it only loads
on the Current page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Internal-page hover popups now show the source's monogram alongside
the title / abstract / metadata when one exists. Two-column grid is
gated on .has-monogram so popups for pieces without an authored mark
keep their default single-column body. The serialised SVG comes from
the rendered page's own .frontmatter-mark--monogram figure, excluded
when it is the symmetric-layout placeholder roundel so empty-slot
pieces do not get a fake mark in the preview.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tightens what gets printed and how. Reading-mode warm tints are
disabled so pages do not repaint cream; the mobile TOC bar's
screen-only body padding is reset; sidenote / footnote treatments
are reworked so the prose flows continuously instead of breaking
into a separate footnotes section; decorative link-icon glyphs
are suppressed while external links keep their underline so a
reader can follow them in the printed copy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
iOS WebKit silently degrades position: sticky to static on direct
flex/grid children of <body>, so the sticky nav was breaking on
mobile. Wrapping everything below the nav in a .page-shell flex
column keeps the sticky-footer math out of body { } and restores
sticky behaviour across browsers. The essay-frontmatter hoisted in
the Marks II commit becomes a body-level sibling of .page-shell so
its monogram and epistemic-figure columns can span viewport width.
* templates/default.html wraps $body$ + footer in .page-shell.
* static/css/layout.css moves the flex-column + min-height math from
body to .page-shell; the body > header rule now excludes
.essay-frontmatter so the essay header does not inherit nav chrome
(sticky, nav-bg, border-bottom).
* static/css/base.css drops the html/body overflow-x: clip — the
page-shell wrapper handles horizontal containment and clipping at
the viewport level was interfering with position: sticky.
* static/css/reading.css updates its #markdownBody centering selector
to match .page-shell > #markdownBody.
* static/css/components.css makes the TOC outline scrollable when
it overflows: bounded max-height tied to the sticky budget plus a
thin themed scrollbar, with overflow: hidden preserved for the
collapse transition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hakyll caches per-page outputs, so pages whose dependencies have not
changed are not recompiled and the rendered $build-time$ in their
footer goes stale relative to a fresh build. The right granularity for
"last built at" is site-wide rather than per-page; wrapping the footer
timestamp in <span data-build-time> and rewriting it after Hakyll lets
every page reflect the current build without paying recompilation cost.
* tools/stamp-build-time.py walks _site/**/*.html after Hakyll runs and
rewrites each element wrapped in [data-build-time] to the same format
Contexts.hs:buildTimeField emits, so a fresh render and the post-pass
agree.
* templates/partials/footer.html wraps $build-time$ in
<span class="footer-build-time" data-build-time>...</span> so the
sweep has a stable selector.
* Makefile invokes the sweep between embed.py and compress-assets so
the .gz/.br sidecars include the fresh stamp.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends the Phase-1 monogram mark system to every long-form content
type (essays, blog posts, poems, fiction, music) and introduces a
coverage audit so gaps are visible.
* build/Marks.hs gains hasMonogram (predicate), monogramSvgFieldFor +
hasMonogramFieldFor (for explicit-path callers like the /build/ and
/stats/ pages). Contexts.hs exports hasMonogramField as a siteCtx
boolean so templates can conditionally render the slot without
emitting an empty <div>.
* essay.html, blog-post.html, reading.html: hoist the frontmatter
block out of <main id="markdownBody"> so the monogram + epistemic
marks render as wrapper chrome rather than indexable prose; left
+ right mark slots are now unconditional (CSS handles the empty
state) so the layout is grid-stable across pieces.
* templates/partials/item-card.html: optional monogram chip on cards
(item-card--has-monogram modifier), gated on $has-monogram$ so
monogram-less pieces stay flush.
* build/Stats.hs grows a "Marks coverage" telemetry section: per-type
pieces / monogram / epistemic-figure counts + a coverage rollup,
rendered between epistemic and output on /build/.
* tools/audit-marks.py: coverage report (ASCII table) walking
content/**/*.md, plus a pre-commit hook at
tools/hooks/pre-commit-marks.sh that runs the same scan against
newly-staged .md files. New `make audit-marks` runs the report
manually; the hook gates commits.
* static/css/marks.css: layout for the new frontmatter slots and the
item-card monogram chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Preserve external works the site cites against link rot, host them at
permanent /archive/<slug>/ URLs in site chrome, and treat them as
first-class citizens of the backlinks and similar-pages indexes.
Curated, not crawled: the author adds one line to archive/manifest.yaml
and the build fetches, hashes, snapshots, and indexes the work.
* archive/manifest.yaml + tools/archive.py (fetch / refresh / wayback /
check / gc) — PDFs downloaded directly, HTML pages snapshotted with a
vendored monolith (tools/bin/monolith @ 2.10.1) into a single
self-contained file with the archive CSP and a noarchive robots meta
injected. Per-entry PROVENANCE.json committed; gitignored .txt
sidecars regenerated from the artifact's SHA-256.
* build/Archive.hs + build/ArchiveIndex.hs + build/Filters/Archive.hs
— Hakyll rules for /archive/ and /archive/<slug>/, a body Pandoc
filter that appends an archive affordance to live citations and
flips dead ones to the local copy on archive.py check's asymmetric
hysteresis (rotted needs 3 fails over >= 14 days; one ok recovers).
* build/Backlinks.hs — keeps archived external URLs through pass 1 and
canonicalises them to /archive/<slug>/ in pass 2, producing a
"Referenced by" section grouped by the fragment each citation
targets. build/Stats.hs gains a "Link archive" telemetry block on
/build/ (count, total size, median age, by-status / by-quality /
by-visibility, orphans).
* Integrity: archive.py fetch and build/Archive.hs (via sha256sum)
both re-hash every committed artifact, so a tampered file halts the
build even with cabal invoked directly or no .venv present. refresh
refuses to replace an uncommitted prior snapshot and rolls back
atomically on any exit path. removed.yaml is honoured by fetch,
wayback, and check using canonical-form (tracking-stripped,
arXiv-canonicalised) comparison.
* visibility: private keeps an entry in-repo but undeployed.
nginx/archive.conf emits X-Robots-Tag: noindex, noarchive for raw
artifacts that cannot carry meta directives.
The full design, phase plan (1-5), and three refinement passes live
in ARCHIVE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
README.profile.md, README.arcana.md, README.simd.md, README.icd.md,
README.neuropose.md never existed in the working tree. Cosmetic
cleanup; the credential-shaped patterns and content/drafts/ entries
are unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Update history: examples to show unquoted ISO dates, matching the
corpus convention (every date: in content/ is unquoted) and drop
the misleading 'prevent YAML date parsing' comment — the Haskell
YAML parser keeps the value as a String either way.
- Add a 'Tag-meta sidecars' subsection under Tags explaining that
content/tag-meta/{portal}.md uses a tooltip-only schema with no
title: and no body. Documents the one place in content/ where the
'every file has a title' rule does not apply.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- import-photo.sh: validate \$SLUG against ^[a-z0-9-]+\$ before
writing under content/photography/; rejects '../' and other
surprises early. Also fail loudly with a clear message if either
'magick' resize or 'magick mogrify -strip' returns non-zero
(prevents shipping a file that still carries EXIF when the strip
silently failed).
- compress-assets.sh: reject non-numeric MIN_SIZE up front (otherwise
the comparison fails later with a cryptic arithmetic error).
- extract-dimensions.py / extract-exif.py / extract-palette.py:
add traceback.print_exc() after the broad except so a corrupt
image produces a stack trace alongside the one-line summary.
- extract-exif.py: switch from Pillow's deprecated _getexif() to
the public getexif() API; pyproject allows Pillow up to 12 where
_getexif may be removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If data/backlinks.json fails to parse, every page that uses the
backlinks context aborts with 'fail'. The JSON is build-generated;
corruption is unlikely but not impossible (interrupted writer, disk
issue). Switch to noResult so the affected pages render without the
backlinks block instead of failing the whole build. The next clean
build regenerates the JSON.
Note: commonplace.yaml and now.yaml deliberately keep fail-fast —
they're hand-edited and silent fallbacks would mask author typos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Stats.hs: median uses (!!) directly after the empty-case equation,
dropping the unreachable empty-fallback arm.
- Stats.hs + BibExtras.hs: switch lazy readFile to strict readFile'
(System.IO). Lazy IO leaves handles open until the value is forced;
errors surface at unpredictable points and the em-dash fallback in
Stats can hide real I/O failures. Strict reads fail at the read.
- Stability.hs: stabilityFromDates uses 'last dates' directly, since
the (newest:_) pattern guarantees non-empty input.
versionHistoryRangeField and versionHistoryRangeEndField bind the
matched list as 'es' and call 'last es', dropping the
reconstruction of (newest : more) just to call last on it.
- Tags.hs: parentOf is a 3-arm case (\[\], \[_\], segs) instead of a
length-based guard around 'init segs'.
- Catalog.hs: renderGroup re-orders so the structurally-guaranteed
(e:_) arm is matched first; the empty arm stays as a coverage stub
with a comment noting it's unreachable per groupBy's contract.
- Utils.hs: trim uses dropWhileEnd instead of double-reverse.
All sites were runtime-safe before; the changes make the safety
structural and shorter to read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Emit a minimal robots.txt that points at the sitemap.
- Emit sitemap.xml covering every dated content page (essays, blog,
fiction, poetry, music) with absolute <loc> and frontmatter-derived
<lastmod>. Standalone pages (about, colophon, etc.) are
intentionally omitted: they're reachable via the main nav, lack
date: frontmatter, and would force a fallback lastmod that
misrepresents staleness.
- Replace the magic 'drop 8' offset in essay routing with
stripPrefix "content/". Same behavior, but reads structurally and
fails closed if the prefix ever changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A VPS_PATH containing whitespace or shell metacharacters would split
on the unquoted expansion and hand rsync extra arguments. The
existing VPS_PATH guard rejects obviously dangerous parents (/srv,
/var, etc.) but does not catch this. Quoting fails closed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches .python-version and the actual development environment.
uv.lock already declared >=3.12 but the project has been on 3.14 for
a while; the floor was just stale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add tools/model-checksums.sha256 with sha256 hashes for the five
Xenova/all-MiniLM-L6-v2 files served from static/models/.
download-model.sh was already plumbed to verify against this file
when present; the file itself was missing, so downloads were
unverified. Now every fetch checks against committed hashes and
fails closed on mismatch.
- Pin embed.py's SentenceTransformer load to a specific HF commit
(c9745ed1d9f207416be6d2e6f8de32d1f16199bf of
sentence-transformers/all-MiniLM-L6-v2). A future model bump can no
longer silently change embedding semantics across builds. Bump
deliberately when validating; re-run a full embed pass to refresh
the semantic + similar-links data.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add nginx/security-headers.conf — server_tokens off, HSTS (1y +
preload), X-Content-Type-Options, X-Frame-Options DENY,
Referrer-Policy, Permissions-Policy, and a usage-scoped CSP. CSP
ships in Report-Only mode; promote to enforcing once the report
stream is clean for a week. CSP allowlists are derived from actual
usage (cdn.jsdelivr.net for KaTeX/Vega, *.basemaps.cartocdn.com for
Leaflet tiles); 'unsafe-inline' and 'unsafe-eval' are documented
inline.
- Add nginx/vhost.conf.example — reference vhost showing the canonical
include order. The live vhost on the VPS remains the source of
truth; this file documents the structure so the VPS config can be
reproduced or audited from the repo.
- Shorten unfingerprinted CSS/JS cache from 24h to 1h. Bug fixes ship
to warm clients within an hour; if assets are ever fingerprinted,
this can move to immutable.
- Refresh README repo layout — add nginx/ entry, drop stale paper/
and spec.md references that never existed in the working tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comprehensive audit of the repo flagging HIGH/MED/LOW/NIT issues across
the Haskell build, Python and shell tools, content + frontmatter,
templates, static assets, nginx snippets, README, .env, and gitignore.
Records what was fixed, what was deferred, and the recommended fix
order so future me has a paper trail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>