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. A render = "document" scheme’s prose is written to resolve from the page it assembles into, so ../../docs/values.md in 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 because name on <a> is obsolete in HTML5 and adds nothing id does 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 cite to cover journals. What ADR-094 did for principles, applied again. Rejected as the same workaround twice: cite is 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.