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.
Files changed (66) hide show
  1. riff_lint-0.1.0/.github/workflows/ci.yml +27 -0
  2. riff_lint-0.1.0/.github/workflows/pypi.yml +51 -0
  3. riff_lint-0.1.0/.gitignore +13 -0
  4. riff_lint-0.1.0/.pre-commit-config.yaml +7 -0
  5. riff_lint-0.1.0/CHANGELOG.md +60 -0
  6. riff_lint-0.1.0/CONTRIBUTING.md +99 -0
  7. riff_lint-0.1.0/LICENSE +21 -0
  8. riff_lint-0.1.0/PKG-INFO +331 -0
  9. riff_lint-0.1.0/README.md +306 -0
  10. riff_lint-0.1.0/pyproject.toml +82 -0
  11. riff_lint-0.1.0/riff.toml +42 -0
  12. riff_lint-0.1.0/samples/clean.md +8 -0
  13. riff_lint-0.1.0/samples/sloppy.docx +0 -0
  14. riff_lint-0.1.0/samples/sloppy.html +11 -0
  15. riff_lint-0.1.0/samples/sloppy.md +15 -0
  16. riff_lint-0.1.0/samples/sloppy.pptx +0 -0
  17. riff_lint-0.1.0/samples/sloppy.txt +3 -0
  18. riff_lint-0.1.0/samples/types/academic_paper.md +3 -0
  19. riff_lint-0.1.0/samples/types/article.md +5 -0
  20. riff_lint-0.1.0/samples/types/blog_post.md +7 -0
  21. riff_lint-0.1.0/samples/types/book_chapter.md +5 -0
  22. riff_lint-0.1.0/samples/types/chat_message.txt +3 -0
  23. riff_lint-0.1.0/samples/types/documentation.md +5 -0
  24. riff_lint-0.1.0/samples/types/email.md +10 -0
  25. riff_lint-0.1.0/samples/types/essay.md +5 -0
  26. riff_lint-0.1.0/samples/types/letter.md +13 -0
  27. riff_lint-0.1.0/samples/types/marketing_copy.md +5 -0
  28. riff_lint-0.1.0/samples/types/memo.md +5 -0
  29. riff_lint-0.1.0/samples/types/notes.md +5 -0
  30. riff_lint-0.1.0/samples/types/poem.md +7 -0
  31. riff_lint-0.1.0/samples/types/press_release.md +5 -0
  32. riff_lint-0.1.0/samples/types/product_description.md +3 -0
  33. riff_lint-0.1.0/samples/types/release_notes.md +11 -0
  34. riff_lint-0.1.0/samples/types/report.md +5 -0
  35. riff_lint-0.1.0/samples/types/resume.md +10 -0
  36. riff_lint-0.1.0/samples/types/review.md +5 -0
  37. riff_lint-0.1.0/samples/types/script.md +14 -0
  38. riff_lint-0.1.0/samples/types/sms.txt +1 -0
  39. riff_lint-0.1.0/samples/types/social_post.md +1 -0
  40. riff_lint-0.1.0/scripts/gen_readme.py +289 -0
  41. riff_lint-0.1.0/src/riff/__init__.py +3 -0
  42. riff_lint-0.1.0/src/riff/cli.py +200 -0
  43. riff_lint-0.1.0/src/riff/doctype.py +92 -0
  44. riff_lint-0.1.0/src/riff/engine.py +55 -0
  45. riff_lint-0.1.0/src/riff/extract.py +315 -0
  46. riff_lint-0.1.0/src/riff/jev.py +162 -0
  47. riff_lint-0.1.0/src/riff/py.typed +0 -0
  48. riff_lint-0.1.0/src/riff/report.py +130 -0
  49. riff_lint-0.1.0/src/riff/rules/__init__.py +14 -0
  50. riff_lint-0.1.0/src/riff/rules/base.py +320 -0
  51. riff_lint-0.1.0/src/riff/rules/jev_rules.py +709 -0
  52. riff_lint-0.1.0/src/riff/rules/static_rules.py +141 -0
  53. riff_lint-0.1.0/src/riff/settings.py +119 -0
  54. riff_lint-0.1.0/src/riff/textutil.py +94 -0
  55. riff_lint-0.1.0/tests/test_base_helpers.py +105 -0
  56. riff_lint-0.1.0/tests/test_cli.py +134 -0
  57. riff_lint-0.1.0/tests/test_custom_rules.py +117 -0
  58. riff_lint-0.1.0/tests/test_doctype.py +157 -0
  59. riff_lint-0.1.0/tests/test_extract.py +132 -0
  60. riff_lint-0.1.0/tests/test_jev_live.py +86 -0
  61. riff_lint-0.1.0/tests/test_jev_unit.py +51 -0
  62. riff_lint-0.1.0/tests/test_report.py +80 -0
  63. riff_lint-0.1.0/tests/test_settings.py +62 -0
  64. riff_lint-0.1.0/tests/test_static_rules.py +78 -0
  65. riff_lint-0.1.0/tests/test_textutil.py +75 -0
  66. 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,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .env
7
+ dist/
8
+ .DS_Store
9
+ mutants/
10
+ .mutmut-cache
11
+ html/
12
+ .coverage
13
+ coverage.xml
@@ -0,0 +1,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.9.6
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
@@ -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.
@@ -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.
@@ -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
+ [![CI](https://github.com/scale-venture-partners/riff/actions/workflows/ci.yml/badge.svg)](https://github.com/scale-venture-partners/riff/actions/workflows/ci.yml)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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.