sloplint 0.3.0 → 0.4.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 +73 -0
- data/README.md +5 -5
- data/docs/SPEC.md +29 -3
- data/lib/sloplint/rules.rb +652 -14
- 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: f1bc057d93ebef8a56816f986a286ce160461f8d3004c037a2f3b35ab764105e
|
|
4
|
+
data.tar.gz: 7c2e243d1795b3d417a815ea3cc0b2e2abe7d5954c7cdbfbd9cfeeeee3120213
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a213224903d49426954ede602803f941cc11617084f5cf93fd9990e003872424fac5ef18a65e615b737eb58c5f7b816252f8fcba303d50f09d037661e98badbe
|
|
7
|
+
data.tar.gz: 48761bf8884bd38d8494a4010c706ddf9f332b21df434d3c730654ee09e0ef0a401048df14d7118999575a538d9058ec511ccb9782fab655ddacf4b41953ce2b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,79 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
The catalog grows from 29 rules to 49. Every new rule was probed against
|
|
11
|
+
pre-2022 Hacker News comments or Project Gutenberg texts before shipping,
|
|
12
|
+
and each narrowing is pinned by an `examples_ok` fixture.
|
|
13
|
+
|
|
14
|
+
### New rules
|
|
15
|
+
|
|
16
|
+
- `worth-saying-plainly` (`rhetorical-tic`, `warning`): the self-rating
|
|
17
|
+
opener, "Worth saying plainly, ...". Needs all three slots -- evaluative
|
|
18
|
+
adjective, speech verb, manner adverb -- at a sentence or paragraph start.
|
|
19
|
+
- `hold-onto-that` (`rhetorical-tic`, `warning`): the sentence-initial
|
|
20
|
+
reader directive "Hold onto that" / "Hold on to this".
|
|
21
|
+
- `cleanly` (`rhetorical-tic`, `warning`): the bare manner adverb, "splits
|
|
22
|
+
cleanly into", "maps cleanly onto".
|
|
23
|
+
- `clean-count` (`rhetorical-tic`, `warning`): a number plus "clean" plus a
|
|
24
|
+
partition noun, "two clean buckets".
|
|
25
|
+
- `cleanest-x` (`rhetorical-tic`, `warning`): the superlative used to rank
|
|
26
|
+
one's own claim, "the cleanest framing is".
|
|
27
|
+
- `clean-x` (`rhetorical-tic`, `info`): "clean" in front of an idea as
|
|
28
|
+
praise, "a clean abstraction".
|
|
29
|
+
- `earns-its-place` (`rhetorical-tic`, `warning`): "earns its place" and
|
|
30
|
+
"earns its keep", the metaphor of a sentence or feature paying rent.
|
|
31
|
+
- `does-a-lot-of-work` (`rhetorical-tic`, `warning`): the remark that points
|
|
32
|
+
at a word and rates its load, "that qualifier does a lot of work here".
|
|
33
|
+
- `failure-mode-here` (`rhetorical-tic`, `warning`): "the failure mode here
|
|
34
|
+
is", the engineering term borrowed for an argument or a person. The
|
|
35
|
+
deictic "here" is the whole narrowing.
|
|
36
|
+
- `thats-the-tension` (`rhetorical-tic`, `warning`): the sentence-initial
|
|
37
|
+
closer "That's the tension." and "That's the bet:".
|
|
38
|
+
- `right-up-until` (`rhetorical-tic`, `warning`): "right up until it
|
|
39
|
+
doesn't". The intensifier is the narrowing -- the plain "until it doesn't"
|
|
40
|
+
is an old human idiom and stays clean.
|
|
41
|
+
- `two-things-true` (`rhetorical-tic`, `warning`): "two things can be true",
|
|
42
|
+
the concession that names neither half.
|
|
43
|
+
- `notice-what-there` (`rhetorical-tic`, `warning`): the self-referential
|
|
44
|
+
attention cue, "Notice what that argument did there", "Read that again".
|
|
45
|
+
- `notice-what` (`rhetorical-tic`, `info`): the bare sentence-initial
|
|
46
|
+
"Notice what ...", the quiet half of the pair above.
|
|
47
|
+
- `none-of-this-is-to-say` (`rhetorical-tic`, `warning`): the sweeping
|
|
48
|
+
concession that retracts an argument nobody made.
|
|
49
|
+
- `if-im-being-honest` (`rhetorical-tic`, `info`): the candor preamble.
|
|
50
|
+
Plain "to be honest" and "I'll be honest" stay clean.
|
|
51
|
+
- `genuinely` (`rhetorical-tic`, `info`, off by default): every
|
|
52
|
+
"genuinely". No allowlist separates the intensifier from the contrastive
|
|
53
|
+
use, so the rule runs only when named in `--select`, beside
|
|
54
|
+
`rule-of-three`.
|
|
55
|
+
- `question-isnt` (`structure`, `info`): the corrective frame in
|
|
56
|
+
interrogative dress, "The question isn't whether X, it's whether Y".
|
|
57
|
+
Requires the resolving clause.
|
|
58
|
+
- `and-thats-fine` (`rhetorical-tic`, `info`): the permission-granting
|
|
59
|
+
closer. The match must open and close a sentence.
|
|
60
|
+
- `less-about-more-about` (`structure`, `info`): the comparative
|
|
61
|
+
reframe, "It's less about X and more about Y". Both halves required.
|
|
62
|
+
|
|
63
|
+
### Changed rules
|
|
64
|
+
|
|
65
|
+
- `sit-with-that` broadened to any object. Two branches: the deictic object
|
|
66
|
+
needs no anchor, any other object needs the sentence-initial imperative.
|
|
67
|
+
- `the-punchline-is` widened to take "the honest answer is" and "the honest
|
|
68
|
+
version is".
|
|
69
|
+
- `worth-naming` and `worth-saying-plainly` widened together and stopped
|
|
70
|
+
double-flagging. Both take more verbs; `worth-naming` now yields the
|
|
71
|
+
adverb-bearing form to `worth-saying-plainly`.
|
|
72
|
+
|
|
73
|
+
### Docs
|
|
74
|
+
|
|
75
|
+
- `docs/SPEC.md` credits Wikipedia's "Signs of AI writing" and slopwash.com's
|
|
76
|
+
anti-slop ruleset, next to proselint, vale and write-good.
|
|
77
|
+
- The SPEC rule catalog had drifted ten rules behind the code. It is resynced,
|
|
78
|
+
and `spec/rules_spec.rb` now asserts every rule id appears in `docs/SPEC.md`,
|
|
79
|
+
the same way it already asserts the README counts.
|
|
80
|
+
|
|
8
81
|
## [0.3.0] - 2026-08-02
|
|
9
82
|
|
|
10
83
|
- New rule `load-bearing` (`rhetorical-tic`, `warning`): "load-bearing" used
|
data/README.md
CHANGED
|
@@ -128,18 +128,18 @@ An unknown id or category in `--select`/`--ignore` is a usage error (exit 2, nam
|
|
|
128
128
|
|
|
129
129
|
## The rule catalog
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
49 rules across four categories. `sloplint rules` prints them; `sloplint rules --json` gives an agent the enumerable form.
|
|
132
132
|
|
|
133
|
-
- **rhetorical-tic** (
|
|
133
|
+
- **rhetorical-tic** (36) the cadence patterns: `no-x-no-y`, `no-x-no-y-frag`, `thats-the-whole`, `thats-how-x`, `announced-takeaway`, `exact-exactly`, `load-bearing`, `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 more.
|
|
134
134
|
- **puffery** (5) Wikipedia's "signs of AI writing": `puffery-words` (vibrant, nestled, groundbreaking, in the heart of), `rich-tapestry`, `vital-role`, `stands-serves-as`, `underscores-highlights`.
|
|
135
|
-
- **structure** (
|
|
135
|
+
- **structure** (7) `not-just-x-but-y`, `not-x-but-y` (the bare corrective), `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`, and `rule-of-three`.
|
|
136
136
|
- **hedging** (1) `vague-attribution`: "some critics argue," "it is widely regarded."
|
|
137
137
|
|
|
138
138
|
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.
|
|
139
139
|
|
|
140
|
-
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`. 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.
|
|
140
|
+
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.
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
Two rules ship **off by default**, and both 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.
|
|
143
143
|
|
|
144
144
|
### Markdown handling
|
|
145
145
|
|
data/docs/SPEC.md
CHANGED
|
@@ -56,6 +56,8 @@ a specimen of slop, which is reason enough on its own.
|
|
|
56
56
|
- **proselint** — subcommand CLI (`check`, `version`, `dump-config`), `--output-format full|json|compact`, LSP-style diagnostics (line/column/severity/code/message), config file, clean exit codes. We copy this shape.
|
|
57
57
|
- **vale** — markup-aware (skips code blocks, knows Markdown). We do a lighter version: optional `--markdown` to skip fenced code and inline code.
|
|
58
58
|
- **write-good / alex** — naive regex rules, one module per rule. We keep rules as data, not code, so they're trivial to add.
|
|
59
|
+
- **Wikipedia's [Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing)** — the puffery category comes from its "words to watch" boxes, and its habit of grouping tells by the move they make rather than by word is the shape our categories follow.
|
|
60
|
+
- **[slopwash.com](https://slopwash.com)'s anti-slop ruleset** — a prompt-shaped catalogue of AI writing tells, and the prompt for the review that produced the current chat-register rules. We take its method, not its inventory: its vocabulary list (delve, tapestry, "nestled in the heart of") is calibrated to 2023–24 model output and didn't survive probing against writing since, so none of it ships. One family did, the false-intimacy preamble behind `if-im-being-honest`.
|
|
59
61
|
|
|
60
62
|
## Why build fresh instead of extending proselint
|
|
61
63
|
|
|
@@ -250,7 +252,7 @@ Categories (for `--select`/`--ignore` by group):
|
|
|
250
252
|
|
|
251
253
|
- `rhetorical-tic` — the cadence patterns (the user's list below)
|
|
252
254
|
- `puffery` — Wikipedia "words to watch" (boasts, vibrant, nestled, tapestry…)
|
|
253
|
-
- `structure` — rule-of-three, "not just X but Y", em dash, em-dash overuse
|
|
255
|
+
- `structure` — rule-of-three, "not just X but Y", "the question isn't X, it's Y", "less about X more about Y", em dash, em-dash overuse
|
|
254
256
|
- `hedging` — vague attribution ("some critics argue", "it is widely regarded")
|
|
255
257
|
|
|
256
258
|
Severities: `warning` for strong tells, `info` for weak/contextual ones. No
|
|
@@ -269,13 +271,35 @@ false-positive risk, which none has demonstrated.
|
|
|
269
271
|
| `did-not-x-did-not-y` | 2+ "did not …"/"didn't …" in a row | counts items |
|
|
270
272
|
| `dont-verb-it` | "Don't call it X. Call it Y." (negated verb+it, same verb+it) | |
|
|
271
273
|
| `sit-with-that` | "sit with that/this/it", "sit with the discomfort" | |
|
|
274
|
+
| `hold-onto-that` | sentence-initial "hold onto/on to that/this" | imperative only |
|
|
275
|
+
| `cleanly` | "cleanly" anywhere | no verb list; the engineering idiom counts too |
|
|
276
|
+
| `clean-count` | "two/three clean parts/buckets/categories…" | needs a partition noun |
|
|
277
|
+
| `cleanest-x` | "the cleanest framing/formulation", "cleanest way to put it" | noun list only |
|
|
278
|
+
| `clean-x` | "a clean abstraction/distinction/framing", "clean line between" | `info` |
|
|
272
279
|
| `you-already-know` | "you already know" (+ the answer / standalone) | |
|
|
273
280
|
| `is-the-entire` | "X is the entire point/game/business model" | |
|
|
274
281
|
| `the-entire-is` | "the entire point/game/… is" (flip of above) | |
|
|
275
282
|
| `is-real-and-not` | "the X is real, and/not…", "is the real … and it" | skip "real estate/time"; `info` |
|
|
276
|
-
| `the-punchline-is` | "the punchline is/:/?" | |
|
|
277
|
-
| `worth-naming` | "worth naming
|
|
283
|
+
| `the-punchline-is` | "the punchline is/:/?", "the honest answer/version is" | "short version" left out; ordinary writing |
|
|
284
|
+
| `worth-naming` | "worth naming/flagging/separating/spelling out" | skip "naming names"; yields to the rule below when a manner adverb follows; `info` |
|
|
285
|
+
| `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 |
|
|
278
286
|
| `not-nothing` | copula + "not nothing" litotes, any subject | skip personal/there subjects |
|
|
287
|
+
| `exact-exactly` | "exact"/"exactly" | allowlist for the checkable uses; `info` |
|
|
288
|
+
| `load-bearing` | "load-bearing" outside its construction sense | skip building nouns either side |
|
|
289
|
+
| `thats-how-x` | sentence-initial "that's how…" | |
|
|
290
|
+
| `announced-takeaway` | colon-led label: "The pattern/lesson/takeaway…:" | sentence-initial |
|
|
291
|
+
| `earns-its-place` | "earns its place/keep" (any possessive) | possessive required; `warning` |
|
|
292
|
+
| `does-a-lot-of-work` | "does a lot of work here/in that sentence", "a lot of heavy lifting" | plain "the heavy lifting" excluded |
|
|
293
|
+
| `failure-mode-here` | "the failure mode here is" | deictic required; bare "the failure mode is" excluded |
|
|
294
|
+
| `thats-the-tension` | sentence-initial "that's the tension/bet" as a closer | noun must end the clause; "tradeoff"/"catch" excluded |
|
|
295
|
+
| `right-up-until` | "right up until it doesn't/isn't/stops/breaks" | bare "until it doesn't" excluded |
|
|
296
|
+
| `two-things-true` | "two/both things can be/are true" | count fixed at two |
|
|
297
|
+
| `notice-what-there` | "notice what X did there", "read that again" | sentence-initial |
|
|
298
|
+
| `notice-what` | bare sentence-initial "Notice what…" | yields the "there" frame to the rule above; "how" excluded; `info` |
|
|
299
|
+
| `none-of-this-is-to-say` | "none of this/that/the above is to say" | every other "not to say" phrasing excluded |
|
|
300
|
+
| `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 |
|
|
301
|
+
| `genuinely` | any "genuinely" | off by default; no narrowing holds |
|
|
302
|
+
| `and-thats-fine` | "And that's fine/okay." as a whole sentence | "and" required; must open and close the sentence |
|
|
279
303
|
| `is-is` | doubled copula: "what it is is …", "the thing is, is that …" | comma optional |
|
|
280
304
|
|
|
281
305
|
### puffery (Wikipedia: Signs of AI writing)
|
|
@@ -292,6 +316,8 @@ Single flat rule per word-cluster, matched as whole words:
|
|
|
292
316
|
|
|
293
317
|
- `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.
|
|
294
318
|
- `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.
|
|
319
|
+
- `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.
|
|
320
|
+
- `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.
|
|
295
321
|
- `rule-of-three` — three parallel comma items ending a sentence (heuristic; `info` severity, off by default via `--select` since it false-positives).
|
|
296
322
|
- `em-dash` — any em dash; `info`.
|
|
297
323
|
- `em-dash-overuse` — 3+ em dashes in one paragraph; `warning`.
|
data/lib/sloplint/rules.rb
CHANGED
|
@@ -117,12 +117,173 @@ module Sloplint
|
|
|
117
117
|
id: "sit-with-that",
|
|
118
118
|
category: "rhetorical-tic",
|
|
119
119
|
severity: "warning",
|
|
120
|
-
|
|
120
|
+
# Two branches. The deictic object is the tic anywhere in a sentence, so
|
|
121
|
+
# "sit with that/this/it" needs no anchor. Anything else needs the
|
|
122
|
+
# sentence-initial imperative, which is where the tic lives and where the
|
|
123
|
+
# ordinary companion sense ("come and sit with me") mostly is not.
|
|
124
|
+
# Widened from that|this|it|the+word, which both missed the bare abstract
|
|
125
|
+
# object ("sit with uncertainty") and flagged "sit with the baby".
|
|
126
|
+
pattern: /\bsit\s+with\s+(?:that|this|it)\b
|
|
127
|
+
|(?:\A|[.!?]\s+|\n\s*\n)\s*(?:but\s+|and\s+|now\s+|so\s+|just\s+)?
|
|
128
|
+
sit\s+with\s+\S/ix,
|
|
121
129
|
message: '"Sit with that/the discomfort" is therapized LLM filler.',
|
|
122
130
|
suggestion: "Cut it, or say what you actually want the reader to do.",
|
|
123
|
-
examples_bad: [
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
examples_bad: [
|
|
132
|
+
"Just sit with that for a moment.",
|
|
133
|
+
"Sit with uncertainty for a while.",
|
|
134
|
+
"Sit with the discomfort before you answer.",
|
|
135
|
+
"The numbers were worse than that. Sit with what they imply."
|
|
136
|
+
],
|
|
137
|
+
examples_ok: [
|
|
138
|
+
"They sit with their families at dinner.",
|
|
139
|
+
"Come and sit with me on the porch.",
|
|
140
|
+
"I sat with the baby until she slept."
|
|
141
|
+
],
|
|
142
|
+
rationale: "The 'sit with X' imperative is a model comfort tic, rare in real argument. " \
|
|
143
|
+
"The object does not matter: the tic is telling the reader to dwell instead " \
|
|
144
|
+
"of giving them something to dwell on. The sentence-initial imperative does " \
|
|
145
|
+
"flag the plain companion sense (\"Sit with the baby while I run out\"), " \
|
|
146
|
+
"which is accepted -- hence warning, not error."
|
|
147
|
+
),
|
|
148
|
+
Rule.new(
|
|
149
|
+
id: "hold-onto-that",
|
|
150
|
+
category: "rhetorical-tic",
|
|
151
|
+
severity: "warning",
|
|
152
|
+
# Sentence-initial imperative only. Past tense and subordinate clauses
|
|
153
|
+
# ("she held on to that letter", "if you hold onto that phrase") are a
|
|
154
|
+
# different construction, not the tell. Everything AFTER that/this is
|
|
155
|
+
# unconstrained on purpose, which does flag the concrete-object
|
|
156
|
+
# imperative ("Hold on to that rope") -- accepted, hence warning.
|
|
157
|
+
pattern: /(?:\A|[.!?]\s+|\n\s*\n)\s*(?:but\s+|and\s+|now\s+|so\s+)?hold\s+(?:on\s+to|onto)\s+(?:that|this)\b/i,
|
|
158
|
+
message: '"Hold onto that…" is a stock LLM attention cue.',
|
|
159
|
+
suggestion: "Cut it; if the detail matters, use it where it matters.",
|
|
160
|
+
examples_bad: [
|
|
161
|
+
"Hold onto that second half, because it does more work than the first.",
|
|
162
|
+
"Hold on to that for a moment.",
|
|
163
|
+
"The table looked fine. Now hold onto this last figure; it changes everything."
|
|
164
|
+
],
|
|
165
|
+
examples_ok: [
|
|
166
|
+
"Hold that thought while I check the log.",
|
|
167
|
+
"If you hold onto that phrase too long, the sentence sags.",
|
|
168
|
+
"She held on to that letter for years."
|
|
169
|
+
],
|
|
170
|
+
rationale: "The imperative parks a fragment against a payoff the writer has promised " \
|
|
171
|
+
"but not delivered. It fits in front of any sentence in any document, and " \
|
|
172
|
+
"real argument uses the detail again where it matters instead of asking " \
|
|
173
|
+
"the reader to carry it."
|
|
174
|
+
),
|
|
175
|
+
Rule.new(
|
|
176
|
+
id: "cleanly",
|
|
177
|
+
category: "rhetorical-tic",
|
|
178
|
+
severity: "warning",
|
|
179
|
+
# No verb list. The engineering idioms -- a patch applies cleanly, a
|
|
180
|
+
# branch merges cleanly, a build compiles cleanly -- are the same move
|
|
181
|
+
# and get flagged too, on purpose. The word survives in a 19th-century
|
|
182
|
+
# adjective sense ("cleanly dressed", "a cleanly laid table") that the
|
|
183
|
+
# rule also catches, which costs nothing: nobody writes that today.
|
|
184
|
+
pattern: /\bcleanly\b/i,
|
|
185
|
+
message: '"Cleanly" rates the fit instead of showing it.',
|
|
186
|
+
suggestion: "Cut the adverb, or say what actually lined up.",
|
|
187
|
+
examples_bad: [
|
|
188
|
+
"The argument splits cleanly into two parts.",
|
|
189
|
+
"The patch applies cleanly to main.",
|
|
190
|
+
"The new taxonomy maps cleanly onto the old one.",
|
|
191
|
+
"The objection breaks down cleanly, and neither half survives."
|
|
192
|
+
],
|
|
193
|
+
examples_ok: [
|
|
194
|
+
"The branch merged without conflicts.",
|
|
195
|
+
"She wiped the counter clean.",
|
|
196
|
+
"The build finished with no warnings."
|
|
197
|
+
],
|
|
198
|
+
rationale: "The adverb rates the join instead of showing it, and it rates before the " \
|
|
199
|
+
"reader has anything to check. Real material resists -- the leftover case, " \
|
|
200
|
+
"the item in both buckets. A writer who has met the leftovers names them."
|
|
201
|
+
),
|
|
202
|
+
Rule.new(
|
|
203
|
+
id: "clean-count",
|
|
204
|
+
category: "rhetorical-tic",
|
|
205
|
+
severity: "warning",
|
|
206
|
+
# Needs a partition noun. The bare count reaches the laundry: Ulysses
|
|
207
|
+
# has "four clean strokes", Jane Eyre "two clean tuckers".
|
|
208
|
+
pattern: /\b(?:two|three|four|five|six|seven|2|3|4|5|6|7)\s+
|
|
209
|
+
(?:very\s+|fairly\s+|reasonably\s+|pretty\s+)?clean\s+(?:\w+\s+)?
|
|
210
|
+
(?:parts|halves|pieces|buckets|categories|groups|chunks|sections|layers
|
|
211
|
+
|camps|cases|classes|clusters|splits|steps|stages|phases|tiers|bands
|
|
212
|
+
|lanes|axes|dimensions|questions|claims|ideas|moves|jobs|roles)\b/ix,
|
|
213
|
+
message: '"Two clean parts" claims a tidier division than it shows.',
|
|
214
|
+
suggestion: "Name the parts and let the reader see whether they hold.",
|
|
215
|
+
examples_bad: [
|
|
216
|
+
"That leaves two clean buckets for the rest of the work.",
|
|
217
|
+
"The job falls into three clean stages.",
|
|
218
|
+
"You end up with two clean categories and a remainder nobody mentions."
|
|
219
|
+
],
|
|
220
|
+
examples_ok: [
|
|
221
|
+
"She wore two clean shirts that week.",
|
|
222
|
+
"He cut it in four clean strokes.",
|
|
223
|
+
"The kitchen had three clean plates left."
|
|
224
|
+
],
|
|
225
|
+
rationale: "The count and the adjective do the same work twice: the number says the " \
|
|
226
|
+
"division is settled, the adjective says it was easy. Neither is evidence, " \
|
|
227
|
+
"and prose that has met the awkward third case rarely offers both."
|
|
228
|
+
),
|
|
229
|
+
Rule.new(
|
|
230
|
+
id: "cleanest-x",
|
|
231
|
+
category: "rhetorical-tic",
|
|
232
|
+
severity: "warning",
|
|
233
|
+
# Noun list only. "The cleanest way to install the driver" and "the
|
|
234
|
+
# cleanest cut of meat" are ordinary English, so "way" is admitted only
|
|
235
|
+
# in front of a speech verb, and the concrete-capable nouns (cut, line,
|
|
236
|
+
# version, split) are left out entirely.
|
|
237
|
+
pattern: /\bcleanest\s+(?:\w+\s+){0,2}
|
|
238
|
+
(?:framing|formulation|statement|account|argument|idea|definition|summary
|
|
239
|
+
|reading|take|point|story|explanation|distinction|comparison|mapping
|
|
240
|
+
|abstraction)\b
|
|
241
|
+
|\bcleanest\s+way\s+to\s+(?:say|put|frame|state|describe|phrase|express
|
|
242
|
+
|think\s+about)\b/ix,
|
|
243
|
+
message: '"The cleanest framing/way to put it…" ranks your own claim for the reader.',
|
|
244
|
+
suggestion: "Drop the ranking and make the claim; the reader grades it.",
|
|
245
|
+
examples_bad: [
|
|
246
|
+
"The cleanest framing is that nobody was in charge.",
|
|
247
|
+
"That is the cleanest way to put it.",
|
|
248
|
+
"The cleanest organizing idea here is scarcity."
|
|
249
|
+
],
|
|
250
|
+
examples_ok: [
|
|
251
|
+
"This is the cleanest way to install the driver.",
|
|
252
|
+
"She picked the cleanest room in the house.",
|
|
253
|
+
"The cleanest energy source is still hydro."
|
|
254
|
+
],
|
|
255
|
+
rationale: "Self-ranking: the writer tells the reader which of their own claims is " \
|
|
256
|
+
"the good one, which is the reader's job and costs nothing to assert."
|
|
257
|
+
),
|
|
258
|
+
Rule.new(
|
|
259
|
+
id: "clean-x",
|
|
260
|
+
category: "rhetorical-tic",
|
|
261
|
+
severity: "info",
|
|
262
|
+
# The quiet half of cleanest-x, at info because the positive degree is
|
|
263
|
+
# where ordinary usage lives. "A clean separation of concerns" is
|
|
264
|
+
# standard engineering English, so "separation" stays out of the list;
|
|
265
|
+
# "break" is admitted only in "clean break between", never bare, because
|
|
266
|
+
# "make a clean break with the past" is an idiom and not a tell.
|
|
267
|
+
pattern: /\b(?:a|the|one)\s+(?:\w+\s+)?clean\s+(?:\w+\s+)?
|
|
268
|
+
(?:abstraction|distinction|framing|formulation|mapping|through-line
|
|
269
|
+
|story|answer|argument|split|divide)\b
|
|
270
|
+
|\bclean\s+(?:line|break|split)\s+between\b/ix,
|
|
271
|
+
message: '"A clean abstraction / clean framing" praises the idea instead of showing it.',
|
|
272
|
+
suggestion: "Cut the adjective; if it is tidy, the reader will see that.",
|
|
273
|
+
examples_bad: [
|
|
274
|
+
"That gives us a clean abstraction over the queue.",
|
|
275
|
+
"The clean framing is that both sides were guessing.",
|
|
276
|
+
"There is a clean line between advice and instruction."
|
|
277
|
+
],
|
|
278
|
+
examples_ok: [
|
|
279
|
+
"The design has a clean separation of concerns.",
|
|
280
|
+
"He made a clean break with the past.",
|
|
281
|
+
"They ran a clean campaign.",
|
|
282
|
+
"The report gives a clean bill of health."
|
|
283
|
+
],
|
|
284
|
+
rationale: "'Clean' in front of an idea is evaluation, not description -- it says the " \
|
|
285
|
+
"writer approves, and nothing about the idea. It ships at info because the " \
|
|
286
|
+
"same words carry a plain sense a regex cannot separate from the tic."
|
|
126
287
|
),
|
|
127
288
|
Rule.new(
|
|
128
289
|
id: "you-already-know",
|
|
@@ -183,12 +344,27 @@ module Sloplint
|
|
|
183
344
|
id: "the-punchline-is",
|
|
184
345
|
category: "rhetorical-tic",
|
|
185
346
|
severity: "warning",
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
347
|
+
# Same reveal, three nouns. "honest answer" and "honest version" join
|
|
348
|
+
# "punchline" because they do the identical job: rate the sentence as
|
|
349
|
+
# the candid one before the reader gets it. "short version" was tried
|
|
350
|
+
# and left out -- people really do offer a short version, and both the
|
|
351
|
+
# "is" and the colon form of it are ordinary writing.
|
|
352
|
+
pattern: /\bthe\s+(?:punchline|honest\s+(?:answer|version))\s*(?:is\b|[:?])/i,
|
|
353
|
+
message: '"The punchline/honest answer is…" is a stock LLM reveal.',
|
|
354
|
+
suggestion: "Deliver the point without announcing it first.",
|
|
355
|
+
examples_bad: [
|
|
356
|
+
"The punchline is that nobody noticed.",
|
|
357
|
+
"The honest answer is that we never checked.",
|
|
358
|
+
"The honest version: the deadline was never real."
|
|
359
|
+
],
|
|
360
|
+
examples_ok: [
|
|
361
|
+
"The punchline landed perfectly.",
|
|
362
|
+
"The short version is that the cache was cold.",
|
|
363
|
+
"She gave the honest answer without being asked."
|
|
364
|
+
],
|
|
365
|
+
rationale: "Announcing 'the punchline', or grading your own next sentence as the honest " \
|
|
366
|
+
"one, is a model framing device, rare in real prose. The honest-answer form " \
|
|
367
|
+
"carries a second claim on top: that the sentences around it were less so."
|
|
192
368
|
),
|
|
193
369
|
Rule.new(
|
|
194
370
|
id: "worth-naming",
|
|
@@ -198,19 +374,89 @@ module Sloplint
|
|
|
198
374
|
# names" idiom is part of the matched text -- skip: checks the matched
|
|
199
375
|
# text itself, and the tighter /\bworth\s+naming\b/ never captured
|
|
200
376
|
# enough of "worth naming names" for the skip to ever reach it.
|
|
201
|
-
|
|
202
|
-
|
|
377
|
+
#
|
|
378
|
+
# The trailing lookahead hands the adverb-bearing form to
|
|
379
|
+
# worth-saying-plainly, which is the same construction with a manner
|
|
380
|
+
# adverb on the end and reports it at warning. Without it both rules
|
|
381
|
+
# fire on one span. Cost: a mid-sentence "worth naming plainly", which
|
|
382
|
+
# the other rule's sentence anchor won't reach, now goes unflagged.
|
|
383
|
+
pattern: /\bworth\s+(?:naming(?:\s+names)?|flagging|separating|spelling\s+(?:it\s+)?out)\b
|
|
384
|
+
(?!\s+(?:plainly|clearly|bluntly|directly|simply|outright|flatly|straight
|
|
385
|
+
|up\s+front|out\s+loud)\b)/ix,
|
|
386
|
+
message: '"Worth naming/flagging…" is a stock LLM signposting phrase.',
|
|
203
387
|
suggestion: "Just name the thing; skip the meta-announcement.",
|
|
204
388
|
skip: [/naming names/i],
|
|
205
|
-
examples_bad: [
|
|
389
|
+
examples_bad: [
|
|
390
|
+
"One tension is worth naming here.",
|
|
391
|
+
"Two failures are worth flagging before we move on.",
|
|
392
|
+
"The two cases are worth separating."
|
|
393
|
+
],
|
|
394
|
+
# The adverb-bearing form ("worth naming plainly") can't sit here: it
|
|
395
|
+
# belongs to worth-saying-plainly, which flags it, and the cross-rule
|
|
396
|
+
# check requires an ok-fixture to be clean against the whole catalog.
|
|
397
|
+
# The "worth-* pair" example in rules_spec.rb pins that hand-off.
|
|
206
398
|
examples_ok: ["It's worth reading twice.", "It's worth naming names in this report."],
|
|
207
|
-
rationale: "'worth naming'
|
|
399
|
+
rationale: "'worth naming', and its siblings 'worth flagging' and 'worth separating', " \
|
|
400
|
+
"collapse two senses a regex can't tell apart: the AI " \
|
|
208
401
|
"meta-signpost announcing a point is coming ('One tension is worth naming " \
|
|
209
402
|
"here') and the plain sense of a thing worth calling or mentioning, which " \
|
|
210
403
|
"careful writers use too -- Emerson's 'the only thing worth naming to do that' " \
|
|
211
404
|
"is the latter, not the former. A flag here means the phrase is present, not " \
|
|
212
405
|
"which sense it's in."
|
|
213
406
|
),
|
|
407
|
+
Rule.new(
|
|
408
|
+
id: "worth-saying-plainly",
|
|
409
|
+
category: "rhetorical-tic",
|
|
410
|
+
severity: "warning",
|
|
411
|
+
# Two branches, both sentence-initial.
|
|
412
|
+
#
|
|
413
|
+
# First: evaluative adjective + speech verb + manner adverb, all three
|
|
414
|
+
# required. Any two occur in ordinary prose ("worth saying yes to",
|
|
415
|
+
# "said plainly that"); the full stack is the signpost.
|
|
416
|
+
#
|
|
417
|
+
# Second: the bare imperative with the adjective dropped ("Put plainly,"
|
|
418
|
+
# / "Said bluntly,"). It runs a shorter adverb list than the first
|
|
419
|
+
# branch, because the explainer openers are ordinary human writing and
|
|
420
|
+
# would swamp it -- "simply put" and "put simply" are common, so
|
|
421
|
+
# "simply", "clearly", "directly", "straight" and "up front" are all
|
|
422
|
+
# left out of this branch. "To put it bluntly" is likewise safe: the
|
|
423
|
+
# anchor puts "To" where the verb has to be, so it never matches.
|
|
424
|
+
pattern: /(?:\A|[.!?]\s+|\n\s*\n)\s*
|
|
425
|
+
(?:(?:it'?s\s+|that'?s\s+)?
|
|
426
|
+
(?:worth|better|best|easier|simpler|fairer|clearer)\s+
|
|
427
|
+
(?:saying|stating|putting|naming|flagging|separating|said|stated|put
|
|
428
|
+
|spelling\s+(?:it\s+)?out)\s+
|
|
429
|
+
(?:it\s+|this\s+|that\s+)?
|
|
430
|
+
(?:plainly|clearly|bluntly|directly|simply|outright|flatly|straight
|
|
431
|
+
|up\s+front|out\s+loud)
|
|
432
|
+
|(?:put|said|stated)\s+(?:it\s+|this\s+|that\s+)?
|
|
433
|
+
(?:plainly|bluntly|flatly|outright))\b/ix,
|
|
434
|
+
message: '"Worth saying plainly…" rates the sentence before the reader can.',
|
|
435
|
+
suggestion: "Say the thing; if it lands plainly, the reader will notice.",
|
|
436
|
+
examples_bad: [
|
|
437
|
+
"Worth saying plainly, the ceiling is set by the first report.",
|
|
438
|
+
"It's worth stating clearly: the data never arrived.",
|
|
439
|
+
"The batch failed. Worth putting it bluntly, we lost a week.",
|
|
440
|
+
"Put plainly, the ceiling is set by the first report.",
|
|
441
|
+
"The review ran long. Said bluntly, nobody had read the draft."
|
|
442
|
+
],
|
|
443
|
+
examples_ok: [
|
|
444
|
+
"It's worth reading twice.", "The contract is worth saying yes to.",
|
|
445
|
+
"She said plainly that the plan had failed.",
|
|
446
|
+
"He put it bluntly and everyone understood.",
|
|
447
|
+
# The explainer openers the bare branch deliberately leaves alone.
|
|
448
|
+
"Put simply, gravity pulls things down.",
|
|
449
|
+
"Simply put, the cache was never warm.",
|
|
450
|
+
"To put it bluntly, we lost a week.",
|
|
451
|
+
"He stated clearly that the plan had failed.",
|
|
452
|
+
# Hard-wrapped mid-sentence: a line break is not a paragraph break.
|
|
453
|
+
"the argument started long before that and\nworth saying plainly is not how it opened."
|
|
454
|
+
],
|
|
455
|
+
rationale: "The sentence is about the sentence: an unsourced claim that what follows " \
|
|
456
|
+
"deserves attention, made before the reader has anything to weigh it " \
|
|
457
|
+
"against. It is also subjectless -- worth it to whom? -- and fits in front " \
|
|
458
|
+
"of any claim in any document, which is why it says nothing about this one."
|
|
459
|
+
),
|
|
214
460
|
Rule.new(
|
|
215
461
|
id: "not-nothing",
|
|
216
462
|
category: "rhetorical-tic",
|
|
@@ -418,6 +664,344 @@ module Sloplint
|
|
|
418
664
|
"copula and a spoken disfluency. The third is a typo."
|
|
419
665
|
),
|
|
420
666
|
|
|
667
|
+
Rule.new(
|
|
668
|
+
id: "earns-its-place",
|
|
669
|
+
category: "rhetorical-tic",
|
|
670
|
+
severity: "warning",
|
|
671
|
+
# The possessive is the narrowing. "earned a place on the team" and
|
|
672
|
+
# "earn a place in the final" are ordinary; "earns its place" is the
|
|
673
|
+
# metaphor, a thing paying for the room it takes up.
|
|
674
|
+
pattern: /\b(?:earns?|earned|earning)\s+(?:its|their|his|her)\s+(?:place|keep)\b/i,
|
|
675
|
+
message: '"Earns its place/keep" prices the thing instead of showing it.',
|
|
676
|
+
suggestion: "Say what it does; the reader decides whether it was worth the room.",
|
|
677
|
+
examples_bad: [
|
|
678
|
+
"Every paragraph here earns its place.",
|
|
679
|
+
"The third example earns its keep and the other two do not.",
|
|
680
|
+
"That flag earned its place in the interface."
|
|
681
|
+
],
|
|
682
|
+
examples_ok: [
|
|
683
|
+
"She earned a place on the team that year.",
|
|
684
|
+
"He earned enough to keep the lights on.",
|
|
685
|
+
"The tenant paid the rent on time."
|
|
686
|
+
],
|
|
687
|
+
rationale: "The phrase grades the material against an unstated budget the reader never " \
|
|
688
|
+
"saw, and it grades before showing the work, so there is nothing to check it " \
|
|
689
|
+
"against. It belongs to the same borrowed-load family as 'load-bearing': " \
|
|
690
|
+
"a building metaphor applied to a sentence."
|
|
691
|
+
),
|
|
692
|
+
Rule.new(
|
|
693
|
+
id: "does-a-lot-of-work",
|
|
694
|
+
category: "rhetorical-tic",
|
|
695
|
+
severity: "warning",
|
|
696
|
+
# Two arms, both narrowed away from the ordinary sense.
|
|
697
|
+
#
|
|
698
|
+
# "a lot of work" needs a locative ("here", "in that sentence") because
|
|
699
|
+
# the bare phrase is just a statement about effort -- she does a lot of
|
|
700
|
+
# work for the shelter. The locative is what turns it into a remark
|
|
701
|
+
# about a word on the page.
|
|
702
|
+
#
|
|
703
|
+
# "the heavy lifting" is left out entirely and only "a lot of heavy
|
|
704
|
+
# lifting" is admitted: "the GPU does the heavy lifting" is ordinary
|
|
705
|
+
# technical English and far commoner than the prose-criticism sense.
|
|
706
|
+
pattern: /\b(?:does|do|doing|did)\s+a\s+lot\s+of\s+
|
|
707
|
+
(?:heavy\s+lifting\b|work\s+(?:here|in\s+th(?:at|is))\b)/ix,
|
|
708
|
+
message: '"Does a lot of work here" grades the word instead of reading it.',
|
|
709
|
+
suggestion: "Say what the word is doing, or what it hides.",
|
|
710
|
+
examples_bad: [
|
|
711
|
+
"That qualifier does a lot of work here.",
|
|
712
|
+
"The word \"mostly\" is doing a lot of heavy lifting.",
|
|
713
|
+
"\"Reasonable\" does a lot of work in that sentence."
|
|
714
|
+
],
|
|
715
|
+
examples_ok: [
|
|
716
|
+
"The GPU does the heavy lifting.",
|
|
717
|
+
"She does a lot of work for the shelter.",
|
|
718
|
+
"They did the heavy lifting on the migration."
|
|
719
|
+
],
|
|
720
|
+
rationale: "The remark points at a word and rates its load without saying what the load " \
|
|
721
|
+
"is, so the reader learns that something is being smuggled but never what. " \
|
|
722
|
+
"It is the same borrowed-load metaphor as 'load-bearing', one step further " \
|
|
723
|
+
"from the building."
|
|
724
|
+
),
|
|
725
|
+
Rule.new(
|
|
726
|
+
id: "failure-mode-here",
|
|
727
|
+
category: "rhetorical-tic",
|
|
728
|
+
severity: "warning",
|
|
729
|
+
# "here" is the whole narrowing, and it is doing a lot -- the bare "the
|
|
730
|
+
# failure mode is" is ordinary engineering writing about real systems,
|
|
731
|
+
# where the phrase means what it says. The deictic is what marks the
|
|
732
|
+
# borrowed sense: an argument, a draft or a person named as a mechanism
|
|
733
|
+
# with a characteristic way of breaking.
|
|
734
|
+
pattern: /\bthe\s+failure\s+mode\s+here\s+is\b/i,
|
|
735
|
+
message: '"The failure mode here is…" borrows an engineering term for an argument.',
|
|
736
|
+
suggestion: "Say what goes wrong and when, without the mechanism framing.",
|
|
737
|
+
examples_bad: [
|
|
738
|
+
"The failure mode here is that nobody reads past the first paragraph.",
|
|
739
|
+
"It reads well enough. The failure mode here is trusting the summary.",
|
|
740
|
+
"The failure mode here is social, not technical."
|
|
741
|
+
],
|
|
742
|
+
examples_ok: [
|
|
743
|
+
"The failure mode is a stuck relay.",
|
|
744
|
+
"We documented every failure mode in the valve assembly.",
|
|
745
|
+
"The common failure mode here was corrosion."
|
|
746
|
+
],
|
|
747
|
+
rationale: "The phrase treats a piece of writing or a person as a mechanism with a " \
|
|
748
|
+
"characteristic breakage, which sounds diagnostic and commits to nothing: " \
|
|
749
|
+
"no conditions, no frequency, nothing to check. In its home discipline the " \
|
|
750
|
+
"term earns its precision from a part that actually fails."
|
|
751
|
+
),
|
|
752
|
+
Rule.new(
|
|
753
|
+
id: "thats-the-tension",
|
|
754
|
+
category: "rhetorical-tic",
|
|
755
|
+
severity: "warning",
|
|
756
|
+
# Sentence-initial, only two nouns, and the noun must end the clause.
|
|
757
|
+
#
|
|
758
|
+
# "tradeoff" and "catch" were tried and cut: both are ordinary English
|
|
759
|
+
# in this exact frame, and the anchor removes only 60% of their volume,
|
|
760
|
+
# which is not enough. The surviving pair names a state of the argument
|
|
761
|
+
# rather than a thing in the world, which is what makes it the tic.
|
|
762
|
+
#
|
|
763
|
+
# The clause-final guard is what separates the closer from an ordinary
|
|
764
|
+
# sentence that happens to start the same way -- "This is the bet we
|
|
765
|
+
# placed in March" keeps going, and so is not the move.
|
|
766
|
+
pattern: /(?:\A|[.!?]\s+|\n\s*\n)\s*(?:and\s+|so\s+|but\s+)?
|
|
767
|
+
(?:that|this)(?:'s|’s|\s+is)\s+the\s+(?:tension|bet)
|
|
768
|
+
\s*(?:[.!?:;—]|\z)/ix,
|
|
769
|
+
message: '"That\'s the tension/bet." names the shape instead of the thing.',
|
|
770
|
+
suggestion: "State the two things pulling against each other.",
|
|
771
|
+
examples_bad: [
|
|
772
|
+
"That's the tension.",
|
|
773
|
+
"Both readings are defensible. That's the tension.",
|
|
774
|
+
"So that's the bet: cheaper now, slower later."
|
|
775
|
+
],
|
|
776
|
+
examples_ok: [
|
|
777
|
+
"That's the trade-off.",
|
|
778
|
+
"The rope went slack and that's the tension gone.",
|
|
779
|
+
"This is the bet we placed in March."
|
|
780
|
+
],
|
|
781
|
+
rationale: "The sentence labels the argument's shape and stops, which reads as a " \
|
|
782
|
+
"conclusion while resolving nothing -- the reader is told a tension exists " \
|
|
783
|
+
"but not what pulls against what. It is the closer half of the same move as " \
|
|
784
|
+
"'that's the whole point', with the noun swapped for a state of play."
|
|
785
|
+
),
|
|
786
|
+
Rule.new(
|
|
787
|
+
id: "right-up-until",
|
|
788
|
+
category: "rhetorical-tic",
|
|
789
|
+
severity: "warning",
|
|
790
|
+
# The intensifier is the tell, not the reversal. "It works until it
|
|
791
|
+
# doesn't" is an old human idiom and stays clean; stacking "right up"
|
|
792
|
+
# in front of it is the model's version, and the two together are two
|
|
793
|
+
# orders of magnitude rarer than either half.
|
|
794
|
+
pattern: /\bright\s+up\s+until\s+it\s+(?:doesn'?t|isn'?t|stops|breaks)\b/i,
|
|
795
|
+
message: '"Right up until it doesn\'t" is a stock reversal closer.',
|
|
796
|
+
suggestion: "Name the point where it stops working.",
|
|
797
|
+
examples_bad: [
|
|
798
|
+
"The approach works, right up until it doesn't.",
|
|
799
|
+
"The cache stays warm right up until it isn't.",
|
|
800
|
+
"It scales fine right up until it breaks."
|
|
801
|
+
],
|
|
802
|
+
examples_ok: [
|
|
803
|
+
"It works until it doesn't.",
|
|
804
|
+
"She stayed right up until the end.",
|
|
805
|
+
"The pattern held right up until Tuesday."
|
|
806
|
+
],
|
|
807
|
+
rationale: "The closer promises a reversal and withholds it: the reader is told the " \
|
|
808
|
+
"thing fails without being told when, why, or how far in. The bare idiom " \
|
|
809
|
+
"does the same, which is why only the intensified stack is flagged -- a " \
|
|
810
|
+
"writer who reaches for the longer form is reaching for the cadence."
|
|
811
|
+
),
|
|
812
|
+
Rule.new(
|
|
813
|
+
id: "two-things-true",
|
|
814
|
+
category: "rhetorical-tic",
|
|
815
|
+
severity: "warning",
|
|
816
|
+
# Closed phrase, no anchor needed. The optional "both" and the optional
|
|
817
|
+
# "at once" tail are the two ways the sentence is padded; the count word
|
|
818
|
+
# is fixed at two, because "three things can be true" is someone
|
|
819
|
+
# actually counting.
|
|
820
|
+
pattern: /\b(?:two|both)\s+things\s+(?:can\s+(?:both\s+)?be|are)\s+true\b/i,
|
|
821
|
+
message: '"Two things can be true" concedes the shape without conceding anything.',
|
|
822
|
+
suggestion: "Say which two, and which one you think weighs more.",
|
|
823
|
+
examples_bad: [
|
|
824
|
+
"Two things can be true at once.",
|
|
825
|
+
"Both things can be true here.",
|
|
826
|
+
"Two things are true, and the second is the awkward one."
|
|
827
|
+
],
|
|
828
|
+
examples_ok: [
|
|
829
|
+
"Three things can be true at the same time.",
|
|
830
|
+
"Two things went wrong that morning.",
|
|
831
|
+
"Both statements are true and the report says so."
|
|
832
|
+
],
|
|
833
|
+
rationale: "The sentence performs even-handedness and supplies none: it asserts that a " \
|
|
834
|
+
"contradiction is only apparent without naming either half or saying which " \
|
|
835
|
+
"one carries more weight. A writer who has held both claims at once can say " \
|
|
836
|
+
"what they are."
|
|
837
|
+
),
|
|
838
|
+
Rule.new(
|
|
839
|
+
id: "notice-what-there",
|
|
840
|
+
category: "rhetorical-tic",
|
|
841
|
+
severity: "warning",
|
|
842
|
+
# The self-referential half of the attention cue: the sentence points
|
|
843
|
+
# at the writing rather than at anything in the world. Two frames, both
|
|
844
|
+
# sentence-initial -- "notice what X did there" and the bare "read that
|
|
845
|
+
# again". The bare imperative without the "there" frame is the quieter
|
|
846
|
+
# half and lives in notice-what, at info.
|
|
847
|
+
pattern: /(?:\A|[.!?]\s+|\n\s*\n)\s*
|
|
848
|
+
(?:notice\s+what\b[^.!?\n]{0,40}\b(?:did|does|just\s+did)\s+there\b
|
|
849
|
+
|read\s+that\s+again\b)/ix,
|
|
850
|
+
message: '"Notice what it did there" points at the writing, not the world.',
|
|
851
|
+
suggestion: "Make the point; the reader saw the same sentence you did.",
|
|
852
|
+
examples_bad: [
|
|
853
|
+
"Notice what that argument did there.",
|
|
854
|
+
"The claim moved while nobody was looking. Notice what the second clause does there.",
|
|
855
|
+
"Read that again."
|
|
856
|
+
],
|
|
857
|
+
examples_ok: [
|
|
858
|
+
"Notice what happens when the cache is cold.",
|
|
859
|
+
"I did not notice what he did there.",
|
|
860
|
+
"You should read that again before signing."
|
|
861
|
+
],
|
|
862
|
+
rationale: "The cue asks the reader to admire a move the sentence has just made, which " \
|
|
863
|
+
"puts the writing in the frame instead of the subject, and it grades the " \
|
|
864
|
+
"move before the reader has judged it. Where a human writes it, they are " \
|
|
865
|
+
"usually pointing at something outside the text -- a chart, a screen, a list."
|
|
866
|
+
),
|
|
867
|
+
Rule.new(
|
|
868
|
+
id: "notice-what",
|
|
869
|
+
category: "rhetorical-tic",
|
|
870
|
+
severity: "info",
|
|
871
|
+
# The ambiguous half of the pair, and it ships at info because the
|
|
872
|
+
# sentence-initial imperative is also how people point at something
|
|
873
|
+
# real: "Notice what happens around Q3", "Notice what is not on the
|
|
874
|
+
# list". The lookahead hands the self-referential frame to
|
|
875
|
+
# notice-what-there so one span never draws two notes.
|
|
876
|
+
#
|
|
877
|
+
# "how" is left out. "Notice how" draws ten times the volume and half
|
|
878
|
+
# of it is the imperative -- that is ordinary argument, not a tell.
|
|
879
|
+
pattern: /(?:\A|[.!?]\s+|\n\s*\n)\s*notice\s+what\b
|
|
880
|
+
(?![^.!?\n]{0,40}\b(?:did|does|just\s+did)\s+there\b)/ix,
|
|
881
|
+
message: '"Notice what…" tells the reader to look instead of showing them.',
|
|
882
|
+
suggestion: "Point at the thing itself, or cut the instruction.",
|
|
883
|
+
examples_bad: [
|
|
884
|
+
"Notice what the second paragraph leaves out.",
|
|
885
|
+
"The numbers were flat all year. Notice what the summary claims instead.",
|
|
886
|
+
"Notice what nobody is willing to say."
|
|
887
|
+
],
|
|
888
|
+
examples_ok: [
|
|
889
|
+
"Notice how the cache warms on the second request.",
|
|
890
|
+
"I did not notice what he said.",
|
|
891
|
+
"You will notice what is missing soon enough."
|
|
892
|
+
],
|
|
893
|
+
rationale: "The imperative directs attention without carrying any, so the reader is " \
|
|
894
|
+
"told to look before being given anything to look at. Human writers use the " \
|
|
895
|
+
"same sentence to point at something outside the text -- a chart, a screen, " \
|
|
896
|
+
"a list -- which is why this is the quiet half of the pair. Treat a flag " \
|
|
897
|
+
"here as a question: is there anything to see?"
|
|
898
|
+
),
|
|
899
|
+
Rule.new(
|
|
900
|
+
id: "none-of-this-is-to-say",
|
|
901
|
+
category: "rhetorical-tic",
|
|
902
|
+
severity: "warning",
|
|
903
|
+
# Only the "none of" form. Every neighbouring phrasing is ordinary
|
|
904
|
+
# English by an order of magnitude -- "which is not to say", "this is
|
|
905
|
+
# not to say", "that's not to say" -- and admitting any of them would
|
|
906
|
+
# sink the rule. The sweeping subject is the tell: not this sentence
|
|
907
|
+
# but everything above it.
|
|
908
|
+
pattern: /\bnone\s+of\s+(?:this|that|the\s+above)\s+is\s+to\s+say\b/i,
|
|
909
|
+
message: '"None of this is to say…" retracts an argument nobody made.',
|
|
910
|
+
suggestion: "State the limit of the claim, or drop the disclaimer.",
|
|
911
|
+
examples_bad: [
|
|
912
|
+
"None of this is to say the approach is wrong.",
|
|
913
|
+
"None of that is to say the numbers are wrong.",
|
|
914
|
+
"The evidence points one way. None of the above is to say it is settled."
|
|
915
|
+
],
|
|
916
|
+
examples_ok: [
|
|
917
|
+
"That's not to say it was easy.",
|
|
918
|
+
"Which is not to say the report was wrong.",
|
|
919
|
+
"None of this means the project failed."
|
|
920
|
+
],
|
|
921
|
+
rationale: "The sentence guards against a reading the text never invited, which lets " \
|
|
922
|
+
"the writer sound careful without giving anything up. The sweeping subject " \
|
|
923
|
+
"is what separates it from the ordinary hedge: it disclaims everything " \
|
|
924
|
+
"above it at once, so it can be written without knowing what that was."
|
|
925
|
+
),
|
|
926
|
+
Rule.new(
|
|
927
|
+
id: "if-im-being-honest",
|
|
928
|
+
category: "rhetorical-tic",
|
|
929
|
+
severity: "info",
|
|
930
|
+
# The "being honest" frame only. Plain "to be honest" and "I'll be
|
|
931
|
+
# honest" are how people talk and are excluded. info, not warning:
|
|
932
|
+
# writers really do say this out loud.
|
|
933
|
+
pattern: /\bif\s+(?:I['’]?m|I\s+am|we['’]?re|we\s+are)\s+(?:being\s+)?honest\b
|
|
934
|
+
|\bhonestly,\s+the\s+(?:answer|truth)\b/ix,
|
|
935
|
+
message: '"If I\'m being honest…" buys candor without spending anything.',
|
|
936
|
+
suggestion: "Delete the preamble and say the honest thing.",
|
|
937
|
+
examples_bad: [
|
|
938
|
+
"If I'm being honest, the design never worked.",
|
|
939
|
+
"If we're being honest, nobody read the report.",
|
|
940
|
+
"Honestly, the answer is that we guessed."
|
|
941
|
+
],
|
|
942
|
+
examples_ok: [
|
|
943
|
+
"To be honest, I liked the first draft better.",
|
|
944
|
+
"I'll be honest, the meeting ran long.",
|
|
945
|
+
"Honestly, I forgot the deadline."
|
|
946
|
+
],
|
|
947
|
+
rationale: "The preamble implies the surrounding text was less than honest, and it " \
|
|
948
|
+
"promises a confession that the sentence after it rarely delivers. It " \
|
|
949
|
+
"costs the writer nothing and reads as intimacy the reader did not earn."
|
|
950
|
+
),
|
|
951
|
+
Rule.new(
|
|
952
|
+
id: "genuinely",
|
|
953
|
+
category: "rhetorical-tic",
|
|
954
|
+
severity: "info",
|
|
955
|
+
# Off by default. There is no narrowing here: the word is ordinary
|
|
956
|
+
# English at every frequency we measured, and the difference between
|
|
957
|
+
# the tell and the real use is whether a contrast exists in the
|
|
958
|
+
# surrounding argument, which no regex can see. Selectable when a
|
|
959
|
+
# writer wants every occurrence listed back to them.
|
|
960
|
+
default_on: false,
|
|
961
|
+
pattern: /\bgenuinely\b/i,
|
|
962
|
+
message: '"Genuinely" asserts sincerity instead of earning it (heuristic; high false-positive).',
|
|
963
|
+
suggestion: "Cut the adverb. If the sentence needs it, the claim is doing the work.",
|
|
964
|
+
examples_bad: [
|
|
965
|
+
"This is a genuinely hard problem.",
|
|
966
|
+
"The result was genuinely surprising.",
|
|
967
|
+
"It's genuinely useful, and that's rare."
|
|
968
|
+
],
|
|
969
|
+
examples_ok: [
|
|
970
|
+
"The signature was authentic.",
|
|
971
|
+
"She was pleased with the outcome.",
|
|
972
|
+
"The offer turned out to be real."
|
|
973
|
+
],
|
|
974
|
+
rationale: "As an intensifier the word rates the writer's sincerity rather than the " \
|
|
975
|
+
"thing described, and it can be dropped from most sentences without loss. " \
|
|
976
|
+
"It still does real work when it draws a contrast -- genuinely free against " \
|
|
977
|
+
"nominally free -- and nothing in the sentence marks which use is which, " \
|
|
978
|
+
"which is why this one runs only when you ask for it."
|
|
979
|
+
),
|
|
980
|
+
Rule.new(
|
|
981
|
+
id: "and-thats-fine",
|
|
982
|
+
category: "rhetorical-tic",
|
|
983
|
+
severity: "info",
|
|
984
|
+
# Three narrowings, and the rule needs all of them. "and" is required:
|
|
985
|
+
# bare "that's fine" is a reply people write constantly. The match must
|
|
986
|
+
# open a sentence and close it, so the concessive clause -- "and that's
|
|
987
|
+
# fine, but the cost is real" -- never matches.
|
|
988
|
+
pattern: /(?:\A|[.!?]\s+|\n\s*\n)\s*and\s+that['’]?s\s+(?:fine|okay|ok)\s*[.!]/i,
|
|
989
|
+
message: '"And that\'s fine." grants permission nobody asked for.',
|
|
990
|
+
suggestion: "Cut the closer, or say who objected and why they're wrong.",
|
|
991
|
+
examples_bad: [
|
|
992
|
+
"The essay is short. And that's fine.",
|
|
993
|
+
"Nobody reads the appendix. And that's okay.",
|
|
994
|
+
"It only handles one case. And that's fine!"
|
|
995
|
+
],
|
|
996
|
+
examples_ok: [
|
|
997
|
+
"That's fine. Ship it whenever.",
|
|
998
|
+
"The tests are slow and that's fine for now.",
|
|
999
|
+
"And that's fine, but the cost is real."
|
|
1000
|
+
],
|
|
1001
|
+
rationale: "The closer concedes an objection instead of answering it, and the objection " \
|
|
1002
|
+
"was never raised. It reads as reassurance addressed to nobody, and the " \
|
|
1003
|
+
"paragraph is the same without it."
|
|
1004
|
+
),
|
|
421
1005
|
# ── puffery ───────────────────────────────────────────────────────────
|
|
422
1006
|
Rule.new(
|
|
423
1007
|
id: "puffery-words",
|
|
@@ -720,6 +1304,60 @@ module Sloplint
|
|
|
720
1304
|
"habit than most human writing settles into."
|
|
721
1305
|
),
|
|
722
1306
|
|
|
1307
|
+
Rule.new(
|
|
1308
|
+
id: "question-isnt",
|
|
1309
|
+
category: "structure",
|
|
1310
|
+
severity: "info",
|
|
1311
|
+
# The resolving clause is required, so a plain rhetorical question never
|
|
1312
|
+
# matches. "The real question is" is ordinary English and is excluded by
|
|
1313
|
+
# the adjacency of "question" to the negated copula.
|
|
1314
|
+
pattern: /\bthe\s+question\s+is(?:n['’]?t|\s+not)\s+
|
|
1315
|
+
(?:whether|if|how|what|why|who)\b
|
|
1316
|
+
[^.!?\n]{0,80}?,\s*(?:it['’]?s|but)\b/ix,
|
|
1317
|
+
message: "The question isn't X, it's Y -- a correction of a question nobody asked.",
|
|
1318
|
+
suggestion: "Ask the question you mean to ask, and drop the discarded one.",
|
|
1319
|
+
examples_bad: [
|
|
1320
|
+
"The question isn't whether the model works, it's whether anyone trusts it.",
|
|
1321
|
+
"The question is not how fast it runs, but how often it fails.",
|
|
1322
|
+
"The question isn't what we build, it's who we build it for."
|
|
1323
|
+
],
|
|
1324
|
+
examples_ok: [
|
|
1325
|
+
"The real question is whether the funding arrives.",
|
|
1326
|
+
"The question is whether the court will hear the appeal.",
|
|
1327
|
+
"The question isn't settled."
|
|
1328
|
+
],
|
|
1329
|
+
rationale: "This is the corrective frame in interrogative dress: a question is raised " \
|
|
1330
|
+
"only to be swapped for a better one, so the writer scores a point against " \
|
|
1331
|
+
"nobody. The discarded question is usually the reasonable one, and stating " \
|
|
1332
|
+
"the second question alone loses nothing."
|
|
1333
|
+
),
|
|
1334
|
+
Rule.new(
|
|
1335
|
+
id: "less-about-more-about",
|
|
1336
|
+
category: "structure",
|
|
1337
|
+
severity: "info",
|
|
1338
|
+
# The full frame is required at both ends. Bare "less about" and bare
|
|
1339
|
+
# "more about" are ordinary English on their own, and the subject slot
|
|
1340
|
+
# is limited to the pronouns so that a sentence with a real subject --
|
|
1341
|
+
# "the dispute is less about money" -- stays clean.
|
|
1342
|
+
pattern: /\b(?:it['’]?s|it\s+is|this\s+is|that['’]?s|that\s+is)\s+less\s+about\s+
|
|
1343
|
+
[^.!?\n]{1,60}?\b(?:(?:and\s+|,\s*)?more\s+about|than\s+about)\b/ix,
|
|
1344
|
+
message: "Less about X, more about Y -- a reframe that discards a reading nobody offered.",
|
|
1345
|
+
suggestion: "Say what it is about. The discarded half is usually filler.",
|
|
1346
|
+
examples_bad: [
|
|
1347
|
+
"It's less about the tooling and more about the habit.",
|
|
1348
|
+
"This is less about speed, more about consistency.",
|
|
1349
|
+
"That's less about money than about trust."
|
|
1350
|
+
],
|
|
1351
|
+
examples_ok: [
|
|
1352
|
+
"It's less about the tooling than I expected.",
|
|
1353
|
+
"Tell me more about the delay.",
|
|
1354
|
+
"We care less about speed these days."
|
|
1355
|
+
],
|
|
1356
|
+
rationale: "The reframe pairs a rejected description with an accepted one, so the " \
|
|
1357
|
+
"sentence sounds like a correction while correcting nobody. The rejected " \
|
|
1358
|
+
"half is chosen for the contrast, not because a reader proposed it, and " \
|
|
1359
|
+
"the second half stands on its own."
|
|
1360
|
+
),
|
|
723
1361
|
# ── hedging ───────────────────────────────────────────────────────────
|
|
724
1362
|
Rule.new(
|
|
725
1363
|
id: "vague-attribution",
|
data/lib/sloplint/version.rb
CHANGED