Metadata-Version: 2.4
Name: quire-grammar
Version: 0.2.0
Summary: An offline English grammar and style checker — a writer's aid, not a proofreading service.
Author: Roger Cooper
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: model
Requires-Dist: quire-grammar-model>=0.2; extra == "model"
Requires-Dist: onnxruntime>=1.17; extra == "model"
Requires-Dist: tokenizers>=0.15; extra == "model"
Provides-Extra: ml
Requires-Dist: quire-grammar-model>=0.2; extra == "ml"
Requires-Dist: onnxruntime>=1.17; extra == "ml"
Requires-Dist: tokenizers>=0.15; extra == "ml"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: onnxruntime>=1.17; extra == "dev"
Requires-Dist: tokenizers>=0.15; extra == "dev"
Requires-Dist: numpy>=1.26; extra == "dev"
Dynamic: license-file

# quire-grammar

A **standalone, offline English grammar and style checker** — a writer's aid,
not a proofreading service, and a library other writing tools can license
rather than build their own. It runs fully offline and is permissively
licensed end to end.

[Quire Desktop](https://github.com/jutreuter/quire-desktop) is the first
consumer — the rules layer is a hard dependency (the always-on checker) and
the model is the opt-in `[model]` extra, *off if missing* the way WordNet and
SCOWL spell-check are. The dependency runs one way and the `check()` API is
designed to stand on its own.

## Use it

```python
from quire_grammar import check

for c in check("Its been a long day and and the sky is is grey."):
    print(c.rule_id, repr(c.original), "->", c.suggestions, f"({c.confidence})")
```

```
$ quire-grammar "She had a apple. This went better then that."
1:9   [mechanics/a-vs-an]     “a” before “apple” — “an” matches the sound.  →  'an'
1:35  [confusable/then-vs-than]  “then” here reads as “than”.  →  'than'
```

`check()` returns a sorted `list[Correction]` — a stable dataclass of
`start, end, original, category, rule_id, message, suggestions, confidence`.
That signature does not change whether or not the model is installed.

## Install

```
pip install quire-grammar            # rules layer — pure Python, zero deps
pip install quire-grammar[model]     # + the phase-2 model (onnxruntime, tokenizers, ~40 MB weights)
```

Without the `[model]` extra, `check()` runs rules-only and the model
contributes nothing — the same "off if missing" behaviour Quire gives WordNet
and spell-check.

## How it works

**Phase 1 — a deterministic rules layer (this repo, now).** Pure-Python,
**zero runtime dependencies**. Each rule is a narrow, high-precision pattern
for a mistake a *native writer* actually makes. The guard rail is precision:
a checker that cries wolf gets switched off, so the test suite weighs "must
not flag" cases as heavily as "must flag".

| category | rules |
|---|---|
| **mechanics** | doubled word · `a`/`an` · space before punctuation · missing space after a sentence · capitalisation after `.` |
| **confusable** | its/it's · there/their/they're · your/you're · then/than · affect/effect · lose/loose · lead/led · breath/breathe · to/too · whose/who's · passed/past · weather/whether · "should of" → "should have" |
| **agreement** | pronoun subject–verb (`he don't`, `they was`, `I are`) · within-sentence past → present tense drift |
| **punctuation** | comma splice · broken-off subordinate-clause fragment · straight-vs-curly quotes/apostrophes/dashes *(opt-in)* |
| **style** | filter verb · `said angrily` · overlong sentence · dangling modifier · weak intensifier *(opt-in)* · passive voice *(opt-in)* |

`enable` / `disable` take rule ids or whole categories. Opt-in rules are off
until you ask for them.

**Phase 2 — a small local model behind the same `check()` API.** A
GECToR-style edit tagger (DistilBERT-base-cased, 66 MB int8 ONNX, ~40 MB in
the wheel) run on CPU via `onnxruntime` — no GPU is assumed, for training or
inference. It catches homophone and agreement slips the deliberately-narrow
rules miss, gated at a 0.9 confidence floor (tuned so it flags ~1% of
genuinely clean sentences), merged with the rule hits so a rule wins on any
span overlap. Trained **synthetic-first** — `corrupt.py` errors over
public-domain prose, because the usual GEC learner corpora are
research/non-commercial licensed. It ships as the separate
[`quire-grammar-model`](packages/quire-grammar-model/) distribution
(`quire-grammar[model]`); training code and data pipeline live in `train/`
and are **not** shipped.

See [CLAUDE.md](CLAUDE.md) for the design contract and the phase-2 plan.

## Develop

```bash
python -m venv .venv
.venv/bin/pip install -e ".[dev]" -e ./packages/quire-grammar-model
.venv/bin/python -m pytest -q
.venv/bin/ruff check src train tests packages
.venv/bin/mypy
```

The phase-2 training scripts in `train/` need the heavier ML stack —
`.venv/bin/pip install -r train/requirements.txt` (CPU-only, all permissive).
It is **not** a dependency of the shipped package.

`quire-grammar` is MIT; `quire-grammar-model` is Apache-2.0 (it carries a
`distilbert-base-cased` lineage). Everything either depends on — now and
planned — is MIT / BSD / Apache. No GPL, no AGPL, so a licensee can
redistribute the whole thing.
