Context
docs/ is generated. luria index rebuilds the practice and literature
indexes, the tag and status pages, the lineage page, the curation book, the
reports and the README badge region — 59 files — from the sources in
record/. Until now CI regenerated and committed them on every event,
pull requests included.
That is the wrong place for them, and the cost is not theoretical. In one afternoon it produced:
- Two branches conflicting on files neither had edited. Batch A and batch B of #56 touched disjoint sources — four literature notes each, in different clusters — and conflicted on nineteen generated pages, because each had regenerated the same shared index.
- The same conflict again after every merge. A view is rewritten by the
merge to
main, so a branch that already resolved it resolves it again on the next rebase. The batch B branch hit it twice, and the second time also had to absorbluria concretizerenaming the files the first merge had numbered. - A resolution procedure that is a ritual.
git checkout --theirs docs/, rerunluria index, re-stage, commit — mechanical, unreviewable, and performed on files whose correct content is a pure function of the sources anyway.
Nothing was ever learned from resolving one. The conflicts carried no information, which is the tell that the file is in the wrong place rather than that the process needs more care.
Decision
Adopt the split luria uses on its own record (LU-ADR-068), through the same composite actions:
- On a push to
main—docs-generaterunsluria concretizeandluria indexwithviews: "true", commits the regenerated views and the rendered README as the bot, and handsdocs-lintthe SHA it produced. - On a pull request —
docs-checkruns the generate action withviews: "false"and the lint action in the same job. Source repairs — a bare code linked, a journal entry’screated:filled from its path — are committed onto the branch, where the review reads them. No view is written.
The asymmetry is the point, and it follows from what each kind of file is. A view is shared: every branch would rewrite it, so two branches always collide. A repair is local: it touches only what the branch itself authored, so it belongs on the branch.
The principle this is an instance of
Added in v2, because filing this decision without naming the rule is what let the next case be argued from scratch.
The rule is LU-DP-002: one artifact, one writer, and the writer is wherever merges serialize. Handing out fragments and generating the shared view is half of it; generating it in exactly one place is the other half, because a generator that runs on every branch makes every branch a writer — which is the lock again, arriving through the thing that was supposed to remove it.
This decision is the docs/ instance. There are two others, and neither was
recognised as the same thing at the time: merge-allocated codes are
concretized where merges serialize
(LU-ADR-049), and the resolved-identifier lockfile is written by
luria remotes --resolve there rather than by the lint that runs everywhere
(luria#289, whose decision is not numbered yet and so is not cited by code).
The lockfile case is the one worth remembering here, because this record hit
it: remotes.lock.json satisfied everything the first half of the principle
asks for — the sources are fragments, one file per paper, and the lockfile is
derived — and was still a file every paper-filing contribution rewrote: one
file, one entry per cited identifier, touched by fifteen consecutive
contributions. The writer was the linter,
which is why nobody saw it.
Contributors therefore run luria index locally — the reports are how you
read what the lint is about to say — and discard the result before
committing. CLAUDE.md says so.
Alternatives considered
Keep generating on PRs and merge more carefully. This is what we were doing. It scales with the square of the number of concurrent branches and teaches nothing; the fourth time through the ritual is when it became clear the file placement was the bug.
Stop committing docs/ at all and build it at publish time. Cleaner in
principle, and rejected because the record is meant to be readable in the
repository —
ADR-006 is about generating the registry from the record rather
than checking in a separate artefact, and the browsable view being present at
a commit is what makes a link into docs/ stable. Views on main keeps
that; views nowhere does not.
.gitattributes with a merge driver for docs/. Would silence the
conflicts without moving anything, at the cost of a piece of custom
merge machinery every contributor has to have configured locally. It treats
the symptom, and it fails open — an unconfigured checkout silently gets the
old behaviour.
Consequences
- A pull request’s diff is now only the sources. That is a readability win nobody asked for: the batch A PR was 39 changed files, of which 29 were regenerated pages.
mainis the only branch whosedocs/is current. A link intodocs/from a branch may point at a stale page until the merge lands.- The lint on a pull request reads sources rather than checking a view it declined to build, so the staleness check is meaningful in exactly one place — the push — which is where a generator that failed to run is the thing you want to hear about.