This file is the committed map of ledgerpress: the build, data boundary, release path, and load-bearing constraints that should travel with the template.
content/ owns the record: every fact about the adopter, and which sections the CV prints.
content/README.md documents it. The website in web/ and every printed document in cv/ consume
the same content/cv.yaml, content/publications.bib, and content/talks.bib; posts and media live
beside them. Replacing the record must not require editing outside content/ — that is what
npm run check:adopter proves.
Design is code, deliberately: the theme, the printed layout and its curated section headings, routes,
redirects and announcement wording are all edited outside content/, and README.md's "Make it
yours" lists where. Do not move any of them into content/: that was considered and rejected,
because layout and routes cannot follow and the boundary would blur again. The boundary is facts
versus design — keep every document saying the same thing about it.
Three documents are printed: cv/cv.tex, cv/short.tex and cv/teaching.tex. cv/preamble.tex
holds every package, style and macro they share; a block too particular for a macro is a body
fragment they \input, as cv/header.tex (the contact block) and cv/supervision.tex (that
hand-set section) are. The printed design is therefore written once, and a document body is only
which sections it curates, in what order, and how many entries of each. No fact is repeated per
variant, and none may be: a variant carrying its own copy of an appointment breaks the premise.
Three things a document may set for itself instead of editing the shared layout — \cvtypeface,
\cvcoursecols and \cvsourcemaps, defined before its \input{preamble.tex}; README.md's "Three
printed-CV settings a document carries itself" documents them and cv/preamble.tex explains each
where it is read. They exist so an adopter with a forked cv/preamble.tex can drop the fork. Each is
\providecommanded to today's look, so the bundled example is unaffected, and no check asserts any
of them. Do not grow the set into a theming system: a fourth setting needs the same evidence, a real
fork that a shared layout otherwise cannot absorb.
Only cv/cv.tex prints \cvAutoSections: a variant is a curated subset, so printing every section
the body did not name is what it exists not to do — it still honours printed: false, because it
reaches every section through \cvpart. What belongs to a variant is layout — including the entry
count on \cvpart and any \defbibfilter only that document prints. What belongs to the record is
curation, marked on the fact itself, as selected is in content/publications.bib. A section
declared under publications.sections for one document's sake changes the full CV and the website,
because each section subtracts the ones declared before it.
web/src/lib/record.ts owns the complete list of source paths. It locates the repository by walking
up for content/cv.yaml, the file no build can work without. web/src/lib/record.test.ts fails if a
record source points outside content/.
scripts/build-cv-data.mjs turns content/cv.yaml into the committed
cv/generated/cv-data.tex. Never hand-edit generated data. Every top-level list becomes a macro
family without the generator naming the section, and \cvAutoSections carries the printed section
sequence, so a section added to the record reaches the PDF with no .tex edit: cv/cv.tex prints
that sequence and skips the keys it lays out by hand, which \cvdeclare records.
Publication and talk grouping is declared under
publications.sections and talks.sections; no BibTeX type or grouping policy belongs in
cv/cv.tex or the website publication reader.
npm run checkruns every adopter check. Each one passes on any valid record; a check that can only pass on the bundled example belongs undernpm run check:maintainerinstead. README.md documents which set an adopter runs. It regeneratescv/generated/cv-data.texfirst — the one tracked file any check writes — and announces that it does, because the checks after it compare that file with the record.npm testkeeps the freshness gate unchanged, so an uncommitted regeneration still fails CI.npm testruns the repository and website self-checks plus the generated-data freshness gate.npm run buildregenerates CV data and builds the production website.npm run devruns the local site; it forwards to thewebpackage.npm run check:formatis the Prettier gate;npm testdoes not cover formatting.latexmk -xelatex -cd cv/cv.texbuilds a PDF; the same forcv/short.texandcv/teaching.tex. XeLaTeX is required.npm run check:maintainercompares every built PDF with thedata/cv-baseline/entry named after it;bash scripts/check-cv-baseline.sh [pdf]checks one. It is the one check tied to the bundled example, and it skips with one line oncecontent/names someone else. It decides that fromOwner:in that PDF's*-baseline-meta.txtagainstprofile.name, and fails rather than skips when either is unreadable, so a layout change cannot silence it.data/cv-baseline/README.mdlists all three baselines and how to re-record one.npm run check:adoptercreates a tracked-file-only copy, replaces onlycontent/, and proves both the site and PDF cold start.
web/public/.nojekyllmust reach the generated distribution; GitHub Pages otherwise strips Astro's_astroassets.content/media/is staged into the web public media build directory by the web package lifecycle. That destination is ignored build output, never source.web/src/lib/cv.tsuses Vite's?rawimport because Astro relocates prerender bundles. Node-compatible schema and pure helpers stay inweb/src/lib/cv-schema.ts.readCvinweb/src/lib/cv-schema.tsis the one boundary every website reader ofcontent/cv.yamlcrosses,profile:included: nothing underweb/may parse that file a second time. It coerces YAML scalars to text and rejects anything else by naming the file, the field path and what was expected. A schema failure that does not name the field is a bug in that function. Plain-Node scripts outsideweb/are outside this website boundary;scripts/build-cv-data.mjsparses the record itself and coerces withString()where it renders.- The website self-checks read
web/src/lib/fixtures/record/, notcontent/; that directory's README states the rule. Onlyweb/src/lib/live-record.test.tsreads the real record, and everything in it must hold for any valid record. web/src/lib/consistency.tsfails production builds when two hand-authored dates on the same fact contradict. It walks every top-level list and never joins facts by matching prose.- Counts, provenance, omissions, sorting views, announcements, and gaps are derived at build time. Do not replace them with hand-written numbers or copy.
- A
\printbibliographywhose filter matches nothing prints nothing at all, its own heading included. Under a hand-written\sectionthat leaves a title over silence, so every hand-written filtered block goes through\cvbibfiltered(cv/preamble.tex), which names the filter that matched nothing instead of failing the build. The generated sections are exempt and must stay so. - A zero-entry bibliography must skip its entire
refsection; biber otherwise silently emits[0]labels.\cvdeclareand\cvdeclarebibkeep missing sections safe for a minimal record, andscripts/build-cv-data.test.mjsproves every document names only macros they define. scripts/check-deployment-base.mjsbuilds its fixture-base proof into its own ignored throwaway directory and asserts the generated distribution is byte-identical afterwards. That distribution is what the deploy publishes, so a verification build must be structurally incapable of becoming it. It once rebuilt in place and the deploy published the fixture base with every check green.- No adopter-facing check may assert something an adopter is invited to change, and no build may
fail over branding, credit, typography or presentation. Checks are for correctness: a lost
deployment base, a broken link, a contradictory fact, an artifact the code itself promises. Where
a property depends on the record or on an optional artifact, derive the subjects from the build
output or guard the assertion behind the thing it checks — never add a flag, manifest or config
surface to describe what is optional.
scripts/check-deployment-base.mjsis the worked example: it names no font and requires no printed CV. web/public/fonts/LICENSES.mdtravels unchanged with the bundled Ledger fonts.- Root Prettier checks the whole repository with
.prettierrc; keep nested configuration compatible.
ledgerpress is the publishing product. Ledger is the design system in web/src/styles/global.css
and the bundled LedgerSerif faces. Keep those names distinct and intentional.
.github/workflows/deploy.yml builds all three printed documents, runs each baseline, stages them
into the website's ignored assets directory as cv.pdf and cv-<variant>.pdf, builds the website,
and publishes the generated distribution to gh-pages. /cv/ links the full CV as the primary
download and the variants beside it, each offered only when its file is really staged; .gitignore
lists the three staged names.
.github/workflows/cv.yml builds the same three for review, checks each baseline, exposes them as
artifacts, and runs the whole adopter cold start inside its pinned TeX environment. The two must
agree on the set — a document only one of them builds either never reaches a reader or reaches one
unreviewed — and scripts/build-cv-data.test.mjs fails when they disagree. The deploy workflow has
no path filters because almost any tracked file can affect a build.
Keep this file for knowledge useful to almost every future agent session in this project. No value that lives in another file may be restated here; point to the authoritative file or command instead. Prefer rewriting or pruning existing entries over appending new ones. When updating this file, preserve this bar for all agents and keep entries concise.
Keep .claude/CLAUDE.md as the single one-line import of this file for Claude-compatible tools.
Do not add another pointer at the repository root: having both pointers makes those tools load the
same instructions twice.