sloplint 0.6.0 → 0.7.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 +66 -0
- data/README.md +8 -7
- data/docs/SPEC.md +12 -6
- data/lib/sloplint/cli.rb +15 -4
- data/lib/sloplint/engine.rb +7 -8
- data/lib/sloplint/rules.rb +472 -13
- data/lib/sloplint/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33eff57c67530d0745b1ba5009a97128929a73ec27ae09afaba8d9057f0f997b
|
|
4
|
+
data.tar.gz: 3b76104840a3c2ab471651cbdf18dd4a9027707aaf100bc901430c7f9e121216
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d1a47eae97395fa37dd1fa50b9a4894ccae70064d15542abdce0197cf715956d23c6ba75bee7bb4f56bc6ada8986485aff5bb1e06c9ecbb1ac6b763705588e1
|
|
7
|
+
data.tar.gz: cb1ba2ed058855336b71bdc6e10a743e021a5d5d85e0207b8e0564b0fcd9076c9c45eb021eee6fc15a4eac92709b3440ebb6e1487da3c8981853ff37ebd01419
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,72 @@
|
|
|
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
|
+
## [0.7.0] - 2026-09-15
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `--strict` runs the full catalog, including the five rules that ship off
|
|
11
|
+
by default. Before this, the only way to turn one on was to name it in
|
|
12
|
+
`--select`, which also turned off every other rule unless you listed
|
|
13
|
+
those too. `--ignore` still subtracts on top of `--strict`, and
|
|
14
|
+
`--select` is unchanged.
|
|
15
|
+
- `ellipsis-closer` (structure, info) flags the long-sentence-then-short-closer
|
|
16
|
+
kicker `mic-drop-closer` catches, but built on the opposite verb shape: the
|
|
17
|
+
closer's verb phrase is elided down to a bare auxiliary with no object,
|
|
18
|
+
"The agent did." rather than a quantifier plus a full verb. No subject list
|
|
19
|
+
is needed — the tell is the missing verb, not the subject — so "That",
|
|
20
|
+
"This" and "It" are not excluded the way `mic-drop-closer` excludes them.
|
|
21
|
+
A negative lookahead drops a closer that still holds a wh-word, since that
|
|
22
|
+
marks a subordinate clause with its own complement rather than an elided
|
|
23
|
+
one. Reuses `mic-drop-closer`'s long-sentence prefix.
|
|
24
|
+
- `actually-not-x`: the adverb and a trailing "…, not X" in one clause, as in
|
|
25
|
+
"The disclosure actually covered two incidents, not one". Either marker
|
|
26
|
+
alone is ordinary technical English and neither is matched on its own; the
|
|
27
|
+
tell is the two of them doubled up, correcting an alternative the reader was
|
|
28
|
+
never offered. The narrowing is structural, not a word list: the comma in
|
|
29
|
+
front of "not" has to be the first comma of its clause and has to follow a
|
|
30
|
+
word, so a fronted setup ("Despite the name, …") and a parenthetical both
|
|
31
|
+
drop it. Nothing crosses a line break, so two rows of a table cannot weld
|
|
32
|
+
into one correction.
|
|
33
|
+
- `real-x-real-y` (rhetorical-tic, info) flags the same "real" used twice
|
|
34
|
+
attributively in one sentence, in front of two different nouns: "it can
|
|
35
|
+
trigger real API calls or hold real credentials". Bare "real" is an
|
|
36
|
+
ordinary word, so the narrowing is the repetition itself, not a noun
|
|
37
|
+
list. A hyphen on either side takes it out ("real-time", "non-real"), a
|
|
38
|
+
backreference requires the second noun to differ from the first, and a
|
|
39
|
+
short closed list drops the fixed senses of this register (real time,
|
|
40
|
+
real-world, real numbers, real user monitoring, real estate, real money).
|
|
41
|
+
- `phrase-echo` (rhetorical-tic, info, off by default) flags a three-word
|
|
42
|
+
phrase that comes back within a few hundred words, the model reusing a
|
|
43
|
+
phrase it has just minted. Each word must be four characters or more and
|
|
44
|
+
lowercase-led, and one of them six letters with nothing but letters, so "in
|
|
45
|
+
order to", "couldn't have been" and "New York City" never match. The window
|
|
46
|
+
does not cross into a list item or a table row. Off by default: a term of
|
|
47
|
+
art repeats because it must, and in reference prose the pattern fires on
|
|
48
|
+
names by the thousand per million words.
|
|
49
|
+
- `trailing-restatement` (structure, info, off by default) flags the tail
|
|
50
|
+
that says the sentence again in other words: ", which means …", ", which
|
|
51
|
+
is to say …", ", meaning …", and four participles that hang a result off
|
|
52
|
+
the sentence (", making it easier", ", allowing us to", ", giving them
|
|
53
|
+
more", ", leaving you with"). The connective is all a pattern can see, and the
|
|
54
|
+
same connective states a real consequence in careful prose, so the rule
|
|
55
|
+
runs only when selected.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- `check`'s options now work without the command word: `sloplint --markdown -`
|
|
60
|
+
and `sloplint --strict -` scan, as the README has said `sloplint -` does.
|
|
61
|
+
Before, the global parser rejected any option it did not own with
|
|
62
|
+
`invalid option`, so `check` had to be spelled out whenever an option
|
|
63
|
+
came first. An option no command knows still exits 2.
|
|
64
|
+
- `--markdown` now blanks `<!-- -->` comment blocks along with fenced code,
|
|
65
|
+
inline code and URLs. Planning notes kept in comments never render, so
|
|
66
|
+
every rule that fired inside one was a permanent false positive.
|
|
67
|
+
- `rule-of-three`'s message, rationale, README and SPEC entries said it
|
|
68
|
+
flagged three parallel comma items. The pattern requires single words in
|
|
69
|
+
the closing two slots, so a triad of phrases never matched; the text now
|
|
70
|
+
says that.
|
|
71
|
+
|
|
6
72
|
## [0.6.0] - 2026-09-08
|
|
7
73
|
|
|
8
74
|
The catalog grows from 50 rules to 75, and sloplint ships as a Claude Code
|
data/README.md
CHANGED
|
@@ -60,7 +60,7 @@ The recipe sloplint is built around, and the one an agent should use:
|
|
|
60
60
|
cat draft.md | sloplint check --markdown -o json -
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`--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. `check` is the default command, so `sloplint draft.md`, `sloplint -`, and a bare `sloplint` with piped stdin all scan.
|
|
63
|
+
`--markdown` blanks out code, HTML comments, 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. `check` is the default command, so `sloplint draft.md`, `sloplint -`, and a bare `sloplint` with piped stdin all scan.
|
|
64
64
|
|
|
65
65
|
The human-readable form drops `-o json`:
|
|
66
66
|
|
|
@@ -98,9 +98,10 @@ version print the sloplint version
|
|
|
98
98
|
|
|
99
99
|
`check` takes files as arguments, or `-` (or nothing) to read stdin, and these options:
|
|
100
100
|
|
|
101
|
-
- `--markdown` skips fenced code, inline code, and URLs before scanning. Off by default so it never silently eats prose.
|
|
101
|
+
- `--markdown` skips fenced code, inline code, HTML comments, and URLs before scanning. Off by default so it never silently eats prose.
|
|
102
102
|
- `--select IDS` runs only these rules. Accepts comma-separated rule ids or category names.
|
|
103
103
|
- `--ignore IDS` skips these rules. Same id-or-category form.
|
|
104
|
+
- `--strict` runs every rule, including the five that are off by default. `--ignore` still applies on top.
|
|
104
105
|
|
|
105
106
|
`explain` is the command an agent calls to decide whether a flag is worth acting on:
|
|
106
107
|
|
|
@@ -154,11 +155,11 @@ An unknown id or category in `--select`/`--ignore` is a usage error (exit 2, nam
|
|
|
154
155
|
|
|
155
156
|
## The rule catalog
|
|
156
157
|
|
|
157
|
-
|
|
158
|
+
80 rules across four categories. `sloplint rules` prints them; `sloplint rules --json` gives an agent the enumerable form.
|
|
158
159
|
|
|
159
|
-
- **rhetorical-tic** (
|
|
160
|
+
- **rhetorical-tic** (56) 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
161
|
- **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** (
|
|
162
|
+
- **structure** (17) `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), `ellipsis-closer` (the same shape, but the closer's verb is elided down to a bare auxiliary: "The agent did.", at `info`), `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), `trailing-restatement` (the "…, which means …" tail that says the sentence again, off by default), and `rule-of-three`.
|
|
162
163
|
- **hedging** (1) `vague-attribution`: "some critics argue," "it is widely regarded."
|
|
163
164
|
|
|
164
165
|
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.
|
|
@@ -167,11 +168,11 @@ Some tells come in a confident form and an ambiguous one, and those ship as a pa
|
|
|
167
168
|
|
|
168
169
|
`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
|
|
|
170
|
-
|
|
171
|
+
Five rules ship **off by default**. They run when you name them — `sloplint check --select rule-of-three -` — or when you pass `--strict`, which turns the whole catalog on. `rule-of-three` flags three single words in a comma series closing a sentence, which humans do all the time; the closing two items must be single words, so a triad of phrases does not match, because a regex cannot tell one from an ordinary list. `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. `trailing-restatement` flags the "…, which means …" tail and the participles that hang a result off the sentence ("…, making it easier"); the connective is visible and the restatement is not, so a real consequence flags the same way. `phrase-echo` flags a three-word phrase that comes back within a few hundred words; a term of art comes back because it must, and the pattern cannot tell one from a phrase the writer coined.
|
|
171
172
|
|
|
172
173
|
### Markdown handling
|
|
173
174
|
|
|
174
|
-
`--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.
|
|
175
|
+
`--markdown` replaces fenced code, inline code, HTML comments, 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.
|
|
175
176
|
|
|
176
177
|
## Adding a rule
|
|
177
178
|
|
data/docs/SPEC.md
CHANGED
|
@@ -138,9 +138,10 @@ global options:
|
|
|
138
138
|
|
|
139
139
|
check options:
|
|
140
140
|
paths ... files to scan; "-" or no paths reads stdin
|
|
141
|
-
--markdown skip fenced/inline code spans
|
|
141
|
+
--markdown skip fenced/inline code spans, HTML comments, and URLs
|
|
142
142
|
--select IDS only run these rules (comma-separated ids or categories)
|
|
143
143
|
--ignore IDS skip these rules
|
|
144
|
+
--strict run every rule, including the off-by-default ones
|
|
144
145
|
```
|
|
145
146
|
|
|
146
147
|
`check` is the default command. A first argument that is not a command name is
|
|
@@ -165,7 +166,7 @@ Three codes carry the contract. A crash just exits nonzero on its own.
|
|
|
165
166
|
|
|
166
167
|
Empty or whitespace-only input is exit 2, like a mistyped rule id: a scan of
|
|
167
168
|
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
|
+
`--markdown` blanks code, HTML comments, and URLs, so a file that holds only a fenced code
|
|
169
170
|
block still exits 0. Only when every source is empty.
|
|
170
171
|
|
|
171
172
|
## Note (the diagnostic object)
|
|
@@ -262,7 +263,7 @@ Categories (for `--select`/`--ignore` by group):
|
|
|
262
263
|
|
|
263
264
|
- `rhetorical-tic` — the cadence patterns (the user's list below)
|
|
264
265
|
- `puffery` — Wikipedia "words to watch" (boasts, vibrant, nestled, tapestry…)
|
|
265
|
-
- `structure` — rule-of-three, "not just X but Y", "the question isn't X, it's Y", "less about X more about Y", the trailing significance participle, em dash, em-dash overuse
|
|
266
|
+
- `structure` — rule-of-three, "not just X but Y", "the question isn't X, it's Y", "less about X more about Y", the trailing significance participle, the trailing restatement, em dash, em-dash overuse
|
|
266
267
|
- `hedging` — vague attribution ("some critics argue", "it is widely regarded")
|
|
267
268
|
|
|
268
269
|
Severities: `warning` for strong tells, `info` for weak/contextual ones. No
|
|
@@ -298,6 +299,7 @@ false-positive risk, which none has demonstrated.
|
|
|
298
299
|
| `is-the-entire` | "X is the entire point/game/business model" | |
|
|
299
300
|
| `the-entire-is` | "the entire point/game/… is" (flip of above) | |
|
|
300
301
|
| `is-real-and-not` | "the X is real, and/not…", "is the real … and it" | skip "real estate/time"; `info` |
|
|
302
|
+
| `real-x-real-y` | the same "real" used twice attributively in one sentence, in front of two different nouns: "real API calls … real credentials" | needs no noun list -- narrowed by requiring two attributive uses of the intensifier naming two different things; a hyphen on either side of "real" (real-time, non-real) takes it out of the running; a closed list drops the fixed senses (real time, real-world, real numbers/roots, real user (monitoring), real estate, real money) and the function words that continue a predicative "is real" |
|
|
301
303
|
| `the-punchline-is` | "the punchline is/:/?", "the honest answer/version is" | "short version" left out; ordinary writing |
|
|
302
304
|
| `worth-naming` | "worth naming/flagging/separating/spelling out" | skip "naming names"; yields to the rule below when a manner adverb follows; `info` |
|
|
303
305
|
| `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 |
|
|
@@ -323,7 +325,9 @@ false-positive risk, which none has demonstrated.
|
|
|
323
325
|
| `honest-x` | "an honest comparison", "the honest framing" | short noun list; "answer", "assessment", "account" excluded as ordinary; superlative yielded to the rule below |
|
|
324
326
|
| `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 |
|
|
325
327
|
| `genuinely` | any "genuinely" | off by default; no narrowing holds |
|
|
328
|
+
| `actually-not-x` | "actually" and a trailing "…, not X" in one clause | the comma before "not" must be the first comma of the clause and must follow a word, so a fronted setup ("Despite the name, …") and a parenthetical both drop it; never crosses a line break; bare "actually" and "not actually" both left out |
|
|
326
329
|
| `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` |
|
|
330
|
+
| `phrase-echo` | the same three words again within about 400 words | backreference in a lookahead, so the match is the first occurrence; each word 4+ characters and lowercase-led, one of them 6+ letters with nothing but letters, so function-word runs, contractions, proper nouns and Title Case headings are out; a repeat that opens on a quote mark, backtick, emphasis marker or hyphen is out; the gap crosses paragraph breaks but not a list item, a table row or 80+ non-word characters; off by default; `info` |
|
|
327
331
|
| `and-thats-fine` | "And that's fine/okay." as a whole sentence | "and" required; must open and close the sentence |
|
|
328
332
|
| `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
333
|
| `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 |
|
|
@@ -350,11 +354,13 @@ Single flat rule per word-cluster, matched as whole words:
|
|
|
350
354
|
- `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
355
|
- `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
356
|
- `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.
|
|
357
|
+
- `ellipsis-closer` — the same long-sentence-then-short-closer shape as `mic-drop-closer`, but the tell sits in the verb rather than the subject: the closer's verb phrase is elided down to a bare auxiliary with no object, "The agent did."; `info`. No subject list is needed — a one-to-three word subject runs straight into a bare `did/does/do/was/were/is/are/had/has/can/could/would/will/should/might/must` (contracted forms included) and a period, and the closer must be the last thing in the paragraph, same as `mic-drop-closer`, so a closing quotation mark after the period excludes quoted dialogue the same way `short-run` excludes it. A negative lookahead drops a closer that still holds "what", "that", "which", "who", "why" or "how", since those introduce a subordinate clause supplying its own complement rather than an elided one ("Nobody knew who did." asks who did it). Reuses `mic-drop-closer`'s long-sentence prefix rather than a second copy of it. One means nothing; a draft where it repeats is the tell.
|
|
353
358
|
- `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.
|
|
354
359
|
- `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.
|
|
355
360
|
- `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.
|
|
356
361
|
- `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`.
|
|
357
|
-
- `
|
|
362
|
+
- `trailing-restatement` — comma plus "which means", "which is to say", or "meaning" opening on one of a closed set of determiners and pronouns; or one of four participle frames with a pronoun object and a closing word ("making it easier", "allowing us to", "giving them more", "leaving you with"). The closer on "making" is a comparative that ends the clause or leads into "to", "for" or "than". Nothing before the comma is inspected, so a gloss ("_ma_, which means hand") matches; the bare participles ("leaving the door open") never do. Off by default: the pattern sees the connective, not whether the tail restates the head. `info`.
|
|
363
|
+
- `rule-of-three` — three single-word comma items ending a sentence (heuristic; `info` severity, off by default; runs under `--select` or `--strict` since it false-positives).
|
|
358
364
|
- `em-dash` — any em dash; `info`.
|
|
359
365
|
- `em-dash-overuse` — 3+ em dashes in one paragraph; `warning`.
|
|
360
366
|
|
|
@@ -364,8 +370,8 @@ Single flat rule per word-cluster, matched as whole words:
|
|
|
364
370
|
|
|
365
371
|
## Markdown handling
|
|
366
372
|
|
|
367
|
-
`--markdown` blanks out fenced code (```` ``` ````), inline code (`` ` ``),
|
|
368
|
-
URLs before scanning, replacing them with same-length whitespace so line/column
|
|
373
|
+
`--markdown` blanks out fenced code (```` ``` ````), inline code (`` ` ``), HTML
|
|
374
|
+
comments (`<!-- -->`), and URLs before scanning, replacing them with same-length whitespace so line/column
|
|
369
375
|
stay correct. Off by default (plain-text mode) so it never silently eats prose.
|
|
370
376
|
|
|
371
377
|
## Agent-first help text
|
data/lib/sloplint/cli.rb
CHANGED
|
@@ -19,7 +19,14 @@ module Sloplint
|
|
|
19
19
|
opts = { format: "full" }
|
|
20
20
|
parser = global_parser(opts, out:)
|
|
21
21
|
# Split global options from the subcommand and its args.
|
|
22
|
-
|
|
22
|
+
begin
|
|
23
|
+
parser.order!(argv)
|
|
24
|
+
rescue OptionParser::InvalidOption => e
|
|
25
|
+
# `check` is the default command, so its options are accepted before
|
|
26
|
+
# any command word: `sloplint --markdown -`. The global parser does not
|
|
27
|
+
# know them, so put the option back and let check's parser judge it.
|
|
28
|
+
argv.unshift("check", *e.args)
|
|
29
|
+
end
|
|
23
30
|
return 0 if opts[:help_shown] || opts[:version_shown]
|
|
24
31
|
|
|
25
32
|
command = argv.shift
|
|
@@ -46,15 +53,17 @@ module Sloplint
|
|
|
46
53
|
# ── check ───────────────────────────────────────────────────────────────
|
|
47
54
|
def cmd_check(argv, opts, out:, err:, stdin:)
|
|
48
55
|
markdown = false
|
|
56
|
+
strict = false
|
|
49
57
|
select = nil
|
|
50
58
|
ignore = nil
|
|
51
59
|
p = OptionParser.new do |o|
|
|
52
60
|
o.banner = "usage: sloplint check [options] [paths...] (\"-\" or no paths = stdin)"
|
|
53
61
|
o.on("-o", "--output-format FORMAT", %w[full json],
|
|
54
62
|
"Output format: 'full' or 'json' (may also be given before the command).") { |v| opts[:format] = v }
|
|
55
|
-
o.on("--markdown", "Skip fenced/inline code spans and URLs before scanning.") { markdown = true }
|
|
63
|
+
o.on("--markdown", "Skip fenced/inline code spans, HTML comments, and URLs before scanning.") { markdown = true }
|
|
56
64
|
o.on("--select IDS", "Only run these rules (comma-separated rule ids or category names).") { |v| select = v.split(",").map(&:strip) }
|
|
57
65
|
o.on("--ignore IDS", "Skip these rules (comma-separated rule ids or category names).") { |v| ignore = v.split(",").map(&:strip) }
|
|
66
|
+
o.on("--strict", "Run every rule, including the ones that are off by default.") { strict = true }
|
|
58
67
|
end
|
|
59
68
|
p.order!(argv)
|
|
60
69
|
|
|
@@ -65,7 +74,7 @@ module Sloplint
|
|
|
65
74
|
return 2
|
|
66
75
|
end
|
|
67
76
|
|
|
68
|
-
rules = select_rules(select, ignore)
|
|
77
|
+
rules = select_rules(select, ignore, strict)
|
|
69
78
|
paths = argv.empty? ? ["-"] : argv
|
|
70
79
|
by_path = paths.reject { |x| x == "-" }.size > 1
|
|
71
80
|
|
|
@@ -190,9 +199,11 @@ module Sloplint
|
|
|
190
199
|
|
|
191
200
|
# --select/--ignore accept rule ids or category names. Default set excludes
|
|
192
201
|
# default_on:false rules unless they are explicitly selected.
|
|
193
|
-
def select_rules(select, ignore)
|
|
202
|
+
def select_rules(select, ignore, strict = false)
|
|
194
203
|
rules = if select
|
|
195
204
|
RULES.select { |r| select.include?(r.id) || select.include?(r.category) }
|
|
205
|
+
elsif strict
|
|
206
|
+
RULES
|
|
196
207
|
else
|
|
197
208
|
RULES.select(&:default_on)
|
|
198
209
|
end
|
data/lib/sloplint/engine.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Sloplint
|
|
|
14
14
|
|
|
15
15
|
module_function
|
|
16
16
|
|
|
17
|
-
# text: the source. rules: which Rule objects to run. markdown: blank code
|
|
17
|
+
# text: the source. rules: which Rule objects to run. markdown: blank code, HTML comments and URLs first.
|
|
18
18
|
# path: label carried into each Note (e.g. filename or "-" for stdin).
|
|
19
19
|
def scan(text, rules: RULES, markdown: false, path: "-")
|
|
20
20
|
source = text
|
|
@@ -95,14 +95,13 @@ module Sloplint
|
|
|
95
95
|
starts
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# Replace fenced code, inline code, and URLs with same-length
|
|
99
|
-
# line/column stay correct. Newlines are preserved.
|
|
98
|
+
# Replace fenced code, HTML comments, inline code, and URLs with same-length
|
|
99
|
+
# whitespace so line/column stay correct. Newlines are preserved. One pass
|
|
100
|
+
# with one alternation, so whichever construct opens first is the one that
|
|
101
|
+
# gets consumed: a `<!--` quoted inside backticks is inline code, and a
|
|
102
|
+
# backtick inside a comment is part of the comment.
|
|
100
103
|
def blank_markdown(text)
|
|
101
|
-
|
|
102
|
-
text
|
|
103
|
-
.gsub(/```.*?```/m) { |s| blank.call(s) } # fenced code
|
|
104
|
-
.gsub(/`[^`\n]*`/) { |s| blank.call(s) } # inline code
|
|
105
|
-
.gsub(%r{https?://\S+}) { |s| blank.call(s) } # bare URLs
|
|
104
|
+
text.gsub(/```.*?```|<!--.*?-->|`[^`\n]*`|https?:\/\/\S+/m) { |s| s.gsub(/[^\n]/, " ") }
|
|
106
105
|
end
|
|
107
106
|
end
|
|
108
107
|
end
|
data/lib/sloplint/rules.rb
CHANGED
|
@@ -53,6 +53,35 @@ module Sloplint
|
|
|
53
53
|
# letter is a letter.
|
|
54
54
|
WHOLE_CLOSERS = /point|game|thing|deal|story|ballgame|ball#{WRAP_GAP}+game|(?:value|fix)(?=[^[:word:][:space:]'’-]|#{WRAP_GAP}+(?:[^[:word:][:space:]*_`"'‘“(\[{~]|\d+[.)][ \t])|#{WRAP_GAP}*(?:\z|#{PARA_BREAK}|(?:of|to|for|in|on|at|with|here|there|behind|right|though|now|anyway|really|from|over|after|and|but|so|as|that|which|if|when|because|since|unless|until|once|while|where|i|we|you|he|she|it|they|the|a|an|this|these|those|every|any|my|our|your|his|her|their|its)\b))/i
|
|
55
55
|
|
|
56
|
+
# Nouns that only ever name the writer's own construction -- never a
|
|
57
|
+
# concrete object, a person, or an idiom -- and that all four of
|
|
58
|
+
# clean-x, cleanest-x, honest-x and most-honest-x already matched before
|
|
59
|
+
# this constant existed. "comparison" and "through-line" read the same way
|
|
60
|
+
# but are each missing from one rule's original list (clean-x never had
|
|
61
|
+
# "comparison", cleanest-x never had "through-line"), so both stay out of
|
|
62
|
+
# this constant and inline in the three rules that carry them -- putting
|
|
63
|
+
# either one here would add a match the fourth rule never had.
|
|
64
|
+
WRITERS_OWN_CONSTRUCTION_NOUNS = "framing|formulation|mapping|abstraction"
|
|
65
|
+
|
|
66
|
+
# A sentence of at least sixty characters, ending in sentence-ending
|
|
67
|
+
# punctuation followed by one or two spaces -- the setup mic-drop-closer's
|
|
68
|
+
# kicker needs before it looks for the closer. The atomic group (?>...) is
|
|
69
|
+
# load-bearing against catastrophic backtracking; see the comment on
|
|
70
|
+
# mic-drop-closer's pattern for the incident that made it necessary.
|
|
71
|
+
SENTENCE_OF_SIXTY_CHARACTERS_ENDING_IN_PUNCTUATION_AND_SPACE =
|
|
72
|
+
/(?:^|(?<=[.!?])[ \t]{1,2})(?>(?:[^.!?\n\s]|(?<![ \t])[ \t]{1,2}(?![ \t])|\r?\n(?!\s*\n)[ \t]*){60,})[.!?][ \t]{1,2}/
|
|
73
|
+
|
|
74
|
+
# real-x-real-y interpolates this fragment after each of its two "real"s, so
|
|
75
|
+
# a later narrowing can only ever apply to both at once -- two copies typed
|
|
76
|
+
# out separately could drift, one gaining an exception the other never
|
|
77
|
+
# sees. Function words that only ever continue a predicative "is real",
|
|
78
|
+
# then the fixed senses common in this register that are never the
|
|
79
|
+
# doubled-intensifier tell: real time, real-world, the math sense (real
|
|
80
|
+
# numbers/roots), real user (monitoring), real estate, real money.
|
|
81
|
+
REAL_X_REAL_Y_EXCLUDED_NEXT_WORD =
|
|
82
|
+
"and|but|or|nor|yet|too|enough|itself|indeed|which|that|here|there|so" \
|
|
83
|
+
"|time|world|numbers?|roots?|money|estate|user"
|
|
84
|
+
|
|
56
85
|
RULES = [
|
|
57
86
|
# ── rhetorical-tic ────────────────────────────────────────────────────
|
|
58
87
|
Rule.new(
|
|
@@ -343,6 +372,90 @@ module Sloplint
|
|
|
343
372
|
"which is why the rule is off by default; when selected, several in a " \
|
|
344
373
|
"draft should be read as a warning."
|
|
345
374
|
),
|
|
375
|
+
Rule.new(
|
|
376
|
+
id: "phrase-echo",
|
|
377
|
+
category: "rhetorical-tic",
|
|
378
|
+
severity: "info",
|
|
379
|
+
default_on: false,
|
|
380
|
+
# The same three words again a few paragraphs on. Three consecutive
|
|
381
|
+
# words, each four characters or more and lowercase-led, one of them
|
|
382
|
+
# six letters with nothing but letters, and the same three again
|
|
383
|
+
# within about 400 words. That is the whole defence, and it is
|
|
384
|
+
# structural: length drops the function-word runs, the all-letters
|
|
385
|
+
# word drops the contractions and hyphenated compounds that would
|
|
386
|
+
# otherwise pass on characters alone, case drops the names and
|
|
387
|
+
# headings. The gaps inside the phrase are capped at two spaces, as
|
|
388
|
+
# in epistrophe, so a code span blanked by --markdown cannot weld two
|
|
389
|
+
# words into a phrase.
|
|
390
|
+
#
|
|
391
|
+
# The window is a lazy walk of word steps in an atomic group, so
|
|
392
|
+
# nothing backtracks, and it counts Unicode words, since `\w` is
|
|
393
|
+
# ASCII in Ruby. Each gap in the walk is capped at 80 non-word
|
|
394
|
+
# characters and refuses to cross into a list item or a table row:
|
|
395
|
+
# a blanked code fence, a rule of dashes or a bullet is a wall, not a
|
|
396
|
+
# step. The repeat sits in a lookahead so the match, and the excerpt,
|
|
397
|
+
# is the first occurrence alone rather than the whole span; the
|
|
398
|
+
# suggestion says so. The repeat may not open on a quote mark, a
|
|
399
|
+
# backtick, an emphasis marker, a hyphen or a table bar: a quoted
|
|
400
|
+
# self-repeat is deliberate, and "re-shared" is not "shared". One
|
|
401
|
+
# backreference per word so either occurrence may be hard-wrapped.
|
|
402
|
+
#
|
|
403
|
+
# Off by default: in reference prose the rate runs to thousands per
|
|
404
|
+
# million words, all of it terms of art and running epithets.
|
|
405
|
+
pattern: /(?<![\p{Word}'’-])
|
|
406
|
+
(?=(?:[\w'’-]+(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})){0,2}[a-z]{6,}(?![\w'’-]))
|
|
407
|
+
([a-z][\w'’-]{3,})(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})
|
|
408
|
+
([a-z][\w'’-]{3,})(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})
|
|
409
|
+
([a-z][\w'’-]{3,})\b
|
|
410
|
+
(?=(?>(?:(?!\r?\n[ \t]*(?:[-*+•|]|\d+[.)])[ \t])\P{Word}){1,80}\p{Word}+){0,400}?
|
|
411
|
+
(?:(?!\r?\n[ \t]*(?:[-*+•|]|\d+[.)])[ \t])[^\p{Word}"“‘'`*|-]){1,80}
|
|
412
|
+
\1(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})
|
|
413
|
+
\2(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})
|
|
414
|
+
\3\b)/x,
|
|
415
|
+
message: "Three-word phrase that comes back within a few hundred words -- a model reusing its own output.",
|
|
416
|
+
suggestion: "This is the first use and the repeat is ahead. Reword the repeat, unless the phrase is a term the reader needs to see again.",
|
|
417
|
+
examples_bad: [
|
|
418
|
+
"The shortest honest answer that came out of the review was a list.\n\nWhen you write back, the shortest honest answer you can send is the list.",
|
|
419
|
+
"We keep a shared review checklist in the repo, and it is short. Everyone who opens a pull request edits the shared review checklist first.",
|
|
420
|
+
# A hard-wrapped first occurrence.
|
|
421
|
+
"We keep a shared review\nchecklist in the repo. Everyone edits the shared review checklist first.",
|
|
422
|
+
# The last word inside the window.
|
|
423
|
+
"We keep a shared review checklist. #{"word " * 399}The shared review checklist is short."
|
|
424
|
+
],
|
|
425
|
+
examples_ok: [
|
|
426
|
+
# Function words fall out on length.
|
|
427
|
+
"In order to ship we cut scope, and in order to ship again we cut it more.",
|
|
428
|
+
# Four-letter words alone are not enough; one word must be six letters, letters only.
|
|
429
|
+
"It would have been better, and it would have been faster.",
|
|
430
|
+
"It couldn't have been worse, and it couldn't have been better.",
|
|
431
|
+
"We saw every top-ten list here, and every top-ten list there.",
|
|
432
|
+
# Proper nouns and Title Case headings fall out on case.
|
|
433
|
+
"Grand Central Station has one, and Grand Central Station wants two.",
|
|
434
|
+
"Incident Response Plan\n\nThe Incident Response Plan covers the first hour.",
|
|
435
|
+
# The repeat is beyond the window, in words, in non-ASCII words, or past a wall of dashes.
|
|
436
|
+
"We keep a shared review checklist. #{"word " * 400}The shared review checklist is short.",
|
|
437
|
+
"We keep a shared review checklist. #{"слово " * 400}The shared review checklist is short.",
|
|
438
|
+
"We keep a shared review checklist.\n\n#{"-" * 100}\n\nThe shared review checklist is short.",
|
|
439
|
+
# A quoted repeat is deliberate, whatever the quote mark.
|
|
440
|
+
"The shared review checklist is new. He wrote \"shared review checklist\" on the board.",
|
|
441
|
+
"The shared review checklist is new. He wrote 'shared review checklist' on the board.",
|
|
442
|
+
"The shared review checklist is new. He wrote `shared review checklist` on the board.",
|
|
443
|
+
# A different word form is a different phrase, and so is a hyphenated compound.
|
|
444
|
+
"The shared review checklist grew, and then both shared review checklists grew.",
|
|
445
|
+
"The auto-generated review checklist was long. The hand-generated review checklist was longer.",
|
|
446
|
+
"We use a shared review checklist daily. Nobody re-shared review checklist edits.",
|
|
447
|
+
# A run of spaces where --markdown blanked a code span does not weld a phrase.
|
|
448
|
+
"We keep a shared review checklist here. Everyone edits the shared review checklist first.",
|
|
449
|
+
# List items and table rows are furniture, not prose.
|
|
450
|
+
"- shared review checklist covers pull requests\n- shared review checklist covers deploys",
|
|
451
|
+
"1. Update the shared config file.\n2. Restart the shared config file watcher.",
|
|
452
|
+
"| Task | Status |\n|---|---|\n| A | needs manual review |\n| B | needs manual review |"
|
|
453
|
+
],
|
|
454
|
+
rationale: "A model reuses a phrase it has just minted because its own recent output is " \
|
|
455
|
+
"the likeliest continuation, so the same three words turn up again a few " \
|
|
456
|
+
"paragraphs on, doing no new work. Terms of art repeat too, and the pattern " \
|
|
457
|
+
"cannot tell a coined phrase from a name, so the rule is off by default."
|
|
458
|
+
),
|
|
346
459
|
Rule.new(
|
|
347
460
|
id: "did-not-x-did-not-y",
|
|
348
461
|
category: "rhetorical-tic",
|
|
@@ -892,9 +1005,9 @@ module Sloplint
|
|
|
892
1005
|
# in front of a speech verb, and the concrete-capable nouns (cut, line,
|
|
893
1006
|
# version, split) are left out entirely.
|
|
894
1007
|
pattern: /\bcleanest\s+(?:\w+\s+){0,2}
|
|
895
|
-
(
|
|
896
|
-
|
|
|
897
|
-
|
|
|
1008
|
+
(?:#{WRITERS_OWN_CONSTRUCTION_NOUNS}|comparison
|
|
1009
|
+
|statement|account|argument|idea|definition|summary
|
|
1010
|
+
|reading|take|point|story|explanation|distinction)\b
|
|
898
1011
|
|\bcleanest\s+way\s+to\s+(?:say|put|frame|state|describe|phrase|express
|
|
899
1012
|
|think\s+about)\b/ix,
|
|
900
1013
|
message: '"The cleanest framing/way to put it…" ranks your own claim for the reader.',
|
|
@@ -922,7 +1035,7 @@ module Sloplint
|
|
|
922
1035
|
# "break" is admitted only in "clean break between", never bare, because
|
|
923
1036
|
# "make a clean break with the past" is an idiom and not a tell.
|
|
924
1037
|
pattern: /\b(?:a|the|one)\s+(?:\w+\s+)?clean\s+(?:\w+\s+)?
|
|
925
|
-
(
|
|
1038
|
+
(?:#{WRITERS_OWN_CONSTRUCTION_NOUNS}|distinction|through-line
|
|
926
1039
|
|story|answer|argument|split|divide)\b
|
|
927
1040
|
|\bclean\s+(?:line|break|split)\s+between\b/ix,
|
|
928
1041
|
message: '"A clean abstraction / clean framing" praises the idea instead of showing it.',
|
|
@@ -997,6 +1110,104 @@ module Sloplint
|
|
|
997
1110
|
"are three words apart and identical on the surface. An agent reading the flag " \
|
|
998
1111
|
"has the rest of the sentence to judge; the pattern alone doesn't."
|
|
999
1112
|
),
|
|
1113
|
+
Rule.new(
|
|
1114
|
+
id: "real-x-real-y",
|
|
1115
|
+
category: "rhetorical-tic",
|
|
1116
|
+
severity: "info",
|
|
1117
|
+
# Ships at info, not warning: the only two hits the probe found in
|
|
1118
|
+
# 1.28M words were both false positives, and one corpus of one
|
|
1119
|
+
# register is thin evidence next to honestly, which sits at warning
|
|
1120
|
+
# on 9.7M words across two. A doubled "real" is also defensible on
|
|
1121
|
+
# its own ("No real financial loss, but a real failure") in a way
|
|
1122
|
+
# the warning rules around it are not. The agent reading the flag
|
|
1123
|
+
# decides.
|
|
1124
|
+
#
|
|
1125
|
+
# "If it can trigger real API calls or hold real credentials, it needs
|
|
1126
|
+
# the same rigor." -- the same intensifier, said twice in one sentence,
|
|
1127
|
+
# each time in front of a different noun. Bare "real" is an ordinary
|
|
1128
|
+
# word (a real number, real time, a real problem) and cannot be flagged
|
|
1129
|
+
# on its own; the narrowing here is repetition, not a noun list --
|
|
1130
|
+
# requiring two attributive uses in one sentence, naming two different
|
|
1131
|
+
# things, is what tells the doubled intensifier apart from plain
|
|
1132
|
+
# English, and it needs no list of nouns to do it.
|
|
1133
|
+
#
|
|
1134
|
+
# "Attributive" is enforced by requiring "real" to run straight into
|
|
1135
|
+
# the word it modifies: a space, then a letter, with no comma or
|
|
1136
|
+
# conjunction between. That already keeps the predicative use out
|
|
1137
|
+
# ("The risk is real, and it is growing" -- its own rule, above) since
|
|
1138
|
+
# a predicate "real" is followed by punctuation or "and", never
|
|
1139
|
+
# directly by the next noun.
|
|
1140
|
+
#
|
|
1141
|
+
# A hyphen touching "real" on either side takes it out of the running:
|
|
1142
|
+
# "real-time" and "real-world" are compounds, one modifier, not two
|
|
1143
|
+
# independent uses of the intensifier, and "non-real" is a negation,
|
|
1144
|
+
# not the intensifier at all. (?<!-) and (?!-) drop all three.
|
|
1145
|
+
#
|
|
1146
|
+
# The second "real" must name a word the first one didn't -- a
|
|
1147
|
+
# backreference, so "a real risk ... that real risk" (the same thing,
|
|
1148
|
+
# referred to twice) is ordinary reference, not the tic. Two mentions
|
|
1149
|
+
# only look alike when they're both new: two different real things.
|
|
1150
|
+
#
|
|
1151
|
+
# The closed list after each "real" (REAL_X_REAL_Y_EXCLUDED_NEXT_WORD,
|
|
1152
|
+
# above) is the handful of fixed senses that are common in this
|
|
1153
|
+
# register and are never the tell, plus the function words that only
|
|
1154
|
+
# ever continue a predicative "is real". This gives up "real user"
|
|
1155
|
+
# outside "real user monitoring" and any doubled "real name"/"real
|
|
1156
|
+
# names" pairing that differs only by a plural -- known, deliberate
|
|
1157
|
+
# recall losses rather than a growing list.
|
|
1158
|
+
#
|
|
1159
|
+
# The gap between the two "real"s crosses a hard-wrapped line the same
|
|
1160
|
+
# way WRAP_GAP does (a newline is fine unless it opens a paragraph
|
|
1161
|
+
# break, PARA_BREAK, since a non-breaking space alone on the line in
|
|
1162
|
+
# between still reads as a blank line to the editors that emit one),
|
|
1163
|
+
# but never a sentence-ending mark, so the two "real"s must fall in
|
|
1164
|
+
# one sentence.
|
|
1165
|
+
pattern: /
|
|
1166
|
+
(?<!-)\breal(?!-)[ \t]+
|
|
1167
|
+
(?!(?:#{REAL_X_REAL_Y_EXCLUDED_NEXT_WORD})\b)
|
|
1168
|
+
(?>([a-z][\w'’-]*))\b
|
|
1169
|
+
(?:(?!\breal\b)[^.!?\n]|(?!#{PARA_BREAK})\r?\n){1,200}?
|
|
1170
|
+
(?<!-)\breal(?!-)[ \t]+
|
|
1171
|
+
(?!(?:#{REAL_X_REAL_Y_EXCLUDED_NEXT_WORD})\b)
|
|
1172
|
+
(?!\1\b)
|
|
1173
|
+
(?>[a-z][\w'’-]*)\b
|
|
1174
|
+
/ix,
|
|
1175
|
+
message: 'Doubled "real" ("real X … real Y") repeats the intensifier for emphasis.',
|
|
1176
|
+
suggestion: "Cut one 'real,' or say what actually makes each thing real -- a name, a number, a log line.",
|
|
1177
|
+
examples_bad: [
|
|
1178
|
+
"If it can trigger real API calls or hold real credentials, it needs the same rigor.",
|
|
1179
|
+
"The demo used real customer data and exposed real financial records to the whole team.",
|
|
1180
|
+
"This isn't a mockup; it hits a real database and charges a real credit card.",
|
|
1181
|
+
"The incident caused real financial losses and real reputational damage to the company."
|
|
1182
|
+
],
|
|
1183
|
+
examples_ok: [
|
|
1184
|
+
# The second "real" must name something the first didn't.
|
|
1185
|
+
"The system exposes a real risk, and that real risk must be tracked.",
|
|
1186
|
+
# A hyphen on either side makes it a compound, not two uses.
|
|
1187
|
+
"The dashboard shows real-time metrics and real-time alerts.",
|
|
1188
|
+
# "real world" is on the closed list even unhyphenated.
|
|
1189
|
+
"In the real world this rarely happens, and the real world rewards patience.",
|
|
1190
|
+
# The math sense, both nouns.
|
|
1191
|
+
"The equation has two real roots and no real numbers outside that range.",
|
|
1192
|
+
# "real user" (monitoring) and a hyphenated compound together.
|
|
1193
|
+
"The tool combines real user monitoring with real-time dashboards.",
|
|
1194
|
+
# Two more closed-list nouns.
|
|
1195
|
+
"She invested in real estate and lost real money.",
|
|
1196
|
+
# "non-real" is a negation, not a second use of the intensifier.
|
|
1197
|
+
"The audit flagged real deployment risk but no non-real anomalies.",
|
|
1198
|
+
# Only one "real" in the sentence.
|
|
1199
|
+
"This is a real problem worth solving.",
|
|
1200
|
+
# A paragraph break -- even one where the blank line holds only a
|
|
1201
|
+
# non-breaking space -- ends the sentence; the two paragraphs are
|
|
1202
|
+
# never joined into one hit.
|
|
1203
|
+
"This plan needs real signoff\n \nbefore it touches real production data."
|
|
1204
|
+
],
|
|
1205
|
+
rationale: "\"Real\" is the plainest way to say a thing isn't fake or hypothetical, and it " \
|
|
1206
|
+
"only needs saying once a sentence -- naming it again in front of a second noun " \
|
|
1207
|
+
"doesn't add information, it repeats the reassurance. A person defending a claim " \
|
|
1208
|
+
"from two directions in the same breath writes the two facts and lets one " \
|
|
1209
|
+
"\"real\" cover both."
|
|
1210
|
+
),
|
|
1000
1211
|
Rule.new(
|
|
1001
1212
|
id: "the-punchline-is",
|
|
1002
1213
|
category: "rhetorical-tic",
|
|
@@ -2035,8 +2246,7 @@ module Sloplint
|
|
|
2035
2246
|
# superlative belongs to most-honest-x alone.
|
|
2036
2247
|
pattern: /\b(?:an|the|one|this|that)[ \t]+(?:(?!most\b|more\b)\w+[ \t]+)?
|
|
2037
2248
|
honest[ \t]+(?:\w+[ \t]+)?
|
|
2038
|
-
(
|
|
2039
|
-
|through-line)\b/ix,
|
|
2249
|
+
(?:#{WRITERS_OWN_CONSTRUCTION_NOUNS}|comparison|accounting|through-line)\b/ix,
|
|
2040
2250
|
message: '"An honest comparison / the honest framing" praises the writing, not the thing.',
|
|
2041
2251
|
suggestion: "Cut the adjective and make the comparison; the reader decides if it is honest.",
|
|
2042
2252
|
examples_bad: [
|
|
@@ -2073,9 +2283,10 @@ module Sloplint
|
|
|
2073
2283
|
# politician" is, so no human nouns go in. Nothing in either corpus
|
|
2074
2284
|
# matches. "way to" gets its own branch, mirroring cleanest-x.
|
|
2075
2285
|
pattern: /\bmost[ \t]+honest[ \t]+(?:(?!way\b)\w+[ \t]+){0,2}
|
|
2076
|
-
(
|
|
2077
|
-
|
|
|
2078
|
-
|
|
|
2286
|
+
(?:#{WRITERS_OWN_CONSTRUCTION_NOUNS}|comparison
|
|
2287
|
+
|accounting|assessment|appraisal
|
|
2288
|
+
|reading|account|answer|version|summary|take|signal
|
|
2289
|
+
|through-line)\b
|
|
2079
2290
|
|\bmost[ \t]+honest[ \t]+way[ \t]+to[ \t]+
|
|
2080
2291
|
(?:say|put|frame|state|describe|phrase|think[ \t]+about)\b/ix,
|
|
2081
2292
|
message: '"The most honest framing…" ranks your own claim for the reader.',
|
|
@@ -2126,6 +2337,72 @@ module Sloplint
|
|
|
2126
2337
|
"nominally free -- and nothing in the sentence marks which use is which, " \
|
|
2127
2338
|
"which is why this one runs only when you ask for it."
|
|
2128
2339
|
),
|
|
2340
|
+
Rule.new(
|
|
2341
|
+
id: "actually-not-x",
|
|
2342
|
+
category: "rhetorical-tic",
|
|
2343
|
+
severity: "warning",
|
|
2344
|
+
# Two markers that each correct the reader, doubled up in one clause:
|
|
2345
|
+
# the adverb and the trailing "…, not X". Bare "actually" is not the
|
|
2346
|
+
# tell and is not matched, because "the build actually failed on the
|
|
2347
|
+
# second run" names which run and that is a fact with something behind
|
|
2348
|
+
# it to check.
|
|
2349
|
+
#
|
|
2350
|
+
# The narrowing is one structural constraint, not a word list: the
|
|
2351
|
+
# comma in front of "not" has to be the first comma of its clause and
|
|
2352
|
+
# has to follow a word. A writer correcting an assumption the reader
|
|
2353
|
+
# does hold names it first, and that setup is either a fronted clause
|
|
2354
|
+
# with a comma after it ("Despite the name, …") or a parenthetical
|
|
2355
|
+
# whose closing bracket the comma follows. No earlier comma means
|
|
2356
|
+
# nothing in the clause set the alternative up. A clause starts at the
|
|
2357
|
+
# beginning of the text, at a full stop, question mark, exclamation
|
|
2358
|
+
# mark, semicolon or colon, or at a line break.
|
|
2359
|
+
#
|
|
2360
|
+
# Nothing crosses a line break, so the rule cannot weld two rows of a
|
|
2361
|
+
# table or two items of a list into one correction. The deliberate
|
|
2362
|
+
# misses: a hard-wrapped correction, and a correction whose clause
|
|
2363
|
+
# carries an earlier comma of any kind, an apposition ("The report,
|
|
2364
|
+
# filed late, says the id is actually a byte string, not text.")
|
|
2365
|
+
# included. No pattern tells an apposition from a fronted setup, and
|
|
2366
|
+
# the rule would rather say nothing than guess.
|
|
2367
|
+
pattern: /(?:\A|(?<=[.;:!?\n]))[^.;:!?,\n]{0,200}\K
|
|
2368
|
+
\bactually\b[^.;:!?,\n]{0,120}(?<=[\p{Word}]),[ \t]*not\b/ix,
|
|
2369
|
+
message: '"Actually …, not X" corrects an assumption the text never offered.',
|
|
2370
|
+
suggestion: "State the fact plainly, or name the belief the correction answers.",
|
|
2371
|
+
examples_bad: [
|
|
2372
|
+
"The disclosure actually covered two incidents, not one.",
|
|
2373
|
+
"The identifier is actually a byte string, not text.",
|
|
2374
|
+
"It actually reads the file at startup, not on the first request.",
|
|
2375
|
+
# A colon opens a clause, so the correction after it is still the tell.
|
|
2376
|
+
"Note: the endpoint actually returns two fields, not one."
|
|
2377
|
+
],
|
|
2378
|
+
examples_ok: [
|
|
2379
|
+
# The bare adverb, which names which run failed. Not matched.
|
|
2380
|
+
"The build actually failed on the second run.",
|
|
2381
|
+
# The setup sits in a fronted clause, so the correction answers
|
|
2382
|
+
# something the reader was given. The earlier comma drops both.
|
|
2383
|
+
"Despite the name, the identifier is actually a byte string, not text.",
|
|
2384
|
+
"Although the name suggests otherwise, it is actually a byte string, not text.",
|
|
2385
|
+
# The correction on its own, with no adverb in front of it.
|
|
2386
|
+
"The run that failed was the second, not the first.",
|
|
2387
|
+
# Negation in front of the adverb with no trailing correction. This
|
|
2388
|
+
# was the other candidate shape; every real instance of it carried
|
|
2389
|
+
# its own setup ("normally reserved but not actually registered"),
|
|
2390
|
+
# so it was rejected, and this fixture pins the rejection.
|
|
2391
|
+
"The provider was not actually billed for the usage.",
|
|
2392
|
+
# The correction's comma closes a parenthesis, and the assumption
|
|
2393
|
+
# being corrected is inside it.
|
|
2394
|
+
"The party actually sending the mail (which we assumed was the relay), not the sender.",
|
|
2395
|
+
# Two rows of a table, not a wrapped sentence. The rule never crosses
|
|
2396
|
+
# a line break, so it cannot join them.
|
|
2397
|
+
"Actually running processes\nThreads, not processes"
|
|
2398
|
+
],
|
|
2399
|
+
rationale: "The adverb and the trailing \"not X\" both correct the reader, and a writer " \
|
|
2400
|
+
"needs one or the other, never both. Doubling them marks a contrast against " \
|
|
2401
|
+
"an alternative the reader was never offered: nothing said one incident, so " \
|
|
2402
|
+
"\"not one\" answers nobody. The setup can also sit in the sentence before, " \
|
|
2403
|
+
"which no pattern sees, so a flag on a correction that a previous sentence " \
|
|
2404
|
+
"genuinely set up is the known cost."
|
|
2405
|
+
),
|
|
2129
2406
|
Rule.new(
|
|
2130
2407
|
id: "and-thats-fine",
|
|
2131
2408
|
category: "rhetorical-tic",
|
|
@@ -2642,11 +2919,17 @@ module Sloplint
|
|
|
2642
2919
|
severity: "info",
|
|
2643
2920
|
default_on: false,
|
|
2644
2921
|
pattern: /\b[\w'-]+,\s+[\w'-]+,\s+(?:and\s+)?[\w'-]+[.!?]/,
|
|
2645
|
-
message: "Three
|
|
2922
|
+
message: "Three single words in a comma series closing a sentence (heuristic; high false-positive).",
|
|
2646
2923
|
suggestion: "Fine in moderation; watch for the AI habit of ending on triplets.",
|
|
2647
2924
|
examples_bad: ["It was fast, cheap, and simple."],
|
|
2648
|
-
examples_ok: [
|
|
2649
|
-
|
|
2925
|
+
examples_ok: [
|
|
2926
|
+
"We met on Tuesday afternoon.",
|
|
2927
|
+
# Phrasal items do not match; the closing two slots take one word each.
|
|
2928
|
+
"It was very fast, very cheap, and very simple."
|
|
2929
|
+
],
|
|
2930
|
+
rationale: "Rule-of-three endings are a model habit, but humans use them too — off by default. " \
|
|
2931
|
+
"The closing two items must be single words; a triad of phrases is three ordinary " \
|
|
2932
|
+
"list items to a regex, so those are left unflagged rather than guessed at."
|
|
2650
2933
|
),
|
|
2651
2934
|
# clause-triad-then was cut. The pattern (comma-clause, comma-clause,
|
|
2652
2935
|
# "then" clause) had no way to require the clauses actually be parallel
|
|
@@ -2872,7 +3155,7 @@ module Sloplint
|
|
|
2872
3155
|
# long unpunctuated stretches (the Columbia report has an 864-character
|
|
2873
3156
|
# one) sent this into catastrophic backtracking, 62 seconds for a 2 KB
|
|
2874
3157
|
# window and no completion on the 1.1 MB document.
|
|
2875
|
-
pattern:
|
|
3158
|
+
pattern: /#{SENTENCE_OF_SIXTY_CHARACTERS_ENDING_IN_PUNCTUATION_AND_SPACE}\K
|
|
2876
3159
|
(?:Nothing|Most|None|Everything|Everyone|Nobody|Then|Neither|Both)
|
|
2877
3160
|
(?:,?(?:[ \t]|\r?\n(?!\s*\n))+[\w'’-]+){1,7}[.!?](?=[ \t]*(?:\r?\n[ \t]*(?:\r?\n|\z)|\z))/x,
|
|
2878
3161
|
message: "A short quantifier-led closer after a long sentence is the AI kicker.",
|
|
@@ -2915,6 +3198,90 @@ module Sloplint
|
|
|
2915
3198
|
"where the flag repeats paragraph after paragraph should be read as a " \
|
|
2916
3199
|
"warning, and the fix is usually to delete the closer outright."
|
|
2917
3200
|
),
|
|
3201
|
+
Rule.new(
|
|
3202
|
+
id: "ellipsis-closer",
|
|
3203
|
+
category: "structure",
|
|
3204
|
+
severity: "info",
|
|
3205
|
+
# The same long-sentence-then-short-closer shape as mic-drop-closer,
|
|
3206
|
+
# but the tell lives in the verb, not the subject. mic-drop-closer's
|
|
3207
|
+
# closer opens on a quantifier and keeps a full verb with its object
|
|
3208
|
+
# ("Nothing here needs a new login."). This one carries no verb at
|
|
3209
|
+
# all: the closer's verb phrase has been elided down to the bare
|
|
3210
|
+
# auxiliary that would have introduced it, and the object it
|
|
3211
|
+
# promised never arrives -- "They found an exposed dashboard and
|
|
3212
|
+
# asked the agent running on it to hand over its own key. The agent
|
|
3213
|
+
# did." Because the tell is the missing verb, this rule needs no
|
|
3214
|
+
# subject list the way mic-drop-closer does, and "That", "This" and
|
|
3215
|
+
# "It" are not excluded here the way they are there: "This completes
|
|
3216
|
+
# the roughing operations." ends on a full verb with an object and
|
|
3217
|
+
# never matches (there is no auxiliary at the sentence's end), while
|
|
3218
|
+
# "This did." would, on the same subject mic-drop-closer had to bar.
|
|
3219
|
+
#
|
|
3220
|
+
# The closer is a one-to-three word subject running straight into the
|
|
3221
|
+
# bare auxiliary and a period, and, same as mic-drop-closer, it must
|
|
3222
|
+
# be the last thing in the paragraph -- a blank line or the end of
|
|
3223
|
+
# the text follows it. A closing quotation mark after the period is
|
|
3224
|
+
# not blank, so quoted dialogue never satisfies this and is excluded
|
|
3225
|
+
# the same way short-run excludes it. A negative lookahead drops any
|
|
3226
|
+
# closer that still holds "what", "that", "which", "who", "why" or
|
|
3227
|
+
# "how", because those mark a subordinate clause supplying its own
|
|
3228
|
+
# complement rather than an elided one: "Nobody knew who did." asks
|
|
3229
|
+
# "who did it", which the subject cap alone does not catch, since
|
|
3230
|
+
# "Nobody knew who" is itself only three words.
|
|
3231
|
+
#
|
|
3232
|
+
# Reuses mic-drop-closer's long-sentence prefix rather than pasting a
|
|
3233
|
+
# second copy of it; see that constant's comment for the atomic group
|
|
3234
|
+
# that keeps the scan linear.
|
|
3235
|
+
#
|
|
3236
|
+
# The negated form is bolted on with "n't" for every auxiliary except
|
|
3237
|
+
# two irregular ones: "can" already ends in n, so its negation is
|
|
3238
|
+
# "can't", not "cann't", and "will" negates to the different stem
|
|
3239
|
+
# "won't" rather than "willn't". Both are spelled out rather than
|
|
3240
|
+
# built by suffix, so the two most common contractions in the list
|
|
3241
|
+
# are not silently unmatchable.
|
|
3242
|
+
pattern: /#{SENTENCE_OF_SIXTY_CHARACTERS_ENDING_IN_PUNCTUATION_AND_SPACE}\K
|
|
3243
|
+
(?![^.!?\n]*\b(?:what|that|which|who|why|how)\b)
|
|
3244
|
+
[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[ \t]+
|
|
3245
|
+
(?:(?:did|does|do|was|were|is|are|had|has|could|would|should|might|must)(?:n['’]t)?
|
|
3246
|
+
|can(?:['’]t)?|will|won['’]t)\.
|
|
3247
|
+
(?=[ \t]*(?:\r?\n[ \t]*(?:\r?\n|\z)|\z))/x,
|
|
3248
|
+
message: "A closer that ends on a bare auxiliary is the AI verb-phrase-ellipsis kicker.",
|
|
3249
|
+
suggestion: "Cut the closer, or say what actually happened.",
|
|
3250
|
+
examples_bad: [
|
|
3251
|
+
"They found an exposed dashboard and asked the agent running on it to hand over its own key. The agent did.",
|
|
3252
|
+
"The team spent three weeks arguing about whether the migration was worth the downtime it would cost the on-call rotation. It wasn't.",
|
|
3253
|
+
"The reviewer asked whether a contractor with read access to the shared drive could still open the finance folder after the offboarding ran. She could.",
|
|
3254
|
+
"He asked whether the on-call engineer had actually paged the second responder before escalating past the fifteen-minute window. She had.",
|
|
3255
|
+
# The two irregular negated forms, pinned so a later rewrite of the
|
|
3256
|
+
# suffix can't quietly drop them again.
|
|
3257
|
+
"They double-checked whether the fallback path could still serve read traffic once the primary region failed over during the drill. It can't.",
|
|
3258
|
+
"The team hoped the migration window would close before the seasonal freight peak began overwhelming the warehouse systems. It won't."
|
|
3259
|
+
],
|
|
3260
|
+
examples_ok: [
|
|
3261
|
+
# Not the end of the paragraph.
|
|
3262
|
+
"They found an exposed dashboard and asked the agent running on it to hand over its own key. The agent did. We logged the incident and rotated the key within the hour.",
|
|
3263
|
+
# A subordinate clause, not an elided one -- caught by the wh-word guard.
|
|
3264
|
+
"The team spent three weeks arguing about whether the migration was worth the downtime it would cost the on-call rotation. That is what it did.",
|
|
3265
|
+
# No long sentence in front of it.
|
|
3266
|
+
"The agent did.",
|
|
3267
|
+
# A subject longer than three words -- the cap, not the wh-guard, excludes it.
|
|
3268
|
+
"The engineer who had been paged in the middle of the night finally agreed with what the on-call reviewer had been saying for the better part of an hour about the rollback plan. The whole team already did.",
|
|
3269
|
+
# A full verb with an object, not a bare auxiliary.
|
|
3270
|
+
"Each step already passed local review before it reached the pipeline that runs on every push to the shared branch. It worked.",
|
|
3271
|
+
# A bare demonstrative closing a step is how procedural writing ends
|
|
3272
|
+
# a paragraph, and it ends on a full verb, not an auxiliary. Wording
|
|
3273
|
+
# follows Turning and Boring (1919) and Aviation Engines (1917),
|
|
3274
|
+
# both public domain by date; mic-drop-closer carries the same two
|
|
3275
|
+
# examples for the same reason.
|
|
3276
|
+
"The cutting tools are set to the dimensions required for the finished work, and the stops are locked. This completes the roughing operations.",
|
|
3277
|
+
"The magneto is protected from oil and grit by a cover that is easy to remove for service. This means prolonged life for the magneto."
|
|
3278
|
+
],
|
|
3279
|
+
rationale: "The closer withholds exactly the thing the long setup built toward -- the " \
|
|
3280
|
+
"verb and its object are gone, and only the bare confirmation that something " \
|
|
3281
|
+
"happened is left standing. Ships at info: plenty of ordinary writing drops " \
|
|
3282
|
+
"the verb the same way when it confirms an expectation, and one flag proves " \
|
|
3283
|
+
"nothing; a draft where it repeats is the tell."
|
|
3284
|
+
),
|
|
2918
3285
|
Rule.new(
|
|
2919
3286
|
id: "short-run",
|
|
2920
3287
|
category: "structure",
|
|
@@ -3228,6 +3595,98 @@ module Sloplint
|
|
|
3228
3595
|
"narrator who never appears. Careful writers put the verb in its own clause " \
|
|
3229
3596
|
"with a subject, or leave the significance to the reader."
|
|
3230
3597
|
),
|
|
3598
|
+
Rule.new(
|
|
3599
|
+
id: "trailing-restatement",
|
|
3600
|
+
category: "structure",
|
|
3601
|
+
severity: "info",
|
|
3602
|
+
default_on: false,
|
|
3603
|
+
# The restating tail: "…, which means working through the process
|
|
3604
|
+
# rather than around it". A regex sees the connective and not whether
|
|
3605
|
+
# the tail says the head again, and the sentence that prompted the rule
|
|
3606
|
+
# shares no content words between the two, so no overlap test reaches
|
|
3607
|
+
# it either. Hence off by default.
|
|
3608
|
+
#
|
|
3609
|
+
# Three connectives, and four participle frames that make the same
|
|
3610
|
+
# move. Nothing before the comma is inspected: a gloss ("_ma_, which
|
|
3611
|
+
# means hand") and a real tell after a code span or a bold phrase end
|
|
3612
|
+
# on the same characters, so a guard there costs more hits than it
|
|
3613
|
+
# saves. Glosses are a known cost of the rule. The participle
|
|
3614
|
+
# "meaning" must open on one of a closed set of determiners and
|
|
3615
|
+
# pronouns, which keeps out the noun ("meaning of"), the intention
|
|
3616
|
+
# ("meaning to come back") and the bare-noun gloss ("meaning six"); a
|
|
3617
|
+
# gloss that takes an article ("meaning the red stick") gets through.
|
|
3618
|
+
# Each participle frame needs a pronoun object and a closing word,
|
|
3619
|
+
# because the bare participle is ordinary English ("she left, leaving
|
|
3620
|
+
# the door open"). The closer on "making" is a comparative or
|
|
3621
|
+
# (im)possible and must end the clause or lead into "to", "for" or
|
|
3622
|
+
# "than", since "-er" alone is also "wonder", "offer" and "her". These
|
|
3623
|
+
# verbs stay off trailing-significance-participle, whose list is closed
|
|
3624
|
+
# to verbs an event can be the subject of.
|
|
3625
|
+
pattern: /,#{WRAP_GAP}+
|
|
3626
|
+
(?:which#{WRAP_GAP}+means\b
|
|
3627
|
+
|which#{WRAP_GAP}+is#{WRAP_GAP}+to#{WRAP_GAP}+say\b
|
|
3628
|
+
|meaning#{WRAP_GAP}+(?:that|the|a|an|this|these|those|it|he|she|we|you|they|there|nothing|every|each|any|all|most|some|your|our|their|its)\b
|
|
3629
|
+
|making#{WRAP_GAP}+(?:it|them|us|you|the(?:#{WRAP_GAP}+[\w'’-]+){1,2})#{WRAP_GAP}+
|
|
3630
|
+
(?:[a-z]+er|(?:im)?possible|(?:more|less)(?:#{WRAP_GAP}+[a-z]+)?)
|
|
3631
|
+
(?=#{WRAP_GAP}+(?:to|for|than)\b|[ \t]*[.,;:!?)]|[ \t]*\r?\n|[ \t]*\z)
|
|
3632
|
+
|allowing#{WRAP_GAP}+(?:it|them|us|you)#{WRAP_GAP}+to\b
|
|
3633
|
+
|giving#{WRAP_GAP}+(?:them|us|you)#{WRAP_GAP}+(?:more|less|time|room)\b
|
|
3634
|
+
|leaving#{WRAP_GAP}+(?:them|us|you)#{WRAP_GAP}+with(?:out)?\b)/ix,
|
|
3635
|
+
message: "Trailing clause that says the sentence again, or hangs a result off it.",
|
|
3636
|
+
suggestion: "Cut the tail, or if it states a real consequence, make it its own sentence.",
|
|
3637
|
+
examples_bad: [
|
|
3638
|
+
"We moved the checks into the build step, which means the errors show up before anyone opens a review.",
|
|
3639
|
+
"The new queue drains in order, which is to say nothing jumps ahead of an older job.",
|
|
3640
|
+
"Every job now records its own start time, meaning the log tells you when the run began.",
|
|
3641
|
+
"He signed at once, meaning he had read it already.",
|
|
3642
|
+
"We rebuilt the parser, making the whole pipeline faster.",
|
|
3643
|
+
"The cache is local now, making it easier to reason about.",
|
|
3644
|
+
"The change is small, making it more robust.",
|
|
3645
|
+
"The lock is per row, making it impossible for two writers to collide.",
|
|
3646
|
+
"The cache now lives beside the worker, allowing us to skip the round trip.",
|
|
3647
|
+
"The report ships on Fridays, giving them more time to read it.",
|
|
3648
|
+
"The old flags are gone, leaving you with one switch to learn.",
|
|
3649
|
+
# Nothing before the comma is inspected.
|
|
3650
|
+
"The flag defaults to `false`, which means nothing is written to disk.",
|
|
3651
|
+
"- **Cache is local**, which means the round trip is gone.",
|
|
3652
|
+
"We fixed the bug (the null check), which means the crash is gone.",
|
|
3653
|
+
# Either gap may hard-wrap.
|
|
3654
|
+
"We moved the checks into the build step,\nwhich means the errors show up first.",
|
|
3655
|
+
"We moved the checks into the build step, which\nmeans the errors show up first."
|
|
3656
|
+
],
|
|
3657
|
+
examples_ok: [
|
|
3658
|
+
# "meaning" outside the closed set: the bare-noun gloss, the intention, the noun.
|
|
3659
|
+
"They count on with tatisitupe, meaning six.",
|
|
3660
|
+
"It is an old word, meaning caves.",
|
|
3661
|
+
"He left the room, meaning to come back before dark.",
|
|
3662
|
+
"She read on, meaning of the word aside, and let it pass.",
|
|
3663
|
+
"He shrugged, meaning no harm by it.",
|
|
3664
|
+
# The bare participles are ordinary English.
|
|
3665
|
+
"She left the room, leaving the door open.",
|
|
3666
|
+
"Cut the paper around the frame, leaving a margin for pasting.",
|
|
3667
|
+
"He handed over the keys, giving her a nod.",
|
|
3668
|
+
"The lid lifts off, allowing the steam to escape.",
|
|
3669
|
+
# A pronoun object with no closing word, or the wrong one.
|
|
3670
|
+
"Rub down the leather, making it as smooth as possible.",
|
|
3671
|
+
"The crowd parted, making room for more chairs.",
|
|
3672
|
+
"He pulled the cork, giving it time to breathe.",
|
|
3673
|
+
"He shut the door, leaving them to it.",
|
|
3674
|
+
"The porter took the trunk, leaving it with the station master.",
|
|
3675
|
+
# "-er" that is not a comparative, and a comparative that does not close the clause.
|
|
3676
|
+
"The rain kept up all week, making you wonder whether the trip was worth it.",
|
|
3677
|
+
"She turned the coat inside out, making it her own.",
|
|
3678
|
+
"They argued, making the same point over and over.",
|
|
3679
|
+
"The tide turned, making it matter less than before.",
|
|
3680
|
+
# A gerund list.
|
|
3681
|
+
"The work involves cutting, making and sanding the parts.",
|
|
3682
|
+
# A paragraph break is not a comma.
|
|
3683
|
+
"The checks moved into the build step\n\nWhich means the errors show up first."
|
|
3684
|
+
],
|
|
3685
|
+
rationale: "The tail after the connective says the head again in other words, and a " \
|
|
3686
|
+
"model adds one whenever a sentence feels short of a point. Careful writers " \
|
|
3687
|
+
"use the same connective to state a consequence, and the pattern cannot " \
|
|
3688
|
+
"tell the two apart, so the rule is off by default."
|
|
3689
|
+
),
|
|
3231
3690
|
# ── hedging ───────────────────────────────────────────────────────────
|
|
3232
3691
|
Rule.new(
|
|
3233
3692
|
id: "vague-attribution",
|
data/lib/sloplint/version.rb
CHANGED