A Vale style ported from Harper, Automattic's grammar checker.
Harper keeps its rules in three places: a small DSL, a body of Rust linters, and
a tagged dictionary. This repository reads all three, compiles what it can into
Vale checks, and writes the rest by hand. make measure scores the result
against Harper itself.
Note
Built with Claude and the Vale MCP server. The port was written with
Claude against the
Vale MCP server, which hands an assistant
the linting engine rather than documentation about it — tag_text proved the
two rules that read as broken were a tagger disagreement rather than a defect,
show_blocks explained a match crossing a paragraph break, and audit_style
found a negated class under ignorecase costing 3.6× the \s it replaced.
Install the tools from vale-cli/agent-tools.
I have went to the store for first time.
One was better then the other, and its wheels were fine.
We are better of with a GUI interface. 1:8 warning Use the past participle after a form of `have`. Harper.SimplePastToPastParticiple
1:26 warning This construction requires 'the' before the ordinal. Harper.ForTheNthTime
2:16 warning Use `than` for comparisons. Harper.ThenThan
3:8 error This phrase uses the word `off`. Harper.BetterOffWith
3:25 warning The acronym's last letter already stands for the Harper.RedundantAcronyms
last word.
Every rule that can offer a fix carries one, as the Action on each alert.
Editors and the language server turn those into corrections you accept in
place; on the command line they arrive in vale --output=JSON.
Measured against Harper v2.7.0 over 652 KB of Harper's own test sentences:
| 🎯 Recall | 95.9% | of Harper's findings, this style also makes |
| ✅ Precision | 99.8% | of this style's findings, Harper agrees with |
| ⚖️ F1 | 97.8% | the two together |
| 🎛️ Harper linters implemented | 455 of 823 — 55% |
| 📦 Vale rules shipped | 547 |
| ⚙️ Generated from Harper's source | 416 |
| ✍️ Written by hand | 131 |
One linter can need several rules: a sequence matches a fixed number of
tokens, so a linter accepting phrases of several lengths becomes a numbered
family. Spell checking is not ported.
make measure prints the score, so you can check it.
DIFFERENCES.md is where the gap is accounted for, and
BENCHMARKS.md is what it costs to run.
Needs Vale v3.16.0 or newer: this style uses matchcase, its part-of-speech
rules name a tagger with model, and v3.16.0 fixes the Hunspell affix
continuation classes the dictionary depends on.
It ships as a Vale package. Name it in .vale.ini or _vale.ini,
then run vale sync:
StylesPath = styles
MinAlertLevel = suggestion
Packages = https://github.com/vale-cli/Harper/releases/latest/download/Harper.zip
[*.{md,txt}]
BasedOnStyles = HarperThe URL is spelled out because Vale resolves a bare package name against its
own library, which this style is not in. …/releases/download/TAG/
pins a release, and a path like dist/Harper.zip reads a local archive.
Install it as a package rather than by hand: the part-of-speech rules need the
tagger dictionary the package carries in styles/config/dictionaries/, and
copying the rule files alone leaves them quietly matching nothing.
| source | how | what it produces |
|---|---|---|
.weir rule definitions |
cmd/weirgen |
phrase rules, and sequence where a rule needs a part of speech |
| Rust linters | cmd/rustgen |
the SequenceExpr chain compiled directly; the fix read from the linter's own tests |
dictionary.dict and its affixes |
cmd/dictgen |
the tagger dictionary, expanded by Harper's own tooling |
| the dictionary again | cmd/adjgen |
MoreAdjective, which Harper decides at run time from whatever it can inflect |
| a linter's source, by hand | Harper/*.yml |
the rules no generator can express |
Everything except Harper/ is generated and gitignored; make generate
rebuilds it. The generators skip any name already in Harper/: a rule is
hand-written precisely when the generated one was wrong, so it has to survive
regeneration.
Severity comes from Harper's rule kind, which Vale requires and Harper does not express: outright mistakes are errors, likely mistakes warnings, matters of taste suggestions.
Everything is built against one pinned Harper release, v2.7.0. A rule read
from one version and measured against another is measured against the wrong
thing, so make refuses to run at a different tag.
git clone --branch v2.7.0 https://github.com/Automattic/harper ../harper-upstream
cargo build --release -p harper-cli --manifest-path ../harper-upstream/Cargo.toml
make package # generate the rules, then build dist/Harper.zipNot ../harper: this repository is Harper/, and on a case-insensitive
filesystem (macOS by default) ../harper resolves back to it. HARPER=
overrides the location.
harper-cli is needed because dictionary.dict holds base forms and affix
flags, and Harper applies the affixes itself. Read raw, it gives base forms
tagged as if they were the only forms — running as a bare verb — which is
worse than no dictionary at all, since it overwrites what the model knew.
| Path | What's in it |
|---|---|
✍️ Harper/ |
Rules written by hand — the only rules under version control |
⚙️ cmd/ |
The generators, the packager, and the checkers |
📖 internal/weir |
A parser for Harper's .weir DSL |
🔨 generated/ |
Compiled rules and the tagger dictionary (ignored) |
📦 dist/ |
The packaged style (ignored) |
📏 corpus.txt |
The measurement text (ignored) |
📊 DIFFERENCES.md |
Where this style and Harper disagree, and why |
⏱️ BENCHMARKS.md |
What it costs to run, and how to measure it |
Apache License 2.0 — see LICENSE.
The rules are derived from Harper, Copyright (c) Automattic, Inc. and contributors, also under Apache 2.0. The matched phrases, corrections and messages are Harper's work; NOTICE records that and what this port changed.