An identifier is a reference, not a word — the authoring axis extends to identifiers and comments, and the check is a whitelist because no denylist could be derived
Accepted
Context
MEASURED 2026-08-12 over the whole constellation and the sixteen onboarded sibling projects. adr-085 opened the authoring axis and declared exactly one surface on it — the commit message. This is what the axis did not reach.
· THE RULE EXISTED TWICE, IN TWO PLACES, AND FIRED IN NEITHER. `justfile-convention.ncl` declared `recipe_language = "en"` plus nine denied Spanish words on 2026-08-06 — a second implementation of adr-085's axis, for one file, with a hand-maintained list. The validator that reads it reports `recipes_found: 0` — from the constellation root and from `code/`, both tried — against a justfile whose line 42 is `default:`, so the deny loop iterates an empty list and cannot fire. Stated precisely, because the temptation is to say more: qa `justfile-layout-howto` records the check catching `gen-glosario` and `check-glosario` on an earlier run, and that could NOT be reproduced on 2026-08-12. So the honest claim is not "it never worked" but "it worked once and stopped, and nothing reported the stopping" — which is the worse of the two and the same fifth-reading shape. Every recipe in the constellation is in fact English; the list cannot take credit for it today. Meanwhile ids, filenames, node ids and code comments had no declaration at all.
· WHAT LEAKED, AND IT REACHED THE PROTOCOL SEED. The glossary seed ships `espiral`, `sintesis` and `colapso-yin-yang` as term IDS — while the schema enum for the same concept was already `'Spiral`, in English, in the same repository. The two layers had been fused: whoever wrote the id was answering a `coverage` question (how does this render in Spanish?) inside an identifier, where that question does not arise.
· THE SAME DRIFT SHAPE adr-085 MEASURED, ON A SURFACE IT DID NOT NAME. `outreach/presentations/` holds `disciplina-sin-disparador.md` whose title is "A written rule does not apply itself" — an ENGLISH deck under a Spanish name, made by copying the Spanish deck and translating the content and not the name. Its Spanish twin (`_es.md`, "Una regla escrita no se aplica sola") is correct and must stay correct. Two language-tag conventions were found coexisting in the same constellation: `.es.md` in `positioning/narratives/`, `_es.md` in `presentations/`.
· THE CONSUMER BLAST RADIUS IS SMALL, AND THAT IS A MEASUREMENT NOT A HOPE. Across sixteen onboarded projects: 9 references in 3 of them (`rustelo` 4, `site` 4, `website-htmx-rustelo` 1), every one of them prose in a comment; 3 stray Spanish comments in two more; and ZERO consumer glossaries that define or cite the seed ids in `related_terms`. The interfaces are clean in a way worth recording precisely: zero Spanish identifiers in any Rust crate, zero in CLI subcommands or flags, zero in templates, HTML, JS or CSS. The contamination travels as DATA — the daemon serves `pole-collapse`, the API returns it, the UI renders it — which is why this is a migration and not a refactor.
· THE FINAL COUNT, from the gate this ADR ships: 154 Hard findings — 64 comment blocks, 39 filenames, 36 node ids, 12 term ids, 2 qa ids, 1 mode id, 0 recipe names, 0 routes. Zero false positives on the identifier surfaces.
Decision
THE AUTHORING AXIS EXTENDS TO IDENTIFIERS, AND THE IDENTIFIER CHECK IS INVERTED.
reflection/schemas/lexicon.ncl `identifier_surface_type` — surface · authored · what · sources · excluded · extract · id_fields · lang_suffix · exempt · blind. Plus `'CommentBlock` on `source_only_type`, with a density regime (`min_words` · `warn_pct` · `fail_pct`) alongside the existing whole-text one. ontology/lexicon.ncl seven identifier surfaces, the `code-comment` surface, and `identifier_exemptions` shared across all of them. ontology/registry.ncl carries both axes in the one union consumers read. scripts/check-source-lang.nu the gate. Holds no policy. reflection/wordlists/ the vendored English wordlist and its supplement.
AN IDENTIFIER PASSES THROUGH NO CRITERION, AND THIS IS THE HALF THAT WAS MISSING FROM THE PUBLISHED PAGE. ontoref.dev/sobre-el-idioma decides, for a CONCEPT, whether it is rendered into Spanish or kept — a judgement about a language, revisable, governing posts, case files, decks and resources. It states the identifier half already: "it appears verbatim in the command line… you can no longer get from it to the tool. This holds in every language — it is not an opinion about Spanish." What it never stated is the INVERSE, which therefore survived on implicit consensus: English inside Spanish prose is normal and fine; Spanish inside an English identifier is drift. adr-085 measured what implicit consensus costs, one surface over.
CONSEQUENCE THAT MUST NOT BE LOST: renaming `colapso-yin-yang` to `pole-collapse` does not decide how the concept is written in a Spanish post. That stays with the criterion, on `coverage`. The two layers are independent and were fused only because nothing declared them apart.
THE TEST IS A WHITELIST, AND IT IS A WHITELIST BECAUSE TWO DERIVATIONS WERE TRIED AND BOTH FAILED. This is the load-bearing part of the decision and it was reached by refutation, not by design:
· From the registry's own renderings — the elegant answer, and the one this ADR set out to ship. The registry declares every Spanish form of every governed concept, so the denied forms looked free. Derived: 72 forms. Covered: 0 of the 18 known offenders. `espiral` and `sintesis` carry the SPANISH word as their source form under verdict 'Keep, so they never appear as a rendering at all; and `expediente`, `glosario`, `informe` are not governed concepts — they are ordinary words nobody ever had to translate. A registry of CONCEPTS cannot answer "is this token Spanish?", because the offenders are precisely what nobody governed.
· From the project's own Spanish corpus — tokens present in ES content, absent from EN content, five occurrences or more. Derived: 2328 forms. Covered: 1 of 18.
A hand-written deny list would have caught all 18 and, by construction, never the nineteenth. So the burden is inverted: an identifier token must be an English word, an inflection of one, or a declared exemption. The tail is finite and shrinking rather than infinite and growing, and it catches French, Italian and typos without being told about any of them.
THE DICTIONARY AND THE EXEMPTION LIST SAY DIFFERENT THINGS AND ARE KEPT APART. The supplement records tokens that ARE ordinary English words which a 1934 lemma list lacks (`website`, `workflow`, `database`, `has`, `held`). An exemption records a token that is NOT an English word and is allowed anyway (`adr`, `ncl`, `stratumiops`). Filing one as the other makes the exemption list mean two incompatible things at once, and the misfiling is invisible because the label reads as a decision — adr-071's `do-not-translate` anti-pattern, one layer down.
Constraints
- Hard Which surfaces carry identifiers, and what counts as one on each, is declared in ontology/lexicon.ncl — never inside the checker and never in a carrier.
- Hard No hand-written list of target-language words governs any identifier surface. `deny_recipe_words` is gone and nothing replaces it.
- Hard The gate reads a wordlist versioned in the repository, never a system path.
- Hard An identifier exemption declares why it is exempt; a bare form fails the contract at evaluation.
- Soft The `code-comment` thresholds are recorded with the corpus distribution that produced them, so they can be re-derived rather than argued.
- Hard The published rendering criterion governs content; it does not govern identifiers, and an identifier rename does not change how a concept renders in a target language.
Alternatives considered
- Derive the denied forms from the registry's own Spanish renderings — rejected: Refuted by measurement, not by argument: 72 forms derived, 0 of 18 offenders covered. The seed terms carry the Spanish word as their SOURCE form under 'Keep, so they are never a rendering; and the ordinary words that leaked were never governed concepts at all. This was the design this work set out to ship and it does not function.
- Derive the denied forms from the project's own Spanish corpus — rejected: Also refuted by measurement: 2328 forms derived, 1 of 18 covered. The offenders appear in the English corpus too — English case files discussing 'the expediente' — so the ES-only filter removes exactly the words the check needs.
- Keep a hand-written deny list, moved from `justfile-convention.ncl` to the lexicon and extended to every surface — rejected: It would have caught all 18 known offenders and, by construction, never the nineteenth. The stated fear this work answers is unbounded drift — 'si en el inglés empezamos a poner palabras en español ya empezamos una deriva interminable' — and a denylist has an infinite tail by definition. Moving it to the right file would fix the placement and leave the shape.
- Add identifiers to `source_only` as a sixth field on the existing type — rejected: The two checks cannot share a regime. Prose is counted and banded so a legitimate quotation survives; an identifier is one token and admits no band. Folding them would either give identifiers a tolerance they must not have, or strip prose of the band that keeps the gate survivable.
- Vendor a modern hunspell dictionary wholesale — rejected: It would encumber a published repository with a third-party compilation's licence to obtain roughly forty ordinary English words. Using it as a build-time oracle and vendoring only the derived supplement obtains the same result with no redistribution, and the derivation is stated in the file so it can be re-run.
Anti-patterns
- Calling a hand-written list derived — A checker's vocabulary is described as coming from the governed registry, and nobody runs the derivation to see what it actually yields. Measured here: the registry-derived list covered 0 of 18 known offenders and the corpus-derived one covered 1, while the hand list in the schema covered all 18 — so the hand list looked like the working mechanism and was in fact the only one that could never grow on its own.
- Enumerating what is forbidden when the forbidden set is unbounded — A gate lists the target-language words it refuses. It catches every instance already found and none of the next, and each new entry arrives only after the drift it was meant to prevent. The tell is a list that grows one incident at a time and a comment saying it is meant to grow.
- An ordinary word recorded as an exception — A token that IS an ordinary word of the source language is added to the exemption list because the dictionary in use happens to lack it. Two incompatible claims then hide under one label — 'this is not a word and is allowed anyway' and 'this is a word the wordlist is missing' — and the exemption list stops meaning anything. The tell is an exemption whose form reads as perfectly ordinary language.
- A threshold derived from a corpus the rule does not govern — A measurement sweeps everything reachable rather than the declared extent, so it counts material the rule permits. The distribution comes back with no usable threshold and the rule looks unfoundable — a conclusion about the rule drawn from a fact about the sweep. Measured here: 6166 comment blocks over the whole tree gave a continuous distribution; 4573 over the declared extent gave a gap.