stylefix
Regularize a Markdown file to US or UK English and a style guide (Chicago, Oxford, AP, GPO, APA or Guardian), fully automatically, and locally unless you point it at a hosted model.
stylefix draft.md --target us # -> draft.us.md + draft.us.md.report.md
stylefix draft.md --target uk --oxford-ize # UK with Oxford -ize spellings
stylefix draft.md --target us --dry-run # print a diff, write nothing
stylefix draft.md --target us --changes # list every change as it is made
Vale decides what to change; a small Python wrapper applies the changes. The few words that need judgement (UK licence/license, programme/program, cheque/check, ...) are put to a language model as a multiple-choice question with the surrounding sentence. The model never rewrites prose: it picks one of the options you supply, or KEEP.
Install
uv tool install stylefix # or: pipx install stylefix
stylefix also needs Vale on your PATH:
brew install vale on macOS, choco install vale on Windows, or a release
binary from GitHub on Linux. Python 3.11+.
stylefix --help gives the full option reference with examples. The man
page is in the repository at man/stylefix.1 (man ./man/stylefix.1).
How it works
Every candidate word lands in one of three tiers:
| tier | mechanism | what happens |
|---|---|---|
| deterministic | suffix patterns confirmed against dictionaries, plus a curated table for the irregular families | applied, case preserved |
| adjudicated | words with a sense split (licence/license, programme/program), pattern hits whose rewrite is in no dictionary (coinages, names), any hit that is capitalised mid-sentence or inside a quotation, and the style guide's serial-comma and numeral rules | a language model chooses between the candidates and KEEP |
| review | spellings that look like the other variety but could not be converted; serial-comma and numeral hits when no model is configured | listed in the report and the --changes log |
Two data sources, both from the SCOWL project: VarCon, shipped in
stylefix/dict/, and the Hunspell dictionaries that come with adjudicate.
- VarCon (Variant Conversion Info) is the curated table: about 15,000
spellings mapped to their preferred American, British and Oxford forms,
with sense annotations. It is the source of the Vale substitution rules,
regenerated by
python -m stylefix.wordlist. Where VarCon says a spelling is also a word of its own in the target ("check | verify" alongside "check / cheque | bank"; licence as noun, license as verb), the word is not converted but adjudicated, and VarCon's glosses become the rule of thumb the model reads: "British: 'check' for verify; 'cheque' for bank." - Hunspell dictionaries (en_US, en_GB, read with the pure-Python
spylls) confirm the pattern tier. English keeps coining -ize verbs, so the -ise/-ize, -yse/-yze, -our/-or, -re/-er and -ll-/-l- endings are handled as patterns: a hit is rewritten and looked up. A rewrite that is a real word is applied ("securitise" becomes "securitize" without anyone listing it); one in no dictionary goes to the model ("platformise"); a word already valid in the target is left alone ("advertise", "glamour").
Patterns take what they can prove; the table carries everything else (closed families like -ogue and ae/oe, vocabulary like aluminium/aluminum, and inflections the patterns don't cover). On the same word the table wins.
UK variants. The default UK target writes -ise. --oxford-ize uses
VarCon's Oxford column and flips the -ise/-ize pattern, keeping -yse and
every other British form.
Style guides
--style chooses the mechanical conventions; spelling always follows
--target. stylefix --list-styles prints this table.
| guide | for | dash | serial comma | numerals from | toward | punctuation in quotes | abbreviations |
|---|---|---|---|---|---|---|---|
chicago (US default) |
Chicago Manual of Style | unspaced em | required | 101 | toward | inside | US, UK; PhD; 10 a.m.; vs.; e.g. |
gpo |
US Government Publishing Office Style Manual, the federal standard | unspaced em | required | 10 | toward | inside | U.S., U.K.; Ph.D.; 10 a.m.; vs.; e.g. |
ap |
Associated Press, journalism | spaced em | omitted in a simple series | 10 | toward | inside | U.S., U.K.; Ph.D.; 10 a.m.; vs.; e.g. |
apa |
APA 7th, academic and policy research | unspaced em | required | 10 | no rule | inside | U.S.; PhD; 10 a.m.; vs.; e.g. |
oxford (UK default) |
New Hart's Rules | spaced en | required | 101 | towards | logical (unchanged) | US, UK; PhD; 10 a.m.; vs; e.g. |
guardian |
Guardian/Observer, UK journalism | spaced en | omitted unless needed | 10 | towards | logical (unchanged) | US, UK; PhD; 10am; v; eg, ie, etc |
none |
no guide | unchanged | no rule | no rule | no rule | unchanged | unchanged |
Abbreviations follow the guides' general rules, not a word list. Chicago's rule (10.4), shared by Oxford and the Guardian, is that an abbreviation in full capitals takes no periods, two letters or more: US, UK, EU, NATO, PhD, DC. It is applied to every period-group of capitals. The hazard is personal initials ("J.K. Rowling" keeps its periods), which look identical; so a group followed by a capitalised word is adjudicated, with the readings shown as they would print, and the sentence-end case ("in the U.S. The ...") is handled the same way. AP's rule is the reverse for "most two-letter abbreviations" (U.S., U.N., U.K., B.A., B.C.), with named exceptions (EU, GI, ID) and none in longer ones (FBI, NATO); since AP writes AI, IT and TV without periods, the rule is applied by category: countries, places, degrees and eras. GPO follows AP's forms; APA keeps "U.S." but writes degrees without periods (PhD, MA). Clock times, vs., and e.g., i.e., etc. take each guide's form.
The noun rule. GPO and APA use "U.S." only as an adjective ("U.S.
policy") and write the noun out ("the United States"). Because that is a
part-of-speech question, it is a warning by default under those guides
(each likely noun use is listed for review) and an enforcement on request:
--abbreviation-nouns enforce puts every "the U.S." to the model, which
chooses "United States", the adjective form, or KEEP. off silences it.
Dashes, toward/towards and punctuation placement are applied outright.
Serial commas and numerals need judgement (is "attacks, intentional and
unintentional" a list of three? is "10" a count or a percentage?), so each
hit becomes an adjudication item: the model sees the sentence and the
guide's rule of thumb and chooses between one concrete edit ("ten",
"a, b, and c") and KEEP. With no model configured they are listed for
review instead. A guide from the other variety (--target uk --style chicago) is allowed and noted on stderr.
Guides are a few parameters each in stylefix/styleguides.py; the Vale
rules are generated from them.
After the word-level edits, a punctuation pass handles what a regex rule cannot: curly quotes, primary double (US) or single (UK) quotes with correct nesting, apostrophes, parenthetical dashes (Chicago em dash / Oxford spaced en dash), en dashes in number ranges, and date order (September 4, 2026 / 4 September 2026). Chicago also moves commas and periods inside closing quotes. Front matter, fenced code, inline code, link targets and URLs are never touched.
The report next to the output lists every deterministic swap, every model decision with its confidence and reason, and every alert left for a human.
The model
The quickest way to set one up is
stylefix --setup
which asks where your model is (local Ollama, a hosted API, or your own
server), tests it, and saves the answer where both stylefix and proofix
find it. stylefix --check-endpoint shows what stylefix will use, where
each setting came from, and whether the endpoint answers.
Any OpenAI-compatible /v1/chat/completions endpoint works: Ollama, vLLM,
LiteLLM, llama.cpp's server, or a hosted API. stylefix takes its settings
from, highest first: --url/--model/--timeout; STYLEFIX_LLM_URL,
STYLEFIX_LLM_MODEL, STYLEFIX_LLM_KEY; ~/.config/stylefix/config.toml;
ADJUDICATE_LLM_*; and ~/.config/adjudicate/config.toml, which proofix
reads too. A --url or STYLEFIX_LLM_URL drops keys set in lower layers;
set STYLEFIX_LLM_KEY (or api_key_env in the same config file) alongside
it. With nothing configured, --llm auto (the default) uses a local
Ollama if one is running with a model pulled, and otherwise runs as
--llm none: ambiguous words are left alone and listed in the report,
while capitalised deterministic swaps are applied anyway.
Ollama: nothing to configure; the first installed model is used.
--model picks another.
A hosted API:
# ~/.config/adjudicate/config.toml
[llm]
url = "https://api.openai.com/v1"
model = "gpt-4.1-mini"
api_key_env = "OPENAI_API_KEY" # the key stays in your environment
Self-hosted vLLM or LiteLLM with a reasoning model:
[llm]
url = "http://gpu-box:8000/v1"
model = "qwen3"
thinking_switch = true # send enable_thinking: false, which vLLM honours
Leave model out and the server's first listed model is used. A key the
file or environment names but that is unset, an unknown key, or a wrong
type stops the run with the file and key named. The full table is
documented in adjudicate.
Requests go out in batches of --batch-size items (20), --parallel (4) at a
time, so a serving stack with continuous batching (vLLM) answers a document's
worth of questions in a couple of round-trips. Decisions are cached in
.stylefix/cache.json keyed on target, word and sentence; re-runs are
instant and reproducible, and you can edit a cached choice to override it.
Answers below --min-confidence (0.7), invalid answers, and transport errors
all fall through to the report rather than being applied.
Choosing a model
The question the model answers is narrow (pick one of two or three options,
given a sentence), so any current instruct model handles it; what differs is
speed. scripts/eval_adjudicator.py scores an endpoint on 48 gold cases
(noun/verb splits, sense splits, proper nouns, quotations) and reports latency:
uv run python scripts/eval_adjudicator.py [--url URL --model NAME] [--think] [--parallel 8] [--repeat 4]
One measurement, on a DGX Spark serving Qwen3.8-27B (NVFP4) through vLLM:
| mode | accuracy | per item |
|---|---|---|
thinking off, --parallel 1 |
47/48 | ~1.0 s |
thinking off, --parallel 8 |
47/48 | ~0.33 s |
--think, batch 8, --parallel 4 |
48/48 | ~3.2 s |
The one miss without thinking is a capitalised common noun ("Colour theory")
kept as if it were a title, which is the conservative failure. Thinking buys
the last case at four to ten times the latency; use --think with
--batch-size 8 --timeout 600 for a final pass on an important document,
and the default for everyday runs. Throughput scales with --parallel
because vLLM batches concurrent requests, so a mixture-of-experts model with
few active parameters (e.g. Gemma-4-26B-A4B) will be faster still on large
batches without changing accuracy noticeably.
Exceptions
Words to leave alone come from four places, all optional and all combined:
- A built-in list (
stylefix/accept.txt) of technical and policy terms and acronyms (cybersecurity, sandboxing, LLM, NIST). A test ensures nothing on it is a word the conversion tables know. ~/.config/stylefix/accept.txt, your personal list..stylefix/accept.txtin the working directory, the project's list.--accept FILEfor a one-off.
One term per line, # comments allowed, case-insensitive. They become a
Vale vocabulary and are skipped by every rule. --no-default-accept uses
only the --accept file.
Extending
- Suffix families live in
stylefix/patterns.py: the regexes, the never-lists, and the decision logic. The conversion table comes fromstylefix/dict/varcon.txt.gzthroughstylefix/varcon.py; local fixes go in the override dicts instylefix/wordlist.py. After editing any of these, runpython -m stylefix.wordlistto regenerate the Vale rules (a test fails if they drift). Tests check that the patterns never contradict VarCon and that the old hand tables are still fully covered. - Ambiguous words live in
stylefix/candidates.pyand the matchingAmbiguous.ymltoken regex. - Style rules are plain Vale YAML under
stylefix/styles/<Guide>, generated fromstylefix/styleguides.py; any substitution rule withaction: replaceis applied automatically, anything else goes to the report.
Limitations
- The UK direction does not move punctuation outside quotation marks: whether a comma belongs to the quote is not knowable mechanically.
- Numeral and serial-comma detection is heuristic, so no hit is applied
mechanically: each goes to the model, which chooses the edit or KEEP, or to
the report when no model is available (
--llm none, or no endpoint reachable). .docxfiles: convert with pandoc first (pandoc in.docx -t gfm -o in.md), fix, convert back.
License
MIT (see LICENSE). stylefix/dict/varcon.txt.gz is VarCon, under the
notice in stylefix/dict/README-varcon.txt. The spelling dictionaries come
with adjudicate.
Development
uv sync
uv run pytest
Metadata
Release files for stylefix 0.8.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| stylefix-0.8.1.tar.gz | 394.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stylefix-0.8.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 775.6 kB
Release files / stylefix-0.8.1.tar.gz
| Download URL | stylefix-0.8.1.tar.gz |
|---|---|
| Size | 394.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
31650bc9490e6f8156ccd868a95c2862e531a7d0567ee23dd2c78e608fc720bf
|
|
BLAKE2b-256 checksum How to use checksums |
ff9758b216b4bd921af05bfc0dc197858c7796d5cc256538b8fd32d3b839f0d2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.4
|
Release files / stylefix-0.8.1-py3-none-any.whl
| Download URL | stylefix-0.8.1-py3-none-any.whl |
|---|---|
| Size | 381.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
edf7884e3d4e294e8ca63134ef22a001452038c2ef7e5cb0ac8ec0dcbf3d9852
|
|
BLAKE2b-256 checksum How to use checksums |
98b2a41c7d697b77ecfa8c404bdcc4c84696be6a98fb82d47dc20a9bfd7520f9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.4
|