Development log — September 2026
12 entries. All books.
Contents
- 3 Sep 03:58 — Three lint passes compile into one contract per scheme
- 3 Sep 04:09 — Three facts the record already stated, read as edges
- 3 Sep 04:43 — Provenance goes into the finding, not into a command
- 3 Sep 14:25 — A list in a reference field was read as its first code, silently
- 3 Sep 16:22 — A field that was neither a reference, a tag nor a status
- 3 Sep 18:00 — One field was two types, and had been for sixty decisions
- 3 Sep 19:21 — Requiring arxiv was requiring the wrong thing
- 3 Sep 19:42 — Six conflicts on files nobody wrote
- 3 Sep 21:57 — A temporary code in a workflow file breaks the generation job
- 3 Sep 22:23 — An ADR closed in review: the constraint was the token’s, not the file’s
- 3 Sep 22:39 — The lint reads sources; nobody regenerates a view to check a branch
- 3 Sep 22:50 — The published site was one bot commit behind
Three lint passes compile into one contract per scheme
2026-09-03 03:58:41 · lint · config
The three per-scheme tables were three lint passes, and #141 asked
what one place that answers “what does this scheme demand of an entry?”
would look like. requires (ADR-040), tag_groups (ADR-054) and
references (ADR-060) each arrived as its own loop in luria/lint.py,
each re-reading and re-parsing every document’s frontmatter, and each
spelling its own provenance by hand in the message it printed. The
requires loop lived inside the frontmatter check; the other two were
functions of their own. Nothing related them, so nothing could say, for a
given entry, what the whole set of obligations was or where each came from.
They now compile into one Contract per scheme (luria/contract.py): the
fields an entry must carry, what each must hold, and which of its tags
combine, with every field naming the config keys that declared it. The lint
runs one pass over it (check_contracts); the two standalone functions are
gone and the requires loop has left the frontmatter check. The messages are the ones the record has always printed, byte for
byte, which is what let the existing tests for all three tables be
repointed at the single pass without rewriting an assertion.
This is step A and B of the sequence agreed on the issue: an internal
representation, existing semantics compiled through it, no new authoring
surface and no new command. Deliberately not here: a luria explain verb
(ADR-030 retired standalone report commands nobody ran; provenance belongs
in the finding an author actually meets), a forbidden presence (nothing
would emit it, and it is the only thing that would let presence
constraints conflict), and any contribution from statuses.yaml or
tags.yaml (ADR-054 rejected that home on purpose, and revisiting it needs
a measured failure first).
The one behaviour change, and the ordering
Composition is intersection. A field in both requires and references
used to be checked twice and reported twice — ADR-060’s consequences
section called that noise and left it. Compiling merges the two into one
obligation carrying both declarations, so a missing field is reported once,
with the reference wording since it is the stronger claim. A references
entry with required = false on a field that requires also names stays
required: required and required is required.
The output order of a run with several kinds of violation also moves: a missing required field used to print among the frontmatter findings and now prints with the contract findings, after the status-vocabulary check. Nothing reads the order.
Fired on a real case
This repository’s own schemes declare none of the three tables, so every
contract here compiles to empty and the pass is inert on this record — the
suite pins that. The knowledge-base example is the record that exercises
it: SOTA there declares requires, references and a derived
exactly-one group, and the example tests drive a document through all
three on the new pass. The contract module reads the same config keys the
three loops read, so the cheap check that no key was left behind was
grepping lint.py for requires, references and tag_groups after the
cut and finding only the docstring.
Three facts the record already stated, read as edges
2026-09-03 04:09:26 · site · record
Step C of the #141 sequence: the typed edges ADR-060 deferred, plus the
one this record can actually fire. The proposal wanted a superseded_by:
field; the record’s own convention already carries the successor in the
status note, so the decision (ADR-071) reads the edge out of the note
instead and adds no field. Three edges, all derived: any declared
reference field, named for the field; influenced_by:; and the successor
a canonical Superseded — by CODE note names.
Nothing read the note before. Worth stating because it looked as though
something must: ref_status splits the bare status word off the note and
discards the rest, the site renders the status line verbatim, and the index
only rebases the note’s link. The successor was visible on the superseded
page and invisible from the successor’s — a reader of ADR-035 was never told
it replaced ADR-007.
Reading a note as prose
The extraction is deliberately not a new regex. The note may be a bare code
or a markdown link, so it is unlinked with the fixer’s own UNLINK_RE and
then handed to find_refs, the scheme-driven reference finder (ADR-046).
That buys three things for free: temporary codes match, remote codes come
back as a different kind and are dropped (a remote has no node for an edge
to land on), and a note that cites two codes yields two edges.
Two drafts got the relation wrong, and review caught both. The first
named every code in a Superseded note superseded_by, so a note that runs
on past its successor — ADR-015’s does — would have had the site say
Supersedes of a decision that supersedes nothing. The second kept the
canonical by CODE code as the successor and named every other code in
any status note a status_note relation, on the evidence that a
world-building record has four of ten non-Superseded notes citing a code.
The evidence was right and the relation was wrong: where a code was found
is provenance, and a relation named for a location is a location wearing a
meaning. So the derivation is one function, successor, that reads a
canonical note and nothing else; every other code in a note is a mention,
and mentions belong to the citation scanner once the note is a prose key —
which ADR-072 then did, the same day.
And a third correction, the one that settled it. With the note a
prose field, the derivation still read the successor out of its by CODE
shape. The author’s review said what should have been obvious from #141’s
first line: the point is structured, explicit relationships, and a
relation inferred from free text asks the author to phrase prose so a
regex can read it. So superseded_by: is a built-in reference field on
every scheme — one code or a list, into any local scheme, checked and
resolved, an error when a Superseded document leaves it empty — and the
edge reads the field. The by CODE shape is read exactly once more, by
luria index, as the repair that fills the field: three decisions here
moved, two notes that said only the code were dropped, one that said more
was kept and trimmed by hand. I resisted the field twice on a drift
argument that does not apply to two keys in one frontmatter block; the
record says so.
Six splits became one parse. Reading the note needed the status word
and the note apart, and the codebase already did that in six places with
three spellings of the regex: the status report, the pending report, three
in the vocabulary module, and the first draft of the edges. Status(value, note) on the document replaces them all. Storage followed: ADR-072
gives the note its own field.
Fired on this record
Staging the site over this repository: sixty-six record lines as before, and twenty-six of the sixty-six decision pages now carry a typed edge. The three successions all read in both directions. ADR-016’s line is the busy case — it supersedes ADR-015 and influenced three later decisions — and it reads correctly. ADR-035’s page names the two principles it shaped, resolved to their anchors in the design-principles document by the same resolver that links them everywhere else; nothing in the site module spells a target.
The declared-reference direction cannot fire here (this record declares no
references), so it is pinned by the knowledge-base example’s shape in the
tests: a practice’s page shows Source, the paper’s shows Cited as
source by.
What was not built
The lint the edge makes possible — a Superseded document whose note cites
no code. The audit in the decision’s alternatives is the reason: three of
three name a successor, so by the gate agreed on the issue there is no
failed convention to enforce against. It is one function over the graph
when a record needs it.
Provenance goes into the finding, not into a command
2026-09-03 04:43:50 · lint · docs
Step D of the #141 sequence, and the one the author reversed himself on
after review. The proposal wanted luria explain CODE before any new
semantics. The record’s own precedent (ADR-030) says a standalone report
verb is a surface nobody runs, so the work went the other way: every
contract finding now cites the key that declared the obligation, and the
generated record page lists each scheme’s whole contract from the same
renderer. Decision recorded as ADR-073.
What “provenance” was before this. Every finding ended in
“(luria.toml)“. Correct, and content-free: there is one file, so the word
told the reader nothing they could act on. Field.because already carried
the key paths from step A; nothing rendered them. The renderer now groups
the keys by file — (luria.toml: schemes.SOTA.requires, schemes.SOTA.references.source) for a merged obligation — so a second
file, if one ever contributes, appears as luria.toml: …; statuses.yaml: …
without the renderer learning about it.
Tag groups gained provenance they never had. A group finding named the
group and its members and nothing else. A derived group (primary_for,
ADR-060) is the interesting case: its members come from a vocabulary file
the group’s own table never mentions, and a reader of the finding had no
way to know which file to edit. The citation now says
members from record/topics.yaml primary_for. That needed the contract to
know the vocabulary path, which it now carries.
One renderer, two consumers
describe() in the contract module is the single description; the record
page prints it and the findings cite through the same _cite. The cheap
check that they agree is the test that asserts every key path appears in
describe()’s output and the tests that assert the same paths in the
findings — one set of strings, matched from both sides.
The record page for this repository now says, truthfully, that no scheme here demands anything beyond the standard fields, and names the three tables that would change that. That sentence is the whole section for most adopters, which is fine: the page is generated, so declaring a table is enough to replace it.
The dogfood the issue asked for
The knowledge-base example carried requires = ["source"] for the rule
“every practice names the paper behind it”. ADR-060 measured that
requires accepts a decision’s code and a sentence as a paper, and the
example was still teaching the untyped form — the second experiment #141
named. It now declares source a LIT reference, and the example test
fires both halves: a missing source and a prose source both fail. The
arxiv field on a paper stays a plain requires, because it names nothing
in the record, which is exactly the distinction ADR-060 drew between the
two tables.
Fired on this record
Nothing changes in this repository’s lint output — no scheme here declares a table — so the guard was fired on the example and on the fixtures: findings for a required field, a merged field, a wrong-scheme reference and a derived group each cite their key.
A list in a reference field was read as its first code, silently
2026-09-03 14:25:10 · lint · config
A downstream world-building record reported that a list-valued reference
field passed lint with only its first element checked. Confirmed in one
line: reference_code(str(["SCENE-001", "SCENE-004"])) returns
SCENE-001. The list is stringified, the regex finds the first code, the
rest is never read, and nothing says so. The edge derivation did the same
and emitted one edge for two codes.
This predates the stack and the stack repeated it. check_references
had the same str(raw) since ADR-060; #142 preserved it deliberately (its
whole claim was byte-identical findings) and #143 copied the idiom when it
built edges. A behaviour-preserving refactor preserves the bugs, which is
what it is for — the point is that the bug then has one home instead of
two, and the fix here touched values_of once and both consumers followed.
The fix is a declaration, not a coercion
The tempting one-liner is to accept a scalar or a list everywhere. That
would have silenced the report and thrown away the shape: the anthology’s
source is one paper by design, and a practice citing two would pass. So
a reference now declares many = true, and the asymmetry is deliberate: a
list in a scalar field is a finding that names the remedy, a single value
in a plural field is a list of one. Reasoning in ADR-075.
Fixture design, and a mistake in it
The first version of the tests declared follows required and then
complained that the two supporting scenes — which exist only to be
resolved against — had no follows of their own. Obvious in hindsight: a
required reference is required of every document in the scheme, including
the ones a test filed as targets. The tests now filter to the document
under test rather than weakening the declaration, which keeps the required
case honest.
What was not done
min/max cardinalities, and any change to how a plain requires field
reads a list (any truthy value satisfies it, list or not, as before). The
reviewer’s larger observation — that Field.reference wants to become a
type once controlled-vocabulary fields arrive — is left for that decision.
A field that was neither a reference, a tag nor a status
2026-09-03 16:22:30 · config · record
The vocabulary-field decision (ADR-076) went from a draft on a
verdict-shaped PR to an implementation on the same PR, with the decision
flipped Active and adjusted where building it changed the answer. The
need came from a world-building record: worlds: [A, C] on 37 of 75
scenes, six closed values, absent meaning B, a page per world wanted.
Most of it already existed as two special cases. statuses.yaml and
tags.yaml each pair a frontmatter field with a scheme-local YAML file.
The new module reads a third file the same way, the contract compiles the
field beside requires and references, and every consumer that already
switched on reference gained one more branch. The index and value pages
are the tag pages with the tag replaced by an effective value, which is
the one genuinely new idea: an entry with the field absent is listed under
the default, and its file is not touched.
What building it changed in the decision
requiredanddefaultare exclusive, as a config error. Drafted as two independent keys; a field with a default is never absent, sorequiredwould have been a key that says nothing, and a key that says nothing reads as though it did.- The table is
fields, notvocabularies. The draft proposed a fourth parallel table; review of the typed-edges work made the case thatstatusandtagsare vocabulary-backed fields already, so the normal form is onefieldstable keyed by the frontmatter field with a type key —vocabulary = "worlds"today, the other kinds whenrequiresandreferencesconsolidate. Switched before merge, which is the cheap moment; ADR-063’s two-grammar argument is why it would not stay cheap. - The built-in axes are reserved.
fields.tagswould have readtags.yamlas a closed vocabulary and quietly changed whattags:means. Refused at load. - The type is two optional attributes, not a sum. The draft wrote
holds: Any | Ref | Vocabulary. Building it,referenceandvocabularyas twoNone-able attributes was the smaller change and kept every consumer honest; the ADR now says so and names the day the three collapse. - The record line links the written values to their pages, and never shows the default — settled the way the draft answered it.
Fired on a real shape
This repository has no such field, so the example world-bible is the
reporting record in three scenes: a scene that says nothing and sits in
the main line, one in world A, one in B and C, with a plural follows
across them. luria index on it writes the index block, three value pages
and the record-page line; luria lint is clean; and the two failure modes
— a world the file does not name, a follows element that resolves to
nothing — each produce their finding. The example runs in CI like the
other five.
Two rendering defects showed up only on the rendered example, which is
the argument for rendering one: the value page’s heading ran the field
name into the value’s label, and two outbound follows edges rendered as
two Follows bits instead of one. Both fixed before the PR left draft.
What was not built
A render switch for the value pages (declaring the vocabulary is the
opt-in, as it is for tags), an open-vocabulary flag (that is tags:), and
a file key for sharing a vocabulary between schemes — the sequence
tags.yaml went through (ADR-060), to be repeated when a record needs it.
One field was two types, and had been for sixty decisions
2026-09-03 18:00:58 · record · lint
The status field carried two types in one scalar, and the fix I had
deferred twice was the cheap one. Review of the typed-edges work made
the case that status: Superseded — by ADR-035 is a vocabulary value and
a prose note stored together, that the note is prose by ADR-051’s own rule
(it renders), and that every place splitting them apart is a symptom. I
agreed, then filed the split as “a separate migration decision” behind a
field-typing ADR that does not exist yet. The author’s reply was the right
one: the two fields sit next to each other in the same frontmatter, so the
drift argument I had reached for does not apply, and a few records moving
is not a cost worth deferring over. Decided as ADR-072.
What it took, counted. One of() reading both forms; one set_status
writing the new one, used by the index’s repair and by the migration’s
tombstone so the shape has one spelling; status_note added to the prose
keys; the lint’s status pattern back to five bare words with one new
finding naming the repair; four decisions here and two example documents
moved by running luria index. The templates, the scaffold, and four docs
pages updated. Nothing changed on any rendered page: the display form is
composed from the two fields wherever a reader sees a status.
The finding names the repair
The combined form is not an error a human has to fix by hand. It is the
created: shape from ADR-031 — the file already states both facts — so
the lint says luria repair moves it to status_note: and luria index does. A downstream record on the old form gets one line per
document and one command to run. The parse of the old form stays as its
reader, and the tests keep writing fixtures in the old spelling through a
helper that lands them in the new one, which is the cheapest way to keep
every existing test meaning what it meant.
What the split bought immediately
A code in a Deferred or Rejected note is a citation: found by the
scanner, linked by the fixer, reported when its target retires. That was
the gap the previous devlog entry named as waiting on field typing; it
closed the day the note became a key. The successor is not a citation: it
went to a field of its own, superseded_by:, the same day, and the note
that used to carry it is dropped by the repair when it said only that.
A mistake in the first cut
The prose-scanning test asserted that a note citing ADR-001 yields
exactly one reference, and failed: the body heading # ADR-002 is a
reference too. Obvious once seen, and a reminder that the scanner reads
the whole file.
Requiring arxiv was requiring the wrong thing
2026-09-03 19:21:11 · config · lint
Review of #144 caught the knowledge-base example requiring arxiv of
every paper, which is requiring the wrong thing. A paper’s provenance is
a source, and a source is any of an arXiv identifier, a DOI, or a URL. The
example modelled an anthology that would have failed on its first journal
article. Decided as ADR-074: a field group names the need and lists the
fields that satisfy it, and the lint asks for one.
Why a new table rather than a list inside requires. The finding
wants to say what the fields have in common — no source — and the
record page wants to describe the need, and neither can if the need has
no name. Tag groups had the same shape problem and got a named table for
the same reason (ADR-054); this one mirrors it, rules and all, so a reader
who knows one knows the other.
What it is not. The reviewer’s framing — fields that satisfy a
source type — is the better model and waits on the field-typing work.
The group states the same need in the tables that exist and reads as one
of those types once they do; the decision says so, so the day the type
arrives nobody has to rediscover that the group was its stand-in.
Fired on the example. A third paper, a lab report with only a url:,
lints clean; removing the URL produces the finding naming all three
fields. The example’s config comment now explains the need rather than
the field.
Six conflicts on files nobody wrote
2026-09-03 19:42:37 · ci · record
Every forward merge in the #141 stack conflicted on the decision index,
its tag pages and the devlog book, and the last one against main did
too. Six times, the same three to five generated files, resolved the
same way each time: take either side, luria index, commit. The author
asked the right question — hadn’t we solved this? — and the answer was
half. Sources never conflict (ADR-002). Views conflicted because the
generation job (ADR-029) committed them onto every branch, so two branches
carried two correct copies of one generated file.
The first draft threw the baby out. It stopped the bot committing
anything on a pull request: regenerate in the working tree, lint, discard.
The author asked what happens to the source repairs — the fixer’s links,
a journal entry’s created: — and the honest answer was that they were
made on the runner, linted, and thrown away, so the check passed on a tree
the reviewer never saw and the bot edited sources on main after merge.
The draft had even listed “push only source fixes” as a rejected
alternative, on the grounds that a bot pushing to branches races the
author. That objection is about a shared file rewritten on every merge; a
repair is a small edit to a file the branch already changed. Decided as
ADR-068: repairs are committed on the branch, views on the default
branch only.
Two commit points want two commands. luria index made both kinds of
write in one pass, so the action could not commit one without the other
short of keeping its own list of which files are views — a second copy of
what the generator knows. luria repair now owns the source repairs and
luria index writes views only; every lint finding a repair would clear
names luria repair as its remedy, and a test holds the two sides
together: the remedy has to work, and the index has to leave sources
alone. Idempotence got its own test, because the job runs again on the
commit it pushed.
What the pull-request job looks like now. Check out the head branch,
luria repair, commit and push the diff as the bot, luria index in the
working tree, luria lint on the result. The fork-safe checkout
expression is back, because a repair needs a branch to land on; the
needs:/SHA handoff is still gone, because the lint runs in the same job.
Two things worth knowing that the docs now say: a repair commit pushed
with GITHUB_TOKEN gets no workflow run of its own, so the same-job lint
is its check; and the pull-request job checks the head branch rather than
the merge commit.
What it does not fix. Branches that already carry regenerated views — the five in the stack — keep conflicting with each other until they merge. The routine for those is now written in CONTRIBUTING rather than known.
Fired on itself — eventually. The first three commits on this branch
shipped a workflow GitHub could not parse: the new docs-lint job had
been added beside the old one under the same key, and a duplicate mapping
key fails the file before any trigger is evaluated. Every push produced a
run with no jobs that failed in the same second, under the file’s path
instead of its name — which is what that failure looks like, and which
went unread for three commits because the pull-request checks it
suppressed were the ones that would have said so. Parsed strictly now,
duplicates refused. This pull request is the first to run the new shape:
its own views are regenerated on the runner and not committed here, and
its sources were repaired locally with the new command before push.
A temporary code in a workflow file breaks the generation job
2026-09-03 21:57:22 · ci · record
The first merge under the new generation shape left main red, and the
record was fine. #148 merged; the push job ran luria concretize, which
numbered the decision and rewrote its temporary code everywhere the code
globs reach — including the comment in .github/workflows/ci.yml that
cited it. GitHub refuses a push from the workflow token that modifies
.github/workflows/, so the bot’s one commit, views and rename together,
was rejected, and the lint job that needs: it never ran.
The hazard is structural, not a typo. A workflow file is in the code
globs on purpose — its comments cite decisions, and the reference lint
should see a retired one there. A temporary code is the one kind of
citation the job must later rewrite, in the one directory it can never
write. So the guard is a lint error, not a report: a temporary code in a
workflow file is always wrong, and the remedy is mechanical — cite the
number once the decision has one, or say it in prose. Fired once on the
real case before the fix: it named ci.yml:24 and the code.
The fix on this branch cites by prose. Naming the number the concretizer was about to assign would have been true and unresolvable: the document does not exist until the run that creates it. Prose has no such dependency.
An ADR closed in review: the constraint was the token’s, not the file’s
2026-09-03 22:23:23 · record · ci
#150 was closed with two comments, and both were right. The decision
behind #149’s guard said a workflow file never cites a temporary code.
A person running luria concretize locally rewrites that file like any
other and pushes it like any other; only the generation job’s push is
refused, and only because it runs on the workflow’s own token, which
cannot write under .github/workflows/. The decision was about the
token and had been written about the file.
So the guard moved onto the dial. A lint error is for what is always
wrong; a finding that is wrong on one token and fine on another is a
warning class (ADR-035). workflow-temp-codes is reported by default,
and this repository and the scaffold name it in fail_on because their
jobs run on the workflow token. A project that hands its job a personal
access token with the workflow scope, or an App token with workflow
write, leaves the class alone and the bot rewrites the file — and, as a
side effect worth having, that job’s pushes trigger workflow runs, so a
repair commit on a pull request gets a check of its own. Decided as ADR-069.
The second comment became a decision too. Three drafts in one day carried a “never” the mechanism did not enforce, and the same reviewer sent each one back with the same note. ADR-070: a decision is stated for what it chooses, and a prohibition is reserved for a constraint that has been verified against the thing that enforces it. The note is now something a review can cite instead of repeat.
The lint reads sources; nobody regenerates a view to check a branch
2026-09-03 22:39:27 · ci · record
Why was I regenerating views to check a branch? The author asked, and
the answer was a leftover: ADR-068 had split repairs from views on the
writing side and left the checking side alone. luria lint still ran the
staleness check, which compares committed views on disk against the
generator’s output — a question about the default branch, where views are
committed, and one a branch cannot answer, since it carries the default
branch’s copies. So the pull-request job regenerated first and linted the
runner’s tree, and my local routine copied the job: regenerate, lint,
clean the working tree by hand. That clean-up is how two hand-written doc
edits were lost this afternoon, docs/ holding sources and views alike.
Staleness moved to where it belongs. luria index --check already
ran in the generation job right after it regenerated, on the default
branch; it is now the one staleness check there is, and it carries the
README region and the orphan sweep of a stale view with it. luria lint
reads sources. The one view-directory rule it keeps — a hand-written file
inside one is a violation (ADR-021) — is computed against the generator’s
output in memory, so it writes nothing, and a leftover that says it was
generated is left to luria index rather than reported as a person’s.
The pull-request job is now luria repair, commit, luria lint. No
luria index on a branch at all, which is the answer to the very first
question asked about the job today. The generate action’s input is
views: "false"; commit-views named a distinction that no longer
exists. ADR-068 is at version 2, its decision and consequences corrected
in place.
One redundancy given up, named in the ADR. A view on the default
branch stale without the job having run would fail --check and not the
lint; the same job runs both on every push there.
Fired on itself. This branch was linted without a view written: the first pull request whose local check left the working tree clean.
The published site was one bot commit behind
2026-09-03 22:50:03 · ci · site
The Pages build did not need to run on a pull request, and on the
default branch it was running at the wrong moment. The author noticed
the first half: with no view written on a branch (ADR-068), a preview
build there renders the default branch’s views under the branch’s
sources, four minutes of Node and Quartz for a page that is wrong by
construction. Looking at the trigger found the second half. The workflow
ran on push to main, which is the merge commit — before the
generation job has regenerated and committed the views — and the bot’s
own push, made with the workflow token and marked to skip CI, triggers
nothing. So the site had published one bot commit behind on every merge
since ADR-029, and nobody could have seen it from the site, because the
views it showed were internally consistent, merely last merge’s.
The fix is the dependency the author named. needs: cannot cross
workflows, and folding the Pages jobs into the CI workflow would take
away the adopter’s ability to delete one file and stop publishing
(ADR-042). workflow_run is the cross-workflow needs:: Pages fires when
the CI workflow completes on the default branch, by which time the
generation job has pushed, and the checkout is the head of the default
branch — the bot’s commit. It runs only when that CI run succeeded, since
a failed run has no regenerated views to publish. Nothing on a pull
request. ADR-042 at version 2, the consequence corrected in place and the
cost named: a site that stops building now fails after the merge, not on
the branch.
Not fired on itself yet. A workflow_run trigger reads the workflow
file on the default branch, so this pull request’s own run proves nothing
about it; the first merge after this one is the test, and the thing to
check is that the site shows that merge’s entry.