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>