- 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>
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>
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>
- 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>
- 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>