Checks and Decision Records

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.sh holds the cases the build sandbox cannot have: the ext4 branch and the rejection cases.
  • installer-modes.sh runs the real installer with --check-only and 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.sh runs Emacs in batch mode against a temporary tree and asserts that a broken config.el cannot 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.role is an opinion. A value the distribution sets with mkDefault is an opinion, and a consumer outranks it. A value mkHost sets 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-index fails 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.