ADR-042: The record publishes as a Quartz vault: paths preserved, sources withheld, frontmatter surfaced
Status Active · Version 2 · Filed 2026-08-09 · Issue #13 · Influenced by ADR-005 · ADR-021 · ADR-029 · Influenced ADR-043
Context
The record is a graph and reads like a list. Every decision cites the
principles it stands on, the decisions it supersedes and the ones that
influenced it; luria link --fix has made every one of those citations a
followable link (ADR-005). On GitHub that graph is only ever
traversed one hop at a time, and the shape of it — which decisions are
load-bearing, which principle everything leans on, which corner nothing
cites — is invisible.
#13 asked for the obvious
remedy: publish the record with Obsidian/Quartz, whose graph view and
backlinks are exactly that shape rendered. It also flagged a worry — that
Luria interprets wikilinks differently from Obsidian, since a [[CODE]]
here resolves through the scheme config while a vault resolves by
filename, so aliases might be needed.
That worry turned out to be answered already, and the answer is worth
recording because it is not obvious: the vault never sees a wikilink.
ADR-025 has luria link --fix consume them at source, rewriting
each into an ordinary markdown link, precisely so files read as plain
markdown wherever views aren’t generated. A site generator is one more such
place. The two interpretations never meet, so there is nothing to reconcile.
What is left of that worry is the vault-authoring case — someone editing
record/ in Obsidian, typing [[ADR-013]], wanting completion. For a
file-per-code scheme the two conventions agree by construction, and the
staging adds each document’s code as an aliases: entry so the short URL
(/ADR-025) agrees too.
Decision
luria site --out DIR stages the record as a Quartz vault —
DIR/content/ plus a quartz.config.ts derived from luria.toml — and
actions/site builds it with a pinned Quartz, handing the caller a
directory for actions/upload-pages-artifact. Luria publishes its own record
with the same action adopters get, referenced by local path, so a change to
the action is exercised by the pull request that makes it
(ADR-029).
Three rules, and the third is the one that took a build to find.
1. Paths are preserved, so links are never rewritten. Every relative
target in this record was spelled by the fixer for the directory its prose
lands in. Staging into the same tree shape means those targets keep
resolving, and the site inherits the fixer’s work whole. The alternative —
flattening the vault and rewriting links to match — is a second link
resolver, and two of those drift (DP-4).
Quartz needs one setting for this to hold: markdownLinkResolution: "relative". Its default re-resolves a relative link by basename and lands
docs/README.md’s decisions/README.md on the wrong page — silently, with
no broken link to notice.
2. A source that renders into a view is withheld; the view is published.
Not a list of directories — a derivation. Config.link_base(path) already
answers which directory do this file’s links resolve against?, and a file
whose answer is not its own directory is a fragment, a journal entry, or a
document-scheme source: prose written to be read somewhere else. Publishing
one in place breaks every link in it, and duplicates the view it renders
into. The rule is one line, it needs no maintenance when the layout moves,
and the test asserts the invariant rather than the set
(DP-3).
This is ADR-021’s read/write boundary showing up in a new
place, with a nuance: the boundary is not “publish docs/, hide record/”.
record/decisions.d/ is where the record’s links point, so it is a read
surface too. What decides is the link frame, not the directory.
3. A link that leaves the published set goes to the repository. The
record cites workflows, the scaffold, the licence — real files, and not
pages. An image is copied into the vault so it renders; anything else
becomes a source_url link. What cannot be placed is counted and printed
rather than emitted dead (DP-1).
And one addition rather than a copy: the record line. Each scheme
document gets, under its title, a rendered line carrying status, date,
issue and influenced_by. Those facts live in frontmatter, which a site
renders as nothing at all — so without this a superseded decision reads on
the web as current, which is the worst failure this record can have. The
line is composed as wikilinks and expanded by the same resolver everything
else uses, so the fixer still owns every target.
Configuration is [luria.site], and every key of it derives from
issue_url for a GitHub project — title, Pages URL, and the source base —
so the conventional case needs no table at all. Only exclude is genuinely
per-project.
Quartz is pinned to a tag (v4.5.2), not tracked. A generator that
moves under you turns an unrelated merge into a broken publish, and the
config luria site writes is written against one version’s plugin API.
Alternatives considered
- Publish
docs/only. The read surface, and the obvious reading of ADR-021. It fails on the first click: the decision index is a table of links intorecord/decisions.d/, so adocs/-only site is a site whose main page is forty broken links. It also produces exactly the graph worth nothing — one index node pointing at nothing. - Publish everything, including the fragment and journal sources. Richer graph, one build, no rules. It was tried first, and the build reported it: 59 pages with broken links, every one of them a source whose prose is rendered into a view somewhere else. The rule in the decision is what that failure taught, and the duplicate-content problem (a devlog entry beside the book containing it) would have been the next complaint.
- Rewrite links into a flattened vault — one directory of pages, Obsidian-style. Cheaper URLs, and a second implementation of “where does this reference point”, which is the drift DP-4 names. The fixer would then be one of two authorities on a target, and the one nobody runs.
- Quartz v5. Its config is YAML, which would have been generated rather
than templated — a real improvement. Its plugin installer crashes on
.scssunder Node 22 (ERR_UNKNOWN_FILE_EXTENSION, loading a plugin’s components throughtsx), so v5.0.0 cannot build at all here. Revisit on the next patch release; the migration isquartz.config.ts→ a YAML document, which is a change to one templated file. - Expand wikilinks at build time, to give the vault the Obsidian shape. There is nothing to expand: ADR-025 consumes them at source. This alternative only looks live if you have not read that decision, which is why it is written down here.
- Do nothing. The graph stays a property of the corpus that nobody can
see, and every “what does this decision rest on?” stays a
grep.
Consequences
- The record is browsable at a URL, with a graph, backlinks, full-text search and per-tag pages — none of which anybody maintains, because all of them are derived from citations the lint already guarantees are links.
- A new failure surface: the site can break while the record is fine.
The Pages workflow builds after the generation job has committed the
views on the default branch (
workflow_runon the CI workflow), so it publishes the record as regenerated rather than the merge commit one bot commit behind — which is what a push-triggered build had done, the bot’s own push triggering nothing. Nothing builds on a pull request: a branch carries no views of its own (ADR-068), and a preview would render the default branch’s views under the branch’s sources. A site that stops building therefore fails on the default branch, after the merge; the build’s reference-integrity value below is what that costs, and the staleness it removes is what it buys. - Enabling Pages is a repository setting no file can carry. Settings →
Pages → Source must be “GitHub Actions”; until it is,
deployfails with “Pages is not enabled” whilebuildstays green. Said in the workflow, in the action, and in adopting, because it is the one step that cannot be scaffolded. - The build is a reference-integrity check the lint does not have. It
found a real defect on its first run: the decision index composed rebased
link targets without normalizing them, emitting
../../record/decisions.d/../../docs/design-principles.md#dp-2— valid on GitHub, which collapses it, and broken under a generator that does not. Filed as #67 and fixed here; the shipped view now carries the short form. - Quartz’s version is now a thing to bump deliberately. The pin is in
actions/site, one place, andluria site’s config template is written against it. A test asserts the two settings the record depends on, because a Quartz upgrade is exactly where a default quietly comes back. luria sitewrites into the project by default (build/site), which the scan then has to skip — otherwise the second publish publishes the first one’s output. Guarded and tested.