Document ownership and checks
tour.md explains the language and spec.md decides it. This says how each is laid out, so that a reader looking for a rule knows where to look and an editor adding one knows where to put it. It is derived from the documents and is not an authority over them.
Both are arranged by subject
Neither document is in the order its contents were written. tour.md teaches in subject sections, from comments through to the toolchain, and spec.md follows the same subjects in the same order, so a question about arrays is answered in one place whichever document it is asked of.
spec.md is three parts:
| part | what it holds |
|---|---|
| the grammar of the enabled kernel, [1740] to [1830] | what the compiler accepts today, and nothing else. It shrinks as the language grows. |
| the rules the tour left unsaid, [1840] to [2000] | the permanent rules a tutorial omits because a reader supplies them. Grouped: names, scopes and doc comments, types and literal context, what an operator takes, places and assignment, calls and errors, module values, and the boundaries the compiler owns. |
| the decisions this document took, D1 to D252 | fourteen subject sections, each decision with what the tour said before, what was chosen, the alternative, and the fixture that pins it. |
A construct id is a stable citation anchor and never moves. The consequence is that the numbers no longer ascend down the page: [1950] sits beside [1890] because both say what an operator takes, and D1 sits beside D233 because both say what a declaration introduces. Inside a register section the numbers still ascend, so a run of decisions that built one subject together is read in the order it was built. Nothing that cites an id has to know where it sits.
Where a new rule goes
A rule about the shape of accepted source belongs in the grammar, and check_grammar_corpus will hold it to the fixture corpus. The structural editor grammar in highlight/tree-sitter transcribes the same shape, and the gate's editor-grammar job parses the corpus with it; check.py does not, so run ./highlight/test.sh --integration inside nix develop after a grammar or positive fixture change, which python3 scripts/activations.py names when the changes reach it. A change to the editor grammar or its queries takes two commits: the editors fetch the grammar by commit, so after the commit that changes it, python3 highlight/grammar_pin.py moves their pins to that commit, and its rewrite is committed on its own (highlight/README.md). Until then test.sh passes every other check and fails on the pins. A rule about what an accepted program means belongs in [1840] onward, in the group whose subject it shares. If the rule was a choice rather than a transcription of something tour.md already said, it also gets a register entry, in the register section matching its subject — and that entry names the fixture that pins it, which check.py requires to exist.
A new construct, a ### [NNNN] in either document, is held in four more places, and check.py refuses the tree until each holds it: a row in the construct inventory of compiler/tests/registers.md; fixtures whose constructs: metadata claims it when the inventory calls it executed or compiled, and none when it is deferred, transferred or advisory; for an implemented construct, at least one guarantee row of D148's register in spec.md, whose absence check.py reports as "implemented construct [NNNN] has no guarantee row"; and compiler/tests/constructs.matrix, regenerated by python3 check.py --generate. Its id is one no document uses, placed between its neighbours by subject: ids step by ten, and where the tens between two neighbours are taken, as every one is up to [2000], a unit id between them is the practice, as [1795] and [1975] were.
The subject is decided by what the rule decides, not by its heading. Two register entries were filed under the wrong subject on a first pass for exactly that reason: D154 reads as a function-outcome rule and is a core/diag rule, and D185 reads as a scope rule and is the condition-binding form.
What the reorganisation preserved, and how that is known
The reorganisation moved units; it did not rewrite them. All 443 units — 270 in spec.md, 173 in tour.md — moved with their content unchanged, checked by parsing both documents before and after and comparing every unit by its key. The prose that was edited is the prose that the moves made false: the two documents' own descriptions of how they are ordered.
check.py's grammar corpus is the standing evidence that the language did not change with the documents. It derives the positive fixture corpus from spec.md's own grammar while the Ada parser meets the same corpus from the other side, so a grammar broken while being moved fails there rather than in a review.
That covers the syntax, and it is worth being exact about what it leaves uncovered. Nothing in this repository checks that a prose rule from [1840] on, or a register entry, still means what it meant: a rule whose meaning changed while it was being reworded would pass every check here. What stands behind those is that they were moved rather than rewritten, which is a property of how this pass was done and not one anything re-checks. An editor who rewords one is on their own, and the fixture named in a register entry is the nearest thing to a check that the entry still describes the compiler.
Mechanical document checks
The named-file check resolves repository file references in live documents and Ada comments. External URLs, including their queries and fragments, are outside that local-file check; local references beside them still have to resolve. Network link validation belongs to the link workflow.
check.py also checks the grammar in spec.md: it reads the productions, holds every rule to being defined and reachable, and derives every .ldn under compiler/tests/fixtures/positive. A negative fixture is held to being underivable only when the frontend is what refuses it: one a later stage refuses is legal source and must derive, which its codes: is what says (see below). A grammar change that breaks a fixture, or a fixture the grammar cannot derive, fails there. check.py does not parse the corpus with the structural editor grammar in highlight/tree-sitter; the gate's editor-grammar job does, and ./highlight/test.sh --integration runs that pass inside nix develop, which python3 scripts/activations.py names whenever the changes reach the grammar, a positive, runtime or ABI fixture or core/. A change to the editor grammar or its queries takes two commits, the second moving the editors' pins with python3 highlight/grammar_pin.py (highlight/README.md). Do not weaken a fixture to make a grammar change pass — the corpus is the agreement the parser has to meet, and the parser suite requires the same verdict from the other side.
A negative fixture's codes: is an ordered list, and it also says which stage refused the fixture: check.py reads which codes the frontend raises out of the two packages that raise them and requires the grammar to derive a program that only a later stage refused. Do not read a stage off a code's number — the catalogue's header forbids it, and L0010 began in lexical refusal and is now raised only by the parser.
Four tables in the compiler are transcriptions of the grammar rather than paraphrases of it, and check.py compares each with its source. Landin.Tokens' reserved words must be spec.md's own keyword production. Landin.Syntax.Precedence must have [1820]'s levels in [1820]'s order, with the same operators at each, the same fold, the same prefix set and first sets that agree with the grammar's own. The parser's refusal tables cover the words [1760] does not reserve, so only the parser can meet them; the checker's refused-type table covers scalar type names the grammar admits but the kernel has deferred. Both tables must spell words the tour writes, cite paragraphs that exist, and state each refused form's standing: a recorded source-form boundary, a withdrawn form, or a transfer to a successor roadmap. Landin.Types spells the thirteen enabled scalar names separately because it maps each onto a machine width. Add a level, an operator, a refused construct or a type in one place and the check says which other place disagrees.
The five documents are Markdown, and the form carries invariants rather than being a style. A construct is ### [NNNN] Title at column 0 and nothing else is; a citation is inline and can never be mistaken for a definition, which in the .txt form it could — 33 lines looked like definitions and were citations, and 16 real definitions sat indented inside one example, which is how [1050] was missed twice. A Landin example is a fenced block tagged landin, a production is tagged landin-grammar, and an untagged fence is a fault: what a block is is stated rather than guessed, and the heuristic it replaced accepted 114 lines of English as code. Prose is never indented four spaces, because Markdown reads that as a code block — an aligned list belongs in a table.
R§n and H§n citations preserved in the roadmap refer to an external design archive; the tracked repository does not depend on that archive.
The documents selected in docs/site/render_html.py are published as reading copies described in the site notes. The text files are the sources; the pages are generated and never edited by hand. See docs/site/README.md for the scope.
The mark lives in assets/, not in the site renderer. assets/icon.svg is the drawing — 701 as a path, so no renderer needs Futura — and assets/landin_icon.py is every rendering of it: the light, dark, contrast, currentColor, monochrome and prefers-color-scheme variants, the inline fragment, and the data: URL a page carries. Its four colours are the site's own, and check.py holds them to the stylesheet in docs/site/render_html.py and to the drawing's own attributes. Add a variant there rather than in a consumer, and keep it standard-library-only so the site keeps its no-dependency build. See assets/README.md.
The two faces the pages are set in are declared in assets/fonts.py, not in the site renderer. Each family is the subset webfont package its source delivers, and assets/fonts.py is every rendering of them: the @font-face block a page carries, the --ui and --mono stacks, and the list of files copied beside the pages. Nunito Sans is under the OFL and vendored in assets/fonts/; MonoLisaCode is under a foundry EULA that forbids passing the files on, so it lives in the private landin-fonts repository, found through LANDIN_FONTS or beside this one, and a host without it renders in the fallback stack while the publishing workflow refuses. The module reads each family's own stylesheet rather than transcribing thirty unicode-range lists, and check.py holds every character of every rendered document to falling inside a subset of both families, because a range nobody covers is a paragraph in a fallback face that no word count can see. Add a family there rather than in a consumer, keep it standard-library-only, and do not trim the subsets to today's documents. See assets/fonts/README.md.
Syntax highlighting lives in highlight/, not in the site renderer. highlight/landin_highlight.py owns the lexical vocabulary rendered by the pages, Pygments, TextMate, Vim, Notepad++, Nano and Kate; highlight/tree-sitter/ is the checked structural grammar whose queries feed Neovim, Helix and Zed. Emacs provides both native and tree-sitter modes. Generate copied artifacts with highlight/generate.py, add a keyword at the shared source rather than in a consumer, and keep the mandatory generator path standard-library-only so the site keeps its no-dependency build. check.py's own list of reserved words is deliberately separate: that one is about legality, this one about colour. See highlight/README.md.