stylefix 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. stylefix-0.7.0/.gitignore +8 -0
  2. stylefix-0.7.0/LICENSE +21 -0
  3. stylefix-0.7.0/PKG-INFO +290 -0
  4. stylefix-0.7.0/README.md +269 -0
  5. stylefix-0.7.0/examples/sample.md +29 -0
  6. stylefix-0.7.0/man/stylefix.1 +361 -0
  7. stylefix-0.7.0/pyproject.toml +49 -0
  8. stylefix-0.7.0/scripts/eval_adjudicator.py +135 -0
  9. stylefix-0.7.0/stylefix/__init__.py +3 -0
  10. stylefix-0.7.0/stylefix/abbreviations.py +200 -0
  11. stylefix-0.7.0/stylefix/accept.txt +94 -0
  12. stylefix-0.7.0/stylefix/adjudicate.py +27 -0
  13. stylefix-0.7.0/stylefix/apply.py +3 -0
  14. stylefix-0.7.0/stylefix/candidates.py +105 -0
  15. stylefix-0.7.0/stylefix/changelog.py +66 -0
  16. stylefix-0.7.0/stylefix/cli.py +415 -0
  17. stylefix-0.7.0/stylefix/context.py +4 -0
  18. stylefix-0.7.0/stylefix/conventions.py +172 -0
  19. stylefix-0.7.0/stylefix/dict/README-varcon.txt +582 -0
  20. stylefix-0.7.0/stylefix/dict/varcon.txt.gz +0 -0
  21. stylefix-0.7.0/stylefix/dictionary.py +3 -0
  22. stylefix-0.7.0/stylefix/llm.py +4 -0
  23. stylefix-0.7.0/stylefix/patterns.py +217 -0
  24. stylefix-0.7.0/stylefix/report.py +65 -0
  25. stylefix-0.7.0/stylefix/spellcheck.py +68 -0
  26. stylefix-0.7.0/stylefix/styleguides.py +153 -0
  27. stylefix-0.7.0/stylefix/styleitems.py +221 -0
  28. stylefix-0.7.0/stylefix/styles/AP/Numbers.yml +9 -0
  29. stylefix-0.7.0/stylefix/styles/AP/Toward.yml +13 -0
  30. stylefix-0.7.0/stylefix/styles/APA/Numbers.yml +9 -0
  31. stylefix-0.7.0/stylefix/styles/Chicago/Numbers.yml +9 -0
  32. stylefix-0.7.0/stylefix/styles/Chicago/Toward.yml +13 -0
  33. stylefix-0.7.0/stylefix/styles/GPO/Numbers.yml +9 -0
  34. stylefix-0.7.0/stylefix/styles/GPO/Toward.yml +13 -0
  35. stylefix-0.7.0/stylefix/styles/Guardian/Numbers.yml +9 -0
  36. stylefix-0.7.0/stylefix/styles/Guardian/Towards.yml +10 -0
  37. stylefix-0.7.0/stylefix/styles/Oxford/Numbers.yml +9 -0
  38. stylefix-0.7.0/stylefix/styles/Oxford/Towards.yml +10 -0
  39. stylefix-0.7.0/stylefix/styles/ToUK/Ambiguous.yml +7 -0
  40. stylefix-0.7.0/stylefix/styles/ToUK/Ise1.yml +1509 -0
  41. stylefix-0.7.0/stylefix/styles/ToUK/Ise2.yml +1509 -0
  42. stylefix-0.7.0/stylefix/styles/ToUK/Ise3.yml +1509 -0
  43. stylefix-0.7.0/stylefix/styles/ToUK/Ise4.yml +1094 -0
  44. stylefix-0.7.0/stylefix/styles/ToUK/Oxford1.yml +1509 -0
  45. stylefix-0.7.0/stylefix/styles/ToUK/Oxford2.yml +1509 -0
  46. stylefix-0.7.0/stylefix/styles/ToUK/Oxford3.yml +1509 -0
  47. stylefix-0.7.0/stylefix/styles/ToUK/Oxford4.yml +1108 -0
  48. stylefix-0.7.0/stylefix/styles/ToUK/PatEr.yml +8 -0
  49. stylefix-0.7.0/stylefix/styles/ToUK/PatIse.yml +8 -0
  50. stylefix-0.7.0/stylefix/styles/ToUK/PatIze.yml +8 -0
  51. stylefix-0.7.0/stylefix/styles/ToUK/PatOr.yml +8 -0
  52. stylefix-0.7.0/stylefix/styles/ToUK/Variants1.yml +1509 -0
  53. stylefix-0.7.0/stylefix/styles/ToUK/Variants2.yml +1509 -0
  54. stylefix-0.7.0/stylefix/styles/ToUK/Variants3.yml +1317 -0
  55. stylefix-0.7.0/stylefix/styles/ToUS/Ambiguous.yml +7 -0
  56. stylefix-0.7.0/stylefix/styles/ToUS/PatIse.yml +8 -0
  57. stylefix-0.7.0/stylefix/styles/ToUS/PatLl.yml +8 -0
  58. stylefix-0.7.0/stylefix/styles/ToUS/PatOur.yml +8 -0
  59. stylefix-0.7.0/stylefix/styles/ToUS/PatRe.yml +8 -0
  60. stylefix-0.7.0/stylefix/styles/ToUS/Variants1.yml +1509 -0
  61. stylefix-0.7.0/stylefix/styles/ToUS/Variants2.yml +1509 -0
  62. stylefix-0.7.0/stylefix/styles/ToUS/Variants3.yml +1509 -0
  63. stylefix-0.7.0/stylefix/styles/ToUS/Variants4.yml +1509 -0
  64. stylefix-0.7.0/stylefix/styles/ToUS/Variants5.yml +1509 -0
  65. stylefix-0.7.0/stylefix/styles/ToUS/Variants6.yml +1509 -0
  66. stylefix-0.7.0/stylefix/styles/ToUS/Variants7.yml +697 -0
  67. stylefix-0.7.0/stylefix/vale_runner.py +113 -0
  68. stylefix-0.7.0/stylefix/varcon.py +170 -0
  69. stylefix-0.7.0/stylefix/wordlist.py +145 -0
  70. stylefix-0.7.0/tests/conftest.py +16 -0
  71. stylefix-0.7.0/tests/legacy_table.py +210 -0
  72. stylefix-0.7.0/tests/test_abbreviations.py +63 -0
  73. stylefix-0.7.0/tests/test_adjudicate.py +68 -0
  74. stylefix-0.7.0/tests/test_apply.py +42 -0
  75. stylefix-0.7.0/tests/test_changelog.py +36 -0
  76. stylefix-0.7.0/tests/test_context.py +39 -0
  77. stylefix-0.7.0/tests/test_conventions.py +56 -0
  78. stylefix-0.7.0/tests/test_docs.py +58 -0
  79. stylefix-0.7.0/tests/test_e2e.py +157 -0
  80. stylefix-0.7.0/tests/test_endpoint_cli.py +91 -0
  81. stylefix-0.7.0/tests/test_patterns.py +106 -0
  82. stylefix-0.7.0/tests/test_spellcheck.py +29 -0
  83. stylefix-0.7.0/tests/test_styleguides.py +37 -0
  84. stylefix-0.7.0/tests/test_styleitems.py +119 -0
  85. stylefix-0.7.0/tests/test_wordlist.py +67 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ .stylefix/
3
+ __pycache__/
4
+ *.egg-info/
5
+ dist/
6
+ *.us.md
7
+ *.uk.md
8
+ *.report.md
stylefix-0.7.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Brož
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.5
2
+ Name: stylefix
3
+ Version: 0.7.0
4
+ Summary: Regularize a Markdown file to US or UK English and a style guide, using Vale plus a language model for the ambiguous cases.
5
+ Project-URL: Homepage, https://github.com/AndrewBroz/stylefix
6
+ Project-URL: Issues, https://github.com/AndrewBroz/stylefix/issues
7
+ Author: Andrew Brož
8
+ License-Expression: MIT AND LicenseRef-VarCon
9
+ License-File: LICENSE
10
+ License-File: stylefix/dict/README-varcon.txt
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: adjudicate<0.4,>=0.3
19
+ Requires-Dist: spylls>=0.1.7
20
+ Description-Content-Type: text/markdown
21
+
22
+ # stylefix
23
+
24
+ Regularize a Markdown file to **US or UK English** and a **style guide**
25
+ (Chicago, Oxford, AP, GPO, APA or Guardian), fully automatically, and locally unless you point it at a hosted model.
26
+
27
+ ```
28
+ stylefix draft.md --target us # -> draft.us.md + draft.us.md.report.md
29
+ stylefix draft.md --target uk --oxford-ize # UK with Oxford -ize spellings
30
+ stylefix draft.md --target us --dry-run # print a diff, write nothing
31
+ stylefix draft.md --target us --changes # list every change as it is made
32
+ ```
33
+
34
+ [Vale](https://vale.sh) decides what to change; a small Python wrapper applies
35
+ the changes. The few words that need judgement (UK *licence*/*license*,
36
+ *programme*/*program*, *cheque*/*check*, ...) are put to a language model as a
37
+ multiple-choice question with the surrounding sentence. The model never
38
+ rewrites prose: it picks one of the options you supply, or KEEP.
39
+
40
+ ## Install
41
+
42
+ ```
43
+ uv tool install stylefix # or: pipx install stylefix
44
+ ```
45
+
46
+ stylefix also needs [Vale](https://vale.sh/docs/install) on your PATH:
47
+ `brew install vale` on macOS, `choco install vale` on Windows, or a release
48
+ binary from GitHub on Linux. Python 3.11+.
49
+
50
+ `stylefix --help` gives the full option reference with examples. The man
51
+ page is in the repository at `man/stylefix.1` (`man ./man/stylefix.1`).
52
+
53
+ ## How it works
54
+
55
+ Every candidate word lands in one of three tiers:
56
+
57
+ | tier | mechanism | what happens |
58
+ |---|---|---|
59
+ | deterministic | **suffix patterns** confirmed against dictionaries, plus a curated table for the irregular families | applied, case preserved |
60
+ | 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 |
61
+ | 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 |
62
+
63
+ **Two data sources, both from the SCOWL project: VarCon, shipped in
64
+ `stylefix/dict/`, and the Hunspell dictionaries that come with adjudicate.**
65
+
66
+ - **VarCon** (Variant Conversion Info) is the curated table: about 15,000
67
+ spellings mapped to their preferred American, British and Oxford forms,
68
+ with sense annotations. It is the source of the Vale substitution rules,
69
+ regenerated by `python -m stylefix.wordlist`. Where VarCon says a spelling
70
+ is *also* a word of its own in the target ("check | verify" alongside
71
+ "check / cheque | bank"; licence as noun, license as verb), the word is
72
+ not converted but adjudicated, and VarCon's glosses become the rule of
73
+ thumb the model reads: "British: 'check' for verify; 'cheque' for bank."
74
+ - **Hunspell dictionaries** (en_US, en_GB, read with the pure-Python
75
+ `spylls`) confirm the pattern tier. English keeps coining -ize verbs, so
76
+ the -ise/-ize, -yse/-yze, -our/-or, -re/-er and -ll-/-l- endings are
77
+ handled as patterns: a hit is rewritten and looked up. A rewrite that is
78
+ a real word is applied ("securitise" becomes "securitize" without anyone
79
+ listing it); one in no dictionary goes to the model ("platformise"); a
80
+ word already valid in the target is left alone ("advertise", "glamour").
81
+
82
+ Patterns take what they can prove; the table carries everything else
83
+ (closed families like -ogue and ae/oe, vocabulary like aluminium/aluminum,
84
+ and inflections the patterns don't cover). On the same word the table
85
+ wins.
86
+
87
+ **UK variants.** The default UK target writes -ise. `--oxford-ize` uses
88
+ VarCon's Oxford column and flips the -ise/-ize pattern, keeping -yse and
89
+ every other British form.
90
+
91
+ ## Style guides
92
+
93
+ `--style` chooses the mechanical conventions; spelling always follows
94
+ `--target`. `stylefix --list-styles` prints this table.
95
+
96
+ | guide | for | dash | serial comma | numerals from | toward | punctuation in quotes | abbreviations |
97
+ |---|---|---|---|---|---|---|---|
98
+ | `chicago` (US default) | Chicago Manual of Style | unspaced em | required | 101 | toward | inside | US, UK; PhD; 10 a.m.; vs.; e.g. |
99
+ | `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. |
100
+ | `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. |
101
+ | `apa` | APA 7th, academic and policy research | unspaced em | required | 10 | no rule | inside | U.S.; PhD; 10 a.m.; vs.; e.g. |
102
+ | `oxford` (UK default) | New Hart's Rules | spaced en | required | 101 | towards | logical (unchanged) | US, UK; PhD; 10 a.m.; vs; e.g. |
103
+ | `guardian` | Guardian/Observer, UK journalism | spaced en | omitted unless needed | 10 | towards | logical (unchanged) | US, UK; PhD; 10am; v; eg, ie, etc |
104
+ | `none` | no guide | unchanged | no rule | no rule | no rule | unchanged | unchanged |
105
+
106
+ **Abbreviations** follow the guides' general rules, not a word list.
107
+ Chicago's rule (10.4), shared by Oxford and the Guardian, is that an
108
+ abbreviation in full capitals takes no periods, two letters or more: US,
109
+ UK, EU, NATO, PhD, DC. It is applied to every period-group of capitals.
110
+ The hazard is personal initials ("J.K. Rowling" keeps its periods), which
111
+ look identical; so a group followed by a capitalised word is adjudicated,
112
+ with the readings shown as they would print, and the sentence-end case
113
+ ("in the U.S. The ...") is handled the same way. AP's rule is the reverse
114
+ for "most two-letter abbreviations" (U.S., U.N., U.K., B.A., B.C.), with
115
+ named exceptions (EU, GI, ID) and none in longer ones (FBI, NATO); since AP
116
+ writes AI, IT and TV without periods, the rule is applied by category:
117
+ countries, places, degrees and eras. GPO follows AP's forms; APA keeps
118
+ "U.S." but writes degrees without periods (PhD, MA). Clock times, vs., and
119
+ e.g., i.e., etc. take each guide's form.
120
+
121
+ **The noun rule.** GPO and APA use "U.S." only as an adjective ("U.S.
122
+ policy") and write the noun out ("the United States"). Because that is a
123
+ part-of-speech question, it is a warning by default under those guides
124
+ (each likely noun use is listed for review) and an enforcement on request:
125
+ `--abbreviation-nouns enforce` puts every "the U.S." to the model, which
126
+ chooses "United States", the adjective form, or KEEP. `off` silences it.
127
+
128
+ Dashes, toward/towards and punctuation placement are applied outright.
129
+ Serial commas and numerals need judgement (is "attacks, intentional and
130
+ unintentional" a list of three? is "10" a count or a percentage?), so each
131
+ hit becomes an adjudication item: the model sees the sentence and the
132
+ guide's rule of thumb and chooses between one concrete edit ("ten",
133
+ "a, b, and c") and KEEP. With no model configured they are listed for
134
+ review instead. A guide from the other variety (`--target uk --style
135
+ chicago`) is allowed and noted on stderr.
136
+
137
+ Guides are a few parameters each in `stylefix/styleguides.py`; the Vale
138
+ rules are generated from them.
139
+
140
+ After the word-level edits, a punctuation pass handles what a regex rule
141
+ cannot: curly quotes, primary double (US) or single (UK) quotes with correct
142
+ nesting, apostrophes, parenthetical dashes (Chicago em dash / Oxford spaced en
143
+ dash), en dashes in number ranges, and date order (September 4, 2026 /
144
+ 4 September 2026). Chicago also moves commas and periods inside closing
145
+ quotes. Front matter, fenced code, inline code, link targets and URLs are
146
+ never touched.
147
+
148
+ The report next to the output lists every deterministic swap, every model
149
+ decision with its confidence and reason, and every alert left for a human.
150
+
151
+ ## The model
152
+
153
+ The quickest way to set one up is
154
+
155
+ ```
156
+ stylefix --setup
157
+ ```
158
+
159
+ which asks where your model is (local Ollama, a hosted API, or your own
160
+ server), tests it, and saves the answer where both stylefix and proofix
161
+ find it. `stylefix --check-endpoint` shows what stylefix will use, where
162
+ each setting came from, and whether the endpoint answers.
163
+
164
+ Any OpenAI-compatible `/v1/chat/completions` endpoint works: Ollama, vLLM,
165
+ LiteLLM, llama.cpp's server, or a hosted API. stylefix takes its settings
166
+ from, highest first: `--url`/`--model`/`--timeout`; `STYLEFIX_LLM_URL`,
167
+ `STYLEFIX_LLM_MODEL`, `STYLEFIX_LLM_KEY`; `~/.config/stylefix/config.toml`;
168
+ `ADJUDICATE_LLM_*`; and `~/.config/adjudicate/config.toml`, which proofix
169
+ reads too. A `--url` or `STYLEFIX_LLM_URL` drops keys set in lower layers;
170
+ set `STYLEFIX_LLM_KEY` (or `api_key_env` in the same config file) alongside
171
+ it. With nothing configured, `--llm auto` (the default) uses a local
172
+ Ollama if one is running with a model pulled, and otherwise runs as
173
+ `--llm none`: ambiguous words are left alone and listed in the report,
174
+ while capitalised deterministic swaps are applied anyway.
175
+
176
+ **Ollama**: nothing to configure; the first installed model is used.
177
+ `--model` picks another.
178
+
179
+ **A hosted API**:
180
+
181
+ ```toml
182
+ # ~/.config/adjudicate/config.toml
183
+ [llm]
184
+ url = "https://api.openai.com/v1"
185
+ model = "gpt-4.1-mini"
186
+ api_key_env = "OPENAI_API_KEY" # the key stays in your environment
187
+ ```
188
+
189
+ **Self-hosted vLLM or LiteLLM with a reasoning model**:
190
+
191
+ ```toml
192
+ [llm]
193
+ url = "http://gpu-box:8000/v1"
194
+ model = "qwen3"
195
+ thinking_switch = true # send enable_thinking: false, which vLLM honours
196
+ ```
197
+
198
+ Leave `model` out and the server's first listed model is used. A key the
199
+ file or environment names but that is unset, an unknown key, or a wrong
200
+ type stops the run with the file and key named. The full table is
201
+ documented in [adjudicate](https://github.com/AndrewBroz/adjudicate#choosing-the-model-endpoint).
202
+
203
+ Requests go out in batches of `--batch-size` items (20), `--parallel` (4) at a
204
+ time, so a serving stack with continuous batching (vLLM) answers a document's
205
+ worth of questions in a couple of round-trips. Decisions are cached in
206
+ `.stylefix/cache.json` keyed on target, word and sentence; re-runs are
207
+ instant and reproducible, and you can edit a cached choice to override it.
208
+ Answers below `--min-confidence` (0.7), invalid answers, and transport errors
209
+ all fall through to the report rather than being applied.
210
+
211
+ ### Choosing a model
212
+
213
+ The question the model answers is narrow (pick one of two or three options,
214
+ given a sentence), so any current instruct model handles it; what differs is
215
+ speed. `scripts/eval_adjudicator.py` scores an endpoint on 48 gold cases
216
+ (noun/verb splits, sense splits, proper nouns, quotations) and reports latency:
217
+
218
+ ```
219
+ uv run python scripts/eval_adjudicator.py [--url URL --model NAME] [--think] [--parallel 8] [--repeat 4]
220
+ ```
221
+
222
+ One measurement, on a DGX Spark serving Qwen3.8-27B (NVFP4) through vLLM:
223
+
224
+ | mode | accuracy | per item |
225
+ |---|---|---|
226
+ | thinking off, `--parallel 1` | 47/48 | ~1.0 s |
227
+ | thinking off, `--parallel 8` | 47/48 | ~0.33 s |
228
+ | `--think`, batch 8, `--parallel 4` | 48/48 | ~3.2 s |
229
+
230
+ The one miss without thinking is a capitalised common noun ("Colour theory")
231
+ kept as if it were a title, which is the conservative failure. Thinking buys
232
+ the last case at four to ten times the latency; use `--think` with
233
+ `--batch-size 8 --timeout 600` for a final pass on an important document,
234
+ and the default for everyday runs. Throughput scales with `--parallel`
235
+ because vLLM batches concurrent requests, so a mixture-of-experts model with
236
+ few active parameters (e.g. Gemma-4-26B-A4B) will be faster still on large
237
+ batches without changing accuracy noticeably.
238
+
239
+ ## Exceptions
240
+
241
+ Words to leave alone come from four places, all optional and all combined:
242
+
243
+ 1. A built-in list (`stylefix/accept.txt`) of technical and policy terms
244
+ and acronyms (cybersecurity, sandboxing, LLM, NIST). A test ensures
245
+ nothing on it is a word the conversion tables know.
246
+ 2. `~/.config/stylefix/accept.txt`, your personal list.
247
+ 3. `.stylefix/accept.txt` in the working directory, the project's list.
248
+ 4. `--accept FILE` for a one-off.
249
+
250
+ One term per line, `#` comments allowed, case-insensitive. They become a
251
+ Vale vocabulary and are skipped by every rule. `--no-default-accept` uses
252
+ only the `--accept` file.
253
+
254
+ ## Extending
255
+
256
+ - **Suffix families** live in `stylefix/patterns.py`: the regexes, the
257
+ never-lists, and the decision logic. The conversion table comes from
258
+ `stylefix/dict/varcon.txt.gz` through `stylefix/varcon.py`; local fixes
259
+ go in the override dicts in `stylefix/wordlist.py`. After editing any of
260
+ these, run `python -m stylefix.wordlist` to regenerate the Vale rules (a
261
+ test fails if they drift). Tests check that the patterns never contradict
262
+ VarCon and that the old hand tables are still fully covered.
263
+ - **Ambiguous words** live in `stylefix/candidates.py` and the matching
264
+ `Ambiguous.yml` token regex.
265
+ - **Style rules** are plain Vale YAML under `stylefix/styles/<Guide>`, generated
266
+ from `stylefix/styleguides.py`; any substitution rule with `action: replace` is
267
+ applied automatically, anything else goes to the report.
268
+
269
+ ## Limitations
270
+
271
+ - The UK direction does not move punctuation outside quotation marks: whether
272
+ a comma belongs to the quote is not knowable mechanically.
273
+ - Numeral and serial-comma detection is heuristic, so no hit is applied
274
+ mechanically: each goes to the model, which chooses the edit or KEEP, or to
275
+ the report when no model is available (`--llm none`, or no endpoint reachable).
276
+ - `.docx` files: convert with pandoc first
277
+ (`pandoc in.docx -t gfm -o in.md`), fix, convert back.
278
+
279
+ ## License
280
+
281
+ MIT (see `LICENSE`). `stylefix/dict/varcon.txt.gz` is VarCon, under the
282
+ notice in `stylefix/dict/README-varcon.txt`. The spelling dictionaries come
283
+ with [adjudicate](https://github.com/AndrewBroz/adjudicate).
284
+
285
+ ## Development
286
+
287
+ ```
288
+ uv sync
289
+ uv run pytest
290
+ ```
@@ -0,0 +1,269 @@
1
+ # stylefix
2
+
3
+ Regularize a Markdown file to **US or UK English** and a **style guide**
4
+ (Chicago, Oxford, AP, GPO, APA or Guardian), fully automatically, and locally unless you point it at a hosted model.
5
+
6
+ ```
7
+ stylefix draft.md --target us # -> draft.us.md + draft.us.md.report.md
8
+ stylefix draft.md --target uk --oxford-ize # UK with Oxford -ize spellings
9
+ stylefix draft.md --target us --dry-run # print a diff, write nothing
10
+ stylefix draft.md --target us --changes # list every change as it is made
11
+ ```
12
+
13
+ [Vale](https://vale.sh) decides what to change; a small Python wrapper applies
14
+ the changes. The few words that need judgement (UK *licence*/*license*,
15
+ *programme*/*program*, *cheque*/*check*, ...) are put to a language model as a
16
+ multiple-choice question with the surrounding sentence. The model never
17
+ rewrites prose: it picks one of the options you supply, or KEEP.
18
+
19
+ ## Install
20
+
21
+ ```
22
+ uv tool install stylefix # or: pipx install stylefix
23
+ ```
24
+
25
+ stylefix also needs [Vale](https://vale.sh/docs/install) on your PATH:
26
+ `brew install vale` on macOS, `choco install vale` on Windows, or a release
27
+ binary from GitHub on Linux. Python 3.11+.
28
+
29
+ `stylefix --help` gives the full option reference with examples. The man
30
+ page is in the repository at `man/stylefix.1` (`man ./man/stylefix.1`).
31
+
32
+ ## How it works
33
+
34
+ Every candidate word lands in one of three tiers:
35
+
36
+ | tier | mechanism | what happens |
37
+ |---|---|---|
38
+ | deterministic | **suffix patterns** confirmed against dictionaries, plus a curated table for the irregular families | applied, case preserved |
39
+ | 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 |
40
+ | 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 |
41
+
42
+ **Two data sources, both from the SCOWL project: VarCon, shipped in
43
+ `stylefix/dict/`, and the Hunspell dictionaries that come with adjudicate.**
44
+
45
+ - **VarCon** (Variant Conversion Info) is the curated table: about 15,000
46
+ spellings mapped to their preferred American, British and Oxford forms,
47
+ with sense annotations. It is the source of the Vale substitution rules,
48
+ regenerated by `python -m stylefix.wordlist`. Where VarCon says a spelling
49
+ is *also* a word of its own in the target ("check | verify" alongside
50
+ "check / cheque | bank"; licence as noun, license as verb), the word is
51
+ not converted but adjudicated, and VarCon's glosses become the rule of
52
+ thumb the model reads: "British: 'check' for verify; 'cheque' for bank."
53
+ - **Hunspell dictionaries** (en_US, en_GB, read with the pure-Python
54
+ `spylls`) confirm the pattern tier. English keeps coining -ize verbs, so
55
+ the -ise/-ize, -yse/-yze, -our/-or, -re/-er and -ll-/-l- endings are
56
+ handled as patterns: a hit is rewritten and looked up. A rewrite that is
57
+ a real word is applied ("securitise" becomes "securitize" without anyone
58
+ listing it); one in no dictionary goes to the model ("platformise"); a
59
+ word already valid in the target is left alone ("advertise", "glamour").
60
+
61
+ Patterns take what they can prove; the table carries everything else
62
+ (closed families like -ogue and ae/oe, vocabulary like aluminium/aluminum,
63
+ and inflections the patterns don't cover). On the same word the table
64
+ wins.
65
+
66
+ **UK variants.** The default UK target writes -ise. `--oxford-ize` uses
67
+ VarCon's Oxford column and flips the -ise/-ize pattern, keeping -yse and
68
+ every other British form.
69
+
70
+ ## Style guides
71
+
72
+ `--style` chooses the mechanical conventions; spelling always follows
73
+ `--target`. `stylefix --list-styles` prints this table.
74
+
75
+ | guide | for | dash | serial comma | numerals from | toward | punctuation in quotes | abbreviations |
76
+ |---|---|---|---|---|---|---|---|
77
+ | `chicago` (US default) | Chicago Manual of Style | unspaced em | required | 101 | toward | inside | US, UK; PhD; 10 a.m.; vs.; e.g. |
78
+ | `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. |
79
+ | `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. |
80
+ | `apa` | APA 7th, academic and policy research | unspaced em | required | 10 | no rule | inside | U.S.; PhD; 10 a.m.; vs.; e.g. |
81
+ | `oxford` (UK default) | New Hart's Rules | spaced en | required | 101 | towards | logical (unchanged) | US, UK; PhD; 10 a.m.; vs; e.g. |
82
+ | `guardian` | Guardian/Observer, UK journalism | spaced en | omitted unless needed | 10 | towards | logical (unchanged) | US, UK; PhD; 10am; v; eg, ie, etc |
83
+ | `none` | no guide | unchanged | no rule | no rule | no rule | unchanged | unchanged |
84
+
85
+ **Abbreviations** follow the guides' general rules, not a word list.
86
+ Chicago's rule (10.4), shared by Oxford and the Guardian, is that an
87
+ abbreviation in full capitals takes no periods, two letters or more: US,
88
+ UK, EU, NATO, PhD, DC. It is applied to every period-group of capitals.
89
+ The hazard is personal initials ("J.K. Rowling" keeps its periods), which
90
+ look identical; so a group followed by a capitalised word is adjudicated,
91
+ with the readings shown as they would print, and the sentence-end case
92
+ ("in the U.S. The ...") is handled the same way. AP's rule is the reverse
93
+ for "most two-letter abbreviations" (U.S., U.N., U.K., B.A., B.C.), with
94
+ named exceptions (EU, GI, ID) and none in longer ones (FBI, NATO); since AP
95
+ writes AI, IT and TV without periods, the rule is applied by category:
96
+ countries, places, degrees and eras. GPO follows AP's forms; APA keeps
97
+ "U.S." but writes degrees without periods (PhD, MA). Clock times, vs., and
98
+ e.g., i.e., etc. take each guide's form.
99
+
100
+ **The noun rule.** GPO and APA use "U.S." only as an adjective ("U.S.
101
+ policy") and write the noun out ("the United States"). Because that is a
102
+ part-of-speech question, it is a warning by default under those guides
103
+ (each likely noun use is listed for review) and an enforcement on request:
104
+ `--abbreviation-nouns enforce` puts every "the U.S." to the model, which
105
+ chooses "United States", the adjective form, or KEEP. `off` silences it.
106
+
107
+ Dashes, toward/towards and punctuation placement are applied outright.
108
+ Serial commas and numerals need judgement (is "attacks, intentional and
109
+ unintentional" a list of three? is "10" a count or a percentage?), so each
110
+ hit becomes an adjudication item: the model sees the sentence and the
111
+ guide's rule of thumb and chooses between one concrete edit ("ten",
112
+ "a, b, and c") and KEEP. With no model configured they are listed for
113
+ review instead. A guide from the other variety (`--target uk --style
114
+ chicago`) is allowed and noted on stderr.
115
+
116
+ Guides are a few parameters each in `stylefix/styleguides.py`; the Vale
117
+ rules are generated from them.
118
+
119
+ After the word-level edits, a punctuation pass handles what a regex rule
120
+ cannot: curly quotes, primary double (US) or single (UK) quotes with correct
121
+ nesting, apostrophes, parenthetical dashes (Chicago em dash / Oxford spaced en
122
+ dash), en dashes in number ranges, and date order (September 4, 2026 /
123
+ 4 September 2026). Chicago also moves commas and periods inside closing
124
+ quotes. Front matter, fenced code, inline code, link targets and URLs are
125
+ never touched.
126
+
127
+ The report next to the output lists every deterministic swap, every model
128
+ decision with its confidence and reason, and every alert left for a human.
129
+
130
+ ## The model
131
+
132
+ The quickest way to set one up is
133
+
134
+ ```
135
+ stylefix --setup
136
+ ```
137
+
138
+ which asks where your model is (local Ollama, a hosted API, or your own
139
+ server), tests it, and saves the answer where both stylefix and proofix
140
+ find it. `stylefix --check-endpoint` shows what stylefix will use, where
141
+ each setting came from, and whether the endpoint answers.
142
+
143
+ Any OpenAI-compatible `/v1/chat/completions` endpoint works: Ollama, vLLM,
144
+ LiteLLM, llama.cpp's server, or a hosted API. stylefix takes its settings
145
+ from, highest first: `--url`/`--model`/`--timeout`; `STYLEFIX_LLM_URL`,
146
+ `STYLEFIX_LLM_MODEL`, `STYLEFIX_LLM_KEY`; `~/.config/stylefix/config.toml`;
147
+ `ADJUDICATE_LLM_*`; and `~/.config/adjudicate/config.toml`, which proofix
148
+ reads too. A `--url` or `STYLEFIX_LLM_URL` drops keys set in lower layers;
149
+ set `STYLEFIX_LLM_KEY` (or `api_key_env` in the same config file) alongside
150
+ it. With nothing configured, `--llm auto` (the default) uses a local
151
+ Ollama if one is running with a model pulled, and otherwise runs as
152
+ `--llm none`: ambiguous words are left alone and listed in the report,
153
+ while capitalised deterministic swaps are applied anyway.
154
+
155
+ **Ollama**: nothing to configure; the first installed model is used.
156
+ `--model` picks another.
157
+
158
+ **A hosted API**:
159
+
160
+ ```toml
161
+ # ~/.config/adjudicate/config.toml
162
+ [llm]
163
+ url = "https://api.openai.com/v1"
164
+ model = "gpt-4.1-mini"
165
+ api_key_env = "OPENAI_API_KEY" # the key stays in your environment
166
+ ```
167
+
168
+ **Self-hosted vLLM or LiteLLM with a reasoning model**:
169
+
170
+ ```toml
171
+ [llm]
172
+ url = "http://gpu-box:8000/v1"
173
+ model = "qwen3"
174
+ thinking_switch = true # send enable_thinking: false, which vLLM honours
175
+ ```
176
+
177
+ Leave `model` out and the server's first listed model is used. A key the
178
+ file or environment names but that is unset, an unknown key, or a wrong
179
+ type stops the run with the file and key named. The full table is
180
+ documented in [adjudicate](https://github.com/AndrewBroz/adjudicate#choosing-the-model-endpoint).
181
+
182
+ Requests go out in batches of `--batch-size` items (20), `--parallel` (4) at a
183
+ time, so a serving stack with continuous batching (vLLM) answers a document's
184
+ worth of questions in a couple of round-trips. Decisions are cached in
185
+ `.stylefix/cache.json` keyed on target, word and sentence; re-runs are
186
+ instant and reproducible, and you can edit a cached choice to override it.
187
+ Answers below `--min-confidence` (0.7), invalid answers, and transport errors
188
+ all fall through to the report rather than being applied.
189
+
190
+ ### Choosing a model
191
+
192
+ The question the model answers is narrow (pick one of two or three options,
193
+ given a sentence), so any current instruct model handles it; what differs is
194
+ speed. `scripts/eval_adjudicator.py` scores an endpoint on 48 gold cases
195
+ (noun/verb splits, sense splits, proper nouns, quotations) and reports latency:
196
+
197
+ ```
198
+ uv run python scripts/eval_adjudicator.py [--url URL --model NAME] [--think] [--parallel 8] [--repeat 4]
199
+ ```
200
+
201
+ One measurement, on a DGX Spark serving Qwen3.8-27B (NVFP4) through vLLM:
202
+
203
+ | mode | accuracy | per item |
204
+ |---|---|---|
205
+ | thinking off, `--parallel 1` | 47/48 | ~1.0 s |
206
+ | thinking off, `--parallel 8` | 47/48 | ~0.33 s |
207
+ | `--think`, batch 8, `--parallel 4` | 48/48 | ~3.2 s |
208
+
209
+ The one miss without thinking is a capitalised common noun ("Colour theory")
210
+ kept as if it were a title, which is the conservative failure. Thinking buys
211
+ the last case at four to ten times the latency; use `--think` with
212
+ `--batch-size 8 --timeout 600` for a final pass on an important document,
213
+ and the default for everyday runs. Throughput scales with `--parallel`
214
+ because vLLM batches concurrent requests, so a mixture-of-experts model with
215
+ few active parameters (e.g. Gemma-4-26B-A4B) will be faster still on large
216
+ batches without changing accuracy noticeably.
217
+
218
+ ## Exceptions
219
+
220
+ Words to leave alone come from four places, all optional and all combined:
221
+
222
+ 1. A built-in list (`stylefix/accept.txt`) of technical and policy terms
223
+ and acronyms (cybersecurity, sandboxing, LLM, NIST). A test ensures
224
+ nothing on it is a word the conversion tables know.
225
+ 2. `~/.config/stylefix/accept.txt`, your personal list.
226
+ 3. `.stylefix/accept.txt` in the working directory, the project's list.
227
+ 4. `--accept FILE` for a one-off.
228
+
229
+ One term per line, `#` comments allowed, case-insensitive. They become a
230
+ Vale vocabulary and are skipped by every rule. `--no-default-accept` uses
231
+ only the `--accept` file.
232
+
233
+ ## Extending
234
+
235
+ - **Suffix families** live in `stylefix/patterns.py`: the regexes, the
236
+ never-lists, and the decision logic. The conversion table comes from
237
+ `stylefix/dict/varcon.txt.gz` through `stylefix/varcon.py`; local fixes
238
+ go in the override dicts in `stylefix/wordlist.py`. After editing any of
239
+ these, run `python -m stylefix.wordlist` to regenerate the Vale rules (a
240
+ test fails if they drift). Tests check that the patterns never contradict
241
+ VarCon and that the old hand tables are still fully covered.
242
+ - **Ambiguous words** live in `stylefix/candidates.py` and the matching
243
+ `Ambiguous.yml` token regex.
244
+ - **Style rules** are plain Vale YAML under `stylefix/styles/<Guide>`, generated
245
+ from `stylefix/styleguides.py`; any substitution rule with `action: replace` is
246
+ applied automatically, anything else goes to the report.
247
+
248
+ ## Limitations
249
+
250
+ - The UK direction does not move punctuation outside quotation marks: whether
251
+ a comma belongs to the quote is not knowable mechanically.
252
+ - Numeral and serial-comma detection is heuristic, so no hit is applied
253
+ mechanically: each goes to the model, which chooses the edit or KEEP, or to
254
+ the report when no model is available (`--llm none`, or no endpoint reachable).
255
+ - `.docx` files: convert with pandoc first
256
+ (`pandoc in.docx -t gfm -o in.md`), fix, convert back.
257
+
258
+ ## License
259
+
260
+ MIT (see `LICENSE`). `stylefix/dict/varcon.txt.gz` is VarCon, under the
261
+ notice in `stylefix/dict/README-varcon.txt`. The spelling dictionaries come
262
+ with [adjudicate](https://github.com/AndrewBroz/adjudicate).
263
+
264
+ ## Development
265
+
266
+ ```
267
+ uv sync
268
+ uv run pytest
269
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ title: Colour Management Programme
3
+ author: The Centre for Analysis
4
+ ---
5
+
6
+ # Colour management programme
7
+
8
+ The Centre for Digital Behaviour organises its work around a simple licence
9
+ model. Members practise what they preach: every programme is analysed, every
10
+ storey of the building is catalogued, and "colour" is spelt the British way --
11
+ whilst our American colleagues favour 'color'.
12
+
13
+ ## Practical details
14
+
15
+ - The licence fee is 10 pounds and the practice runs 3-5 sessions a week.
16
+ - Please send your cheque to the Labour Party office by 4 September 2026.
17
+ - The theatre programme lists the metre readings, the tyre pressures, and the
18
+ disc brakes we've organised for the aluminium trailer.
19
+ - As the Defence Ministry's report put it, "the colour scheme was unauthorised".
20
+
21
+ The program crashed after the check cleared, and the story about the storey
22
+ made us tire of the whole thing. It's the Joneses' problem now, towards the end
23
+ of the '90s.
24
+
25
+ ```python
26
+ colour = "unchanged in code" # organise nothing here
27
+ ```
28
+
29
+ See `colour_utils.organise()` and [the colour guide](https://example.org/colour).