Skip to content

Repository files navigation

Harper for Vale

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.

👀 What it looks like

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.

📊 How close is it?

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.

🚀 Getting started

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 = Harper

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

🔧 How the rules are built

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.

🏗️ Building

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

Not ../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.

🗂️ Repository layout

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

⚖️ License

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.

About

A Vale-compatible implementation of the Harper grammar checker.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages