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 +7 -0
- data/CHANGELOG.md +42 -0
- data/LICENSE +21 -0
- data/README.md +179 -0
- data/bin/sloplint +5 -0
- data/docs/SPEC.md +308 -0
- data/lib/sloplint/cli.rb +212 -0
- data/lib/sloplint/engine.rb +78 -0
- data/lib/sloplint/output.rb +37 -0
- data/lib/sloplint/rules.rb +373 -0
- data/lib/sloplint/version.rb +5 -0
- data/lib/sloplint.rb +7 -0
- metadata +87 -0
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
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.
|