Context
A fragment link — 2026-09.md#20260914042025 — is resolved by whatever
renders the page, and the renderers do not agree on what an anchor is.
<a id="x"> an element with that id. Reached by navigation and by
`getElementById` alike.
<a name="x"> neither. The HTML spec has a real navigation fall back to
`a[name]`; nothing else does.
## Heading an id from every renderer there is, derived from the text.
Luria emitted the middle one. journal.render_book wrote
<a name="20260914042025"></a> before each entry, and render_document wrote
<a name="dp-3"></a> before each source it assembled.
That works in the repository, and on GitHub, and in any editor preview —
because those are real navigations. It does not work on a Quartz site, which
is what luria site builds and what this record publishes to. Quartz is a
single-page app; its router scrolls with
document.getElementById(decodeURIComponent(url.hash.substring(1)))
(v4.5.2, quartz/components/scripts/spa.inline.ts, both call sites), which
finds an id and nothing else.
89 of the 100 fragment links in this repository resolved by name and no
other way. Every entry link on every devlog index — 55 in September alone —
and every citation of a design principle. All of them landed at the top of the
page they named.
The shape of the failure is the thing to take from it: it was correct
everywhere a contributor would check and wrong only where a reader would
read. Every check luria runs reads markdown, and docs/devlog/2026-09.md
resolves perfectly as a path; the anchor is genuinely in the file; the link is
genuinely spelled right. Nothing short of loading the built site could see it.
ADR-094 measured this and got the cause wrong — it recorded that Quartz
“drops the element”, and rejected “emit anchors that survive the publisher” on
that basis, as a fix that would need re-finding for each publisher. Quartz
drops nothing: the <a> is in the published HTML. The rejected alternative was
one attribute, and correct. That ADR is corrected in place and versioned
(ADR-001’s rule: the choice stood, the reason was wrong).
Decision
The generator emits id. Both emitters, one attribute each. id is
reachable by every means name was and by getElementById as well, so this
is strictly a widening — no link that worked stops working.
doc_refs reads either spelling. A project whose principles are still one
hand-written file may anchor them by name, and that file resolves fine where
it is read. Refusing to read it would turn a publishing defect into a parsing
one.
A check, with a --fix. luria lint reports a fragment link whose target
answers to it by <a name=> alone; luria link --fix rewrites the anchor.
The check is what makes this stay fixed — the generator is not the only thing
that writes an anchor, and the next hand-written one would put the bug back
silently, which is the polarity DP-3 rules out.
It reads the render, not the tree. This took three tries and is the whole of what the check is.
Sources alone cannot see the defect: the motivating case is a journal index linking into a journal book, and both are generated. A check shaped so it cannot see what it exists for is not a check (DP-4).
The committed views cannot be used either, and CI is what said so. A branch
carries the default branch’s copies of every view and deliberately does not
update them (ADR-068, the amendment on where views land), so a check reading
them fails every pull request that touches an anchor — and names a repair the
author is not allowed to make. check_view_dirs has always known this, and
its docstring says it plainly: “a branch carries the default branch’s copies
and has nothing to be stale against.”
So the check is handed every source, and every view as the generator would
write it — adr_index.outputs(), in memory. That asks the question that is
actually about this source tree: will the views this record produces contain
a fragment nothing can scroll to? It is silent on a branch whose sources are
right and whose committed views are old, and it fires the moment an emitter
or a stub writes a name again.
Two smaller things it also had to get right:
- It resolves from
link_base, not from the file’s own directory. Arender = "document"scheme’s prose is written to resolve from the page it assembles into, so../../docs/values.mdin a source is correct there and nonsense from the source’s folder. Resolving naively reported five real links as broken targets. - One finding per unreachable fragment. A stub is the authored part of a generated page, so its prose is found twice — once where a person wrote it and once in the view. The view copy is dropped when an authored one exists, because the authored copy is the one somebody can edit.
The repair edits the file holding the anchor. The link is spelled
correctly; the thing it names cannot be found. Which file that is cannot be
read off the link’s target — a <a name=> in a stub is reported against the
view it renders into, and the view is not the thing to edit. So --fix
repairs every authored document carrying a reported fragment, and never a
view. Loose, and safe to be: only anchors some link actually named are
touched, and turning name into id is an improvement wherever it lands.
Alternatives considered
<a id="x" name="x">. Belt and braces, and it costs nothing. Rejected becausenameon<a>is obsolete in HTML5 and adds nothingiddoes not already do — carrying it would be carrying the spelling that caused this, in the file that documents the fix.- Give the entries their own pages instead of anchors in a book. The other reading of “the link is broken”: stop assembling. It is a real option for a journal and a bad trade for this one — a book read in order is the point of the granularity setting — and it would not have touched the principles case at all, where the anchors and the assembly are both wanted.
- Leave the generator and widen
citeto cover journals. What ADR-094 did for principles, applied again. Rejected as the same workaround twice:citeis a preference about where a citation should land, and using it to route around an anchor that does not work makes it impossible to tell the two reasons apart later. This change is why ADR-094’s key is now a preference. - Check that every fragment resolves to something. Broader, and tempting.
Rejected for this change: a fragment naming a heading is resolved by a slug
each renderer computes slightly differently, so the check would have to
model several slugifiers to avoid reporting links that work. The finding
here needs none of that —
name-only is unambiguous, mechanically repairable, and the whole of what was actually broken.
Consequences
luria index rewrites every anchor in this record’s views in one pass, and
the 89 links resolve. Any adopter’s record does the same on its next build;
nothing in a source has to change.
The check renders the record to answer its question, which makes it the most
expensive reference rule — adr_index.outputs() is the same work luria index does. It runs once per lint and shares nothing with the generation
job, which is a cost worth revisiting if the lint gets slow. It buys the
thing that made the first two versions of this check useless: an answer that
does not depend on which branch you are standing on.