Checks and Decision Records
Table of Contents
This page is for someone who changes the distribution, or who wants to know what keeps it honest. It lists what the 23 flake checks guard, how to run them, the contributing rules in short, and what the decision records hold.
The claim underneath all of it is the one on the front page: if it is not in the flake, it does not exist. The checks are how the flake tests itself, so a breakage shows up in your change instead of in a stranger's rebuild.
1. Run the checks
You need nix with flakes enabled. Nothing else: the development shell carries
the linters, and no check builds a host closure, so you do not need to run
Emanix, or NixOS, to run them.
nix flake check # every check nix flake check --no-build # the same evaluation, building nothing nix build .#checks.x86_64-linux.prose nix develop # a shell with nixd, nixpkgs-fmt, deadnix, statix nix fmt # format with nixpkgs-fmt
The third command runs one check. Replace prose with any name below. Run it
from the root of the distribution's checkout.
Caution: nix flake check builds eight of the checks, and those build Emacs.
The first run takes time and disk. --no-build evaluates all 23 and builds none,
which is what the cheap continuous-integration job does on every pull request.
The job that builds them runs on main.
1.1. What "evaluated, never built" means
The role-* checks and template-host compose a throwaway host through the real
mkHost and compute its derivation path. They do not realise the closure. A
workstation closure source-compiles the compositor, and a check that cannot run
safely on the machine you are using is not a check. The username in those hosts
is deliberately not the author's: a module that hardcodes a real one fails here,
not on someone else's machine.
2. What the 23 checks guard
2.1. Evaluation and the three role labels (4)
| Check | What it finds |
|---|---|
role-workstation, role-server, role-wsl |
The distribution evaluates for each role label. A do-nothing consumer module goes through homeModules, so that seam is evaluated, not merely accepted |
template-host |
The template that a new machine is built from evaluates, including the compositor module. It is the only thing a newcomer has, and one that fails does so on a console with no context. Two fixtures exercise both paths of the optional hardware module |
2.2. Theme (2)
| Check | What it finds |
|---|---|
palette-contrast |
Every accent and text slot of the high-contrast palettes clears WCAG AAA (7:1) against its base. The Catppuccin palettes are exempt by name; see Theming |
theme-switch |
The theme switch works. It runs in a plain batch Emacs, with every side effect stubbed and a temporary theme tree |
2.3. Emacs and the agent integration (10)
Seven of these build Emacs; theme-switch above is the eighth. The unit tests run
in batch Emacs. The rest are cheap greps or a real execution.
| Check | What it finds |
|---|---|
prose |
The prose renderer: face remaps, the reading column, document magnification, and the org-modern and org-appear integration |
web |
The web layer: template-file detection, mode dispatch and the formatter choice, against the real packages |
welcome |
The welcome buffer finds its dismissal marker and your configuration repository |
modeline-segments |
The status bar's extension point works. Nothing in the distribution registers a segment, so without this check only a consumer would notice a break |
arc-bridge |
The assistant's dispatch and key contracts, tested without the assistant, a model server or a database |
agent-shell-sync |
The buffer-sync patch's unit tests |
agent-shell-api |
The upstream agent-shell functions this integration calls still exist in the package the hosts actually run. Its author calls the API unstable, so a bump that renames one would otherwise surface weeks later as a key that errors |
agent-shell-glue |
Greps over the agent glue: autoloads exist, no absolute home path, no top-level require of agent-shell, and the payload key styles are handled |
agent-acp-wrapper |
Runs both adapter wrappers. The Emacs daemon has a minimal PATH, and an adapter that finds node through it works in a terminal and dies in the daemon |
arc-glue |
Greps over the assistant glue: no hardcoded home directory, because the distribution cannot know your username; the whole corpus is still enabled, so the assistant does not quietly know only the Emacs manuals; the key contract is still defined; and no drift back to a retired name |
2.4. Hardware (1)
| Check | What it finds |
|---|---|
hardware-gpu |
Each value of emanix.hardware.gpu produces the right forced initrd modules, and a GPU value with firmware disabled fails evaluation. A wrong value is a black screen on a machine with no other console |
2.5. The installer (1)
| Check | What it finds |
|---|---|
disk-layout |
The generated disk layout equals its recorded fixtures, and literal facts about it hold (partition type codes, mount options and paths). The fixtures come from the generator, so a diff alone is circular; the literal assertions catch a fixture re-recorded from a broken generator |
2.6. Documentation and index (5)
| Check | What it finds |
|---|---|
welcome-keys |
Every C-* key the welcome buffer advertises is bound by a live binding form in the shipped Lisp. It caught the docs naming a package for weeks after the code had replaced it |
check-index |
Every check is named in the walkthrough's table of checks. A check nobody can find is a check nobody runs |
decisions-index |
Every decision record is linked from the records' index |
retired-names |
No live .nix file names a directory that was renamed on 2026-08-30. A comment cannot fail a build; this does |
comment-budget |
No comment block runs past 30 consecutive lines. A longer block is an essay, and an essay belongs in a decision record |
2.7. Tests that are not flake checks
Three scripts in tests/ are not checks, because they need a live filesystem, a
real subprocess or a nested nix eval. Run them before a pull request:
./tests/disk-layout.sh ./tests/installer-modes.sh EMACS=$(nix build --no-link --print-out-paths .#emacs)/bin/emacs \ ./tests/init-guard.sh
disk-layout.shholds the cases the build sandbox cannot have: the ext4 branch and the rejection cases.installer-modes.shruns the real installer with--check-onlyand asserts that a known host takes the pre-staged path, that no argument or an unknown host name enters interactive mode, and that an invalid hostname is rejected.init-guard.shruns Emacs in batch mode against a temporary tree and asserts that a brokenconfig.elcannot take the desktop down. It needs the distribution's own Emacs.
3. Contributing, in short
The rules are mostly negative, and a reasonable patch can break one and still
evaluate. The distribution's CONTRIBUTING.md is the authority; this is the
summary.
- The module system decides where a file goes.
home/holds Home Manager modules,modules/holds NixOS modules,emacs/the Emacs configuration,lib/the plain-value library and the host composer,checks/the checks,tests/the scripts the checks call. An option lives in the file whose concern it is, on the tier whose module system it belongs to. - Emanix is one shape. It ships no role profiles, hostnames, usernames, keys, secrets, disk layouts or hardware selections. A fact about a machine or a person belongs in the consuming flake. A general capability belongs here. See Architecture.
mkHost's arguments are invariants;emanix.roleis an opinion. A value the distribution sets withmkDefaultis an opinion, and a consumer outranks it. A valuemkHostsets from its own arguments is an invariant; overriding it makes an incoherent host.- Checks are evaluated, never built. Do not turn a check into a build so that it can test more.
- Every check must be able to fail. A check that passes for every input proves nothing. Add its negative control, or say in the header why one is impossible. A guard once passed on a mistranscribed layout for exactly this reason.
- A comment states the decision; the working-out goes in a decision record. File headers run to about 12 lines, a comment on one binding to about 6, and a link to a record to 2. Only the 30-line ceiling is enforced. Over budget means relocate, never delete.
- Naming is enforced. If you rename something, add the old name to the retired list in the same commit.
- A new module is wired and checked. Put it in the tier its module system
dictates, wire it in, declare its options with a description of what the
consumer is choosing, and run
nix flake check. If nothing evaluates your module, add a check that does. - A new check is named in the walkthrough's table, or
check-indexfails the build. - A seam is justified when a consumer legitimately needs to differ. It is not a convenience for one consumer's one host.
- Commits. One concern per commit, an imperative subject, and a body that says why. The history is a design record. State in a pull request what you measured and what you rejected.
Before a pull request, run nix flake check, the three scripts above, and the
linters: nix develop -c nixpkgs-fmt --check ., deadnix . and statix check ..
4. Decision records
The distribution keeps a directory of decision records. A record holds what a code comment cannot carry: a measurement, a rejected alternative, an incident or an exit condition. The comment in the code states the decision and links to the record.
The directory is maintained. If a decision changes, its record changes with it, and a record that describes a system that no longer exists is a bug. A second directory of earlier design history is frozen: it is a record of what was decided then, and it is not edited to match the present.
There are five records:
| Topic | The decision |
|---|---|
| Text-to-speech | The distribution ships none. The graphical-desktop module of NixOS turns on a speech service whose voice data alone is 645 MiB, and the override keeps it off. A host can turn it on again with a plain definition |
| Redistributable firmware | It is on for every host, graphical or not. It adds roughly 791 MiB to a headless server that has no wifi to bring up. The record argues that an installer whose result cannot see its wifi has installed nothing, and that the cost of being wrong is asymmetric. The rejected alternative gates the import on the host shape |
| The EWM patches | The patches the distribution applies to the compositor, and each one's exit condition — the upstream change that makes it droppable. The live patch teaches the compositor to parse a hexadecimal key name; a retired one is recorded with the reason it went |
| The welcome-key check | What welcome-keys verifies, why it checks the buffer's keys against the bindings instead of generating the buffer from them, and what its extraction pattern cannot see |
| The GPU Emacs | EWM hosts run Emacs from the compositor project's own fork, with its build recipe taken from emacs-overlay's 31.1 because the stable channel has no Emacs 31. The record gives the reasons and three exit conditions |
Read a record before you change the code that links to it. Its exit condition tells you when the workaround stops being needed.
5. What you should now know
nix flake check tests the distribution against itself in 23 checks, and
nix build .#checks.x86_64-linux.<name> runs one. Three more scripts need a live
filesystem and run by hand. The contributing rules say where a file goes, what
the distribution refuses to ship, and that a check must be able to fail. The five
decision records hold the working-out behind the comments. For the settings you
may change without patching anything, see Emacs Settings and
Options.