# ADR 055: typos (Spell Checking)

- HTML version: https://robbiepalmer.me/projects/personal-site/adrs/055-typos
- Project: Personal Site (https://robbiepalmer.me/projects/personal-site.md)
- Status: Accepted
- Date: 2026-07-20

# 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. So
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](/projects/personal-site/adrs/048-sonarqube))
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

| Tool                               | Role                               | Pros                                                                                                                                    | Cons                                                                                                                              | Verdict                                                                                  |
| ---------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **typos** *(accepted)*             | Known-misspelling checker          | Very low 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.**                                                                            |
| **cspell**                         | Dictionary spell-checker           | Higher 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.                                                    |
| **codespell**                      | Known-misspelling checker (Python) | Same philosophy as typos.                                                                                                               | Python toolchain in a Node monorepo; smaller corpus; slower.                                                                      | Rejected: typos dominates it.                                                            |
| **Vale**                           | Prose *style* linter               | Enforces style guides, not just spelling.                                                                                               | A style-rule programme is a much bigger commitment than the gap (spelling) warrants.                                              | Rejected: [Less Is More](/projects?tab=philosophy#less-is-more); could complement later. |
| **AI reviewers only** (status quo) | Non-deterministic                  | Already 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](/projects?tab=philosophy#the-goldilocks-zone), with
codespell as the decade-old fallback if it ever stalls.

# Relation To Building Philosophy

* [Less Is More](/projects?tab=philosophy#less-is-more). One binary, one small
  TOML file, one step in an existing workflow — no service, no dictionary
  sprawl.
* [Short Feedback Loops](/projects?tab=philosophy#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](/projects?tab=philosophy#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](/projects?tab=philosophy#respect-goodharts-and-conways-laws).
  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](/projects/personal-site/adrs/024-renovate)).

# 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.

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
