riff-lint 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.
- riff_lint-0.1.0/.github/workflows/ci.yml +27 -0
- riff_lint-0.1.0/.github/workflows/pypi.yml +51 -0
- riff_lint-0.1.0/.gitignore +13 -0
- riff_lint-0.1.0/.pre-commit-config.yaml +7 -0
- riff_lint-0.1.0/CHANGELOG.md +60 -0
- riff_lint-0.1.0/CONTRIBUTING.md +99 -0
- riff_lint-0.1.0/LICENSE +21 -0
- riff_lint-0.1.0/PKG-INFO +331 -0
- riff_lint-0.1.0/README.md +306 -0
- riff_lint-0.1.0/pyproject.toml +82 -0
- riff_lint-0.1.0/riff.toml +42 -0
- riff_lint-0.1.0/samples/clean.md +8 -0
- riff_lint-0.1.0/samples/sloppy.docx +0 -0
- riff_lint-0.1.0/samples/sloppy.html +11 -0
- riff_lint-0.1.0/samples/sloppy.md +15 -0
- riff_lint-0.1.0/samples/sloppy.pptx +0 -0
- riff_lint-0.1.0/samples/sloppy.txt +3 -0
- riff_lint-0.1.0/samples/types/academic_paper.md +3 -0
- riff_lint-0.1.0/samples/types/article.md +5 -0
- riff_lint-0.1.0/samples/types/blog_post.md +7 -0
- riff_lint-0.1.0/samples/types/book_chapter.md +5 -0
- riff_lint-0.1.0/samples/types/chat_message.txt +3 -0
- riff_lint-0.1.0/samples/types/documentation.md +5 -0
- riff_lint-0.1.0/samples/types/email.md +10 -0
- riff_lint-0.1.0/samples/types/essay.md +5 -0
- riff_lint-0.1.0/samples/types/letter.md +13 -0
- riff_lint-0.1.0/samples/types/marketing_copy.md +5 -0
- riff_lint-0.1.0/samples/types/memo.md +5 -0
- riff_lint-0.1.0/samples/types/notes.md +5 -0
- riff_lint-0.1.0/samples/types/poem.md +7 -0
- riff_lint-0.1.0/samples/types/press_release.md +5 -0
- riff_lint-0.1.0/samples/types/product_description.md +3 -0
- riff_lint-0.1.0/samples/types/release_notes.md +11 -0
- riff_lint-0.1.0/samples/types/report.md +5 -0
- riff_lint-0.1.0/samples/types/resume.md +10 -0
- riff_lint-0.1.0/samples/types/review.md +5 -0
- riff_lint-0.1.0/samples/types/script.md +14 -0
- riff_lint-0.1.0/samples/types/sms.txt +1 -0
- riff_lint-0.1.0/samples/types/social_post.md +1 -0
- riff_lint-0.1.0/scripts/gen_readme.py +289 -0
- riff_lint-0.1.0/src/riff/__init__.py +3 -0
- riff_lint-0.1.0/src/riff/cli.py +200 -0
- riff_lint-0.1.0/src/riff/doctype.py +92 -0
- riff_lint-0.1.0/src/riff/engine.py +55 -0
- riff_lint-0.1.0/src/riff/extract.py +315 -0
- riff_lint-0.1.0/src/riff/jev.py +162 -0
- riff_lint-0.1.0/src/riff/py.typed +0 -0
- riff_lint-0.1.0/src/riff/report.py +130 -0
- riff_lint-0.1.0/src/riff/rules/__init__.py +14 -0
- riff_lint-0.1.0/src/riff/rules/base.py +320 -0
- riff_lint-0.1.0/src/riff/rules/jev_rules.py +709 -0
- riff_lint-0.1.0/src/riff/rules/static_rules.py +141 -0
- riff_lint-0.1.0/src/riff/settings.py +119 -0
- riff_lint-0.1.0/src/riff/textutil.py +94 -0
- riff_lint-0.1.0/tests/test_base_helpers.py +105 -0
- riff_lint-0.1.0/tests/test_cli.py +134 -0
- riff_lint-0.1.0/tests/test_custom_rules.py +117 -0
- riff_lint-0.1.0/tests/test_doctype.py +157 -0
- riff_lint-0.1.0/tests/test_extract.py +132 -0
- riff_lint-0.1.0/tests/test_jev_live.py +86 -0
- riff_lint-0.1.0/tests/test_jev_unit.py +51 -0
- riff_lint-0.1.0/tests/test_report.py +80 -0
- riff_lint-0.1.0/tests/test_settings.py +62 -0
- riff_lint-0.1.0/tests/test_static_rules.py +78 -0
- riff_lint-0.1.0/tests/test_textutil.py +75 -0
- riff_lint-0.1.0/uv.lock +1093 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Sync
|
|
22
|
+
run: uv sync
|
|
23
|
+
- name: Lint
|
|
24
|
+
run: uv run ruff check src tests scripts
|
|
25
|
+
- name: Test
|
|
26
|
+
# Live Jev tests skip automatically without TYPESAFE_API_KEY.
|
|
27
|
+
run: uv run pytest -q --cov
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
check-version:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Check tag matches pyproject.toml version
|
|
16
|
+
run: |
|
|
17
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
18
|
+
PKG_VERSION=$(grep -m1 '^version' pyproject.toml | sed -E 's/version = "(.*)"/\1/')
|
|
19
|
+
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
|
|
20
|
+
echo "release tag $GITHUB_REF_NAME does not match pyproject.toml version $PKG_VERSION" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
build:
|
|
25
|
+
needs: check-version
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: astral-sh/setup-uv@v5
|
|
30
|
+
- name: Build sdist and wheel
|
|
31
|
+
run: uv build
|
|
32
|
+
- uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
name: Publish to PyPI
|
|
39
|
+
needs: build
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
environment: pypi
|
|
42
|
+
permissions:
|
|
43
|
+
id-token: write # trusted publishing (OIDC); no API token needed
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist
|
|
49
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
50
|
+
with:
|
|
51
|
+
packages-dir: dist
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
|
|
5
|
+
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Raised the `JEV502` (vague-abstraction) threshold to 0.85. On a corpus of real professional
|
|
11
|
+
documents it was the most-fired rule and flagged concrete text, so it now triggers only on
|
|
12
|
+
strong cases; egregious vagueness still fires.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- PyPI publish workflow: a GitHub release (tag `vX.Y.Z`) builds and publishes to PyPI via Trusted Publishing.
|
|
16
|
+
- Tuned rules from a full per-type dogfood: `JEV304` (promotional) skips ad-copy types where
|
|
17
|
+
selling is the point; `JEV207` (boilerplate) and `JEV502` (abstraction) skip terse genres
|
|
18
|
+
(notes, SMS, chat, script) and no longer flag conventional sign-offs or functional lines.
|
|
19
|
+
Cleared the false positives this surfaced on riff's own README (35 to 1).
|
|
20
|
+
- Calibrated the documentation rules against riff's own README (dogfooding): `JEV601` and
|
|
21
|
+
`JEV602` are now opt-in (they over-fired on real docs), `JEV001` skips `documentation`, and
|
|
22
|
+
`samples/types/` now holds a neutral public example of every supported document type.
|
|
23
|
+
- Type-specific rules mined from seminal style texts (JEV601-JEV670): task orientation and
|
|
24
|
+
undefined terms (docs), buried conclusion (report/memo), editorializing (news), feature-not-
|
|
25
|
+
benefit (marketing), on-the-nose dialogue (script), forced rhyme (poem), vague changelog entry
|
|
26
|
+
(release notes), and weak resume bullets. Each cites its source(s); the README maps sources by type.
|
|
27
|
+
- Document-type classification: riff detects the kind of writing (email, memo, SMS, essay,
|
|
28
|
+
blog post, report, and ~20 more) with one Jev pass, and rules can gate on it via
|
|
29
|
+
`applies_to` / `skip_for`. New `JEV112` flags a greeting or sign-off outside an email or
|
|
30
|
+
letter. Force the type with `--type`, or turn classification off with `--no-classify`;
|
|
31
|
+
custom rules can gate on type too.
|
|
32
|
+
- Test-quality pass: broader unit coverage (extraction for every format, reporter,
|
|
33
|
+
text metrics, rule-registry helpers, and offline `jev` helpers), a 90% coverage
|
|
34
|
+
floor enforced in CI, and a mutmut mutation-testing setup with docs.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- The summary no longer reports "All checks passed" when Jev requests failed; a
|
|
38
|
+
keyless-or-errored run with zero findings is now flagged as incomplete.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- Generic message-level rules: `JEV010` formulaic (template) structure, `JEV111`
|
|
42
|
+
canned low-pressure sign-off, `JEV207` interchangeable boilerplate, and `JEV208`
|
|
43
|
+
faux-personalization. These catch AI-shaped outreach that is clean sentence by
|
|
44
|
+
sentence.
|
|
45
|
+
- Document-scope Jev rules: a rule can be asked once over the whole text (`scope =
|
|
46
|
+
"document"`), not just per paragraph.
|
|
47
|
+
- Custom rules: define your own `[[custom_rules]]` in `riff.toml` (a Jev question or
|
|
48
|
+
a list of banned phrases) without touching the codebase. Firm- or domain-specific
|
|
49
|
+
checks live here; the built-in rules stay generic.
|
|
50
|
+
- Initial release: a prose linter with ruff-style rule codes over Markdown, plain
|
|
51
|
+
text, HTML, Word (`.docx`), and PowerPoint (`.pptx`).
|
|
52
|
+
- 48 rules: 42 semantic rules answered by TypeSafe's Jev model and 6 deterministic
|
|
53
|
+
code rules (glyphs, structure, and clarity metrics).
|
|
54
|
+
- Rule sources: [tropes.fyi](https://tropes.fyi/tropes-md) AI-writing tells,
|
|
55
|
+
Williams' *Style: Lessons in Clarity and Grace*, and Strunk & White's
|
|
56
|
+
*The Elements of Style*.
|
|
57
|
+
- Configuration via `riff.toml` or `[tool.riff]` with ruff-style
|
|
58
|
+
`select`/`ignore`/`extend-select`, per-rule Jev thresholds, and metric limits.
|
|
59
|
+
- CLI: `--list-rules`, `--explain CODE`, `--no-jev`, `--format json`,
|
|
60
|
+
`--debug-jev`, and both `riff FILE` and `riff -f FILE` forms.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Contributing to riff
|
|
2
|
+
|
|
3
|
+
Thanks for your interest. riff is a prose linter: it flags AI-writing tells and
|
|
4
|
+
clarity problems with ruff-style rule codes.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
riff uses [uv](https://docs.astral.sh/uv/).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
git clone https://github.com/scale-venture-partners/riff
|
|
12
|
+
cd riff
|
|
13
|
+
uv sync
|
|
14
|
+
uv run riff --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The semantic rules call [TypeSafe's Jev](https://typesafe.ai) model. Set
|
|
18
|
+
`TYPESAFE_API_KEY` in your environment to run them (create a key at
|
|
19
|
+
<https://console.typesafe.ai/>). Without a key, use `--no-jev` to run the
|
|
20
|
+
deterministic rules alone.
|
|
21
|
+
|
|
22
|
+
## Checks
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv run ruff check src tests scripts # lint
|
|
26
|
+
uv run pytest --cov # tests + coverage (floor: 90%)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The static tests run offline. The live Jev tests skip automatically when
|
|
30
|
+
`TYPESAFE_API_KEY` is unset, so a bare `pytest` is green without a key; set the
|
|
31
|
+
key to exercise the model path. Coverage is enforced at 90% in CI; `jev.py`'s
|
|
32
|
+
network calls are excluded from the floor since only the live tests reach them.
|
|
33
|
+
|
|
34
|
+
### Mutation testing
|
|
35
|
+
|
|
36
|
+
Coverage says a line ran; mutation testing says a test would *notice* if the line
|
|
37
|
+
were wrong. We use [mutmut](https://mutmut.readthedocs.io/) on the deterministic
|
|
38
|
+
modules to keep tests strict.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES uv run mutmut run # macOS needs the flag
|
|
42
|
+
uv run mutmut results # list survivors
|
|
43
|
+
uv run mutmut show <mutant-id> # see one mutant's diff
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A survivor is a mutation no test caught. Prefer killing it with an exact
|
|
47
|
+
assertion (a specific value, count, or position) over a loose one. Expected
|
|
48
|
+
survivors: `jev.py` (network) and `cli.py` (I/O) are covered by unit and live
|
|
49
|
+
tests instead, and some mutations of log/error strings are equivalent and safe to
|
|
50
|
+
leave. mutmut is not run in CI (too slow); run it locally when changing the
|
|
51
|
+
deterministic modules.
|
|
52
|
+
|
|
53
|
+
## How rules work
|
|
54
|
+
|
|
55
|
+
Each rule is registered in `src/riff/rules/` and has a stable code:
|
|
56
|
+
|
|
57
|
+
- **Jev rules** (`jev_rules.py`) are one `Noul` question asked about a paragraph,
|
|
58
|
+
phrased so a high probability means the tell is present. This is where almost
|
|
59
|
+
every rule lives — pattern matching misses paraphrases and fires on look-alikes,
|
|
60
|
+
so anything that depends on meaning or context is a model question.
|
|
61
|
+
- **Code rules** (`static_rules.py`) are only for what Jev cannot do: exact glyphs
|
|
62
|
+
it never sees (curly quotes, arrows), structural facts in markup, cross-document
|
|
63
|
+
comparison, and arithmetic metrics (sentence length, reading grade).
|
|
64
|
+
|
|
65
|
+
### Adding a Jev rule
|
|
66
|
+
|
|
67
|
+
Add a `jev_rule(...)` call with a unique code, a clear `question` (use a `not_for`
|
|
68
|
+
field when it could be confused with a neighbouring rule), a `threshold`, and
|
|
69
|
+
`default=False` if it is noisy or niche. Then:
|
|
70
|
+
|
|
71
|
+
1. Probe it on a few positive and negative examples before committing, so it fires
|
|
72
|
+
on real violations and stays quiet on good prose.
|
|
73
|
+
2. Add a case to the tests where practical.
|
|
74
|
+
3. Regenerate the rule table: `uv run python scripts/gen_readme.py`.
|
|
75
|
+
|
|
76
|
+
## Style
|
|
77
|
+
|
|
78
|
+
- Line length 120, ruff-formatted (`select = ["E", "F", "I", "UP", "B"]`).
|
|
79
|
+
- Comments state the constraint that makes the code correct, not change history.
|
|
80
|
+
|
|
81
|
+
## Pull requests
|
|
82
|
+
|
|
83
|
+
Keep changes focused. Run the checks above and regenerate the README rule table if
|
|
84
|
+
you touched the catalog. Describe what the rule flags and why in the PR.
|
|
85
|
+
|
|
86
|
+
## Releasing
|
|
87
|
+
|
|
88
|
+
Publishing to PyPI is automated via `.github/workflows/pypi.yml`, which runs when a GitHub
|
|
89
|
+
release is published. To cut a release:
|
|
90
|
+
|
|
91
|
+
1. Bump `version` in `pyproject.toml` and update `CHANGELOG.md`.
|
|
92
|
+
2. Tag the commit `vX.Y.Z` (the tag version must match `pyproject.toml`, or the workflow fails).
|
|
93
|
+
3. Publish a GitHub release for that tag. The workflow builds the sdist and wheel with `uv build`
|
|
94
|
+
and uploads them with [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) —
|
|
95
|
+
no API token.
|
|
96
|
+
|
|
97
|
+
One-time setup on PyPI (maintainer): add a trusted publisher for the `riff-lint` project pointing
|
|
98
|
+
at this repository, workflow `pypi.yml`, and environment `pypi`. Until that exists, the publish
|
|
99
|
+
step will fail; everything up to it (version check and build) still runs.
|
riff_lint-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scale Venture Partners
|
|
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.
|
riff_lint-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: riff-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small, fast prose linter: ruff-style rule codes for writing, backed by TypeSafe's Jev model
|
|
5
|
+
Project-URL: Repository, https://github.com/scale-venture-partners/riff
|
|
6
|
+
Project-URL: Issues, https://github.com/scale-venture-partners/riff/issues
|
|
7
|
+
Author: Scale Venture Partners
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai-writing,editing,jev,linter,prose,style,typesafe,writing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
21
|
+
Requires-Dist: python-docx>=1.1
|
|
22
|
+
Requires-Dist: python-pptx>=1.0
|
|
23
|
+
Requires-Dist: typesafe-sdk>=0.6.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# riff
|
|
27
|
+
|
|
28
|
+
[](https://github.com/scale-venture-partners/riff/actions/workflows/ci.yml)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
A small, fast prose linter. It reads a document, flags the writing tells and clarity
|
|
32
|
+
problems it finds, and reports them with **ruff-style rule codes** you can select, ignore,
|
|
33
|
+
and configure. Static rules are pure Python and run in milliseconds; the semantic rules are
|
|
34
|
+
one call each to [TypeSafe's **Jev**](https://typesafe.ai) model, which returns a calibrated
|
|
35
|
+
probability per judgment instead of generated text.
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
$ riff draft.md
|
|
39
|
+
draft.md:1:1: RIF002 heading is Title Case; use sentence case
|
|
40
|
+
Understanding The Impact Of Modern Technology
|
|
41
|
+
draft.md:3:1: JEV001 Announces what it is about to say instead of saying it p=0.92
|
|
42
|
+
Before we dive in, let me lay out what this section will cover.
|
|
43
|
+
draft.md:5:1: JEV301 Overused AI filler vocabulary used as filler p=0.88
|
|
44
|
+
We leverage a robust, seamless framework to unlock value.
|
|
45
|
+
draft.md:9:1: JEV103 A standalone quotable line that carries no real information p=0.79
|
|
46
|
+
Every metric that rewards volume punishes leverage.
|
|
47
|
+
|
|
48
|
+
12 findings in 1 file (JEV301 ×3, JEV001 ×2, RIF002 ×1).
|
|
49
|
+
Jev: 14 calls, 9,210 input tokens (~$0.0004)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
riff is **not on PyPI yet**, so install it from source. It uses
|
|
55
|
+
[uv](https://docs.astral.sh/uv/).
|
|
56
|
+
|
|
57
|
+
**Clone and run:**
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
git clone https://github.com/scale-venture-partners/riff
|
|
61
|
+
cd riff
|
|
62
|
+
uv sync
|
|
63
|
+
uv run riff --help
|
|
64
|
+
uv run riff draft.md
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Run directly from GitHub, no clone,** with uvx (the repo is private, so this uses
|
|
68
|
+
your existing GitHub credentials via git):
|
|
69
|
+
|
|
70
|
+
```console
|
|
71
|
+
uvx --from git+https://github.com/scale-venture-partners/riff riff draft.md
|
|
72
|
+
# SSH works too, if that's how you authenticate to GitHub:
|
|
73
|
+
uvx --from git+ssh://git@github.com/scale-venture-partners/riff riff draft.md
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
To install it as a persistent `riff` command on your PATH from the repo:
|
|
77
|
+
|
|
78
|
+
```console
|
|
79
|
+
uv tool install git+https://github.com/scale-venture-partners/riff
|
|
80
|
+
riff --help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
When riff is published to PyPI, `uv tool install riff-lint` will install the same
|
|
84
|
+
`riff` command (the `riff` name itself is taken on PyPI).
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
The examples below assume `riff` is on your PATH (via `uv tool install` above). From a
|
|
89
|
+
clone without installing, prefix each with `uv run` (e.g. `uv run riff draft.md`).
|
|
90
|
+
|
|
91
|
+
```console
|
|
92
|
+
riff draft.md # lint one file (positional)
|
|
93
|
+
riff -f report.docx # or the -f/--file flag form
|
|
94
|
+
riff *.md notes.txt slides.pptx # many files, mixed formats
|
|
95
|
+
riff draft.md --no-jev # static rules only, no API key needed
|
|
96
|
+
riff draft.md --select JEV,RIF # only these codes/prefixes
|
|
97
|
+
riff draft.md --ignore JEV002 # keep defaults, drop one rule
|
|
98
|
+
riff draft.md --debug-jev # print every Jev probability, to tune thresholds
|
|
99
|
+
riff email.txt --type email # force the document type (skips classification)
|
|
100
|
+
riff draft.md --no-classify # don't classify; type-specific rules run everywhere
|
|
101
|
+
riff draft.md --format json # machine-readable output
|
|
102
|
+
riff --list-rules # the full catalog
|
|
103
|
+
riff --explain JEV001 # one rule in detail
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Supported formats:** Markdown (`.md`), plain text (`.txt`), HTML (`.html`), Word (`.docx`),
|
|
107
|
+
PowerPoint (`.pptx`). Findings report a line and column for text formats, and a paragraph or
|
|
108
|
+
slide label for Office formats.
|
|
109
|
+
|
|
110
|
+
**Exit codes:** `0` clean, `1` findings, `2` usage or file error.
|
|
111
|
+
|
|
112
|
+
## The Jev-backed rules
|
|
113
|
+
|
|
114
|
+
The semantic rules need a TypeSafe API key. Set `TYPESAFE_API_KEY` in the environment (create
|
|
115
|
+
one at <https://console.typesafe.ai/>). Without it, `--no-jev` runs the static rules alone; if
|
|
116
|
+
you select a Jev rule with no key, riff stops and tells you the two ways to fix it rather than
|
|
117
|
+
degrading silently.
|
|
118
|
+
|
|
119
|
+
Each Jev rule is one yes/no (Noul) question asked about a single paragraph. It is phrased so a
|
|
120
|
+
high probability means the tell is present. riff prints that probability (`p=0.93`) on every Jev
|
|
121
|
+
finding, and you set a per-rule `threshold` to tune sensitivity. The document text is sent to the
|
|
122
|
+
API as data; treat any content you lint accordingly.
|
|
123
|
+
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
riff reads `riff.toml` (or `.riff.toml`), or a `[tool.riff]` table in `pyproject.toml`, from the
|
|
127
|
+
file's directory upward. Command-line flags override the file. `select`/`ignore` take full codes
|
|
128
|
+
(`JEV001`) or prefixes (`JEV`, `RIF1`), with ruff's semantics.
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
# riff.toml
|
|
132
|
+
select = ["RIF", "CLR", "JEV"] # omit to use every default-on rule
|
|
133
|
+
ignore = ["JEV002"] # drop reasoning-leak (noisy on reflective writing)
|
|
134
|
+
extend-select = ["JEV401"] # turn on a default-off rule (passive voice)
|
|
135
|
+
jev = true # set false to skip semantic rules
|
|
136
|
+
model = "jev-latest"
|
|
137
|
+
max-sentence-words = 40 # CLR001 threshold
|
|
138
|
+
max-reading-grade = 12 # CLR002 threshold
|
|
139
|
+
|
|
140
|
+
[thresholds] # per-rule Jev sensitivity, 0..1
|
|
141
|
+
JEV001 = 0.7
|
|
142
|
+
JEV103 = 0.75
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Document types
|
|
146
|
+
|
|
147
|
+
Before the rules run, riff classifies the whole document with one Jev question: is it an
|
|
148
|
+
email, a memo, an SMS, an essay, a blog post, a report, and so on. The detected type prints
|
|
149
|
+
above the findings (`type: email (0.88)`).
|
|
150
|
+
|
|
151
|
+
Types let a rule apply to some kinds of writing and not others. A greeting and sign-off are
|
|
152
|
+
normal in an **email** but a tell in a **memo** or **SMS**, so the built-in `JEV112` rule skips
|
|
153
|
+
`email` and `letter` and flags a greeting anywhere else.
|
|
154
|
+
|
|
155
|
+
- Force the type with `--type email` (or any type below). This skips classification, so it
|
|
156
|
+
needs no API key and is the escape hatch when the classifier is wrong or the input is a short
|
|
157
|
+
excerpt.
|
|
158
|
+
- Turn classification off with `--no-classify`. Type-specific rules then run everywhere,
|
|
159
|
+
since the type is unresolved. An unresolved type never silently drops a rule.
|
|
160
|
+
|
|
161
|
+
The type identifiers are: `sms`, `email`, `chat_message`, `memo`, `letter`, `essay`,
|
|
162
|
+
`blog_post`, `article`, `report`, `academic_paper`, `book_chapter`, `documentation`,
|
|
163
|
+
`release_notes`, `marketing_copy`, `social_post`, `product_description`, `review`,
|
|
164
|
+
`press_release`, `resume`, `script`, `poem`, `notes`, `other`.
|
|
165
|
+
|
|
166
|
+
## Custom rules
|
|
167
|
+
|
|
168
|
+
The built-in rules are deliberately generic. Anything specific to your team,
|
|
169
|
+
house style, or domain you add in your own `riff.toml` with `[[custom_rules]]`. No
|
|
170
|
+
code changes, no fork. Two kinds:
|
|
171
|
+
|
|
172
|
+
```toml
|
|
173
|
+
# A Jev rule: a yes/no question asked about your text (needs TYPESAFE_API_KEY).
|
|
174
|
+
[[custom_rules]]
|
|
175
|
+
code = "TEAM001"
|
|
176
|
+
name = "no-competitor-names"
|
|
177
|
+
type = "jev"
|
|
178
|
+
summary = "Names a competitor"
|
|
179
|
+
question = "Does this passage name a specific competing product or company?"
|
|
180
|
+
threshold = 0.6
|
|
181
|
+
scope = "block" # "block" (per paragraph) or "document" (once over the whole text)
|
|
182
|
+
skip_for = ["email", "letter"] # never run for these document types
|
|
183
|
+
# applies_to = ["memo", "report"] # or: run ONLY for these types
|
|
184
|
+
|
|
185
|
+
# A phrase rule: literal strings flagged offline, no API key needed.
|
|
186
|
+
[[custom_rules]]
|
|
187
|
+
code = "TEAM002"
|
|
188
|
+
name = "house-style-bans"
|
|
189
|
+
type = "phrase"
|
|
190
|
+
summary = "House-style banned phrase"
|
|
191
|
+
phrases = ["circle back", "synergy", "leverage", "boil the ocean"]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Custom codes must not collide with a built-in code. They are enabled by default and
|
|
195
|
+
obey the same `select`/`ignore`/`threshold` controls as built-in rules, so
|
|
196
|
+
`--select TEAM` runs only yours and `--ignore TEAM002` drops one.
|
|
197
|
+
|
|
198
|
+
## Rules
|
|
199
|
+
|
|
200
|
+
62 rules (56 semantic, 6 static). A `·` means off by default; enable it with `--select` or `--extend-select`.
|
|
201
|
+
|
|
202
|
+
### CLR — Clarity metrics (arithmetic Jev can't do)
|
|
203
|
+
|
|
204
|
+
| Code | Rule | On | Kind | What it flags | Source |
|
|
205
|
+
|------|------|----|------|---------------|--------|
|
|
206
|
+
| `CLR001` | long-sentence | | static | Sentence longer than the configured word limit | Williams |
|
|
207
|
+
| `CLR002` | hard-to-read | · | static | Paragraph reading grade above the configured level | Williams |
|
|
208
|
+
| `CLR003` | duplicate-paragraph | | static | Paragraph repeated near-verbatim elsewhere | tropes.fyi |
|
|
209
|
+
|
|
210
|
+
### JEV — Semantic rules (Jev model) — the bulk of the catalog
|
|
211
|
+
|
|
212
|
+
| Code | Rule | On | Kind | What it flags | Source |
|
|
213
|
+
|------|------|----|------|---------------|--------|
|
|
214
|
+
| `JEV001` | preamble | | Jev | Announces what it is about to say instead of saying it | tropes.fyi |
|
|
215
|
+
| `JEV002` | reasoning-leak | | Jev | Narrates its own writing or thinking process | tropes.fyi |
|
|
216
|
+
| `JEV003` | premise-stacking | · | Jev | Makes its point only after a wall of its own evidence | tropes.fyi |
|
|
217
|
+
| `JEV004` | tie-back | | Jev | Closes by restating the answer and looping back to the question | tropes.fyi |
|
|
218
|
+
| `JEV005` | belaboring | · | Jev | Defends a minor point against an objection nobody raised | tropes.fyi |
|
|
219
|
+
| `JEV006` | signposted-conclusion | | Jev | Explicitly announces that it is concluding | tropes.fyi |
|
|
220
|
+
| `JEV007` | fractal-summary | · | Jev | Restates itself at the start or end of a section | tropes.fyi |
|
|
221
|
+
| `JEV008` | enumerated-prose | | Jev | A listicle disguised as prose ("The first… The second…") | tropes.fyi |
|
|
222
|
+
| `JEV009` | never-ending-conclusion | · | Jev | The ending stacks clause after clause instead of stopping | tropes.fyi |
|
|
223
|
+
| `JEV010` | formulaic-structure | | Jev | Follows a formulaic template, hitting every expected beat in order | tropes.fyi |
|
|
224
|
+
| `JEV101` | stakes-inflation | | Jev | Inflates ordinary stakes to world-historical significance | tropes.fyi |
|
|
225
|
+
| `JEV102` | invented-concept-label | | Jev | Coins an abstract term as if it were established | tropes.fyi |
|
|
226
|
+
| `JEV103` | quotable-bait | | Jev | A standalone quotable line that carries no real information | tropes.fyi |
|
|
227
|
+
| `JEV104` | forced-figurative | · | Jev | A simile or metaphor reached for to sound clever, not to clarify | tropes.fyi |
|
|
228
|
+
| `JEV105` | false-vulnerability | · | Jev | Performative self-awareness or risk-free 'honesty' | tropes.fyi |
|
|
229
|
+
| `JEV106` | collaborative-we | · | Jev | Drifts into an unearned collective 'we' | tropes.fyi |
|
|
230
|
+
| `JEV107` | rule-of-three | · | Jev | Stacks parallel triples (tricolons) back to back | tropes.fyi |
|
|
231
|
+
| `JEV108` | false-suspense | | Jev | A "here's the kicker" transition promising a revelation | tropes.fyi |
|
|
232
|
+
| `JEV109` | pedagogical-voice | | Jev | A hand-holding, teacher-to-student voice | tropes.fyi |
|
|
233
|
+
| `JEV111` | formulaic-close | | Jev | A canned, low-pressure sign-off | tropes.fyi |
|
|
234
|
+
| `JEV112` | misplaced-greeting | | Jev | A personal greeting or sign-off where the format doesn't call for one | tropes.fyi |
|
|
235
|
+
| `JEV110` | futurist-invitation | · | Jev | "Imagine a world where…" salesmanship | tropes.fyi |
|
|
236
|
+
| `JEV201` | one-point-dilution | · | Jev | Restates one idea several ways without adding anything | tropes.fyi |
|
|
237
|
+
| `JEV202` | superficial-analysis | | Jev | Attaches hollow significance to a mundane fact | tropes.fyi |
|
|
238
|
+
| `JEV203` | despite-challenges | · | Jev | Raises a problem only to immediately wave it away | tropes.fyi |
|
|
239
|
+
| `JEV204` | vague-attribution | | Jev | Attributes a claim to an unnamed authority | tropes.fyi |
|
|
240
|
+
| `JEV205` | appeal-to-familiarity | · | Jev | Asserts canonical status without evidence | tropes.fyi |
|
|
241
|
+
| `JEV207` | generic-boilerplate | | Jev | Interchangeable boilerplate that could describe almost anyone | tropes.fyi |
|
|
242
|
+
| `JEV208` | faux-personalization | · | Jev | Sprinkles specifics to seem researched without genuine detail | tropes.fyi |
|
|
243
|
+
| `JEV206` | rapid-fire-analogies | · | Jev | Lists historical companies or shifts to build false authority | tropes.fyi |
|
|
244
|
+
| `JEV301` | ai-vocabulary | | Jev | Overused AI filler vocabulary used as filler | tropes.fyi |
|
|
245
|
+
| `JEV302` | magic-adverb | | Jev | An adverb inflating significance ('quietly', 'fundamentally') | tropes.fyi |
|
|
246
|
+
| `JEV303` | ornate-noun | | Jev | An ornate or grandiose noun where a plain word fits | tropes.fyi |
|
|
247
|
+
| `JEV304` | promotional-language | | Jev | Reads like marketing copy rather than description | tropes.fyi |
|
|
248
|
+
| `JEV305` | empty-transition | · | Jev | A filler transition that connects nothing | tropes.fyi |
|
|
249
|
+
| `JEV306` | serves-as-dodge | · | Jev | A pompous copula ('serves as', 'stands as') instead of 'is' | tropes.fyi |
|
|
250
|
+
| `JEV307` | synonym-cycling | · | Jev | Cycles synonyms for one referent instead of repeating the word | tropes.fyi |
|
|
251
|
+
| `JEV308` | comma-clipped-tail | · | Jev | A short tail hung off a comma instead of landing the point | tropes.fyi |
|
|
252
|
+
| `JEV401` | passive-voice | · | Jev | Passive voice where the actor matters | Williams |
|
|
253
|
+
| `JEV402` | nominalization | · | Jev | The action is buried in an abstract noun | Williams |
|
|
254
|
+
| `JEV403` | wordy-phrase | | Jev | A multi-word phrase where one word would do | Williams |
|
|
255
|
+
| `JEV404` | hedging | · | Jev | Vague hedging that weakens the claim without adding precision | Williams |
|
|
256
|
+
| `JEV501` | negative-statement | | Jev | Says what something is not, instead of what it is | Strunk & White, The Elements of Style |
|
|
257
|
+
| `JEV502` | vague-abstraction | | Jev | Abstract, general language where concrete detail would serve | Strunk & White, The Elements of Style |
|
|
258
|
+
| `JEV503` | weak-intensifier | | Jev | Leans on 'very', 'rather', 'pretty', 'quite' for emphasis | Strunk & White, The Elements of Style |
|
|
259
|
+
| `JEV504` | loose-sentence-chain | · | Jev | Two or more clauses strung together with and / but / so / which | Strunk & White, The Elements of Style |
|
|
260
|
+
| `JEV505` | faulty-parallelism | · | Jev | Coordinate ideas in a series expressed in mismatched forms | Strunk & White, The Elements of Style |
|
|
261
|
+
| `JEV601` | not-task-oriented | · | Jev | Documentation that describes the thing instead of telling the reader how to use it | Hargis et al., Developing Quality Technical Information (IBM) |
|
|
262
|
+
| `JEV602` | undefined-term | · | Jev | An acronym or specialized term used without being defined on first use | Hargis et al., Developing Quality Technical Information (IBM); Microsoft Writing Style Guide |
|
|
263
|
+
| `JEV610` | buried-conclusion | | Jev | Makes the reader work through context before stating the conclusion | Minto, The Pyramid Principle; Garner, HBR Guide to Better Business Writing |
|
|
264
|
+
| `JEV620` | editorializing | | Jev | Opinion or loaded language inserted into what is presented as reporting | AP Stylebook; Kovach & Rosenstiel, The Elements of Journalism |
|
|
265
|
+
| `JEV630` | feature-not-benefit | | Jev | Lists features without translating them into a benefit to the reader | Ogilvy, Ogilvy on Advertising; Bly, The Copywriter's Handbook |
|
|
266
|
+
| `JEV640` | on-the-nose-dialogue | | Jev | Dialogue that states feelings or plot directly instead of implying them | McKee, Story; Field, Screenplay |
|
|
267
|
+
| `JEV650` | forced-rhyme | | Jev | Rhyme that distorts word choice or syntax to hit the rhyme | Oliver, A Poetry Handbook; Fry, The Ode Less Travelled |
|
|
268
|
+
| `JEV660` | vague-changelog-entry | | Jev | A release note that says nothing specific ('various improvements') | Keep a Changelog (keepachangelog.com) |
|
|
269
|
+
| `JEV670` | weak-resume-bullet | | Jev | A resume entry with no strong action verb or concrete result | Resume conventions (strong action verbs, quantified results) |
|
|
270
|
+
|
|
271
|
+
### RIF — Typography and structure (exact checks Jev can't see)
|
|
272
|
+
|
|
273
|
+
| Code | Rule | On | Kind | What it flags | Source |
|
|
274
|
+
|------|------|----|------|---------------|--------|
|
|
275
|
+
| `RIF001` | decorative-unicode | | static | Curly quotes or arrows (glyphs Jev can't see) | tropes.fyi |
|
|
276
|
+
| `RIF002` | title-case-heading | | static | Heading capitalizes every word | tropes.fyi |
|
|
277
|
+
| `RIF003` | bold-first-bullets | | static | Most list items open with a bold lead-in | tropes.fyi |
|
|
278
|
+
|
|
279
|
+
## Sources
|
|
280
|
+
|
|
281
|
+
The rule set comes from tropes.fyi, Williams, and Strunk & White:
|
|
282
|
+
|
|
283
|
+
- AI writing tells, from [tropes.fyi](https://tropes.fyi/tropes-md): negative parallelism,
|
|
284
|
+
em-dash addiction, magic adverbs, signposted conclusions, and the rest.
|
|
285
|
+
- Clarity and grace, from Joseph M. Williams, *Style: Lessons in Clarity and Grace*: wordy
|
|
286
|
+
phrases, nominalizations, passive voice, sentence length, and readability.
|
|
287
|
+
- The Elements of Style, by Strunk & White: put statements in positive form, use concrete
|
|
288
|
+
language, cut weak intensifiers, avoid loose-sentence chains, and keep parallel form.
|
|
289
|
+
|
|
290
|
+
Almost every tell is a Jev judgment: pattern-matching misses paraphrases and fires on look-alikes,
|
|
291
|
+
so anything that depends on meaning or context is a model question, not a regex. Only what Jev
|
|
292
|
+
genuinely cannot do stays in code — exact glyphs it never sees (curly quotes, arrows), structural
|
|
293
|
+
facts in markup, cross-document duplicate detection, and arithmetic metrics (sentence length, grade).
|
|
294
|
+
|
|
295
|
+
### Sources by document type
|
|
296
|
+
|
|
297
|
+
The type-specific rules are mined from a style text per document type — Minto for reports, Ogilvy
|
|
298
|
+
for marketing, Hargis for docs, and so on. Where a form has no single canonical work, we cite the recognized guide or
|
|
299
|
+
convention. A rule may cite more than one source, and these overlap with the general catalog above.
|
|
300
|
+
|
|
301
|
+
| Type | Source(s) mined |
|
|
302
|
+
|---|---|
|
|
303
|
+
| essay, book_chapter | Zinsser, *On Writing Well*; King, *On Writing* |
|
|
304
|
+
| article, press_release | *AP Stylebook*; Kovach & Rosenstiel, *The Elements of Journalism* |
|
|
305
|
+
| report, memo | Minto, *The Pyramid Principle*; Garner, *HBR Guide to Better Business Writing* |
|
|
306
|
+
| academic_paper | Sword, *Stylish Academic Writing*; Williams, *Style* |
|
|
307
|
+
| documentation | Hargis et al., *Developing Quality Technical Information* (IBM); Microsoft / Google style guides |
|
|
308
|
+
| marketing_copy, product_description | Ogilvy, *Ogilvy on Advertising*; Bly, *The Copywriter's Handbook* |
|
|
309
|
+
| script | McKee, *Story*; Field, *Screenplay* |
|
|
310
|
+
| poem | Oliver, *A Poetry Handbook*; Fry, *The Ode Less Travelled* |
|
|
311
|
+
| release_notes | *Keep a Changelog*; *Semantic Versioning* |
|
|
312
|
+
| email, letter | Shipley & Schwalbe, *Send*; Garner, *HBR Guide* |
|
|
313
|
+
| blog_post, social_post | Handley, *Everybody Writes* |
|
|
314
|
+
| review | Barnet, *A Short Guide to Writing About …* |
|
|
315
|
+
| sms, chat_message | Crystal, *Txtng: The Gr8 Db8*; McCulloch, *Because Internet* (descriptive, not prescriptive) |
|
|
316
|
+
| resume | conventions: strong action verbs, quantified results, no first person |
|
|
317
|
+
|
|
318
|
+
## Development
|
|
319
|
+
|
|
320
|
+
```console
|
|
321
|
+
uv sync
|
|
322
|
+
uv run pytest # static tests run offline; live Jev tests skip without a key
|
|
323
|
+
uv run ruff check src tests
|
|
324
|
+
uv run python scripts/gen_readme.py # regenerate the rule table below
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how rules work and how to add one.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
[MIT](LICENSE) © Scale Venture Partners.
|