ADR 025: typos (Spell Checking)

Accepted
2026-07-20

This legacy URL now resolves to the canonical ADR.

Context

This repo is unusually prose-heavy for a codebase: a blog, project write-ups, recipes, and 55-and-counting ADRs, all published to a public site that doubles as a portfolio. Nothing deterministic checks any of that text for spelling. markdownlint and remark check structure, not words; AI reviewers catch typos non-deterministically, and only in the diff under review, never in the backlog.

The backlog made the case concretely: a first run over the repo found six real misspellings sitting in published blog posts, some for six years ("Volatily" in a table header four times, "commerical", "efficently").

The standing objection to spell-checking code repos is false-positive noise. Dictionaries hate identifiers, and a checker that cries wolf gets deleted. The tool choice hinges on precision more than recall.

Decision

Adopt typos (the Rust CLI), mise-pinned like the other linters:

  • mise run //:lint:typos in the root lint aggregate, plus a lint:typos:fix task (--write-changes) matching the check/fix split of the Markdown tasks.
  • A blocking step in the existing Lint CI workflow alongside markdownlint/yamllint/actionlint. The baseline is green because the six real findings were fixed in the adopting change. Lint CI's trigger now also covers **/*.mdx, where most prose lives.
  • A committed _typos.toml documenting the false-positive taxonomy the first run produced: proper nouns split from CamelCase ("Hashi" from HashiCorp, "Symetry" from SymetryML), product names ("Vertica", "Juni"), legitimate words outside the dictionary ("unparseable"), and two path exclusions, the Fuse.js ADR (whose deliberate typo examples demonstrate fuzzy search) and a generated design artifact full of encoded SVG data.

Why It Adds Something New

Spelling is a class of defect no existing tool owns: it lives mostly in content files that Biome, CodeQL, and SonarQube never parse, and the tools that do parse them (markdownlint, remark) check syntax. typos applies the stack's standard determinism argument (ADR 048) to the one artifact type this repo publishes more of than code.

typos is chosen over the generic spell-checkers for its design point: instead of flagging every word not in a dictionary (cspell's model, which needs per-project word lists to stay quiet), it only flags known misspellings via edit-distance against a curated corpus. That asymmetry is why the first full-repo run produced 29 findings rather than thousands, and why 6 of the 29 were real.

Alternatives Considered

ToolRoleProsConsVerdict
typos (accepted)Known-misspelling checkerLow false-positive rate by design; single fast binary; understands CamelCase/snake_case; TOML config; --write-changes fix mode.Won't catch a novel misspelling absent from its corpus.Accepted.
cspellDictionary spell-checkerHigher recall; huge dictionary ecosystem.Inverted noise profile: every identifier and name needs allowlisting; word-list maintenance becomes a chore that erodes the gate.Rejected: precision over recall here.
codespellKnown-misspelling checker (Python)Same philosophy as typos.Python toolchain in a Node monorepo; smaller corpus; slower.Rejected: typos dominates it.
ValeProse style linterEnforces style guides, not just spelling.A style-rule programme is a much bigger commitment than the gap (spelling) warrants.Rejected: Less Is More; could complement later.
AI reviewers only (status quo)Non-deterministicAlready running.Six-year-old typos in published posts show the miss rate; never audits the backlog.Rejected.

Where It Sits On The Adoption Curve

Early-to-late majority. typos has become the default spell-check gate in large Rust/Python/JS open-source projects and pre-commit setups; stable CLI, maintained corpus. Comfortably in the Goldilocks zone, with codespell as the decade-old fallback if it ever stalls.

Relation To Building Philosophy

  • Less Is More. One binary, one small TOML file, one step in an existing workflow, no service, no dictionary sprawl.
  • Short Feedback Loops. A typo in a new blog post fails the PR that introduces it, instead of shipping to the public site until a reader notices.
  • LLM-Optimized. Agents generate most content drafts; a deterministic spelling floor means the same slip fails the same way every time. The committed config also teaches agents which "misspellings" here are intentional.
  • Respect Goodhart's Law. Every allowlist entry is a reviewed line in _typos.toml with a stated reason, not a silent suppression.

Gaps & Risks

  • Recall is bounded by the corpus: a misspelling typos has never seen passes. Accepted. The low-noise design is what makes the gate sustainable.
  • British/American variants are both accepted by default, so inconsistent variant usage isn't flagged. That's a style concern (Vale's domain), out of scope.
  • Corpus updates can add findings: a mise version bump may flag words it previously ignored; Renovate surfaces that as an explicit PR (ADR 024).

Consequences

Positive

  • Six real misspellings in published content fixed; spelling regressions now blocked at PR time across code, docs, and content.
  • Near-zero-noise gate: 23 of 29 first-run findings were resolved by config taxonomy, not suppressed ad hoc.
  • Free, single binary, mise/CI parity.

Negative

  • One more blocking lint step (sub-second on this repo).
  • _typos.toml grows as new proper nouns enter the content.