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.
- prose_preflight-0.1.0/.claude-plugin/marketplace.json +16 -0
- prose_preflight-0.1.0/.claude-plugin/plugin.json +12 -0
- prose_preflight-0.1.0/.github/workflows/ci.yml +22 -0
- prose_preflight-0.1.0/.github/workflows/release.yml +26 -0
- prose_preflight-0.1.0/.gitignore +4 -0
- prose_preflight-0.1.0/CLAUDE.md +142 -0
- prose_preflight-0.1.0/LICENSE +21 -0
- prose_preflight-0.1.0/PKG-INFO +215 -0
- prose_preflight-0.1.0/README.md +194 -0
- prose_preflight-0.1.0/pyproject.toml +35 -0
- prose_preflight-0.1.0/skills/prose-deepread/SKILL.md +66 -0
- prose_preflight-0.1.0/skills/prose-preflight/SKILL.md +104 -0
- prose_preflight-0.1.0/src/prose_preflight/__init__.py +0 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/__init__.py +25 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/_text.py +60 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/acronym.py +46 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/claim.py +34 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/readability.py +60 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/sentence_length.py +30 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/structure.py +46 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/terminology.py +38 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/units.py +66 -0
- prose_preflight-0.1.0/src/prose_preflight/checks/vale.py +92 -0
- prose_preflight-0.1.0/src/prose_preflight/default.yaml +161 -0
- prose_preflight-0.1.0/src/prose_preflight/extract/__init__.py +17 -0
- prose_preflight-0.1.0/src/prose_preflight/extract/document.py +59 -0
- prose_preflight-0.1.0/src/prose_preflight/extract/latex.py +89 -0
- prose_preflight-0.1.0/src/prose_preflight/extract/markdown.py +51 -0
- prose_preflight-0.1.0/src/prose_preflight/finding.py +16 -0
- prose_preflight-0.1.0/src/prose_preflight/run_all.py +190 -0
- prose_preflight-0.1.0/tests/fixtures/README.md +2 -0
- prose_preflight-0.1.0/tests/fixtures/acronym.expected.json +6 -0
- prose_preflight-0.1.0/tests/fixtures/acronym.md +8 -0
- prose_preflight-0.1.0/tests/fixtures/claim.expected.json +22 -0
- prose_preflight-0.1.0/tests/fixtures/claim.md +5 -0
- prose_preflight-0.1.0/tests/fixtures/config.yaml +12 -0
- prose_preflight-0.1.0/tests/fixtures/readability.expected.json +10 -0
- prose_preflight-0.1.0/tests/fixtures/readability.md +14 -0
- prose_preflight-0.1.0/tests/fixtures/sentence_length.expected.json +6 -0
- prose_preflight-0.1.0/tests/fixtures/sentence_length.md +13 -0
- prose_preflight-0.1.0/tests/fixtures/structure.expected.json +14 -0
- prose_preflight-0.1.0/tests/fixtures/structure.md +15 -0
- prose_preflight-0.1.0/tests/fixtures/terminology.expected.json +8 -0
- prose_preflight-0.1.0/tests/fixtures/terminology.md +16 -0
- prose_preflight-0.1.0/tests/fixtures/units.expected.json +22 -0
- prose_preflight-0.1.0/tests/fixtures/units.md +5 -0
- prose_preflight-0.1.0/tests/fixtures/vale.expected.json +4 -0
- prose_preflight-0.1.0/tests/fixtures/vale.md +3 -0
- prose_preflight-0.1.0/tests/test_checks.py +92 -0
- prose_preflight-0.1.0/tests/test_extract.py +107 -0
- prose_preflight-0.1.0/tests/test_finding.py +27 -0
- prose_preflight-0.1.0/tests/test_packaging.py +16 -0
- prose_preflight-0.1.0/tests/test_run_all.py +119 -0
- prose_preflight-0.1.0/tests/test_terminology.py +32 -0
- prose_preflight-0.1.0/tests/test_vale.py +59 -0
- 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,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
|
+
[](https://pypi.org/project/prose-preflight/)
|
|
29
|
+
[](https://www.python.org)
|
|
30
|
+
[](https://vale.sh)
|
|
31
|
+
[](https://docs.astral.sh/uv/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](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
|
+
[](https://pypi.org/project/prose-preflight/)
|
|
8
|
+
[](https://www.python.org)
|
|
9
|
+
[](https://vale.sh)
|
|
10
|
+
[](https://docs.astral.sh/uv/)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](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
|