prose-preflight 0.1.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 (56) hide show
  1. prose_preflight-0.1.0/.claude-plugin/marketplace.json +16 -0
  2. prose_preflight-0.1.0/.claude-plugin/plugin.json +12 -0
  3. prose_preflight-0.1.0/.github/workflows/ci.yml +22 -0
  4. prose_preflight-0.1.0/.github/workflows/release.yml +26 -0
  5. prose_preflight-0.1.0/.gitignore +4 -0
  6. prose_preflight-0.1.0/CLAUDE.md +142 -0
  7. prose_preflight-0.1.0/LICENSE +21 -0
  8. prose_preflight-0.1.0/PKG-INFO +215 -0
  9. prose_preflight-0.1.0/README.md +194 -0
  10. prose_preflight-0.1.0/pyproject.toml +35 -0
  11. prose_preflight-0.1.0/skills/prose-deepread/SKILL.md +66 -0
  12. prose_preflight-0.1.0/skills/prose-preflight/SKILL.md +104 -0
  13. prose_preflight-0.1.0/src/prose_preflight/__init__.py +0 -0
  14. prose_preflight-0.1.0/src/prose_preflight/checks/__init__.py +25 -0
  15. prose_preflight-0.1.0/src/prose_preflight/checks/_text.py +60 -0
  16. prose_preflight-0.1.0/src/prose_preflight/checks/acronym.py +46 -0
  17. prose_preflight-0.1.0/src/prose_preflight/checks/claim.py +34 -0
  18. prose_preflight-0.1.0/src/prose_preflight/checks/readability.py +60 -0
  19. prose_preflight-0.1.0/src/prose_preflight/checks/sentence_length.py +30 -0
  20. prose_preflight-0.1.0/src/prose_preflight/checks/structure.py +46 -0
  21. prose_preflight-0.1.0/src/prose_preflight/checks/terminology.py +38 -0
  22. prose_preflight-0.1.0/src/prose_preflight/checks/units.py +66 -0
  23. prose_preflight-0.1.0/src/prose_preflight/checks/vale.py +92 -0
  24. prose_preflight-0.1.0/src/prose_preflight/default.yaml +161 -0
  25. prose_preflight-0.1.0/src/prose_preflight/extract/__init__.py +17 -0
  26. prose_preflight-0.1.0/src/prose_preflight/extract/document.py +59 -0
  27. prose_preflight-0.1.0/src/prose_preflight/extract/latex.py +89 -0
  28. prose_preflight-0.1.0/src/prose_preflight/extract/markdown.py +51 -0
  29. prose_preflight-0.1.0/src/prose_preflight/finding.py +16 -0
  30. prose_preflight-0.1.0/src/prose_preflight/run_all.py +190 -0
  31. prose_preflight-0.1.0/tests/fixtures/README.md +2 -0
  32. prose_preflight-0.1.0/tests/fixtures/acronym.expected.json +6 -0
  33. prose_preflight-0.1.0/tests/fixtures/acronym.md +8 -0
  34. prose_preflight-0.1.0/tests/fixtures/claim.expected.json +22 -0
  35. prose_preflight-0.1.0/tests/fixtures/claim.md +5 -0
  36. prose_preflight-0.1.0/tests/fixtures/config.yaml +12 -0
  37. prose_preflight-0.1.0/tests/fixtures/readability.expected.json +10 -0
  38. prose_preflight-0.1.0/tests/fixtures/readability.md +14 -0
  39. prose_preflight-0.1.0/tests/fixtures/sentence_length.expected.json +6 -0
  40. prose_preflight-0.1.0/tests/fixtures/sentence_length.md +13 -0
  41. prose_preflight-0.1.0/tests/fixtures/structure.expected.json +14 -0
  42. prose_preflight-0.1.0/tests/fixtures/structure.md +15 -0
  43. prose_preflight-0.1.0/tests/fixtures/terminology.expected.json +8 -0
  44. prose_preflight-0.1.0/tests/fixtures/terminology.md +16 -0
  45. prose_preflight-0.1.0/tests/fixtures/units.expected.json +22 -0
  46. prose_preflight-0.1.0/tests/fixtures/units.md +5 -0
  47. prose_preflight-0.1.0/tests/fixtures/vale.expected.json +4 -0
  48. prose_preflight-0.1.0/tests/fixtures/vale.md +3 -0
  49. prose_preflight-0.1.0/tests/test_checks.py +92 -0
  50. prose_preflight-0.1.0/tests/test_extract.py +107 -0
  51. prose_preflight-0.1.0/tests/test_finding.py +27 -0
  52. prose_preflight-0.1.0/tests/test_packaging.py +16 -0
  53. prose_preflight-0.1.0/tests/test_run_all.py +119 -0
  54. prose_preflight-0.1.0/tests/test_terminology.py +32 -0
  55. prose_preflight-0.1.0/tests/test_vale.py +59 -0
  56. prose_preflight-0.1.0/uv.lock +401 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "prose-preflight",
3
+ "owner": {
4
+ "name": "Dominik Reisach"
5
+ },
6
+ "description": "Deterministic prose preflight checks for scientific writing, as an installable Claude Code plugin.",
7
+ "plugins": [
8
+ {
9
+ "name": "prose-preflight",
10
+ "source": "./",
11
+ "description": "Run deterministic prose checkers over a manuscript and fix what the JSON report reports.",
12
+ "category": "writing",
13
+ "keywords": ["prose", "writing", "proofreading", "scientific-writing"]
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "prose-preflight",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic prose checkers for scientific writing, plus a thin skill that drives them: one command, one compact JSON report.",
5
+ "author": {
6
+ "name": "Dominik Reisach"
7
+ },
8
+ "repository": "https://github.com/domrice/prose-preflight",
9
+ "license": "MIT",
10
+ "keywords": ["prose", "writing", "proofreading", "vale", "readability", "scientific-writing"],
11
+ "skills": ["./skills/prose-preflight", "./skills/prose-deepread"]
12
+ }
@@ -0,0 +1,22 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ test:
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - uses: actions/checkout@v4
9
+ - uses: astral-sh/setup-uv@v5
10
+ - run: uv sync --locked
11
+ - run: uv run ruff check . && uv run ruff format --check .
12
+ - run: uv run pytest
13
+ - run: uv build
14
+ # Runs the package the way uvx will, with no repo on disk. A real
15
+ # invocation, not `--version`: that never loads default.yaml, so a config
16
+ # missing from the wheel would still pass. It also proves the bundled Vale
17
+ # binary resolves in a fresh environment.
18
+ - name: Smoke-test the wheel
19
+ run: |
20
+ uv run --isolated --with dist/*.whl \
21
+ prose-preflight tests/fixtures/units.md > report.json
22
+ jq -e '.total > 0 and (.counts.category | has("units"))' report.json
@@ -0,0 +1,26 @@
1
+ # Publishes to PyPI when a v* tag is pushed. Uses trusted publishing, so there is no
2
+ # API token to store: configure the publisher once at
3
+ # https://pypi.org/manage/project/prose-preflight/settings/publishing/
4
+ # (owner: domrice, repo: prose-preflight, workflow: release.yml, environment: pypi).
5
+ name: release
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ permissions:
15
+ contents: read
16
+ id-token: write # trusted publishing
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v5
20
+ - run: uv sync --locked
21
+ - run: uv run pytest
22
+ - name: Check the tag matches the packaged version
23
+ run: |
24
+ test "v$(uv run python -c 'from importlib.metadata import version; print(version("prose-preflight"))')" = "${GITHUB_REF_NAME}"
25
+ - run: uv build
26
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ dist/
4
+ *.egg-info/
@@ -0,0 +1,142 @@
1
+ # CLAUDE.md
2
+
3
+ Guidance for Claude Code (claude.ai/code) when working in this repository.
4
+
5
+ ## What this repo is
6
+
7
+ `prose-preflight` is an **agent skill**: deterministic CLI checkers plus a thin `SKILL.md`
8
+ that tells an agent how to drive them. The scripts are the analyzer, not the agent.
9
+
10
+ The agent runs one command, reads one compact JSON report, and reports it. It does **not**
11
+ edit the document — not errors, not mechanical suggestions. Fixes happen only when the user
12
+ asks for a specific one in a later turn. `skills/prose-preflight/SKILL.md` is the binding
13
+ agent contract: report, never edit, never auto-apply `review`.
14
+
15
+ No model is ever called from inside the Python — that would cost determinism, offline runs,
16
+ and per-run money, which is the same trade that ruled out LanguageTool. Judgment lives in the
17
+ agent turn: preflight gives `review` findings a verdict, and `prose-deepread` is the one
18
+ opt-in exception to "never read the source", run only when the user asks for it by name.
19
+
20
+ ## The token-efficiency contract
21
+
22
+ This is the reason the project exists. Violating it defeats the skill.
23
+
24
+ - **One command, one report.** `run_all.py` once, one JSON document. Never invoke individual
25
+ checkers, never read Vale output, never pipe raw tool stdout into context.
26
+ - **Never read the source document to find problems.** Findings carry `line`/`col` and a
27
+ short `excerpt`, so edits can be targeted without a full read.
28
+ - **Reports are summarized by default.** Top level is counts by severity/category/section;
29
+ findings are capped (`--max-findings`, default 20), filled round-robin across categories so
30
+ one noisy category cannot crowd out the rest; `--max-findings 0` lifts the cap. Never let a
31
+ 900-issue Vale run flood the transcript.
32
+ - **`--md PATH` writes the full report to disk.** The agent summarizes stdout and links the
33
+ file; it never transcribes findings through context.
34
+ - **`SKILL.md` stays short** (< 120 lines): the workflow and the JSON shape, nothing else.
35
+ Rule catalogs and rationale live in `README.md` and `default.yaml`'s comments.
36
+ - **No checker prints prose.** Diagnostics to stderr; stdout is JSON only.
37
+
38
+ ## Layout
39
+
40
+ ```
41
+ src/prose_preflight/
42
+ finding.py the one record every checker emits
43
+ run_all.py CLI: extract, fan out, aggregate, cap, serialize
44
+ default.yaml the only config; package data, resolved via importlib.resources
45
+ extract/ document.py (Document + shared passes), markdown.py, latex.py
46
+ checks/ acronym claim readability sentence_length structure terminology units vale
47
+ + _text.py (shared helpers) + __init__.py (CHECKERS registry)
48
+ skills/prose-preflight/SKILL.md the agent-facing workflow
49
+ skills/prose-deepread/SKILL.md the opt-in full-read pass (the only thing allowed to read the source)
50
+ .claude-plugin/ plugin.json + marketplace.json
51
+ ```
52
+
53
+ Four layers, each replaceable without touching the others. The constraints that make that
54
+ true:
55
+
56
+ 1. **Extraction** — `extract/__init__.py` dispatches on file suffix (unknown suffix reads as
57
+ plain text). `Document.masked` is a character-for-character copy of the source with fenced
58
+ code, display math, inline code and inline math replaced by spaces, so a regex over
59
+ `masked` yields match offsets that *are* the source `line`/`col`. Every indexed element
60
+ keeps its 1-based source line. A reader that loses line numbers is not acceptable — every
61
+ downstream finding depends on it.
62
+
63
+ 2. **Checkers** — each module exposes exactly `check(doc: Document, rule: dict) ->
64
+ list[Finding]`. Pure, stateless, knowing nothing about each other. They receive their
65
+ slice of the resolved config; the runner does the lookup, the `enabled` gate, and the
66
+ sorting, so checkers may return findings in any order. Adding a checker is a module plus
67
+ one line in `CHECKERS` — never a change to the runner's logic.
68
+
69
+ 3. **Config** — one bundled `default.yaml`, shipped as package data (a repo-root `profiles/`
70
+ would be invisible to `uvx`, which runs from a cached wheel). `--config FILE` deep-merges
71
+ over it. **If a new document convention requires a code change, the design is wrong** —
72
+ generalize the checker and push the specifics into YAML.
73
+
74
+ 4. **Runner** — extracts, fans out, aggregates, caps, serializes. No domain knowledge.
75
+
76
+ ### The `Finding` record
77
+
78
+ One dataclass, shared by every checker. Changing it is a breaking change across the repo.
79
+
80
+ ```
81
+ check str stable rule id, e.g. "terminology.variant"
82
+ category str grammar|style|readability|terminology|acronym|structure|units|claim
83
+ severity str error | warning | review
84
+ line, col int 1-based
85
+ excerpt str short, trimmed
86
+ message str one line, imperative
87
+ section str|None nearest enclosing heading
88
+ suggestion str|None the replacement, when mechanical and safe
89
+ ```
90
+
91
+ `severity: review` is a load-bearing distinction, not a softer warning. Claim-calibration,
92
+ hedging, and overclaiming checks emit **only** `review`. Nothing marked `review` is ever
93
+ auto-applied — it is surfaced to a human or reasoned about by the agent in context. This is
94
+ the boundary between deterministic checking and qualitative judgment, and it must never blur.
95
+
96
+ ## Scope discipline
97
+
98
+ Each addition costs install weight, runtime, and report noise.
99
+
100
+ **In:** Markdown/LaTeX/plain text · Vale · readability (`textstat`) · sentence-length outliers
101
+ · terminology · acronyms · section structure · numbers and units · claim calibration · JSON
102
+ and Markdown reports. The extractor also indexes citations and labels; no checker consumes
103
+ them yet.
104
+
105
+ **Deferred:** DOCX/PDF extraction (Pandoc/PyMuPDF weight); spaCy/Stanza (hundreds of MB for
106
+ segmentation `textstat` plus a regex already handles); Pint (a unit *calculus*, where these
107
+ checks are formatting — NBSP, `%` spacing, ranges, p-values — so regex is right); DOI network
108
+ validation (breaks determinism and offline runs); statistical-reporting checks;
109
+ figure/table cross-references; `--profile NAME` layering (one audience today; adding it later
110
+ is a lookup returning a bundled path, purely additive).
111
+
112
+ **Vale over LanguageTool:** a single static binary with YAML config, fully offline and
113
+ deterministic, versus a Java server or API that makes runs non-reproducible. Vale is pinned in
114
+ `pyproject.toml` and ships with the package, but the checker must still degrade around it: if
115
+ the binary is unavailable, emit one `vale.unavailable` warning and continue. Never abort.
116
+
117
+ ## Commands
118
+
119
+ Python is managed with `uv`; `uv.lock` is committed and `requires-python = ">=3.11"`.
120
+
121
+ ```bash
122
+ uv sync # install, from the lockfile
123
+ uvx prose-preflight FILE # the distributed invocation
124
+ uv run prose-preflight FILE --md PREFLIGHT.md # from source, full report to disk
125
+ uv run prose-preflight FILE --checks terminology,acronym --max-findings 0
126
+ uv run pytest # all tests
127
+ uv run pytest tests/test_checks.py -k units # one test
128
+ uv run ruff check . && uv run ruff format .
129
+ ```
130
+
131
+ Pushing a `v*` tag publishes to PyPI via trusted publishing (`.github/workflows/release.yml`,
132
+ which also asserts the tag matches the packaged version). CI smoke-tests the built wheel with
133
+ `uv run --isolated`, so a `default.yaml` missing from the wheel or an unresolvable Vale binary
134
+ fails there rather than in a user's environment.
135
+
136
+ ## Tests
137
+
138
+ Fixtures in `tests/fixtures/` are intentionally flawed documents paired with expected-findings
139
+ JSON. `tests/test_checks.py` parametrizes one pair per checker and asserts the exact set of
140
+ `(check, line)` pairs — this is what keeps checkers from drifting into false positives, which
141
+ are far more costly here than misses: every false positive burns agent tokens and erodes trust
142
+ in the report. `tests/test_packaging.py` guards the package data and entry point.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dom
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,215 @@
1
+ Metadata-Version: 2.5
2
+ Name: prose-preflight
3
+ Version: 0.1.0
4
+ Summary: Deterministic prose checkers for scientific writing, driven by an agent skill.
5
+ Project-URL: Homepage, https://github.com/domrice/prose-preflight
6
+ Project-URL: Issues, https://github.com/domrice/prose-preflight/issues
7
+ Author-email: Dominik Reisach <praises_sauces.0i@icloud.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agent-skill,linter,proofreading,prose,scientific-writing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Text Processing :: Linguistic
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: pyyaml
18
+ Requires-Dist: textstat
19
+ Requires-Dist: vale==3.21.0.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ <div align="center">
23
+
24
+ # Prose Preflight
25
+
26
+ **Your manuscript, checked by scripts, read by an agent.**
27
+
28
+ [![PyPI](https://img.shields.io/pypi/v/prose-preflight?style=flat-square&logo=pypi&logoColor=white&color=fa934e&labelColor=121417)](https://pypi.org/project/prose-preflight/)
29
+ [![Python](https://img.shields.io/badge/python-%E2%89%A5%203.11-fa934e?style=flat-square&logo=python&logoColor=white&labelColor=121417)](https://www.python.org)
30
+ [![Vale](https://img.shields.io/badge/Vale-bundled-fa934e?style=flat-square&labelColor=121417)](https://vale.sh)
31
+ [![uv](https://img.shields.io/badge/uv-managed-fa934e?style=flat-square&logo=uv&logoColor=white&labelColor=121417)](https://docs.astral.sh/uv/)
32
+ [![License](https://img.shields.io/badge/license-MIT-fa934e?style=flat-square&labelColor=121417)](LICENSE)
33
+ [![last commit](https://img.shields.io/github/last-commit/domrice/prose-preflight?style=flat-square&color=fa934e&labelColor=121417)](https://github.com/domrice/prose-preflight/commits/main)
34
+
35
+ </div>
36
+
37
+ ---
38
+
39
+ > No server. No API key. No network. No opinions about your argument.
40
+ > Eight deterministic checkers read a Markdown, LaTeX, or plain-text manuscript and emit one
41
+ > compact JSON report. The scripts analyze; the agent reports. Same input, same findings,
42
+ > every time.
43
+
44
+ An LLM asked to proofread a text will read all 9,000 words into its context, forget the
45
+ middle, and confidently invent a citation error on page 4. This tool doesn't. The checkers
46
+ find the problems and hand over line, column, and a short excerpt — the agent summarizes
47
+ that and never reads the manuscript to find anything.
48
+
49
+ ## Install
50
+
51
+ **1. Claude Code plugin**
52
+
53
+ ```
54
+ /plugin marketplace add domrice/prose-preflight
55
+ /plugin install prose-preflight
56
+ ```
57
+
58
+ **2. Any other harness** (Cursor, Codex, Copilot, Gemini, Windsurf, …)
59
+
60
+ ```bash
61
+ npx skills@latest add domrice/prose-preflight
62
+ ```
63
+
64
+ **3. CLI only, no agent** — for CI or pre-commit
65
+
66
+ ```bash
67
+ uvx prose-preflight text.md
68
+ ```
69
+
70
+ **4. From source** (contributors)
71
+
72
+ ```bash
73
+ git clone https://github.com/domrice/prose-preflight && cd prose-preflight
74
+ uv sync && uv run pytest
75
+ ```
76
+
77
+ Routes 1–3 resolve the package from PyPI, so none of them needs the repo on disk and
78
+ `SKILL.md` carries no paths. Needs [Python ≥ 3.11](https://www.python.org); `uvx` supplies
79
+ its own interpreter, so route 3 needs nothing but [uv](https://docs.astral.sh/uv/).
80
+
81
+ ## Use
82
+
83
+ ```bash
84
+ uvx prose-preflight text.md
85
+ ```
86
+
87
+ The full report is written to `PREFLIGHT_text.md` — one per document, so checking a second
88
+ file never overwrites the first. A capped JSON summary goes to stdout, with `report` naming
89
+ that file. That split is
90
+ the whole point — the agent reads stdout, links the file, and stays small.
91
+
92
+ | flag | does |
93
+ | ------------------ | ------------------------------------------------ |
94
+ | `--md PATH` | move the report (default `PREFLIGHT_<file>.md`) |
95
+ | `--max-findings N` | cap stdout findings (default `20`; `0` lifts it) |
96
+ | `--checks a,b` | run a subset |
97
+ | `--config FILE` | deep-merge a YAML file over the bundled config |
98
+ | `--version` | the packaged version |
99
+
100
+ Findings are capped round-robin across categories, so 400 Vale alerts cannot crowd out the
101
+ one missing Methods section. `counts` and `total` always cover everything; `truncated` says
102
+ what was dropped.
103
+
104
+ ## The two skills
105
+
106
+ The CLI is deterministic on purpose — offline, no API key, same input same output. So
107
+ nothing calls a model inside it. The judgment lives in the agent that drives it, where
108
+ it is free, and it ships as two skills:
109
+
110
+ - **`prose-preflight`** runs the command and reports it, but does not just print it. It
111
+ diagnoses what the counts mean, and gives every `severity: review` finding a verdict —
112
+ `keep`, `soften`, or `cut`, with a rewrite. The checker can see that a hedge is there;
113
+ only the agent can weigh it against the argument. It never edits the document.
114
+ - **`prose-deepread`** is the opposite of preflight: it reads the manuscript end to end
115
+ and looks for what no regex can reach — claims the results do not support, terms used
116
+ before they are defined, sections that do not deliver what their heading promises, and
117
+ passages that can be cut without losing an argument. Expensive, so it runs only when
118
+ you ask for it by name. It never edits either.
119
+
120
+ ## Config
121
+
122
+ Every check is configurable from one YAML file. Write it once per project:
123
+
124
+ ```bash
125
+ uvx prose-preflight --init-config # writes prose-preflight.yaml, every key commented
126
+ uvx prose-preflight text.md # picked up automatically from the working directory
127
+ ```
128
+
129
+ A `prose-preflight.yaml` next to where you run is used without any flag; `--config FILE`
130
+ points elsewhere. Keep only what you change — the rest falls back to the defaults. Lists are
131
+ replaced, not appended, and unknown keys are reported on stderr, so a typo never fails
132
+ silently.
133
+
134
+ Two lists ship **empty**, because there is no universal right answer and a wrong entry costs
135
+ you a false positive on every run: `terminology.terms` (your journal's preferred spellings)
136
+ and `structure.required_sections` (most documents are fragments, not whole papers). Fill
137
+ them in and those checks switch on.
138
+
139
+ ```yaml
140
+ checks:
141
+ terminology:
142
+ terms:
143
+ finite element: [finite-element, FE]
144
+ sentence_length:
145
+ max_words: 32
146
+ structure:
147
+ required_sections:
148
+ [Abstract, Introduction, Methods, Results, Discussion, References]
149
+ units:
150
+ disabled_rules: [range_dash] # if your journal insists on hyphens
151
+ ```
152
+
153
+ Everything a document convention needs lives in YAML. If a new convention would need a code
154
+ change, that's a bug in the checker, not a missing feature.
155
+
156
+ ## Checks
157
+
158
+ | check | flags | severity |
159
+ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------- |
160
+ | `vale.*` | grammar, spelling, style — whatever your Vale config says | error/warning |
161
+ | `terminology.variant` | `dataset` where the config wants `data set` (opt-in: `terms`) | warning |
162
+ | `acronym.undefined` | used without expansion at first use | warning |
163
+ | `structure.missing_section` · `.section_order` | required sections, absent or out of order (opt-in) | error/warning |
164
+ | `sentence_length.long` | sentences over `max_words` | warning |
165
+ | `readability.flesch` · `.fog` | per section, skipping ones too short to score | warning |
166
+ | `units.space_before_unit` · `.percent_spacing` · `.range_dash` · `.p_value_spacing` | `12 mm` → NBSP, `5 %` → `5%`, `3-7` → `3–7`, `p<.05` → `p < 0.05` | warning |
167
+ | `claim.hedge` · `.booster` | "may possibly suggest", "clearly demonstrates" | **review** |
168
+
169
+ `review` is not a quieter warning. It is the line between what a script can decide and what
170
+ it cannot: hedging and overclaiming are judgment calls about whether your evidence carries
171
+ your claim. Nothing marked `review` is ever auto-applied, by either the tool or the agent.
172
+
173
+ Code, math, verbatim blocks, and LaTeX command sequences are blanked before any regex runs —
174
+ character-for-character, so every offset stays a true source position. Your equations are
175
+ not prose and are not judged as such.
176
+
177
+ ## Vale
178
+
179
+ [Vale](https://vale.sh) powers grammar and style and is pinned as a dependency — no
180
+ `brew install`, no manual step. The binary is fetched once on first run and cached; after
181
+ that, runs are offline.
182
+
183
+ Vale has no LaTeX reader, so a `.tex` file is handed to it as masked prose — commands,
184
+ preamble, math, and verbatim already blanked, line for line — and the alerts still carry true
185
+ source positions. Markdown and plain text go to Vale directly, parsed by its own reader.
186
+
187
+ If that download is blocked (air-gapped machine, proxy, locked-down CI), the run emits one
188
+ `vale.unavailable` warning and the other seven checkers still report. Vale is never a hard
189
+ prerequisite. Install it yourself and it's picked up from `PATH`:
190
+
191
+ ```bash
192
+ brew install vale # or: apt install vale / scoop install vale
193
+ ```
194
+
195
+ ## Develop
196
+
197
+ ```bash
198
+ uv sync # from the lockfile
199
+ uv run prose-preflight text.md --md out.md
200
+ uv run pytest # all tests
201
+ uv run pytest tests/test_checks.py -k units
202
+ uv run ruff check . && uv run ruff format .
203
+ ```
204
+
205
+ Every checker has a fixture pair in `tests/fixtures/`: an intentionally flawed document and
206
+ the exact set of `(check, line)` pairs it must produce. Pinning positions is deliberate —
207
+ false positives cost more here than misses, since each one burns agent tokens and spends
208
+ trust in the report.
209
+
210
+ Adding a checker is one module exposing `check(doc, rule) -> list[Finding]` plus one line in
211
+ the registry. If it also needs a change to the runner, the design was wrong.
212
+
213
+ ## License
214
+
215
+ MIT
@@ -0,0 +1,194 @@
1
+ <div align="center">
2
+
3
+ # Prose Preflight
4
+
5
+ **Your manuscript, checked by scripts, read by an agent.**
6
+
7
+ [![PyPI](https://img.shields.io/pypi/v/prose-preflight?style=flat-square&logo=pypi&logoColor=white&color=fa934e&labelColor=121417)](https://pypi.org/project/prose-preflight/)
8
+ [![Python](https://img.shields.io/badge/python-%E2%89%A5%203.11-fa934e?style=flat-square&logo=python&logoColor=white&labelColor=121417)](https://www.python.org)
9
+ [![Vale](https://img.shields.io/badge/Vale-bundled-fa934e?style=flat-square&labelColor=121417)](https://vale.sh)
10
+ [![uv](https://img.shields.io/badge/uv-managed-fa934e?style=flat-square&logo=uv&logoColor=white&labelColor=121417)](https://docs.astral.sh/uv/)
11
+ [![License](https://img.shields.io/badge/license-MIT-fa934e?style=flat-square&labelColor=121417)](LICENSE)
12
+ [![last commit](https://img.shields.io/github/last-commit/domrice/prose-preflight?style=flat-square&color=fa934e&labelColor=121417)](https://github.com/domrice/prose-preflight/commits/main)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ > No server. No API key. No network. No opinions about your argument.
19
+ > Eight deterministic checkers read a Markdown, LaTeX, or plain-text manuscript and emit one
20
+ > compact JSON report. The scripts analyze; the agent reports. Same input, same findings,
21
+ > every time.
22
+
23
+ An LLM asked to proofread a text will read all 9,000 words into its context, forget the
24
+ middle, and confidently invent a citation error on page 4. This tool doesn't. The checkers
25
+ find the problems and hand over line, column, and a short excerpt — the agent summarizes
26
+ that and never reads the manuscript to find anything.
27
+
28
+ ## Install
29
+
30
+ **1. Claude Code plugin**
31
+
32
+ ```
33
+ /plugin marketplace add domrice/prose-preflight
34
+ /plugin install prose-preflight
35
+ ```
36
+
37
+ **2. Any other harness** (Cursor, Codex, Copilot, Gemini, Windsurf, …)
38
+
39
+ ```bash
40
+ npx skills@latest add domrice/prose-preflight
41
+ ```
42
+
43
+ **3. CLI only, no agent** — for CI or pre-commit
44
+
45
+ ```bash
46
+ uvx prose-preflight text.md
47
+ ```
48
+
49
+ **4. From source** (contributors)
50
+
51
+ ```bash
52
+ git clone https://github.com/domrice/prose-preflight && cd prose-preflight
53
+ uv sync && uv run pytest
54
+ ```
55
+
56
+ Routes 1–3 resolve the package from PyPI, so none of them needs the repo on disk and
57
+ `SKILL.md` carries no paths. Needs [Python ≥ 3.11](https://www.python.org); `uvx` supplies
58
+ its own interpreter, so route 3 needs nothing but [uv](https://docs.astral.sh/uv/).
59
+
60
+ ## Use
61
+
62
+ ```bash
63
+ uvx prose-preflight text.md
64
+ ```
65
+
66
+ The full report is written to `PREFLIGHT_text.md` — one per document, so checking a second
67
+ file never overwrites the first. A capped JSON summary goes to stdout, with `report` naming
68
+ that file. That split is
69
+ the whole point — the agent reads stdout, links the file, and stays small.
70
+
71
+ | flag | does |
72
+ | ------------------ | ------------------------------------------------ |
73
+ | `--md PATH` | move the report (default `PREFLIGHT_<file>.md`) |
74
+ | `--max-findings N` | cap stdout findings (default `20`; `0` lifts it) |
75
+ | `--checks a,b` | run a subset |
76
+ | `--config FILE` | deep-merge a YAML file over the bundled config |
77
+ | `--version` | the packaged version |
78
+
79
+ Findings are capped round-robin across categories, so 400 Vale alerts cannot crowd out the
80
+ one missing Methods section. `counts` and `total` always cover everything; `truncated` says
81
+ what was dropped.
82
+
83
+ ## The two skills
84
+
85
+ The CLI is deterministic on purpose — offline, no API key, same input same output. So
86
+ nothing calls a model inside it. The judgment lives in the agent that drives it, where
87
+ it is free, and it ships as two skills:
88
+
89
+ - **`prose-preflight`** runs the command and reports it, but does not just print it. It
90
+ diagnoses what the counts mean, and gives every `severity: review` finding a verdict —
91
+ `keep`, `soften`, or `cut`, with a rewrite. The checker can see that a hedge is there;
92
+ only the agent can weigh it against the argument. It never edits the document.
93
+ - **`prose-deepread`** is the opposite of preflight: it reads the manuscript end to end
94
+ and looks for what no regex can reach — claims the results do not support, terms used
95
+ before they are defined, sections that do not deliver what their heading promises, and
96
+ passages that can be cut without losing an argument. Expensive, so it runs only when
97
+ you ask for it by name. It never edits either.
98
+
99
+ ## Config
100
+
101
+ Every check is configurable from one YAML file. Write it once per project:
102
+
103
+ ```bash
104
+ uvx prose-preflight --init-config # writes prose-preflight.yaml, every key commented
105
+ uvx prose-preflight text.md # picked up automatically from the working directory
106
+ ```
107
+
108
+ A `prose-preflight.yaml` next to where you run is used without any flag; `--config FILE`
109
+ points elsewhere. Keep only what you change — the rest falls back to the defaults. Lists are
110
+ replaced, not appended, and unknown keys are reported on stderr, so a typo never fails
111
+ silently.
112
+
113
+ Two lists ship **empty**, because there is no universal right answer and a wrong entry costs
114
+ you a false positive on every run: `terminology.terms` (your journal's preferred spellings)
115
+ and `structure.required_sections` (most documents are fragments, not whole papers). Fill
116
+ them in and those checks switch on.
117
+
118
+ ```yaml
119
+ checks:
120
+ terminology:
121
+ terms:
122
+ finite element: [finite-element, FE]
123
+ sentence_length:
124
+ max_words: 32
125
+ structure:
126
+ required_sections:
127
+ [Abstract, Introduction, Methods, Results, Discussion, References]
128
+ units:
129
+ disabled_rules: [range_dash] # if your journal insists on hyphens
130
+ ```
131
+
132
+ Everything a document convention needs lives in YAML. If a new convention would need a code
133
+ change, that's a bug in the checker, not a missing feature.
134
+
135
+ ## Checks
136
+
137
+ | check | flags | severity |
138
+ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------- |
139
+ | `vale.*` | grammar, spelling, style — whatever your Vale config says | error/warning |
140
+ | `terminology.variant` | `dataset` where the config wants `data set` (opt-in: `terms`) | warning |
141
+ | `acronym.undefined` | used without expansion at first use | warning |
142
+ | `structure.missing_section` · `.section_order` | required sections, absent or out of order (opt-in) | error/warning |
143
+ | `sentence_length.long` | sentences over `max_words` | warning |
144
+ | `readability.flesch` · `.fog` | per section, skipping ones too short to score | warning |
145
+ | `units.space_before_unit` · `.percent_spacing` · `.range_dash` · `.p_value_spacing` | `12 mm` → NBSP, `5 %` → `5%`, `3-7` → `3–7`, `p<.05` → `p < 0.05` | warning |
146
+ | `claim.hedge` · `.booster` | "may possibly suggest", "clearly demonstrates" | **review** |
147
+
148
+ `review` is not a quieter warning. It is the line between what a script can decide and what
149
+ it cannot: hedging and overclaiming are judgment calls about whether your evidence carries
150
+ your claim. Nothing marked `review` is ever auto-applied, by either the tool or the agent.
151
+
152
+ Code, math, verbatim blocks, and LaTeX command sequences are blanked before any regex runs —
153
+ character-for-character, so every offset stays a true source position. Your equations are
154
+ not prose and are not judged as such.
155
+
156
+ ## Vale
157
+
158
+ [Vale](https://vale.sh) powers grammar and style and is pinned as a dependency — no
159
+ `brew install`, no manual step. The binary is fetched once on first run and cached; after
160
+ that, runs are offline.
161
+
162
+ Vale has no LaTeX reader, so a `.tex` file is handed to it as masked prose — commands,
163
+ preamble, math, and verbatim already blanked, line for line — and the alerts still carry true
164
+ source positions. Markdown and plain text go to Vale directly, parsed by its own reader.
165
+
166
+ If that download is blocked (air-gapped machine, proxy, locked-down CI), the run emits one
167
+ `vale.unavailable` warning and the other seven checkers still report. Vale is never a hard
168
+ prerequisite. Install it yourself and it's picked up from `PATH`:
169
+
170
+ ```bash
171
+ brew install vale # or: apt install vale / scoop install vale
172
+ ```
173
+
174
+ ## Develop
175
+
176
+ ```bash
177
+ uv sync # from the lockfile
178
+ uv run prose-preflight text.md --md out.md
179
+ uv run pytest # all tests
180
+ uv run pytest tests/test_checks.py -k units
181
+ uv run ruff check . && uv run ruff format .
182
+ ```
183
+
184
+ Every checker has a fixture pair in `tests/fixtures/`: an intentionally flawed document and
185
+ the exact set of `(check, line)` pairs it must produce. Pinning positions is deliberate —
186
+ false positives cost more here than misses, since each one burns agent tokens and spends
187
+ trust in the report.
188
+
189
+ Adding a checker is one module exposing `check(doc, rule) -> list[Finding]` plus one line in
190
+ the registry. If it also needs a change to the runner, the design was wrong.
191
+
192
+ ## License
193
+
194
+ MIT