The project index was the last hand-maintained duplicate of CV data, and it
had already drifted: a superseded Weenix line count and an end date the CV
disagreed with. It now renders from yaml-source/data/projects.yml, which
grows from three entries to nine and gains `group` (drives the index's
sections, first-appearance order, like now.yaml) and `links`.
Pmacs, Levshell, and Epiphany join under "Personal" — self-directed tools
rather than coursework or employment — written from the repos rather than
from memory. They are web-only: personal infrastructure is not academic
output, and the résumé is a full page. LeVCS keeps its essay; project
writeups stay essays, since a writeup is an informal presentation of a
project and not a record of it.
web_visible is a third visibility axis alongside cv_visible and
resume_visible, defaulting to cv_visible so nothing changes until it is
set. The file already knew how to let the CV and résumé disagree about an
entry; generating a third surface from the same data would otherwise have
collapsed that, and a document handed to a chosen reader is not a page that
gets crawled. This is what makes web-only projects expressible — and what
would let the xAI entry sit quietly on the CV while staying off the site,
if that is wanted later. It stays off both for now.
Two LaTeX conversions added while moving the markdown prose into the YAML:
$\rightarrow$ and $\sim$. The latter fixes a bug I introduced doing it —
"~10 crates" meant "about ten", but bare ~ is LaTeX's non-breaking space,
so it rendered "( 10 crates" and dropped the approximation.
Weenix's description now carries the site's fuller tail (the CS 169 origin
and the later pipes/preemption work) rather than the CV's terser "Brown CS
169." Single-sourcing means picking one, and this is the better one; the CV
grows by a clause and the résumé still fits on one page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUGesXiMmACsLBTGG1xuEU
The vita page carried a hand-typed duplicate of education, publications,
presentations, and experience, and had drifted from the PDFs in about
fifteen places — Shu Lab still one entry titled "Undergraduate
Researcher" three months after the degree, Weenix at ~7,000 lines, CHASE
still "accepted" after being presented, a preprint missing entirely, and
two different code URLs for the same paper.
build/Vita.hs reads yaml-source/data/*.yml — the same files the PDF
pipeline builds from — and renders education, experience (keeping the
CV's research/industry split), publications, presentations, and contact.
Drift is now structurally impossible rather than merely discouraged.
Notes on the implementation:
- The YAML is LaTeX-flavoured because xelatex is its first consumer, so
values pass through a small closed converter (\textbf, \textit,
\texttt, \href, $\times$, $\delta$, \#, {,}, ~, --, ---). Escaping
happens before conversion; unhandled commands pass through visibly
rather than silently dropping their argument.
- Scalars are parsed loosely: the same logical field is sometimes a YAML
number and sometimes a string (`year: 2026` vs `year: "2026--2027"`),
and the PDF pipeline does not care, so this does not force it to.
- personal.yml's phone number is deliberately not read. It is printed on
the CV, which is handed to a chosen reader; this page is crawled.
- Contact chips carry labels (ORCID, Forgejo, GitHub) rather than the
CV's full display values, which exist because paper cannot be clicked.
The page keeps only what nothing else on the site owns: the pointers and
the four research threads. In-progress work belongs to /current, the
engineering index to /cv/projects/, and the narrative to /me/ — whose
abstract already described this page as the formal record.
vita.css composes on item-card.css and mirrors now.css's headings and
chip geometry, so /about and /current read as one system. Link chips get
the only interaction polish on the page, being the one affordance a
printed CV cannot offer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUGesXiMmACsLBTGG1xuEU
Dated facts:
- CCV affiliation ended July 2026; drop the "(HPC-supported)" role suffix
and the Phase 2/3 in-progress claim (also on the résumé's PQC entry).
- Weenix ended August 2025 at 10,000 lines; the site said ~7,000 and
January–May 2025.
- CHASE 2026 presented August 3; moved to now.yaml's shipped list and
marked Presented on the CV.
- Shu Lab split at the degree boundary: Undergraduate Researcher and
Technical Lead (Oct 2023 – May 2026), then Research Affiliate
(May 2026 – Present, bulletless — the work is described above it).
- DTU advisor/research-group TBD note removed.
Claims that had started to decay:
- Math preprints read "Preprint" rather than "arXiv (forthcoming)", and
now.yaml calls them revising rather than in-review.
- SIMD/PQC is paused pending a compute move to DTU.
- Semantic-embeddings work dropped; MTG no longer claims OSCAR.
Redundancy:
- NSF affiliation description reduced to the two qualifications its title
and role line don't already state; MARS V grant note to the funding
terms; DTU fellowship placeholder deleted; NeuroAI held to one line.
Structure:
- xAI toggled off on both documents (retained in the file, two flags away).
- Contract work renders inside Industry Experience above NeuroAI as
"Independent Research Contractor"; Selected Contract Work is now
conditional so the heading can't render empty.
- Templates: guard bulletless entries, and fix an unguarded exp.location
in the CV that printed a literal "None" for entries without one.
now.yaml: pmacs, levshell, and epiphany notes rewritten against the actual
repos (all three were badly out of date); epiphany added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUGesXiMmACsLBTGG1xuEU
yaml-source/ was fully gitignored, so the YAML that drives both the CV and
the résumé had no version history and no backup — it existed only on one
machine. Track the pipeline *source* (data/, templates/, build.py,
Makefile) and keep only the artifacts (build/, output/, editor backups)
local, matching the existing intent that rendered PDFs ship via static/.
This is also a prerequisite for the site build: the vita page currently
hand-copies education, publications, presentations, and experience out of
these files, and has drifted from them in ~15 places. Generating it needs
the data present in the repo.
Includes the .claude/settings.local.json ignore rule already staged in the
working tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUGesXiMmACsLBTGG1xuEU
Six follow-ups from the link-archive audit, all the items with a
code-shaped fix. Behaviour verified per feature (stubbed + live where a
network or browser was needed); ARCHIVE.md status notes updated alongside.
- suggest: `archive.py suggest` + `make archive-suggest` print works
cited in data/*.bib (url wins, DOI-only resolves to doi.org/...) but
absent from the manifest, as manifest-ready lines. Read-only, offline.
- aliases: optional `aliases:` manifest field for equivalent URLs no
normalisation can derive (DOI vs. landing URL). Enforced like canonical
URLs on both sides (archive.py pre-scan + Archive.hs validator);
ArchiveIndex drops alias keys matching a takedown. FIPS 203 now carries
its DOI form, so the simd paper's DOI citation resolves.
- check scheduling: systemd user timer (systemd/archive-check.{service,
timer}, symlink-installed) runs the rot scan daily. cmd_check gains an
offline canary guard so an unattended scan on an offline machine leaves
state untouched instead of mass-flipping entries to rotted.
- Wayback fallback: an original already dead at first fetch falls back to
its most recent existing Wayback capture (raw id_ bytes through the
normal pipeline), honouring a preserved X-Archive-Orig noarchive and
recording `fetched-from`. Dead-only, never during refresh.
- bibliography annotation: Filters.Archive exports annotateBlock;
Citations.hs applies it to each CSL-rendered entry, so bibliography
links get the same affordance / rotted-flip as body links.
- search-UI filter: archive "exclude/only" + "link status" filters on the
search page, backed by a new data/archive-meta.json. Scoped apart from
the epistemic `status` filter (own state, classes, labels) rather than
renamed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
positionPopup clamped horizontally but never vertically: the
flip-above branch positioned tall popups (rich layouts, lead figures)
above the visible region whenever the target sat near the top of the
screen. Placement is now: below if it fits, above if THAT fits, else
the roomier side — then clamped into the viewport on both axes.
.link-popup is additionally capped at viewport height (matching GAP,
overflow-y: auto) so the clamp always has room to work, and a
dimension-less image that loads after positioning re-clamps instead of
growing past the edge.
Verified by simulating the clamp across five viewport scenarios: the
near-top bug case moves from 470px above the viewport to fully
visible; the fits-below and flip-above cases are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reference popups (provider-rendered: arXiv, Wikipedia, CrossRef, …)
get a glanceable layout: wider container (560px), larger title and
body type, and a full-width image banner under the source label.
Internal page previews and item-card popups (new/library pages) keep
the compact layout — the shared popup element toggles
.link-popup--rich per show based on the rendered content.
- arXiv: a new best-effort enrich step fetches the paper's LaTeXML
HTML rendition and pulls the first figure as a lead image. Enrich is
time-boxed (1.8s) so the metadata popup is never held hostage; late
results refresh the cache for the next hover. Figures letterbox with
object-fit: contain (plots must not crop); Wikipedia photos
cover-crop with an upper focal point. width/height attrs reserve
aspect ratio so positioning is stable before the image loads.
- Wikipedia thumbnails request 480px for the banner width.
- nginx: new ^~ /proxy/arxiv-html/ location backed by arxiv.org proper
(export.arxiv.org serves the Atom API but 429s the /html/ asset
tree); 404s cached 1d (the common no-HTML-rendition case). All four
proxy locations switched to ^~ — without it, static-assets.conf's
per-extension regex location outranks plain prefixes and serves a
local 404 for any proxied URL ending in an image extension, which is
exactly how the first figure fetch failed.
Installed and verified live: proxied page (200, 298KB), figure (200
image/png), API unchanged, no-rendition 404 path; the full client
resolution chain (relative src -> proxy path -> guard -> image)
validated against production.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
add_header is non-additive: any location declaring its own add_header
drops all server-context headers. archive.conf already re-included the
baseline for exactly this reason, but static-assets.conf (four cache
locations — including the JS/CSS responses where nosniff matters most)
and popup-proxy.conf (three proxy locations) did not. All seven now
re-include snippets/security-headers.conf.
Proxy locations additionally hide the upstream's own
STS/CSP/X-Frame-Options before re-adding ours: browsers honor only the
FIRST Strict-Transport-Security header (RFC 6797 §8.1), so arXiv's
max-age=300 passing through ahead of ours would have downgraded the
domain's cached HSTS policy on every popup fetch.
Server side (installed + verified live): security-headers.conf and
archive.conf wired into the vhost in vhost.conf.example's canonical
order; nginx-mod-brotli installed and loaded, so the .br sidecars
compress-assets.sh has always shipped are now actually served
(Content-Encoding: br verified). CSP remains Report-Only. Verified
headers on /, /css/*.css (baseline + Cache-Control together),
/archive/ (baseline + X-Robots-Tag), and /proxy/* (baseline +
X-Cache-Status, single STS).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The root cause of 'PDF/arXiv previews simply do not work' was twofold:
1. nginx/popup-proxy.conf was never installed on the VPS — every
/proxy/* request (arXiv, PubMed, Internet Archive) returned nginx's
default 404. Now installed (snippets + http{}-context cache/limit
zones in conf.d, included in the vhost, nginx -t verified, reloaded).
2. The snippet itself had a latent bug that only surfaced once
installed: with a VARIABLE upstream, a URI part on proxy_pass is
passed literally — every request hit the upstream's homepage
(archive.org HTML where JSON was expected, arXiv 429s, NCBI doc-page
redirects). Fixed with explicit prefix-strip rewrites; bad cached
responses purged. All three proxies verified returning real data,
including a live arXiv title resolve.
Client-side improvements:
- arXiv match covers old-style IDs (cs/9901002, math.GT/0309136,
cond-mat/...v1) alongside new-style, and .pdf-suffixed /pdf/ URLs
(regex verified against six forms)
- Wikipedia popups show the article's lead image: pageimages rides
along the existing extracts call (pithumbsize=320), rendered via a
new https-only image slot in renderPopup with float styling;
upload.wikimedia.org added to the CSP's img-src
- pdf-thumbs now walks all of static/ (pdfjs pruned), so /cv.pdf and
/resume.pdf — the most-linked internal PDFs, previously thumbnail-less
and therefore popup-less — get hover previews
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- add-popup-source.sh: slug validated against ^[a-z0-9-]+$ before nginx
interpolation; UPSTREAM_HOST derived unconditionally so the CSP
reminder fires in the no-proxy case — which is exactly when the host
must be added to connect-src (AUDIT §4.8)
- refreeze.sh: backs up the freeze and restores it on a failed resolve
instead of leaving the repo with no freeze file (§4.9)
- einops gets the policy-mandated upper bound and a comment naming its
consumer (nomic's remote modeling code) (§1.5)
- Makefile: pdftoppm failures warn instead of vanishing in the while
pipeline; .NOTPARALLEL guards deploy's clean->build->sign ordering
against -j invocations (§8.4)
- Atomic writers (embed, archive, the three sidecar extractors):
PID-unique temp names so concurrent runs can't interleave, cleanup on
failure everywhere, fsync where the artifact is not trivially
regenerable (§4.10)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>