sloplint 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2530123f6520e2abc31a875cc53787cc4492669c2bc7919c5ab19fa3541601a1
4
+ data.tar.gz: 61b2542547165a0f8e632c2ce89a679386edc8bbde24a71f92f69091603bf6e8
5
+ SHA512:
6
+ metadata.gz: 607445e1bdb23eacb5d460b01e72d1ebb140136974c7988ebdd023c7db4cccfd4540e1e4ddb68031938fe936d73d173c52d0818d3573ea045cbbc0b7db46696c
7
+ data.tar.gz: 55faafabedfbde617ea5cfed7e3ac89a3e7e82f24521346b21b6ddc3fe0a775e41dd1bfeacc5860326fcfaa7ab006316a9b9ab6b09fbe956678cf6de172a71c5
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format loosely
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [0.1.0] - Unreleased
7
+
8
+ First public release.
9
+
10
+ - 24-rule catalog across four categories: rhetorical-tic, puffery, structure,
11
+ hedging. See `sloplint rules` or `README.md` for the full list.
12
+ - `check`, `rules`, `explain`, `version` commands; JSON and human-readable
13
+ output; `--select`/`--ignore` by rule id or category; `--markdown` to skip
14
+ fenced code, inline code, and URLs before scanning.
15
+ - Exit codes: `0` clean, `1` notes found, `2` bad arguments or invalid input
16
+ (including invalid UTF-8 and unknown `--select`/`--ignore` ids).
17
+ - Zero runtime dependencies; requires Ruby >= 3.3.
18
+ - `-v`/`--version` print the version and exit 0. optparse auto-registers its
19
+ own `--version` switch on any parser that doesn't define one, and that
20
+ default printed "version unknown" to the real stdout and hard-exited,
21
+ bypassing the `out:`/return-a-code contract every other path honors --
22
+ caught by hand-testing the freshly built gem before this release shipped.
23
+ - Rationale text (`sloplint explain`) across six rules no longer narrates the
24
+ rule's own implementation -- pattern scoping, anchoring, tuning history,
25
+ corpus-driven thresholds. It states why the construct reads as AI writing
26
+ and nothing else.
27
+
28
+ Rules narrowed against a corpus of real human prose (the Federalist Papers,
29
+ Moby-Dick, Walden) before release:
30
+
31
+ - `not-just-x-but-y` anchored on a preceding copula so it no longer flags
32
+ ordinary correlative conjunctions ("not only... but...").
33
+ - `puffery-words` dropped the bare adjective "profound" and narrowed "in the
34
+ heart of" to require a place object, both unguarded false-positive sources.
35
+ - `em-dash-overuse`, `thats-how-x`, and `announced-takeaway` scoped to real
36
+ paragraphs (blank-line boundaries) instead of source lines, so hit counts
37
+ no longer swing on Markdown line-wrapping.
38
+ - `clause-triad-then` removed: went 0-for-40 on its own description against
39
+ the human corpus, and no reliable narrowing was found.
40
+ - `no-x-no-y`'s rationale corrected to match what the pattern actually does
41
+ (fires at two items; the frequency gap between human and model prose is
42
+ the justification, not an item-count claim the pattern didn't enforce).
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benjamin Jackson
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.
data/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # sloplint
2
+
3
+ A dependency-free CLI that scans prose for the tells of AI-generated **slop** and reports them as linting notes. Think `proselint`, but aimed narrowly at the rhetorical tics and puffery that mark LLM writing: the "no X, no Y" chains, the "rich tapestry of," the "some critics argue" hedging. It writes JSON an agent can act on and human text a person can read.
4
+
5
+ The primary reader is an agent (Claude Code and friends) that runs sloplint, reads the JSON, and rewrites what it flags. Humans are the secondary reader, and everything is built to keep the false-positive rate low enough that a flag is worth trusting.
6
+
7
+ ## What it catches, and what it doesn't
8
+
9
+ A pattern earns a place in the catalog only if it shows up constantly in AI writing and rarely in careful human writing. Passive voice, weak adverbs, wordiness, clichés a person reaches for too: those belong in `proselint` or `write-good`, not here. sloplint is not a general prose linter and never tries to be. It hunts the specific fingerprints of a language model, so an agent can act on a flag instead of second-guessing it.
10
+
11
+ ## Installation
12
+
13
+ Requires **Ruby 3.3+** and nothing else. The runtime is standard library only (`optparse`, `json`, native regex).
14
+
15
+ ```bash
16
+ gem install sloplint
17
+ ```
18
+
19
+ Or build from source:
20
+
21
+ ```bash
22
+ git clone https://github.com/benjaminjackson/sloplint
23
+ cd sloplint
24
+ gem build sloplint.gemspec
25
+ gem install ./sloplint-*.gem
26
+ ```
27
+
28
+ Either way, that puts a `sloplint` executable on your path.
29
+
30
+ ## Quick start
31
+
32
+ The recipe sloplint is built around, and the one an agent should use:
33
+
34
+ ```bash
35
+ cat draft.md | sloplint check --markdown -o json -
36
+ ```
37
+
38
+ `--markdown` blanks out code and URLs first, `-o json` emits the machine-readable form, and `-` reads stdin. Exit 0 means clean, 1 means notes found, anything higher is an error. A bare `sloplint` with piped stdin is the same as `sloplint check -`.
39
+
40
+ The human-readable form drops `-o json`:
41
+
42
+ ```
43
+ $ echo "A rich tapestry of vibrant cultures. That's exactly the point." | sloplint check -
44
+ -:1:3: warning rich-tapestry "rich tapestry"/"tapestry of" is a signature AI cliché.
45
+ excerpt: rich tapestry
46
+ fix: Cut the metaphor; name the actual things.
47
+
48
+ -:1:20: warning puffery-words Wikipedia-style puffery word/phrase — a common AI tell.
49
+ excerpt: vibrant
50
+ fix: Replace with a concrete, specific detail or cut it.
51
+
52
+ -:1:38: warning exactly-the "exactly the point/kind/problem/…" is an overused LLM emphasis tic.
53
+ excerpt: That's exactly the point
54
+ fix: Drop 'exactly the'; state the point without the intensifier.
55
+ ```
56
+
57
+ ## Commands
58
+
59
+ ```
60
+ sloplint [-o full|json] <command> [args]
61
+
62
+ check scan paths (or stdin) for AI-slop tells and report notes [default]
63
+ rules list the rule catalog (add --json for the machine-readable form)
64
+ explain ID print one rule's message, rationale, and a bad/ok example
65
+ version print the sloplint version
66
+ ```
67
+
68
+ `check` takes files as arguments, or `-` (or nothing) to read stdin, and these options:
69
+
70
+ - `--markdown` skips fenced code, inline code, and URLs before scanning. Off by default so it never silently eats prose.
71
+ - `--select IDS` runs only these rules. Accepts comma-separated rule ids or category names.
72
+ - `--ignore IDS` skips these rules. Same id-or-category form.
73
+
74
+ `explain` is the command an agent calls to decide whether a flag is worth acting on:
75
+
76
+ ```
77
+ $ sloplint explain no-x-no-y
78
+ no-x-no-y (rhetorical-tic, warning)
79
+
80
+ "No X, no Y" chain (%{count} items) reads as AI cadence.
81
+
82
+ Why: LLMs love asyndetic negation triplets. A careful writer rarely stacks three.
83
+ Fix: Cut the chain or make it one plain sentence.
84
+
85
+ Flags: No fluff, no filler, no jargon.
86
+ Does not: No parking on Sundays.
87
+ ```
88
+
89
+ ## The note
90
+
91
+ One match is one note. JSON output is an array of these, or an object keyed by path when more than one file is scanned. The schema is the contract:
92
+
93
+ ```json
94
+ {
95
+ "path": "draft.md",
96
+ "line": 12,
97
+ "column": 5,
98
+ "severity": "warning",
99
+ "rule": "no-x-no-y",
100
+ "category": "rhetorical-tic",
101
+ "message": "\"No X, no Y\" chain (3 items) reads as AI cadence.",
102
+ "excerpt": "No fluff, no filler, no jargon",
103
+ "count": 3,
104
+ "suggestion": "Cut the chain or make it one plain sentence."
105
+ }
106
+ ```
107
+
108
+ `line` and `column` are 1-indexed and point at the start of the match. `count` appears only when the rule tallies items (a "no X, no Y" chain, a "did not, did not" chain). `suggestion` is a short fix hint.
109
+
110
+ ## Exit codes
111
+
112
+ Three codes carry the contract. A crash exits nonzero on its own.
113
+
114
+ | code | meaning |
115
+ |------|---------|
116
+ | 0 | ran, no notes |
117
+ | 1 | ran, notes found |
118
+ | 2 | bad arguments or usage error |
119
+
120
+ An unknown id or category in `--select`/`--ignore` is a usage error (exit 2, naming the id) rather than a silent no-op, so a typo can't masquerade as a clean scan.
121
+
122
+ ## The rule catalog
123
+
124
+ 24 rules across four categories. `sloplint rules` prints them; `sloplint rules --json` gives an agent the enumerable form.
125
+
126
+ - **rhetorical-tic** (15) the cadence patterns: `no-x-no-y`, `thats-the-whole`, `thats-how-x`, `announced-takeaway`, `exactly-the`, `you-already-know`, `sit-with-that`, `thats-not-nothing`, and more.
127
+ - **puffery** (5) Wikipedia's "signs of AI writing": `puffery-words` (vibrant, nestled, groundbreaking, in the heart of), `rich-tapestry`, `vital-role`, `stands-serves-as`, `underscores-highlights`.
128
+ - **structure** (3) `not-just-x-but-y`, `em-dash-overuse` (three or more em dashes in a paragraph), and `rule-of-three`.
129
+ - **hedging** (1) `vague-attribution`: "some critics argue," "it is widely regarded."
130
+
131
+ Severity is `warning` for strong tells, `info` for weak or contextual ones. No rule currently ships at `error`; the tier is reserved for a pattern with essentially zero false-positive risk, and none has earned that yet.
132
+
133
+ One rule ships **off by default**: `rule-of-three` flags three parallel comma items closing a sentence, which humans do all the time, so it false-positives. It runs only when you name it: `sloplint check --select rule-of-three -`.
134
+
135
+ ### Markdown handling
136
+
137
+ `--markdown` replaces fenced code, inline code, and URLs with same-length whitespace before scanning, so line and column stay correct. Without it, sloplint treats the whole file as prose and will flag text inside your code fences. Pass `--markdown` whenever the input is Markdown.
138
+
139
+ ## Adding a rule
140
+
141
+ Rules are data, not code. Each is a `Data.define` object in `lib/sloplint/rules.rb` with a regex, a message, a suggestion, and one bad and one ok fixture:
142
+
143
+ ```ruby
144
+ Rule.new(
145
+ id: "no-x-no-y",
146
+ category: "rhetorical-tic",
147
+ severity: "warning",
148
+ pattern: /\bno\s+[\w'-]+,\s+no\s+[\w'-]+(?:,?\s+(?:and\s+)?no\s+[\w'-]+)*/i,
149
+ message: '"No X, no Y" chain (%{count} items) reads as AI cadence.',
150
+ suggestion: "Cut the chain or make it one plain sentence.",
151
+ count_group: /\bno\b/i, # optional: a regex tallied over the match
152
+ skip: [/real estate/i], # optional: drop the note if these match
153
+ examples_bad: ["No fluff, no filler, no jargon."],
154
+ examples_ok: ["No parking on Sundays."],
155
+ rationale: "Asyndetic negation chains are a signature model cadence, rare in human prose."
156
+ )
157
+ ```
158
+
159
+ Adding a rule is one entry plus its fixtures. `rules_spec.rb` iterates the catalog and asserts every `examples_bad` produces at least one note and every `examples_ok` produces none, so a rule without fixtures, or one whose regex is too greedy, fails the suite.
160
+
161
+ ## Development
162
+
163
+ No `Gemfile` -- install `rspec` and `rake` yourself (`gem install rspec rake`), then:
164
+
165
+ ```bash
166
+ rake spec # or: rspec
167
+ ```
168
+
169
+ `rules_spec.rb` checks every rule against its fixtures. `cli_spec.rb` covers exit codes, stdin, JSON schema, `--select` and `--ignore`, and that `--markdown` skips code. A slop fixture in `spec/fixtures/` doubles as an integration check.
170
+
171
+ See [`docs/SPEC.md`](docs/SPEC.md) for the full design, including why this is a fresh tool rather than a proselint extension.
172
+
173
+ ## Author
174
+
175
+ Benjamin Jackson ([@benjaminjackson](https://github.com/benjaminjackson))
176
+
177
+ ## License
178
+
179
+ MIT
data/bin/sloplint ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/sloplint/cli"
5
+ exit Sloplint::CLI.run(ARGV)
data/docs/SPEC.md ADDED
@@ -0,0 +1,308 @@
1
+ # sloplint — spec
2
+
3
+ A CLI that scans prose for the tells of AI-generated "slop" and reports them as
4
+ linting notes. Think `proselint`, but narrowly aimed at the rhetorical tics and
5
+ puffery that mark LLM writing.
6
+
7
+ Primary consumer is an **agent** (Claude Code and friends) that runs sloplint,
8
+ reads the JSON, and rewrites the flagged text. Humans are the secondary
9
+ consumer. Every design choice below favors machine-readability and a help text
10
+ an agent can act on without guessing.
11
+
12
+ ## The one test for every rule
13
+
14
+ A pattern earns a place in the catalog only if it **shows up constantly in AI
15
+ writing and rarely in careful human writing.** That's the whole filter.
16
+
17
+ If a thoughtful human writer does it all the time too — passive voice, weak
18
+ adverbs, comma splices, wordiness — it belongs in proselint or write-good, not
19
+ here. sloplint is not a general prose linter and never tries to be. It hunts
20
+ the specific fingerprints of a language model: the cadences, the puffery, the
21
+ reflexive both-sidesing that a person almost never produces but an LLM produces
22
+ by the paragraph.
23
+
24
+ Two payoffs from holding this line: a low false-positive rate (so an agent can
25
+ trust a flag instead of second-guessing it), and no overlap with tools that
26
+ already do general prose well. When a rule is borderline, ask the test again and
27
+ cut it if the answer is soft.
28
+
29
+ ## Prior art we're borrowing from
30
+
31
+ - **proselint** — subcommand CLI (`check`, `version`, `dump-config`), `--output-format full|json|compact`, LSP-style diagnostics (line/column/severity/code/message), config file, clean exit codes. We copy this shape.
32
+ - **vale** — markup-aware (skips code blocks, knows Markdown). We do a lighter version: optional `--markdown` to skip fenced code and inline code.
33
+ - **write-good / alex** — naive regex rules, one module per rule. We keep rules as data, not code, so they're trivial to add.
34
+
35
+ ## Why build fresh instead of extending proselint
36
+
37
+ proselint is a general prose linter. Extend it and our AI-tell rules land next
38
+ to its checks for passive voice, clichés, and date formatting — a user who just
39
+ wants "does this read like a bot?" can't get that without running everything and
40
+ filtering. The narrow focus is the whole product, and folding into proselint
41
+ dilutes it on day one.
42
+
43
+ The practical cost is worse. Upstreaming our rules puts our release schedule at
44
+ the mercy of their review and their view of scope. Forking means maintaining a
45
+ whole prose linter to ship what is really a few hundred lines of regex. Both are
46
+ bad trades for the size of this thing.
47
+
48
+ And the part we actually care about — the agent-facing design (`explain` and
49
+ `rules` commands, the JSON schema as a fixed contract, the copy-paste recipe in
50
+ `--help`) — isn't in proselint. We'd be bolting it onto someone else's CLI (and
51
+ proselint is Python; we're Ruby). Writing our own shell is about a day, and
52
+ proselint already showed us
53
+ what that shell should look like. The hard part was never the CLI; it's the
54
+ rules and holding down false positives, and proselint helps with neither.
55
+
56
+ One additive move, not either-or: once the rules exist as data, we can also
57
+ package them as a proselint plugin, so people already using proselint get our
58
+ checks without us inheriting their codebase. Cheap, because the rules are
59
+ already pure data.
60
+
61
+ ## Language & shape
62
+
63
+ Ruby 3.3+, **standard library only** (`optparse`, `json`, native regex). No gem
64
+ dependencies at runtime.
65
+
66
+ Rationale: rules are regexes; the whole thing is a scanner plus an output
67
+ formatter. A dependency-free `gem install sloplint` is the robust, boring
68
+ choice. The name is free on RubyGems (taken on PyPI, so this also sidesteps the
69
+ collision). Ships a `sloplint` executable via the gemspec's `bin`.
70
+
71
+ Package layout (standard gem):
72
+
73
+ ```
74
+ sloplint/
75
+ bin/sloplint # thin shim: require "sloplint/cli"; exit Sloplint::CLI.run(ARGV)
76
+ lib/sloplint.rb # requires the pieces below
77
+ lib/sloplint/version.rb
78
+ lib/sloplint/cli.rb # optparse, subcommands, exit codes
79
+ lib/sloplint/rules.rb # RULES: array of Rule (Data) objects — the catalog
80
+ lib/sloplint/engine.rb # Engine.scan(text, rules:, config:) -> [Note]
81
+ lib/sloplint/output.rb # format_human / format_json / format_compact
82
+ docs/
83
+ SPEC.md # this file
84
+ spec/
85
+ rules_spec.rb # each rule: >=1 positive, >=1 negative fixture
86
+ cli_spec.rb # exit codes, stdin, output formats
87
+ spec_helper.rb
88
+ sloplint.gemspec
89
+ .rspec
90
+ Rakefile # rake spec
91
+ ```
92
+
93
+ RSpec is a **development** dependency (in the gemspec's `add_development_
94
+ dependency`), so the runtime stays dependency-free.
95
+
96
+ ## CLI surface
97
+
98
+ ```
99
+ sloplint [GLOBAL] <command> [ARGS]
100
+
101
+ commands:
102
+ check scan paths (or stdin) and report notes [default command]
103
+ rules list the rule catalog (human or --json)
104
+ explain ID print one rule's description, examples, and rationale
105
+ version print version
106
+
107
+ global options:
108
+ -o, --output-format full | json (default: full)
109
+
110
+ check options:
111
+ paths ... files to scan; "-" or no paths reads stdin
112
+ --markdown skip fenced/inline code spans
113
+ --select IDS only run these rules (comma-separated ids or categories)
114
+ --ignore IDS skip these rules
115
+ ```
116
+
117
+ Bare `sloplint` with piped stdin behaves as `sloplint check -`. This is the
118
+ common agent path: `cat draft.md | sloplint check --markdown -o json -`.
119
+
120
+ Deliberately left out of v1 (add when a real need shows up, not before):
121
+ `compact` output, `--min-severity`, `--max-notes`, color, `-q/--quiet`, and a
122
+ `--demo` flag (the slop fixture lives in `spec/` instead).
123
+
124
+ ## Exit codes
125
+
126
+ Three codes carry the contract. A crash just exits nonzero on its own.
127
+
128
+ | code | meaning |
129
+ |------|---------|
130
+ | 0 | ran, **no notes** |
131
+ | 1 | ran, **notes found** |
132
+ | 2 | bad arguments / usage error |
133
+
134
+ ## Note (the diagnostic object)
135
+
136
+ One match = one Note. JSON output is an array of these (or an object keyed by
137
+ path when multiple files are scanned).
138
+
139
+ ```json
140
+ {
141
+ "path": "draft.md",
142
+ "line": 12,
143
+ "column": 5,
144
+ "severity": "warning",
145
+ "rule": "no-x-no-y",
146
+ "category": "rhetorical-tic",
147
+ "message": "\"No X, no Y\" chain (3 items) reads as AI cadence.",
148
+ "excerpt": "No fluff, no filler, no jargon.",
149
+ "count": 3,
150
+ "suggestion": "Cut the chain or make it one plain sentence."
151
+ }
152
+ ```
153
+
154
+ - `line`/`column` are 1-indexed, pointing at the start of the match.
155
+ - `count` present when the rule counts items (the "badge" in the examples).
156
+ - `suggestion` is a short fix hint; agents may use it, humans see it too.
157
+
158
+ ## Rule model
159
+
160
+ A rule is data, not a function. `rules.rb` holds an array of `Rule` objects
161
+ built with `Data.define` (immutable value objects, Ruby 3.2+):
162
+
163
+ ```ruby
164
+ Rule = Data.define(
165
+ :id, :category, :severity, :pattern, :message, :suggestion,
166
+ :examples_bad, :examples_ok, :count_group, :skip
167
+ ) do
168
+ # sensible defaults for the optional fields
169
+ def initialize(count_group: nil, skip: [], **rest) = super
170
+ end
171
+
172
+ RULES = [
173
+ Rule.new(
174
+ id: "no-x-no-y",
175
+ category: "rhetorical-tic",
176
+ severity: "warning",
177
+ pattern: /.../i, # regex literal; add /m if multiline
178
+ message: "...", # may reference %{count}
179
+ suggestion: "...",
180
+ examples_bad: ["No fluff, no filler, no jargon."],
181
+ examples_ok: ["No parking on Sundays."], # must NOT match; asserted in tests
182
+ count_group: nil, # optional: capture group to tally
183
+ skip: [/real estate/i, /real time/i] # optional exclusions
184
+ ),
185
+ # ...
186
+ ]
187
+ ```
188
+
189
+ Adding a rule = appending one entry + one bad and one ok fixture. That's the
190
+ whole extension story. No new files, no plugin system (YAGNI).
191
+
192
+ ### Why Ruby literals, not JSON/YAML
193
+
194
+ The rules are data, but they live in a `.rb` array on purpose:
195
+
196
+ - **YAML** ships with Ruby, but regex in YAML is a string that has to be
197
+ re-parsed and re-escaped — every `\b` doubled, no `/i` flags inline, and the
198
+ good/bad fixtures drift away from the pattern they test.
199
+ - **JSON** is stdlib too but worse for regex: same escaping tax, no comments,
200
+ no multiline patterns. Both external formats also need a load-and-validate
201
+ layer that a Ruby array gets for free — a typo is a syntax error at require
202
+ time, not a silent miss.
203
+
204
+ A Ruby array with real regex literals (`/.../i`) is the nicest place to *author*
205
+ regex — flags, comments, and fixtures all in one spot. The usual reason to move
206
+ rules to a file (non-devs editing them, third-party rule packs) isn't real yet.
207
+ If it becomes real, the migration is cheap precisely because the rules are
208
+ already pure data: write one loader, point it at a JSON dir, done.
209
+
210
+ Categories (for `--select`/`--ignore` by group):
211
+
212
+ - `rhetorical-tic` — the cadence patterns (the user's list below)
213
+ - `puffery` — Wikipedia "words to watch" (boasts, vibrant, nestled, tapestry…)
214
+ - `structure` — rule-of-three, "not just X but Y", em-dash overuse
215
+ - `hedging` — vague attribution ("some critics argue", "it is widely regarded")
216
+
217
+ Severities: `warning` for strong tells, `info` for weak/contextual ones. No
218
+ rule ships at `error` yet -- reserved for a pattern with essentially zero
219
+ false-positive risk, which none has demonstrated.
220
+
221
+ ## Rule catalog (v1)
222
+
223
+ ### rhetorical-tic (from the request)
224
+
225
+ | id | catches | notes |
226
+ |----|---------|-------|
227
+ | `no-x-no-y` | 2+ "no …" items in a row | counts items |
228
+ | `thats-the-whole` | "that/this is the whole point/game/thing…" | |
229
+ | `did-not-x-did-not-y` | 2+ "did not …"/"didn't …" in a row | counts items |
230
+ | `dont-verb-it` | "Don't call it X. Call it Y." (negated verb+it, same verb+it) | |
231
+ | `sit-with-that` | "sit with that/this/it", "sit with the discomfort" | |
232
+ | `you-already-know` | "you already know" (+ the answer / standalone) | |
233
+ | `is-the-entire` | "X is the entire point/game/business model" | |
234
+ | `the-entire-is` | "the entire point/game/… is" (flip of above) | |
235
+ | `is-real-and-not` | "the X is real, and/not…", "is the real … and it" | skip "real estate/time" |
236
+ | `the-punchline-is` | "the punchline is/:/?" | |
237
+ | `worth-naming` | "worth naming", "it's worth naming that…", "Worth naming:" | skip "naming names" |
238
+ | `thats-not-nothing` | "that/this/it/which is not nothing" | |
239
+
240
+ ### puffery (Wikipedia: Signs of AI writing)
241
+
242
+ Single flat rule per word-cluster, matched as whole words:
243
+
244
+ - `puffery-words` — boasts a, vibrant, rich (history/cultural/tapestry), nestled, in the heart of (gated to a place object), groundbreaking, renowned, diverse array, breathtaking, natural beauty, stands as a testament, indelible mark, deeply rooted.
245
+ - `stands-serves-as` — "stands as / serves as", "is a testament/reminder to".
246
+ - `vital-role` — "plays a (vital/crucial/pivotal/significant/key) role".
247
+ - `underscores-highlights` — "underscores/highlights/emphasizes its (importance/significance)".
248
+ - `rich-tapestry` — "rich tapestry", "tapestry of".
249
+
250
+ ### structure
251
+
252
+ - `not-just-x-but-y` — "not just X, but (also) Y", "not only … but".
253
+ - `rule-of-three` — three parallel comma items ending a sentence (heuristic; `info` severity, off by default via `--select` since it false-positives).
254
+ - `em-dash-overuse` — >N em dashes per paragraph (default N=2); `info`.
255
+
256
+ ### hedging
257
+
258
+ - `vague-attribution` — "some (critics/experts/observers) (argue/say/believe)", "it is widely (regarded/considered/seen)", "many would argue".
259
+
260
+ ## Markdown handling
261
+
262
+ `--markdown` blanks out fenced code (```` ``` ````), inline code (`` ` ``), and
263
+ URLs before scanning, replacing them with same-length whitespace so line/column
264
+ stay correct. Off by default (plain-text mode) so it never silently eats prose.
265
+
266
+ ## Agent-first help text
267
+
268
+ This is a first-class requirement, not an afterthought.
269
+
270
+ - `sloplint --help` opens with a one-line what-it-does, then a **copy-pasteable
271
+ agent recipe** block:
272
+
273
+ ```
274
+ # Recommended for agents:
275
+ cat FILE | sloplint check --markdown -o json -
276
+ # exit 0 = clean, 1 = notes found, >1 = error
277
+ # each note: {path,line,column,severity,rule,message,excerpt,suggestion}
278
+ ```
279
+
280
+ - Every option has a full-sentence help string (no telegraphic fragments).
281
+ - `sloplint rules` prints the catalog: id, category, severity, one-line
282
+ description — and with `--json`, the machine version an agent can enumerate.
283
+ - `sloplint explain no-x-no-y` prints the rule's message, rationale, a bad
284
+ example and an ok (non-matching) example. Agents call this to decide whether a
285
+ flag is worth acting on.
286
+ - JSON output is stable and documented here; the schema is the contract.
287
+ - `--help` epilog links to `sloplint explain` and `docs/spec.md`.
288
+
289
+ ## Testing
290
+
291
+ RSpec (dev dependency), run via `rake spec`.
292
+
293
+ - Every rule ships `examples_bad` (must produce ≥1 note) and `examples_ok`
294
+ (must produce 0). `rules_spec.rb` iterates the catalog and asserts both — a
295
+ new rule without fixtures fails CI.
296
+ - `cli_spec.rb` covers: exit codes (0/1/2), stdin path, `-o json` parses and
297
+ matches the schema, `--select`/`--ignore`, `--markdown` skips code.
298
+ - A built-in slop fixture doubles as an integration example (known note count).
299
+
300
+ ## Explicitly out of scope for v1
301
+
302
+ - Config files (`.sloplintrc`). Add when someone needs per-project rule tuning;
303
+ `--select`/`--ignore` cover the common case first.
304
+ - LSP server mode, editor plugins, autofix/rewrite. sloplint *flags*; the agent
305
+ rewrites. Autofix is a separate tool if ever.
306
+ - Non-English. Languages other than English are a v2 conversation.
307
+ - ML/embedding-based detection. This is a regex linter on purpose — fast,
308
+ explainable, zero-dependency. Statistical detection is a different product.