Checks

The checks are the standard's oracle. Every principle names at least one check that tests it, and CI fails if one names none.

Each check keeps one order: a program verifies what a program can verify exactly, for free; a model works only where meaning is at stake; judgment takes what no model should settle. Every check runs in polynomial time and space, and none costs money: the one paid backend exists but is off.

Tiers, and how often they run

Tier Needs Runs Finds
program the Python standard library every push errors, warnings, questions
meaning sentence embeddings (fastembed) every push, model cached questions
syntax a parser (spaCy) weekly, and on demand warnings
judgment a person, or a model if given every push: listed answers
Folder Holds
program/ the checks that need no model: paths, folders, links, size, outline, copies, terms, shapes, sentences, figures, stylesheet, citations, bibliography keys, history
figures/ the program-tier checks that read figures: drawn or typed, described, in step with their Graphviz source, readable on a phone
meaning/ the checks that compare meaning: paraphrased copies, page mode, name against content, section cohesion
syntax/ the checks that read a parse: dependency length, clause nesting, links between sentences
framework/ what every check is built on: the interface, the prose units, the judgment backends
run.py the one entry point
config.toml every threshold, tagged PROVISIONAL where it is a convention, and the default tiers
test_checks.py the checks' own oracle: a planted violation per check
test_framework.py the runner, the config, the judgment step, and each way a run can fail
test_tiers.py the meaning and syntax tiers' planted cases, skipped when their library is missing
test_properties.py properties every input must keep, tried on generated inputs from a seeded source

What a check's findings can be trusted for, and what it costs, is said once, in its docstring. Each tier's README shows it in the Guarantee column, and a test fails when a check does not say. Which principle each check serves is said once, on the principle's Check: line; principle-coverage holds every principle to naming at least one registered check.

Kinds of finding

An exact finding is an error that fails the run, a heuristic one a warning to fix or explain, and an open one a judgment sent to a backend.
Graphviz source
digraph findings {
  graph [rankdir=LR, nodesep=0.4, ranksep=0.5, bgcolor="white", pad=0.2, fontname="Inter"];
  node  [shape=box, style="rounded,filled", fontname="Inter", fontsize=13, margin="0.2,0.1", penwidth=1.5, fontcolor="#1F2328"];
  edge  [fontname="Inter", fontsize=11, color="#3D434B", fontcolor="#55575F", penwidth=1.3, arrowsize=0.8];
  F [label=<<b>a finding</b>>, fillcolor="#FFFFFF", color="#3D434B"];
  E [label=<<b>error</b><br/><font point-size="11" color="#55575F">the run fails</font>>, fillcolor="#FBEDE4", color="#B4541A"];
  W [label=<<b>warning</b><br/><font point-size="11" color="#55575F">fix it, or say why it is false</font>>, fillcolor="#FBEDE4", color="#B4541A", style="rounded,filled,dashed"];
  J [label=<<b>judgment</b><br/><font point-size="11" color="#55575F">to a backend: none, flag (default),</font><br/><font point-size="11" color="#55575F">local model, or api (paid, off)</font>>, fillcolor="#E8EEF8", color="#2F5DA8"];
  F -> E [label=" exact"];
  F -> W [label=" heuristic"];
  F -> J [label=" open"];
}
Backend What happens Cost
none questions dropped free
flag (default) questions written to checks/out/needs-judgment.md for the person or agent already at work free
local each question sent to a model on this machine through an OpenAI-compatible endpoint, set only by READER_ERGONOMICS_LOCAL_URL and READER_ERGONOMICS_LOCAL_MODEL free, local
api the same protocol to a paid provider (READER_ERGONOMICS_API_URL, _KEY, _MODEL); refuses to run unless [judgment.api] enabled = true paid, off

A model's answer is recorded as advice next to its question; it never fails the run. An answer that settles a question goes in checks/judgments.toml, committed with the repository, with the fingerprint of the file it concerns: the question stays answered until that file changes. Open questions are also written as entries to fill in, in checks/out/judgments-to-answer.toml. An answer quotes what its reason rests on, and judgment-quotes checks that each quote is in the answered file: a reason copied from another file's answer then fails.

Use

python3 checks/run.py .                    # tiers from config
python3 checks/run.py . --tiers program    # no model at all
python3 checks/run.py . --out verdicts.txt # also write one line per finding, then summary and exit status
python3 checks/run.py path/to/repo         # another; its config wins
python3 checks/run.py . --online --only references
python3 -m unittest discover -s checks     # test the checks
pip install -r checks/meaning/requirements.txt   # the meaning tier
pip install -r checks/syntax/requirements.txt    # the syntax tier

--out FILE writes kind check path:line message per finding (errors, warnings, and the judgment questions still open), then summary errors=N warnings=N judgments=N seconds=S tiers=... and exit N, replacing the last run's file. Standard output is unchanged.

A tier whose library is missing is skipped with a notice, never failed. .github/workflows/checks.yml runs the program and meaning tiers on every push and pull request. It runs the syntax tier and the online references once a week.

What other tools already check

The tools were searched on 2026-10-28, when 31 checks existed:

  • no counterpart, for 11: none of the tools found checks overview first, terms defined across files, a missing figure, document mode, paraphrase, a name against its content, or a stylesheet's measure;
  • a partial counterpart, a tool that checks a related but different thing, for 13;
  • a tool that does the same job, for 7.

Of the checks added since the search, bibliography-keys was compared and has a partial counterpart, and the others were not compared: 32 of the 45 checks registered today were compared. The table below names, for each check compared, the closest tool and how it differs.

The program-tier checks stay on the standard library, so one command runs them with nothing to install; a repository that already uses one of these tools can keep it.

Check Tool that covers it How it differs
links, references lychee also web links and fragments; no check of a DOI or arXiv form
shape-conformance, outline-depth markdownlint MD043, mdschema required headings in order; mdschema also sets a maximum heading depth
long-sentences, paragraph-length Vale a rule from a style package or a custom one, not a statement count
duplication jscpd copies of tokens, not of normalised prose
sufficient-contrast axe-core measures the rendered page (see below)
tree-shape, for cycles import-linter, for Python cycles and layers per module; none of the tools searched counts the entries of a folder
portable-paths pre-commit-hooks case conflicts, illegal Windows names and symlinks, as separate hooks
bibliography-keys bibtex-tidy --duplicates one BibTeX file, by DOI, key, abstract, or author and title; not across repositories
co-change code-maat coupling per file from the history, not per folder

Partial matches not in the table: ESLint's max-lines (physical lines, not statements), ls-lint (names, not content), markdownlint MD045 (alt text only), Vale's conditional rule (acronyms only) and eslint-plugin-unicorn's expiring-todo (a date written in the comment).

What is not checked here

The rendered page: contrast and measure are checked on the stylesheet's declared values, not on the pixels. A browser accessibility checker measures the rendered page exactly.

What passes here without being exercised

Some checks pass on this repository without being exercised: what they read does not exist here, or they are switched off. A clean run is no proof that they work; their tests in test_checks.py are.

Check Why it finds nothing here Where it bites
standard-pin, standard-links no workflow checks out the standard, since this is the standard in each extension
bibliography-keys, against the family family = []: this standard reads no extension (decision 0003); a key held twice in one file is still checked in each extension
provisional-tags a tag becomes a question after 90 days, and the history began on 2026-10-28 from late January 2027
references, online resolves links only with --online in the weekly CI run
decision-records, its cost question ask_costs = false in config when set to true