sloplint 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +263 -1
- data/README.md +34 -7
- data/docs/SPEC.md +37 -5
- data/exe/sloplint +13 -0
- data/lib/sloplint/cli.rb +30 -8
- data/lib/sloplint/rules.rb +1807 -15
- data/lib/sloplint/version.rb +1 -1
- metadata +4 -4
- data/bin/sloplint +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0d26e3bebafd7184f09aa41f70d1013e8ca6d9f460d4e0229815c18db08263a
|
|
4
|
+
data.tar.gz: 944923cfab5a213d08a5367bae1dfd75fb4fc71aa9a004fbb59806b5dace1376
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19ad341fcb7a415b737bc4e1fdd3f355d4b6bd91429209477b4ac4605a2f18b7372bf1791e34e9e0e69b6d50b17a02abf959981b7bd32b5345a30ebc3151cb87
|
|
7
|
+
data.tar.gz: 9f0732fe535cc7753ed2b8380687575c4b109b5f9be31409e1d207c9ccb3b7ffbcb69a833427e68bb73c905369b47dd920ce7dd7162e456a61dd7a07c468c853
|
data/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,269 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Format loosely
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
-
## [
|
|
6
|
+
## [0.6.0] - 2026-09-08
|
|
7
|
+
|
|
8
|
+
The catalog grows from 50 rules to 75, and sloplint ships as a Claude Code
|
|
9
|
+
plugin as well as a gem.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- sloplint now ships as a Claude Code plugin as well as a gem. Add the
|
|
14
|
+
marketplace `benjaminjackson/sloplint` and install `sloplint`; the plugin
|
|
15
|
+
carries the Ruby source with it, so nothing is installed from a package
|
|
16
|
+
index and it works where `gem install` cannot reach the network — Claude
|
|
17
|
+
Cowork above all. The plugin adds one skill, `/sloplint:check`, which runs
|
|
18
|
+
the scanner and reports what it returns. The skill refuses to assess prose
|
|
19
|
+
on its own when the scanner cannot run, because a review with no scan
|
|
20
|
+
behind it reads exactly like one with a scan behind it.
|
|
21
|
+
- `not-by-x-but-by-y`: the corrective frame on a repeated preposition, "not
|
|
22
|
+
by luck, but by design". The copula rules cannot see it because nothing
|
|
23
|
+
anchors "not" but a verb or a dash, so the anchor is the preposition, which
|
|
24
|
+
must repeat after "but". Ships at `info`: Thoreau and Melville write it too.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `check` now reads files and stdin as UTF-8 whatever the locale says. A
|
|
29
|
+
sandbox with no `LANG` set — Claude Cowork's, among others — leaves Ruby's
|
|
30
|
+
default external encoding at US-ASCII, and the first em dash in a draft then
|
|
31
|
+
failed the whole scan with `invalid input: invalid byte sequence in
|
|
32
|
+
US-ASCII`. Prose is the only input sloplint takes, so UTF-8 is the
|
|
33
|
+
assumption rather than the locale's guess. Genuinely invalid UTF-8 is still
|
|
34
|
+
exit 2.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `thats-the-whole` now takes `value` and `fix` after "that's the whole",
|
|
39
|
+
alongside `point`, `game`, `thing`, `deal` and `story`. The contracted
|
|
40
|
+
closer is the one an agent writes in technical prose ("That's the whole
|
|
41
|
+
value of the `info` tier", "That's the whole fix"), and `is-the-whole-x`
|
|
42
|
+
sees only the uncontracted "is", so the sentence slipped both rules. The
|
|
43
|
+
uncontracted "That is the whole fix." moves with it, from `info` under
|
|
44
|
+
`is-the-whole-x` to `warning`. The two new nouns must end the sentence or
|
|
45
|
+
the paragraph, or run into one of the words a closer trails off on, so
|
|
46
|
+
`thats-the-whole` leaves "value chain", "value-add" and "fix list" alone
|
|
47
|
+
however the line wraps (`is-the-whole-x` still reads the uncontracted
|
|
48
|
+
"That is the whole value chain" at `info`, as it did), and a curly
|
|
49
|
+
apostrophe now counts as a contraction. Both rules interpolate one `WHOLE_CLOSERS`
|
|
50
|
+
fragment, so the lists cannot drift, and a spec walks every noun through
|
|
51
|
+
every form the two wrappers treat differently. Neither noun appears after
|
|
52
|
+
"that's the whole" in 1.55M words of public-domain prose.
|
|
53
|
+
|
|
54
|
+
- Empty or whitespace-only input
|
|
55
|
+
is now exit 2 with `empty input: nothing to
|
|
56
|
+
check in ...`, instead of exit 0. Reporting a scan of nothing as a clean
|
|
57
|
+
scan is the same trap a mistyped rule id sets, and gets the same answer. The
|
|
58
|
+
text is tested before `--markdown` blanks code and URLs, so a file holding
|
|
59
|
+
only a fenced code block still exits 0; and only an all-empty set of sources
|
|
60
|
+
is an error, so one empty file among several named ones is left alone.
|
|
61
|
+
|
|
62
|
+
- The executable moved from `bin/sloplint` to `exe/sloplint`. claude.ai
|
|
63
|
+
rejects a plugin with a top-level `bin/` directory, which would block
|
|
64
|
+
organization distribution. `gem install sloplint` is unaffected.
|
|
65
|
+
|
|
66
|
+
- `exe/sloplint` now checks `RUBY_VERSION` before loading anything and aborts
|
|
67
|
+
with an explanation, rather than raising `NoMethodError` from `Data.define`.
|
|
68
|
+
The Ruby macOS ships at `/usr/bin/ruby` is 2.6 and hits this.
|
|
69
|
+
|
|
70
|
+
### New rules
|
|
71
|
+
|
|
72
|
+
- `from-x-to-y-chain` (rhetorical-tic, warning) flags two or more
|
|
73
|
+
comma-separated "from X to Y" spans in a row: "from guessing to measuring,
|
|
74
|
+
from hoping to knowing". Each span names two poles and nothing between
|
|
75
|
+
them, and stacking them sweeps across a change without describing it. Two
|
|
76
|
+
human shapes are skipped: the relay, where each span starts where the last
|
|
77
|
+
ended ("from the egg to the worm, from the worm to the fly"), and the
|
|
78
|
+
reduplication, where a span has the same word at both ends ("from hummock
|
|
79
|
+
to hummock"). Operands must open with a letter, so a list of ranges ("from
|
|
80
|
+
1990 to 1995, from 1997 to 2001") is not a chain. With those out it flags once in 2.4M words of pre-2022
|
|
81
|
+
Hacker News (a geographic sweep) and once in 1.25M words of public-domain
|
|
82
|
+
prose (Joyce).
|
|
83
|
+
|
|
84
|
+
- `one-x-one-y` (rhetorical-tic, warning) flags three or more "one X" items
|
|
85
|
+
in a comma chain that stands on its own: "One owner, one repository, one
|
|
86
|
+
weekly prune." The word does no counting; it sets a rhythm. The chain must
|
|
87
|
+
open a sentence or follow a colon, because after a verb ("the flat has one
|
|
88
|
+
bedroom, one bathroom, one balcony") the word is counting. Items are
|
|
89
|
+
letter-led, so an enumeration over numbers is out; the distributive "one
|
|
90
|
+
for you, one for me, one for the pot" is skipped at any length. Nothing in
|
|
91
|
+
2.4M words of pre-2022 Hacker News or 1.25M words of public-domain prose.
|
|
92
|
+
|
|
93
|
+
- `everyone-nobody` (structure, warning) flags the comma-spliced antithesis
|
|
94
|
+
on quantifier subjects: "Everyone wants the dashboard, nobody maintains
|
|
95
|
+
it." The
|
|
96
|
+
balance is what makes the diagnosis sound settled. The comma splice is
|
|
97
|
+
required (with "and" it is a sentence, with a period it is two), the two
|
|
98
|
+
subjects must differ in polarity, and the second clause must close the
|
|
99
|
+
sentence. Nothing in 2.4M words of pre-2022 Hacker News or 1.25M words of
|
|
100
|
+
public-domain prose.
|
|
101
|
+
|
|
102
|
+
- `and-what-it-should` (rhetorical-tic, warning) flags the elliptical tail:
|
|
103
|
+
"List what the assistant knows about the client, and what it should." The
|
|
104
|
+
second clause borrows its verb from the first and closes on a bare modal
|
|
105
|
+
or a negated auxiliary, so the sentence ends on a contrast it never
|
|
106
|
+
states. The comma, the conjunction, and the full stop right after the
|
|
107
|
+
modal are all required; the affirmative copula and do-verb ("and what he
|
|
108
|
+
does.") are complete clauses and stay out, and so does a question.
|
|
109
|
+
Nothing in 2.4M words of pre-2022 Hacker News or 1.25M words of
|
|
110
|
+
public-domain prose.
|
|
111
|
+
|
|
112
|
+
- `abstract-lives-in` (rhetorical-tic, info) flags an abstraction given an
|
|
113
|
+
address: "the craft that lives between the two desks", "its context lives
|
|
114
|
+
in a folder nobody else can open", "the value sits in the follow-up". The
|
|
115
|
+
subject list is closed and abstract, so people and dogs living and sitting
|
|
116
|
+
places never match, and a capitalised subject is a proper noun. "with" is
|
|
117
|
+
left out of the prepositions because "the decision sits with the board" is
|
|
118
|
+
ordinary English for who is responsible, "at" because "the value sits at
|
|
119
|
+
ten million" is a quantity, and "lies in" because "the problem lies in
|
|
120
|
+
the assumption" is where a fault is. It ships at info because the same
|
|
121
|
+
shape says where information literally is ("the knowledge lives in our
|
|
122
|
+
heads"), and a sample of pre-2022 Hacker News biased toward the
|
|
123
|
+
construction turns up a few of those per million words, all human. A
|
|
124
|
+
draft that keeps giving ideas addresses should be read as a warning.
|
|
125
|
+
|
|
126
|
+
- `np-fragment-and` (structure, info) flags a whole sentence made of two
|
|
127
|
+
noun phrases and an "and": "A named owner and a quarterly review." It is
|
|
128
|
+
the fix half of a model's problem-then-fix pair with the verb left out,
|
|
129
|
+
and its usual habitat is a bulleted list, so a list marker may open it.
|
|
130
|
+
Each phrase is one to three words, and no auxiliary or modal may appear,
|
|
131
|
+
contractions included. It ships at info because a lexical verb is
|
|
132
|
+
invisible to the pattern: "A car and a truck collided." has the same
|
|
133
|
+
shape and flags. That sentence is rare in the corpora (nothing in 2.4M
|
|
134
|
+
words of pre-2022 Hacker News, once in 1.25M words of public-domain
|
|
135
|
+
prose), but it is a sentence, so one flag is a question; a draft full of
|
|
136
|
+
them should be read as a warning.
|
|
137
|
+
|
|
138
|
+
- `the-x-is-the-x` (rhetorical-tic, warning) flags the repeated-head
|
|
139
|
+
equative: "the reason it holds up is the reason the other half happens",
|
|
140
|
+
"the problem with the tool is the problem with the team". A backreference
|
|
141
|
+
catches the same abstract head noun on both sides of the copula, so the
|
|
142
|
+
sentence equates two things while naming neither. The head list holds
|
|
143
|
+
only nouns that cannot name an object, since "the key to the front door
|
|
144
|
+
is the key on the red fob" is an identity statement; the two heads must
|
|
145
|
+
share a clause, so an earlier "the cost was low, but shipping is the
|
|
146
|
+
cost" never pairs; and the second head must be followed by a preposition,
|
|
147
|
+
determiner, quantifier, pronoun, plural noun or punctuation, so "the
|
|
148
|
+
answer key" is not a repeat. Once in 2.4M words of pre-2022 Hacker News;
|
|
149
|
+
nothing in 1.25M words of public-domain prose.
|
|
150
|
+
|
|
151
|
+
- `same-determiner-chain` (rhetorical-tic, info) is the quiet cousin of
|
|
152
|
+
`one-x-one-y`: three or more comma-separated items opening on the same
|
|
153
|
+
determiner or quantifier, "every file, every branch, every deploy", caught
|
|
154
|
+
with a backreference. The narrative possessives (my, his, her, their, its)
|
|
155
|
+
are left out, since "his fame, his position, his life" is every
|
|
156
|
+
novelist's. It ships at info because the device is one humans use on
|
|
157
|
+
purpose: about 8 per million words of pre-2022 Hacker News and 14 per
|
|
158
|
+
million
|
|
159
|
+
in public-domain prose. One is a question; several in a draft should be
|
|
160
|
+
read as a warning.
|
|
161
|
+
|
|
162
|
+
- `quip-question` (structure, info) flags the verbless question that opens
|
|
163
|
+
a pitch: "No invite?", "New to the tool?", "Still stuck?". It must start a
|
|
164
|
+
sentence, open on one of a short list of words, and close on the question
|
|
165
|
+
mark within four more words with no auxiliary or contraction, so a real
|
|
166
|
+
question stays out; "Need" and "Want" are left off the list because "Need
|
|
167
|
+
help?" is a question with its verb elided. Ships at info: about six per
|
|
168
|
+
million words of pre-2022 Hacker News, all of them replies asking the
|
|
169
|
+
same shape of a person; nothing in public-domain prose outside Joyce's
|
|
170
|
+
dialogue. Several in a draft should be read as a warning.
|
|
171
|
+
|
|
172
|
+
- `is-the-whole-x` (rhetorical-tic, info) is `thats-the-whole` on any
|
|
173
|
+
subject: "That periodicity is the whole tell.", "Consistency is the real
|
|
174
|
+
test." It yields only the exact sentences the two older rules own
|
|
175
|
+
("that/this is the whole point/game/thing/deal/story" and "is the entire
|
|
176
|
+
point/game/thing/deal/story"), so nothing is reported twice and "This is
|
|
177
|
+
the real test." is not lost. An interrogative subject is out, since a
|
|
178
|
+
question is not a closer; "only", "deal", "thing" and "cost" are left out
|
|
179
|
+
because "is the only thing", "the real deal", "the real thing" and "the
|
|
180
|
+
whole cost" are ordinary speech. Ships at info: about three per million words of
|
|
181
|
+
pre-2022 Hacker News, nothing in public-domain prose. Several in a draft
|
|
182
|
+
should be read as a warning.
|
|
183
|
+
|
|
184
|
+
- `bare-equative` (rhetorical-tic, info) flags a sentence that opens on an
|
|
185
|
+
abstract head noun and equates it with a definite noun phrase: "The tell
|
|
186
|
+
here is the periodicity.", "The lesson is the handoff." The head list is
|
|
187
|
+
the one `the-x-is-the-x` uses, so "The key is the brass thing on the hook"
|
|
188
|
+
is a definition and stays out; the copula (including "isn't") must be
|
|
189
|
+
followed by "the" and a lowercase word, so "The problem is real", "The
|
|
190
|
+
answer is a mess", "the same", "the first" and "the Slack thread" are all
|
|
191
|
+
out; a list marker may open it. Ships at info: about four per million
|
|
192
|
+
words of pre-2022 Hacker News, nothing in public-domain prose. Several in
|
|
193
|
+
a draft should be read as a warning.
|
|
194
|
+
|
|
195
|
+
- `mic-drop-closer` (structure, info) flags the kicker: a sentence of sixty
|
|
196
|
+
or more characters, then a two-to-eight-word closer that ends the
|
|
197
|
+
paragraph and opens on a quantifier or deictic, "Nothing here needs a new
|
|
198
|
+
login.", "Most teams end up with two." A blank line or the end of the
|
|
199
|
+
text must follow the closer, so a bullet followed by another bullet is
|
|
200
|
+
not one; both sentences may be hard-wrapped; and whitespace runs in the
|
|
201
|
+
long sentence are capped, so a URL blanked by `--markdown` cannot make it.
|
|
202
|
+
Ships at info because people end paragraphs this way too, at about 150
|
|
203
|
+
per million words of pre-2022 Hacker News (a comment ends on a verdict);
|
|
204
|
+
one flag means nothing. A draft where it repeats paragraph after
|
|
205
|
+
paragraph is the tell, and the rationale tells the agent to read that as
|
|
206
|
+
a warning.
|
|
207
|
+
|
|
208
|
+
- `short-run` (structure, info) flags three consecutive sentences of thirty
|
|
209
|
+
characters or fewer, each closing on a full stop with no quotation mark:
|
|
210
|
+
"Nobody used it. A named owner. Then a review." The run must start at a
|
|
211
|
+
real sentence boundary, so the short tail of a hard-wrapped sentence never
|
|
212
|
+
opens one; dialogue is out by the boundary, sentences with digits as data,
|
|
213
|
+
initials and abbreviations as not sentence ends, consecutive bullets as a
|
|
214
|
+
list, and questions and exclamations by design. Ships at info: a staccato
|
|
215
|
+
run is a device people use on purpose, at about thirty per million words
|
|
216
|
+
of pre-2022 Hacker News. A draft that keeps doing it is the tell, and the
|
|
217
|
+
rationale tells the agent to read that as a warning.
|
|
218
|
+
|
|
219
|
+
- `epistrophe` (rhetorical-tic, info, off by default) flags two clauses
|
|
220
|
+
that end on the same two-word phrase, the second closing the sentence:
|
|
221
|
+
"built for one desk, and almost no job is done at one desk." Two
|
|
222
|
+
backreferences catch the repeat, so the phrase may be hard-wrapped; an
|
|
223
|
+
article-led phrase, a short second word, a long or punctuated second
|
|
224
|
+
clause, and a clause made of blanked Markdown are all out. It is off by
|
|
225
|
+
default like `rule-of-three`: the figure is one Emerson and Marcus
|
|
226
|
+
Aurelius use on purpose, and on pre-2022 Hacker News most of the 42 hits
|
|
227
|
+
per million words are plain phrase reuse. Select it when a draft is
|
|
228
|
+
suspected of leaning on it; several then should be read as a warning.
|
|
229
|
+
|
|
230
|
+
- `intersection-of` (rhetorical-tic, warning) flags "the intersection of X and
|
|
231
|
+
Y" used as positioning: a writer placed between two fields, saying nothing
|
|
232
|
+
about either. "at" is not required, so "explores the intersection of art and
|
|
233
|
+
technology" flags the same as "sits at the intersection of". Two guards keep
|
|
234
|
+
the literal senses out. A street corner names capitalised streets ("Elm",
|
|
235
|
+
"Broadway", "Highway 12"), so the word after "of" must be lowercase or an
|
|
236
|
+
all-caps acronym; "AI", "UX" and "HCI" are the metaphor's usual operands and
|
|
237
|
+
no street is spelled that way. Geometry and set arithmetic name their operands,
|
|
238
|
+
so a literal noun ("curves", "lines", "arrays", "ranges", "roads") within two
|
|
239
|
+
words of "of" drops the note.
|
|
240
|
+
|
|
241
|
+
- `and-nothing-else` (rhetorical-tic, warning) flags a sentence that closes on
|
|
242
|
+
", and nothing else", ", nothing more", ", nothing further" or ", and no
|
|
243
|
+
more". A model told to return one thing and nothing else carries the phrasing
|
|
244
|
+
into the prose it writes afterwards, where the exclusion repeats what the
|
|
245
|
+
sentence already said.
|
|
246
|
+
- `nothing-else-frag` (rhetorical-tic, warning) flags the same exclusion built
|
|
247
|
+
as a fragment: "Return the JSON. Nothing else."
|
|
248
|
+
- `honestly`, `honest-x` and `most-honest-x` (rhetorical-tic, warning) answer
|
|
249
|
+
the "honest" habit the way `cleanly`, `clean-x` and `cleanest-x` answer the
|
|
250
|
+
"clean" one. `honestly` takes the manner adverb closing on a full stop or
|
|
251
|
+
comma ("these two rows compare honestly"); `honest-x` takes the adjective in
|
|
252
|
+
front of the writer's own construction ("an honest comparison", "the honest
|
|
253
|
+
framing"); `most-honest-x` takes the self-ranking superlative.
|
|
254
|
+
|
|
255
|
+
`honestly` is anchored on position alone, so a dialogue tag ("said Isabel
|
|
256
|
+
honestly") and the sentence-final hedge of casual speech ("it's beyond boring
|
|
257
|
+
honestly") both match. Both are rare: twice in 3.65M words of public-domain
|
|
258
|
+
prose and ten times in 6.07M words of pre-2022 Hacker News. The two noun
|
|
259
|
+
rules flag nothing in either corpus; "an honest answer" and "an honest
|
|
260
|
+
assessment" are left out because on Hacker News they mean a person telling
|
|
261
|
+
the truth, not a writer praising their own framing.
|
|
262
|
+
|
|
263
|
+
Both rules are wide on purpose. There is no verb list and no imperative
|
|
264
|
+
requirement, so the only narrowing is structural, and English has always used
|
|
265
|
+
this tail: the pair flags three times in 1.92M words of public-domain prose
|
|
266
|
+
and twice in 461k words of pre-2022 Hacker News, and `and-nothing-else` flags
|
|
267
|
+
every refrain in "The Raven". Expect to dismiss it on fiction and on quoted
|
|
268
|
+
verse.
|
|
7
269
|
|
|
8
270
|
## [0.5.0] - 2026-08-27
|
|
9
271
|
|
data/README.md
CHANGED
|
@@ -10,7 +10,32 @@ A pattern earns a place in the catalog only if it shows up constantly in AI writ
|
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
|
-
Requires **Ruby 3.3+** and nothing else. The runtime is standard library only (`optparse`, `json`, native regex).
|
|
13
|
+
Requires **Ruby 3.3+** and nothing else. The runtime is standard library only (`optparse`, `json`, native regex). The Ruby that macOS ships at `/usr/bin/ruby` is 2.6 and too old; `brew install ruby` gets you a current one.
|
|
14
|
+
|
|
15
|
+
### In Claude Cowork or the Claude desktop app
|
|
16
|
+
|
|
17
|
+
Open **Customize → Plugins → Add marketplace** and paste exactly this:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
benjaminjackson/sloplint
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Not the web address of the page you are reading — just those two words with the slash, and no slash on the end. `https://github.com/benjaminjackson/sloplint` works too, but a link copied from a page you were browsing usually has more on the end of it and will fail. Then install **sloplint** from the list.
|
|
24
|
+
|
|
25
|
+
Installing this way needs no package install of any kind, which is why it works in Cowork, where `gem install` cannot reach the internet. Claude Code carries the source along with the plugin and runs it in place.
|
|
26
|
+
|
|
27
|
+
New rules reach you when you click **Update** on the marketplace. Nothing updates on its own.
|
|
28
|
+
|
|
29
|
+
Once it is installed, ask Claude to check a draft, or run `/sloplint:check`.
|
|
30
|
+
|
|
31
|
+
### In Claude Code
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
/plugin marketplace add benjaminjackson/sloplint
|
|
35
|
+
/plugin install sloplint@sloplint
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### In a terminal
|
|
14
39
|
|
|
15
40
|
```bash
|
|
16
41
|
gem install sloplint
|
|
@@ -125,22 +150,24 @@ Three codes carry the contract. A crash exits nonzero on its own.
|
|
|
125
150
|
| 1 | ran, notes found |
|
|
126
151
|
| 2 | bad arguments or usage error |
|
|
127
152
|
|
|
128
|
-
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.
|
|
153
|
+
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. Input that is empty or only whitespace is exit 2 for the same reason: a pipe that delivered nothing must not read as a clean draft. Only when every source is empty — one empty file among several named ones is taken as deliberate.
|
|
129
154
|
|
|
130
155
|
## The rule catalog
|
|
131
156
|
|
|
132
|
-
|
|
157
|
+
75 rules across four categories. `sloplint rules` prints them; `sloplint rules --json` gives an agent the enumerable form.
|
|
133
158
|
|
|
134
|
-
- **rhetorical-tic** (
|
|
135
|
-
- **puffery** (
|
|
136
|
-
- **structure** (
|
|
159
|
+
- **rhetorical-tic** (53) the cadence patterns: `no-x-no-y`, `no-x-no-y-frag`, `from-x-to-y-chain` ("from guessing to measuring, from hoping to knowing"), `one-x-one-y` ("one reviewer, one queue, one deadline"), `same-determiner-chain` (any other repeated determiner, at `info`), `and-what-it-should` (the elliptical tail: "…, and what it should."), `abstract-lives-in` ("the value sits in the follow-up", at `info`), `the-x-is-the-x` ("the problem with A is the problem with B"), `bare-equative` ("The lesson is the handoff.", at `info`), `thats-the-whole`, `is-the-whole-x` (the same closer on any subject: "Consistency is the real test.", at `info`), `thats-how-x`, `announced-takeaway`, `exact-exactly`, `load-bearing`, `intersection-of`, `you-already-know`, `sit-with-that`, `hold-onto-that`, `cleanly`, `clean-count`, `cleanest-x`, `clean-x`, `not-nothing`, `is-is` (doubled copula), `if-im-being-honest` (the candor preamble, from slopwash.com's "false intimacy"), `and-nothing-else` (the trailing "…, and nothing else"), `honestly`, `honest-x`, `most-honest-x` (the honesty family, built the same way as the four `clean` rules above), `impact-verb` ("the outage impacted four thousand accounts"), `impact-noun-bare` ("the impact of X", at `info` because research prose uses it straight), and more.
|
|
160
|
+
- **puffery** (6) five from Wikipedia's "signs of AI writing" — `puffery-words` (vibrant, nestled, groundbreaking, in the heart of), `rich-tapestry`, `vital-role`, `stands-serves-as`, `underscores-highlights` — plus `impact-noun-vague` ("a significant impact", "make an impact").
|
|
161
|
+
- **structure** (15) `everyone-nobody` (the comma-spliced antithesis: "Everyone wants the dashboard, nobody maintains it."), `np-fragment-and` (the verbless "A named owner and a quarterly review.", at `info`), `quip-question` (the verbless "No invite?", at `info`), `mic-drop-closer` (the short quantifier-led sentence that ends a paragraph after a long one, at `info`; a draft full of them is a warning), `short-run` (three sentences of thirty characters or fewer in a row, at `info`, same caveat), `not-just-x-but-y`, `not-x-but-y` (the bare corrective), `isnt-x-its-y` (the same corrective split across two clauses: "It isn't the tool. It's the habit."), `not-by-x-but-by-y` (the corrective on a repeated preposition: "not by luck, but by design"), `em-dash` (any em dash), `em-dash-overuse` (three or more in one paragraph), `question-isnt` (the corrective frame in interrogative dress), `less-about-more-about`, `trailing-significance-participle` (the "…, showcasing its importance" clause), and `rule-of-three`.
|
|
137
162
|
- **hedging** (1) `vague-attribution`: "some critics argue," "it is widely regarded."
|
|
138
163
|
|
|
139
164
|
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.
|
|
140
165
|
|
|
141
166
|
Some tells come in a confident form and an ambiguous one, and those ship as a pair rather than as one rule stretched over both. `no-x-no-y` wants the comma chain a writer clearly authored; `no-x-no-y-frag` takes the same cadence built from sentence fragments, which ordinary prose also produces, and ships at `info`. Same with `not-just-x-but-y` and `not-x-but-y`, and with `notice-what-there` and `notice-what`. The quiet half is still worth flagging — an agent that reads the rationale can judge — but it should not carry the same weight as the half we're sure about.
|
|
142
167
|
|
|
143
|
-
|
|
168
|
+
`and-nothing-else` and `nothing-else-frag` are a pair of the same shape, but both ship at `warning`. That is a deliberate exception: the fragment half carries a capital letter and a whole-sentence requirement that the comma half has no equivalent of, so it is the *narrower* of the two rather than the quieter one.
|
|
169
|
+
|
|
170
|
+
Three rules ship **off by default**, and they run only when you name them: `sloplint check --select rule-of-three -`. `rule-of-three` flags three parallel comma items closing a sentence, which humans do all the time. `genuinely` flags every occurrence of the word; as an intensifier it rates the writer's sincerity, but it still does real work when it draws a contrast, and nothing in the sentence separates the two. `epistrophe` flags two clauses ending on the same phrase, a named figure that careful writers use on purpose and that, on Hacker News, is mostly plain phrase reuse.
|
|
144
171
|
|
|
145
172
|
### Markdown handling
|
|
146
173
|
|
data/docs/SPEC.md
CHANGED
|
@@ -93,13 +93,15 @@ dependencies at runtime.
|
|
|
93
93
|
Rationale: rules are regexes; the whole thing is a scanner plus an output
|
|
94
94
|
formatter. A dependency-free `gem install sloplint` is the robust, boring
|
|
95
95
|
choice. The name is free on RubyGems (taken on PyPI, so this also sidesteps the
|
|
96
|
-
collision). Ships a `sloplint` executable
|
|
96
|
+
collision). Ships a `sloplint` executable from `exe/` (claude.ai rejects a
|
|
97
|
+
plugin with a top-level `bin/`, and sloplint ships as a Claude Code plugin
|
|
98
|
+
too).
|
|
97
99
|
|
|
98
100
|
Package layout (standard gem):
|
|
99
101
|
|
|
100
102
|
```
|
|
101
103
|
sloplint/
|
|
102
|
-
|
|
104
|
+
exe/sloplint # thin shim: require "sloplint/cli"; exit Sloplint::CLI.run(ARGV)
|
|
103
105
|
lib/sloplint.rb # requires the pieces below
|
|
104
106
|
lib/sloplint/version.rb
|
|
105
107
|
lib/sloplint/cli.rb # optparse, subcommands, exit codes
|
|
@@ -161,6 +163,11 @@ Three codes carry the contract. A crash just exits nonzero on its own.
|
|
|
161
163
|
| 1 | ran, **notes found** |
|
|
162
164
|
| 2 | bad arguments / usage error |
|
|
163
165
|
|
|
166
|
+
Empty or whitespace-only input is exit 2, like a mistyped rule id: a scan of
|
|
167
|
+
nothing must not report as a clean scan. The text is tested before
|
|
168
|
+
`--markdown` blanks code and URLs, so a file that holds only a fenced code
|
|
169
|
+
block still exits 0. Only when every source is empty.
|
|
170
|
+
|
|
164
171
|
## Note (the diagnostic object)
|
|
165
172
|
|
|
166
173
|
One match = one Note. JSON output is an array of these (or an object keyed by
|
|
@@ -270,12 +277,20 @@ false-positive risk, which none has demonstrated.
|
|
|
270
277
|
|----|---------|-------|
|
|
271
278
|
| `no-x-no-y` | 2+ comma-separated "no …" items in a row | counts items |
|
|
272
279
|
| `no-x-no-y-frag` | the same cadence as sentence fragments ("No fluff. No filler.") | counts items; `info` |
|
|
273
|
-
| `thats-the-whole` | "that/this is the whole point/game/thing…" | |
|
|
280
|
+
| `thats-the-whole` | "that/this is the whole point/game/thing…" | also value and fix, the closers agents write in technical prose, when they end the sentence or the paragraph or run into a closer's tail word; either apostrophe; `is-the-whole-x` yields the same list |
|
|
281
|
+
| `is-the-whole-x` | any subject + "is the whole/real/actual/entire N" (tell, point, test, work, …) | opens on the subject word; yields only the exact sentences `thats-the-whole` and `is-the-entire` own; interrogative subjects out; "only", "deal", "thing", "cost" left out; `info` |
|
|
274
282
|
| `did-not-x-did-not-y` | 2+ "did not …"/"didn't …" in a row | counts items |
|
|
283
|
+
| `from-x-to-y-chain` | 2+ comma-separated "from X to Y" spans | counts spans; operands open with a letter, so ranges are out; skips the relay ("to B, from B") and the reduplication ("from X to X") |
|
|
284
|
+
| `one-x-one-y` | 3+ comma-separated "one X" items standing on their own | counts items; must open a sentence or follow a colon, so a chain after a verb is counting; items letter-led; the distributive "one for …" skipped; a pair never flags |
|
|
285
|
+
| `and-what-it-should` | ", and what it should." — a second "what" clause closing on a bare modal or a negated auxiliary | comma, conjunction and full stop required; the affirmative copula and do-verb ("what he does.") are complete clauses and out; a question is out |
|
|
286
|
+
| `abstract-lives-in` | an abstract noun that "lives/sits in/between/inside/…" | closed subject list; capitalised subjects skipped; "with" (responsibility), "at" (quantity), "lies in" and "resides in" left out; `info` |
|
|
287
|
+
| `the-x-is-the-x` | "the X … is the X …": the same abstract head noun on both sides of the copula | backreference; closed list of heads that cannot name an object (key, cost, unit are out); the clause between is capped at 50 chars and may not hold a comma, semicolon or colon; the second head must be followed by a preposition, determiner, quantifier, pronoun, plural noun or punctuation, so compounds are out; "isn't" counts |
|
|
288
|
+
| `same-determiner-chain` | 3+ comma-separated items opening on the same determiner or quantifier (every, each, your, more, …) | backreference; counts items; "one" and "no" left to their own rules; the narrative possessives (my, his, her, their, its) left out; items lowercase-led, so proper nouns are not a chain; `info` |
|
|
289
|
+
| `bare-equative` | sentence-initial "The N (here) is (not) the …" with an abstract head noun | the-x-is-the-x's head list, so concrete heads are out; the copula (is, is not, isn't) must be followed by "the" and a lowercase word, so predicate adjectives, indefinites, pointing complements ("the same/one/first/…") and proper nouns are out; a list marker may open it; `info` |
|
|
275
290
|
| `dont-verb-it` | "Don't call it X. Call it Y." (negated verb+it, same verb+it) | |
|
|
276
291
|
| `sit-with-that` | "sit with that/this/it", "sit with the discomfort" | |
|
|
277
292
|
| `hold-onto-that` | sentence-initial "hold onto/on to that/this" | imperative only |
|
|
278
|
-
| `cleanly` | "cleanly"
|
|
293
|
+
| `cleanly` | "cleanly" into/onto/in two/in half — the partition frame | the preposition is the narrowing; the engineering idiom ("applies cleanly", "separated cleanly", "cleanly compiled") is a checkable fact and stays out, and the clause-final form is left out. `info`: the frame is not the sense, and separating "splits cleanly into two parts" from "retracted cleanly into the well" needs the subject |
|
|
279
294
|
| `clean-count` | "two/three clean parts/buckets/categories…" | needs a partition noun |
|
|
280
295
|
| `cleanest-x` | "the cleanest framing/formulation", "cleanest way to put it" | noun list only |
|
|
281
296
|
| `clean-x` | "a clean abstraction/distinction/framing", "clean line between" | `info` |
|
|
@@ -287,8 +302,11 @@ false-positive risk, which none has demonstrated.
|
|
|
287
302
|
| `worth-naming` | "worth naming/flagging/separating/spelling out" | skip "naming names"; yields to the rule below when a manner adverb follows; `info` |
|
|
288
303
|
| `worth-saying-plainly` | "it's worth saying plainly / better put bluntly…", plus the bare "Put plainly," / "Said bluntly," | sentence-initial; the bare branch drops "simply"/"clearly" so "put simply" and "simply put" stay clean |
|
|
289
304
|
| `not-nothing` | copula + "not nothing" litotes, any subject | skip personal/there subjects |
|
|
290
|
-
| `exact-exactly` | "exact"
|
|
305
|
+
| `exact-exactly` | the reflexive intensifier: "that's exactly", "exactly right", "exactly the point", "the exact problem", "know exactly why" | matches the tell as a closed set of frames rather than matching the word and subtracting an allow-list; a measured quantity ("the exact diameter", "the exact CPU time") is silent by construction. `info` |
|
|
291
306
|
| `load-bearing` | "load-bearing" outside its construction sense | skip building nouns either side |
|
|
307
|
+
| `intersection-of` | "the intersection of X and Y" as positioning | skip street corners, geometry, set arithmetic, airfield surfaces (runway, taxiway, apron), matrix rows and columns, and operands shaped like a US route designator ("US-27A") or a quadrant plus house number ("NE 140th Court") |
|
|
308
|
+
| `impact-verb` | "impact" used as a verb ("the outage impacted 4,000 accounts") | needs an auxiliary or subject pronoun for the base form; "to impact" requires a following object, so the preposition ("prior to impact") stays out; skip the medical and soil sense of "impacted", the struck object of a real collision ("impacted terrain"), the fixed compounds, and hyphenated forms |
|
|
309
|
+
| `impact-noun-bare` | "the impact of X", "measure the impact" — `info` | needs a measuring verb in front or "of" behind; skip the collision sense and the fixed compounds |
|
|
292
310
|
| `thats-how-x` | sentence-initial "that's how…" | |
|
|
293
311
|
| `announced-takeaway` | colon-led label: "The pattern/lesson/takeaway…:" | sentence-initial |
|
|
294
312
|
| `earns-its-place` | "earns its place/keep" (any possessive) | possessive required; `warning` |
|
|
@@ -301,8 +319,14 @@ false-positive risk, which none has demonstrated.
|
|
|
301
319
|
| `notice-what` | bare sentence-initial "Notice what…" | yields the "there" frame to the rule above; "how" excluded; `info` |
|
|
302
320
|
| `none-of-this-is-to-say` | "none of this/that/the above is to say" | every other "not to say" phrasing excluded |
|
|
303
321
|
| `if-im-being-honest` | "if I'm/we're (being) honest", "honestly, the answer/truth" | plain "to be honest" and "I'll be honest" excluded |
|
|
322
|
+
| `honestly` | a word, then "honestly", then a full stop or comma | terminal position required; discourse-marker slot guarded ("and/quite/but honestly,"); no animacy test |
|
|
323
|
+
| `honest-x` | "an honest comparison", "the honest framing" | short noun list; "answer", "assessment", "account" excluded as ordinary; superlative yielded to the rule below |
|
|
324
|
+
| `most-honest-x` | "the most honest framing", "the most honest way to put it" | wider noun list than `honest-x`; no human nouns, so "the most honest person" stays out |
|
|
304
325
|
| `genuinely` | any "genuinely" | off by default; no narrowing holds |
|
|
326
|
+
| `epistrophe` | two clauses ending on the same two-word phrase, the second closing the sentence | two backreferences, so the phrase may be hard-wrapped; no article-led phrase, second word 4+ letters, second clause 5–60 chars with no internal punctuation and capped whitespace; off by default; `info` |
|
|
305
327
|
| `and-thats-fine` | "And that's fine/okay." as a whole sentence | "and" required; must open and close the sentence |
|
|
328
|
+
| `and-nothing-else` | trailing ", and nothing else/more/further", ", and no more" | tail must close the sentence; comma required; bare "no more" needs "and"; "?" excluded |
|
|
329
|
+
| `nothing-else-frag` | the same exclusion as a fragment: "Nothing else." | sentence-initial capital; semicolon excluded; must be the whole sentence; "No more." left out |
|
|
306
330
|
| `is-is` | doubled copula: "what it is is …", "the thing is, is that …" | comma optional |
|
|
307
331
|
|
|
308
332
|
### puffery (Wikipedia: Signs of AI writing)
|
|
@@ -314,11 +338,19 @@ Single flat rule per word-cluster, matched as whole words:
|
|
|
314
338
|
- `vital-role` — "plays a (vital/crucial/pivotal/significant/key) role".
|
|
315
339
|
- `underscores-highlights` — "underscore(s)" + determiner and "underscored/underscoring" anywhere (the emphasis verb); "highlights/emphasizes its (importance/significance)" stays narrow.
|
|
316
340
|
- `rich-tapestry` — "rich tapestry", "tapestry of".
|
|
341
|
+
- `impact-noun-vague` — an intensity adjective plus "impact" (significant, real, meaningful, lasting, massive, huge, profound, big and the rest), or "make/have an impact". "positive" and "negative" stay out: they name a direction, which is more than the intensity words do. "statistically significant impact" is skipped — that one is a finding.
|
|
317
342
|
|
|
318
343
|
### structure
|
|
319
344
|
|
|
320
345
|
- `not-just-x-but-y` — copula + "not just/only/merely/simply/solely X … but (also) Y", plus "not because X, but because Y". Requires the escalation word.
|
|
321
346
|
- `not-x-but-y` — the bare corrective "is not X but Y" with no escalation word; `info`, because the corrective/concession distinction is syntactic and the pattern can only approximate it.
|
|
347
|
+
- `isnt-x-its-y` — the corrective with no conjunction: a negated copula, then a second clause that supplies the replacement ("It isn't the tool. It's the habit."), joined by a period, semicolon, comma, or dash; `info`. Neither complement may open with a pronoun, possessive, preposition ("about" excepted), degree word, or predicate adjective, which keeps out ordinary two-part contrast; some hits are still the same shape written by a person.
|
|
348
|
+
- `not-by-x-but-by-y` — the corrective anchored on a repeated preposition rather than a copula: "not by A, but by B", "not from A but from B" (by, for, from, with, about, in, on, at, to, of, through, because of, out of); `info`. The B side must repeat the A preposition, A is capped at six words, and B may not open with a pronoun. Every corpus hit is the real shape written by a person, which is why it sits at info rather than being narrowed further.
|
|
349
|
+
- `everyone-nobody` — the comma-spliced antithesis on quantifier subjects: one clause opens on everyone/everybody, the other on nobody/no one/none or "one N", joined by a bare comma, the second closing the sentence ("Everyone wants the dashboard, nobody maintains it."). A conjunction, a period, or the same subject twice is not a hinge; "one of/by/per", a measure, a proper noun, "none of which" and "none louder than" are not second subjects; clauses are capped at eighty characters.
|
|
350
|
+
- `np-fragment-and` — a whole sentence made of two noun phrases and an "and", opening on A/An/One at a sentence start or after a list marker ("A named owner and a quarterly review."). One to three words a side, no auxiliary or modal anywhere (contractions included); a lexical verb is invisible, so "A car and a truck collided." flags, which is why it ships at `info`.
|
|
351
|
+
- `quip-question` — the verbless opening question ("No invite?", "New to the tool?", "Still stuck?"): sentence-initial, one of a short list of opening words, one to four more words, no auxiliary or contraction, closing on the question mark; "Need" and "Want" are left out as elided verbs; `info`, because people ask the same shape in conversation.
|
|
352
|
+
- `mic-drop-closer` — a sentence of 60+ characters, then a paragraph-final closer of two to eight words opening on a **quantifier** (Nothing, Most, None, Everything, Everyone, Nobody, Then, Neither, Both): "Nothing here needs a new login."; `info`. The bare demonstratives (That, This, It) were in the list and are out: procedural writing ends a step with one as a matter of course ("This completes the roughing operations."). A blank line or the end of the text must follow the closer; both sentences may be hard-wrapped; whitespace runs in the long sentence are capped so blanked Markdown cannot make it, and the long-sentence prefix is an atomic group so an unpunctuated stretch cannot send it into catastrophic backtracking. The note points at the closer. One means nothing; a draft where it repeats is the tell.
|
|
353
|
+
- `short-run` — three consecutive sentences of thirty characters or fewer, each letter-led and closing on a full stop, no quotation marks or digits, no lone-letter labels (either case, though a possessive is not one) or abbreviations, starting at a real sentence boundary (never on a wrap continuation), crossing a hard wrap but not a paragraph break; `info`. A list marker may open a run but never sit inside one, so consecutive bullets are a list — the marker set covers glyphs, the literal "o" plain-text documents use as a bullet, and numbered and lettered items. Two whole-run exclusions keep document furniture out, each a property of the run rather than of any sentence in it: three single-word sentences in a row is a citation line ("Natl. Inst. Stand. Technol."), and three lowercase-led sentences in a row is transcribed speech ("we all get gas. we go to divert to Albany."). One single-word sentence is the archetypal kicker and stays, and a run that reaches a capital anywhere is prose, so identifier-initial writing ("npm was slow. git blame helped. We moved on.") is untouched. One is a question; a draft that repeats it is the tell.
|
|
322
354
|
- `question-isnt` — "the question isn't/is not (whether|if|how|what|why|who) X, it's/but Y"; `info`. The resolving clause is required, so a plain rhetorical question never matches; "the real question is" is excluded.
|
|
323
355
|
- `less-about-more-about` — "it's/this is/that's less about X (and) more about Y", also "… than about Y"; `info`. Both halves of the frame are required, and the subject slot is limited to the pronouns.
|
|
324
356
|
- `trailing-significance-participle` — comma plus a participle from a closed verb list (highlighting, showcasing, reinforcing, shaping, enhancing, cementing, solidifying, embodying, fostering, facilitating, signalling), the clause a model hangs off a sentence to say what a fact means. Guards drop gerund lists and "signalling to". `driving`, `representing`, `reflecting`, `marking`, `contributing`, `illustrating`, `demonstrating`, `emphasising`, `echoing` and `affirming` stay out: humans write them in the same position, usually with a person as the subject, and the pattern cannot see the subject. `underscoring` is left to `underscores-highlights`.
|
data/exe/sloplint
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Integer compare, not string: "3.10" < "3.3" is true and would reject a future
|
|
5
|
+
# Ruby. Array has <=> but no <, hence the explicit compare. Not Gem::Version --
|
|
6
|
+
# this has to hold when RubyGems is not loaded.
|
|
7
|
+
if (RUBY_VERSION.split(".").map(&:to_i).first(2) <=> [3, 3]) < 0
|
|
8
|
+
abort "sloplint needs Ruby 3.3 or later, found #{RUBY_VERSION}.\n" \
|
|
9
|
+
"macOS ships Ruby 2.6 at /usr/bin/ruby. Install a current one with `brew install ruby`."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative "../lib/sloplint/cli"
|
|
13
|
+
exit Sloplint::CLI.run(ARGV)
|
data/lib/sloplint/cli.rb
CHANGED
|
@@ -10,7 +10,8 @@ require_relative "output"
|
|
|
10
10
|
module Sloplint
|
|
11
11
|
# Command-line shell: optparse, subcommands, exit codes. See docs/SPEC.md.
|
|
12
12
|
#
|
|
13
|
-
# Exit codes: 0 ran/no notes, 1 ran/notes found, 2 bad arguments.
|
|
13
|
+
# Exit codes: 0 ran/no notes, 1 ran/notes found, 2 bad arguments. Empty
|
|
14
|
+
# input is a 2 as well: an unread draft must not report as a clean one.
|
|
14
15
|
module CLI
|
|
15
16
|
module_function
|
|
16
17
|
|
|
@@ -68,20 +69,38 @@ module Sloplint
|
|
|
68
69
|
paths = argv.empty? ? ["-"] : argv
|
|
69
70
|
by_path = paths.reject { |x| x == "-" }.size > 1
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
sources = []
|
|
72
73
|
paths.each do |path|
|
|
74
|
+
# Read as UTF-8 whatever the locale says. A sandbox with no LANG set
|
|
75
|
+
# leaves Ruby's default external encoding at US-ASCII, and then the
|
|
76
|
+
# first em dash raises "invalid byte sequence in US-ASCII" -- on prose
|
|
77
|
+
# that is perfectly valid UTF-8. Prose is the only input sloplint
|
|
78
|
+
# takes, so UTF-8 is the assumption, not the locale's guess.
|
|
73
79
|
text =
|
|
74
80
|
if path == "-"
|
|
75
|
-
stdin.read
|
|
81
|
+
stdin.read.force_encoding(Encoding::UTF_8)
|
|
76
82
|
else
|
|
77
83
|
unless File.file?(path)
|
|
78
84
|
err.puts("sloplint: no such file: #{path}")
|
|
79
85
|
return 2
|
|
80
86
|
end
|
|
81
|
-
File.read(path)
|
|
87
|
+
File.read(path, encoding: Encoding::UTF_8)
|
|
82
88
|
end
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
sources << [path == "-" ? "-" : path, text]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Tested on the raw text, before --markdown blanks code and URLs: a file
|
|
93
|
+
# that holds only a fenced code block did arrive, and scanning it clean
|
|
94
|
+
# is right. Nothing arriving at all is the trap -- the same one a
|
|
95
|
+
# mistyped rule id sets, and it exits 2 for the same reason.
|
|
96
|
+
if sources.all? { |_, text| text.strip.empty? }
|
|
97
|
+
names = sources.map { |label, _| label == "-" ? "stdin" : label }
|
|
98
|
+
err.puts("sloplint: empty input: nothing to check in #{names.join(", ")}")
|
|
99
|
+
return 2
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
all_notes = sources.flat_map do |label, text|
|
|
103
|
+
Engine.scan(text, rules:, markdown:, path: label)
|
|
85
104
|
end
|
|
86
105
|
|
|
87
106
|
case opts[:format]
|
|
@@ -93,7 +112,10 @@ module Sloplint
|
|
|
93
112
|
end
|
|
94
113
|
|
|
95
114
|
all_notes.empty? ? 0 : 1
|
|
96
|
-
|
|
115
|
+
# Invalid UTF-8 reaches this two ways: String#strip in the empty check
|
|
116
|
+
# raises Encoding::CompatibilityError, the engine's regexes raise
|
|
117
|
+
# ArgumentError. Both are the same thing to the reader.
|
|
118
|
+
rescue ArgumentError, Encoding::CompatibilityError => e
|
|
97
119
|
err.puts("sloplint: invalid input: #{e.message}")
|
|
98
120
|
2
|
|
99
121
|
end
|
|
@@ -187,7 +209,7 @@ module Sloplint
|
|
|
187
209
|
|
|
188
210
|
# Recommended for agents:
|
|
189
211
|
cat FILE | sloplint check --markdown -o json -
|
|
190
|
-
# exit 0 = clean, 1 = notes found, >1 = error
|
|
212
|
+
# exit 0 = clean, 1 = notes found, >1 = error (empty input is an error)
|
|
191
213
|
# each note: {path,line,column,severity,rule,category,message,excerpt,context,rationale,suggestion}
|
|
192
214
|
|
|
193
215
|
usage: sloplint [-o full|json] [command] [args]
|