sloplint 0.2.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 +103 -0
- data/README.md +9 -9
- data/docs/SPEC.md +32 -5
- data/lib/sloplint/rules.rb +867 -45
- 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
|
@@ -3,6 +3,109 @@
|
|
|
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
|
+
## [Unreleased]
|
|
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
|
+
|
|
81
|
+
## [0.3.0] - 2026-08-02
|
|
82
|
+
|
|
83
|
+
- New rule `load-bearing` (`rhetorical-tic`, `warning`): "load-bearing" used
|
|
84
|
+
as a borrowed metaphor for anything important, outside its literal
|
|
85
|
+
construction sense (a wall, a column, a beam). Guards on both sides —
|
|
86
|
+
a building noun right after it, or the predicate form ("the wall is
|
|
87
|
+
load-bearing") — leave the construction sense clean.
|
|
88
|
+
- New rule `is-is` (`rhetorical-tic`, `warning`): the doubled copula, "what it
|
|
89
|
+
is is a mistake" / "the thing is, is that...". No anchor needed -- the bare
|
|
90
|
+
pattern scored 0 hits across ~1.9M words of public-domain and modern prose.
|
|
91
|
+
- `exactly-the` replaced by the broader `exact-exactly` (`rhetorical-tic`,
|
|
92
|
+
now `info`): flags "exact/exactly" generally, not just the fixed "exactly
|
|
93
|
+
the point/kind/problem/…" phrase shape, with an allowlist for the places
|
|
94
|
+
it's doing real work (numbers, times, same/opposite/way/etc.). Severity
|
|
95
|
+
drops to `info` because a common word will still slip through on cases
|
|
96
|
+
the allowlist hasn't seen yet.
|
|
97
|
+
- `thats-not-nothing` replaced by the broader `not-nothing`
|
|
98
|
+
(`rhetorical-tic`, `warning`): catches the "is not nothing" litotes
|
|
99
|
+
regardless of subject ("Fifty basis points is not nothing"), not only
|
|
100
|
+
the demonstrative-subject form ("that's not nothing"). Personal-subject
|
|
101
|
+
litotes ("he was not nothing to her") and the "there is not nothing"
|
|
102
|
+
philosophy frame are allowlisted as legitimate human use.
|
|
103
|
+
- `underscores-highlights` broadened (`puffery`, still `info`): "underscores
|
|
104
|
+
the need for", "underscored how fragile", and "underscoring the urgency"
|
|
105
|
+
now match, not only the fixed "underscores/highlights/emphasizes its/the/
|
|
106
|
+
their importance/significance" phrase. The character-noun sense
|
|
107
|
+
("a leading underscore", snake_case) stays excluded.
|
|
108
|
+
|
|
6
109
|
## [0.2.0] - 2026-07-28
|
|
7
110
|
|
|
8
111
|
- `is-real-and-not` and `worth-naming` move from `warning` to `info`. Both
|
data/README.md
CHANGED
|
@@ -51,10 +51,10 @@ $ printf 'The report is a rich tapestry of vibrant detail.\nThat is exactly the
|
|
|
51
51
|
why: Travel-brochure adjectives and phrases that models reach for and careful writers avoid.
|
|
52
52
|
fix: Replace with a concrete, specific detail or cut it.
|
|
53
53
|
|
|
54
|
-
-:2:9:
|
|
55
|
-
excerpt: …tapestry of vibrant detail. That is [exactly the point
|
|
56
|
-
why: 'exactly
|
|
57
|
-
fix:
|
|
54
|
+
-:2:9: info exact-exactly "exact/exactly" is reflexive emphasis unless it names something checkable.
|
|
55
|
+
excerpt: …tapestry of vibrant detail. That is [exactly] the point I keep making about it.
|
|
56
|
+
why: Models reach for 'exact/exactly' as filler emphasis on a claim with nothing to check; it earns its place only next to a number, a name, or a stated identity.
|
|
57
|
+
fix: Cut it, or replace with the number, name, or match it's supposed to be precise about.
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
The brackets mark the match; the rest is there so you can see what you're fixing without opening the file — including *why*, so an agent doesn't have to run `explain` separately to decide whether a flag is worth acting on.
|
|
@@ -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,36 @@ 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
|
|
278
|
-
| `
|
|
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 |
|
|
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 |
|
|
303
|
+
| `is-is` | doubled copula: "what it is is …", "the thing is, is that …" | comma optional |
|
|
279
304
|
|
|
280
305
|
### puffery (Wikipedia: Signs of AI writing)
|
|
281
306
|
|
|
@@ -284,13 +309,15 @@ Single flat rule per word-cluster, matched as whole words:
|
|
|
284
309
|
- `puffery-words` — boasts a, vibrant, rich (history/cultural/tapestry), nestled (gated to a following in/among/between, so the literal verb — a head nestled against a shoulder — doesn't count), in the heart of (gated to a place object), groundbreaking, renowned, diverse array, breathtaking, natural beauty, stands as a testament, indelible mark, deeply rooted.
|
|
285
310
|
- `stands-serves-as` — "stands as / serves as", "is a testament/reminder to".
|
|
286
311
|
- `vital-role` — "plays a (vital/crucial/pivotal/significant/key) role".
|
|
287
|
-
- `underscores-highlights` — "
|
|
312
|
+
- `underscores-highlights` — "underscore(s)" + determiner and "underscored/underscoring" anywhere (the emphasis verb); "highlights/emphasizes its (importance/significance)" stays narrow.
|
|
288
313
|
- `rich-tapestry` — "rich tapestry", "tapestry of".
|
|
289
314
|
|
|
290
315
|
### structure
|
|
291
316
|
|
|
292
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.
|
|
293
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.
|
|
294
321
|
- `rule-of-three` — three parallel comma items ending a sentence (heuristic; `info` severity, off by default via `--select` since it false-positives).
|
|
295
322
|
- `em-dash` — any em dash; `info`.
|
|
296
323
|
- `em-dash-overuse` — 3+ em dashes in one paragraph; `warning`.
|
data/lib/sloplint/rules.rb
CHANGED
|
@@ -33,9 +33,8 @@ module Sloplint
|
|
|
33
33
|
examples_bad: ["No fluff, no filler, no jargon."],
|
|
34
34
|
examples_ok: ["No parking on Sundays."],
|
|
35
35
|
rationale: "Asyndetic negation chains are a signature model cadence, near-absent from " \
|
|
36
|
-
"human prose at any length
|
|
37
|
-
"
|
|
38
|
-
"two (and, rarely, more), but a model reaches for the pattern constantly."
|
|
36
|
+
"human prose at any length. A careful writer occasionally stacks two (and, " \
|
|
37
|
+
"rarely, more), but a model reaches for the pattern constantly."
|
|
39
38
|
),
|
|
40
39
|
Rule.new(
|
|
41
40
|
id: "no-x-no-y-frag",
|
|
@@ -76,11 +75,9 @@ module Sloplint
|
|
|
76
75
|
],
|
|
77
76
|
rationale: "The fragment form of the same cadence, and the weaker signal of the two: the " \
|
|
78
77
|
"comma chain is one authored sentence, while this is just short sentences in " \
|
|
79
|
-
"sequence, which human prose also does. It
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"staccato run ('No one moved. No one spoke.') has the identical shape and is " \
|
|
83
|
-
"not a tell. Treat a flag here as a question, not a verdict."
|
|
78
|
+
"sequence, which human prose also does. It's rare in careful writing, but a " \
|
|
79
|
+
"deliberate staccato run ('No one moved. No one spoke.') has the identical " \
|
|
80
|
+
"shape and is not a tell. Treat a flag here as a question, not a verdict."
|
|
84
81
|
),
|
|
85
82
|
Rule.new(
|
|
86
83
|
id: "thats-the-whole",
|
|
@@ -120,12 +117,173 @@ module Sloplint
|
|
|
120
117
|
id: "sit-with-that",
|
|
121
118
|
category: "rhetorical-tic",
|
|
122
119
|
severity: "warning",
|
|
123
|
-
|
|
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,
|
|
124
129
|
message: '"Sit with that/the discomfort" is therapized LLM filler.',
|
|
125
130
|
suggestion: "Cut it, or say what you actually want the reader to do.",
|
|
126
|
-
examples_bad: [
|
|
127
|
-
|
|
128
|
-
|
|
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."
|
|
129
287
|
),
|
|
130
288
|
Rule.new(
|
|
131
289
|
id: "you-already-know",
|
|
@@ -186,12 +344,27 @@ module Sloplint
|
|
|
186
344
|
id: "the-punchline-is",
|
|
187
345
|
category: "rhetorical-tic",
|
|
188
346
|
severity: "warning",
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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."
|
|
195
368
|
),
|
|
196
369
|
Rule.new(
|
|
197
370
|
id: "worth-naming",
|
|
@@ -201,13 +374,30 @@ module Sloplint
|
|
|
201
374
|
# names" idiom is part of the matched text -- skip: checks the matched
|
|
202
375
|
# text itself, and the tighter /\bworth\s+naming\b/ never captured
|
|
203
376
|
# enough of "worth naming names" for the skip to ever reach it.
|
|
204
|
-
|
|
205
|
-
|
|
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.',
|
|
206
387
|
suggestion: "Just name the thing; skip the meta-announcement.",
|
|
207
388
|
skip: [/naming names/i],
|
|
208
|
-
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.
|
|
209
398
|
examples_ok: ["It's worth reading twice.", "It's worth naming names in this report."],
|
|
210
|
-
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 " \
|
|
211
401
|
"meta-signpost announcing a point is coming ('One tension is worth naming " \
|
|
212
402
|
"here') and the plain sense of a thing worth calling or mentioning, which " \
|
|
213
403
|
"careful writers use too -- Emerson's 'the only thing worth naming to do that' " \
|
|
@@ -215,27 +405,194 @@ module Sloplint
|
|
|
215
405
|
"which sense it's in."
|
|
216
406
|
),
|
|
217
407
|
Rule.new(
|
|
218
|
-
id: "
|
|
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
|
+
),
|
|
460
|
+
Rule.new(
|
|
461
|
+
id: "not-nothing",
|
|
219
462
|
category: "rhetorical-tic",
|
|
220
463
|
severity: "warning",
|
|
221
|
-
|
|
222
|
-
|
|
464
|
+
# Two branches: spelled-out copula with an optional captured subject, and
|
|
465
|
+
# the contracted "X's not nothing". The optional subject + skip: is the
|
|
466
|
+
# load-bearing idiom -- skip: only sees matched text, so the personal
|
|
467
|
+
# subjects a human litotes takes (I/he/she/we/you/they) must be inside
|
|
468
|
+
# the match to be dropped there. The contracted branch captures the word
|
|
469
|
+
# before 's for the same reason ("she's not nothing" must reach the
|
|
470
|
+
# skip). "there" is guarded too: "there is/there's not nothing" is the
|
|
471
|
+
# philosophy frame or dialect, not the closer. "not for nothing" and
|
|
472
|
+
# "something and not nothing" never match at all -- the copula must sit
|
|
473
|
+
# adjacent to "not nothing". Token gaps cross a hard-wrapped line but
|
|
474
|
+
# never a paragraph break, so a paragraph ending "...was not" cannot weld
|
|
475
|
+
# onto one starting "Nothing ...".
|
|
476
|
+
pattern: /\b(?:(?:i|he|she|we|you|they|there)(?:[ \t]|\r?\n(?![ \t]*\r?\n))+)?
|
|
477
|
+
(?:am|is|are|was|were)(?:[ \t]|\r?\n(?![ \t]*\r?\n))+
|
|
478
|
+
not(?:[ \t]|\r?\n(?![ \t]*\r?\n))+nothing\b
|
|
479
|
+
|\b[\w-]+['’]s(?:[ \t]|\r?\n(?![ \t]*\r?\n))+
|
|
480
|
+
not(?:[ \t]|\r?\n(?![ \t]*\r?\n))+nothing\b/ix,
|
|
481
|
+
skip: [/\A(?:i|he|she|we|you|they|there)\b/i],
|
|
482
|
+
message: '"…is not nothing" is a stock LLM understatement.',
|
|
223
483
|
suggestion: "State the magnitude directly instead of the litotes.",
|
|
224
|
-
examples_bad: [
|
|
225
|
-
|
|
226
|
-
|
|
484
|
+
examples_bad: [
|
|
485
|
+
"We cut latency in half, and that's not nothing.",
|
|
486
|
+
"Fifty basis points is not nothing.",
|
|
487
|
+
"Three years of runway is not nothing.",
|
|
488
|
+
"A million users is not nothing.",
|
|
489
|
+
"The margin was not nothing.",
|
|
490
|
+
# Hard-wrapped Markdown: same paragraph, still the tell.
|
|
491
|
+
"The gain was not\nnothing, the report said."
|
|
492
|
+
],
|
|
493
|
+
examples_ok: [
|
|
494
|
+
"That is not enough to matter.",
|
|
495
|
+
# Personal-subject litotes is a human literary move, not the closer
|
|
496
|
+
# tic -- spelled out and contracted.
|
|
497
|
+
"She told him he was not nothing to her.",
|
|
498
|
+
"She's not nothing to me, whatever they say.",
|
|
499
|
+
# "not for nothing": the intervening word breaks adjacency.
|
|
500
|
+
"It was not for nothing that he trained all winter.",
|
|
501
|
+
# The philosophy frame, both shapes it takes.
|
|
502
|
+
"Why is there something and not nothing?",
|
|
503
|
+
"There is not nothing; being persists.",
|
|
504
|
+
# Dialect double negative, not the litotes.
|
|
505
|
+
"There's not nothing we can do.",
|
|
506
|
+
# A paragraph break never welds a match.
|
|
507
|
+
"The answer was not\n\nNothing prepared us for it."
|
|
508
|
+
],
|
|
509
|
+
rationale: "The 'not nothing' litotes is a recognizable model closer; the subject " \
|
|
510
|
+
"varies ('that's not nothing', 'fifty basis points is not nothing') but " \
|
|
511
|
+
"the move is the same. Personal subjects are carved out: 'he was not " \
|
|
512
|
+
"nothing to her' is a human literary litotes, not the closer."
|
|
227
513
|
),
|
|
228
514
|
|
|
229
515
|
Rule.new(
|
|
230
|
-
id: "exactly
|
|
516
|
+
id: "exact-exactly",
|
|
517
|
+
category: "rhetorical-tic",
|
|
518
|
+
severity: "info",
|
|
519
|
+
pattern: /\bexact(?:ly)?\b(?!\s*(?:(?:the\s+)?(?:same|opposite|science|change|replica|copy|location|coordinates|way)\b|[$\d]|noon\b|midnight\b|o'?\s*clock\b))/i,
|
|
520
|
+
message: '"exact/exactly" is reflexive emphasis unless it names something checkable.',
|
|
521
|
+
suggestion: "Cut it, or replace with the number, name, or match it's supposed to be precise about.",
|
|
522
|
+
examples_bad: [
|
|
523
|
+
"That's exactly the point.", "We proved exactly the point we needed.",
|
|
524
|
+
"This is exactly the kind of thing we warned about.",
|
|
525
|
+
"That's the exact problem with the old system.",
|
|
526
|
+
"That's exactly right.", "I know exactly why this happened."
|
|
527
|
+
],
|
|
528
|
+
examples_ok: [
|
|
529
|
+
"She folded it exactly the way he showed her.",
|
|
530
|
+
"The bill came to exactly $42.",
|
|
531
|
+
"We agreed on the exact same design.",
|
|
532
|
+
"It's not an exact science.",
|
|
533
|
+
"Please bring exact change for the bus.",
|
|
534
|
+
"The museum built an exact replica of the ship.",
|
|
535
|
+
"This is an exact copy of the original.",
|
|
536
|
+
"He wanted the exact opposite of what she suggested.",
|
|
537
|
+
"Rescue teams pinpointed the exact location of the wreck.",
|
|
538
|
+
"GPS gave us the exact coordinates of the site.",
|
|
539
|
+
"The train left at exactly noon.",
|
|
540
|
+
"They agreed to meet at exactly midnight.",
|
|
541
|
+
"The meeting starts at exactly 3 o'clock.",
|
|
542
|
+
"She has exacting standards for her students."
|
|
543
|
+
],
|
|
544
|
+
rationale: "Models reach for 'exact/exactly' as filler emphasis on a claim with nothing to " \
|
|
545
|
+
"check; it earns its place only next to a number, a name, or a stated identity."
|
|
546
|
+
),
|
|
547
|
+
Rule.new(
|
|
548
|
+
id: "load-bearing",
|
|
231
549
|
category: "rhetorical-tic",
|
|
232
550
|
severity: "warning",
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
551
|
+
# Two guards, both structural, over the same noun list so they can't
|
|
552
|
+
# drift apart. Forward: a physical building part right after it is the
|
|
553
|
+
# literal sense, checked with a negative lookahead. Backward: the
|
|
554
|
+
# predicate form ("the wall is load-bearing") is literal too, but a
|
|
555
|
+
# fixed-width lookbehind covering this many noun x tense combinations
|
|
556
|
+
# trips a real Ruby/Onigmo lookbehind bug on some inputs (RegexpError
|
|
557
|
+
# at match time, not compile time -- reproduced on em-dash's own
|
|
558
|
+
# fixtures). Pulling the noun+copula into the pattern itself as an
|
|
559
|
+
# optional leading group sidesteps lookbehind entirely: when present,
|
|
560
|
+
# it's captured as part of the match, and skip: (which only ever sees
|
|
561
|
+
# matched text, never surrounding context) drops it there instead.
|
|
562
|
+
pattern: /\b(?:(?:wall|column|beam|post|pillar|joist|stud|masonry|partition|
|
|
563
|
+
structure|frame|footing|foundation|member)s?\s+
|
|
564
|
+
(?:is|are|was|were)\s+)?
|
|
565
|
+
load[-\s]?bearing\b
|
|
566
|
+
(?!\s+(?:wall|column|beam|post|pillar|joist|stud|masonry|partition|
|
|
567
|
+
structure|frame|footing|foundation|member)s?\b)/ix,
|
|
568
|
+
skip: [/\A(?:wall|column|beam|post|pillar|joist|stud|masonry|partition|
|
|
569
|
+
structure|frame|footing|foundation|member)s?\s+
|
|
570
|
+
(?:is|are|was|were)\s+load/ix],
|
|
571
|
+
message: '"load-bearing" outside construction is a borrowed metaphor.',
|
|
572
|
+
suggestion: "Say what the thing holds up, or what breaks without it.",
|
|
573
|
+
examples_bad: [
|
|
574
|
+
"That comma is load-bearing.",
|
|
575
|
+
"The load-bearing assumption is that users read the docs.",
|
|
576
|
+
"Half the argument rests on one load-bearing word.",
|
|
577
|
+
"This paragraph is the load-bearing part of the essay.",
|
|
578
|
+
"Trust was the load-bearing element of the whole deal.",
|
|
579
|
+
"The qualifier is doing load bearing work here."
|
|
580
|
+
],
|
|
581
|
+
examples_ok: [
|
|
582
|
+
"They knocked out a load-bearing wall during the remodel.",
|
|
583
|
+
"The inspector flagged a cracked load-bearing column.",
|
|
584
|
+
"Steel load-bearing beams replaced the old timber.",
|
|
585
|
+
"The load-bearing masonry dates to 1890.",
|
|
586
|
+
"Those columns are load-bearing.",
|
|
587
|
+
"The interior wall is load-bearing, so it stays.",
|
|
588
|
+
"The beam is load-bearing.",
|
|
589
|
+
"That stud was load-bearing, so removing it needed a header beam.",
|
|
590
|
+
"Those joists were load-bearing, engineers confirmed after inspection.",
|
|
591
|
+
"The masonry was load-bearing in the original 1890 structure."
|
|
592
|
+
],
|
|
593
|
+
rationale: "'Load-bearing' is a construction term for a wall or column that holds up the " \
|
|
594
|
+
"structure. Models borrow it as a metaphor for anything important, which just " \
|
|
595
|
+
"restates the sentence's importance without saying what actually holds it up."
|
|
239
596
|
),
|
|
240
597
|
Rule.new(
|
|
241
598
|
id: "thats-how-x",
|
|
@@ -271,7 +628,380 @@ module Sloplint
|
|
|
271
628
|
rationale: "Labelling a claim as the portable lesson does the persuading that the claim " \
|
|
272
629
|
"should be doing — a model habit borrowed from thought-leader prose."
|
|
273
630
|
),
|
|
631
|
+
Rule.new(
|
|
632
|
+
id: "is-is",
|
|
633
|
+
category: "rhetorical-tic",
|
|
634
|
+
severity: "warning",
|
|
635
|
+
# No anchor needed -- the doubled copula alone scored 0 across ~1.9M words.
|
|
636
|
+
# The comma is allowed because "What it is, is a mystery" grates the same
|
|
637
|
+
# way. Sentence and clause punctuation still block the weld ("what it is.
|
|
638
|
+
# Is that…", "here's what it is: is anyone…"), and \b on both ends keeps
|
|
639
|
+
# the pattern out of "his island". \s+ needs no paragraph-break guard: a
|
|
640
|
+
# false match would need a paragraph that begins with "is ".
|
|
641
|
+
pattern: /\bis,?\s+is\b/i,
|
|
642
|
+
message: 'Doubled copula ("is is" / "is, is") — spoken cadence on the page.',
|
|
643
|
+
suggestion: "Drop the cleft and say it straight, or delete the second 'is'.",
|
|
644
|
+
examples_bad: [
|
|
645
|
+
"What this really is is a bet on distribution.",
|
|
646
|
+
"What it is is a rounding error with a press release.",
|
|
647
|
+
"What it is, is a failure of nerve.",
|
|
648
|
+
"The thing is, is that nobody checked the logs."
|
|
649
|
+
],
|
|
650
|
+
examples_ok: [
|
|
651
|
+
# Walden: the cleft with a single copula, which is ordinary English.
|
|
652
|
+
"We have heard of this virtue, but we know not what it is.",
|
|
653
|
+
# Sentence and clause punctuation can't be welded across.
|
|
654
|
+
"I know what it is. Is that a problem?",
|
|
655
|
+
"Here's what it is: is anyone actually reading this?",
|
|
656
|
+
# Both \b anchors matter -- "his island" contains the literal string "is is".
|
|
657
|
+
"He sailed to his island at dawn."
|
|
658
|
+
],
|
|
659
|
+
rationale: "Three things produce a doubled copula and all three read as unedited. The " \
|
|
660
|
+
"wh-cleft ('what it is is a mistake') is grammatical -- 'what it is' is the " \
|
|
661
|
+
"subject and the second 'is' is the verb -- and it is the model's version, a " \
|
|
662
|
+
"frame that stages a definition instead of asserting one, the same move as " \
|
|
663
|
+
"'the punchline is'. The NP form ('the thing is, is that') is the true double " \
|
|
664
|
+
"copula and a spoken disfluency. The third is a typo."
|
|
665
|
+
),
|
|
274
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
|
+
),
|
|
275
1005
|
# ── puffery ───────────────────────────────────────────────────────────
|
|
276
1006
|
Rule.new(
|
|
277
1007
|
id: "puffery-words",
|
|
@@ -335,12 +1065,50 @@ module Sloplint
|
|
|
335
1065
|
id: "underscores-highlights",
|
|
336
1066
|
category: "puffery",
|
|
337
1067
|
severity: "info",
|
|
338
|
-
|
|
339
|
-
|
|
1068
|
+
# "underscored/underscoring" are unambiguously the verb and flag bare.
|
|
1069
|
+
# "underscore/underscores" is also the character noun (a leading
|
|
1070
|
+
# underscore, snake_case docs), so those forms require a following
|
|
1071
|
+
# determiner/wh-word -- the frame the emphasis verb takes, which the
|
|
1072
|
+
# noun never precedes. That guard also skips bare-noun objects
|
|
1073
|
+
# ("underscores concerns about"), an accepted miss: widening the frame
|
|
1074
|
+
# to catch them readmits the noun sense. Token gaps cross a
|
|
1075
|
+
# hard-wrapped line but never a paragraph break. highlights/emphasizes
|
|
1076
|
+
# stay narrowed to the importance/significance frame -- both verbs are
|
|
1077
|
+
# too common in ordinary prose to match broadly.
|
|
1078
|
+
pattern: /\bunderscor(?:ed|ing)\b
|
|
1079
|
+
|\bunderscores?(?:[ \t]|\r?\n(?![ \t]*\r?\n))+
|
|
1080
|
+
(?:a|an|the|its|his|her|their|our|your|my|this|that|these|those|what|why|how|just)\b
|
|
1081
|
+
|\b(?:highlights|emphasizes)\s+(?:its|the|their)\s+(?:importance|significance)\b/ix,
|
|
1082
|
+
message: '"underscores" as emphasis (or "highlights/emphasizes its importance") is stock AI framing.',
|
|
340
1083
|
suggestion: "Show why it matters rather than asserting that it does.",
|
|
341
|
-
examples_bad: [
|
|
342
|
-
|
|
343
|
-
|
|
1084
|
+
examples_bad: [
|
|
1085
|
+
"This underscores its importance to the field.",
|
|
1086
|
+
"The delay underscores the need for better tooling.",
|
|
1087
|
+
"The outage underscored how fragile the pipeline was.",
|
|
1088
|
+
"Underscoring the urgency, the board met twice.",
|
|
1089
|
+
"The results underscore a deeper problem with the method.",
|
|
1090
|
+
"The report underscores just how far behind we are.",
|
|
1091
|
+
"It underscores that the market has moved on.",
|
|
1092
|
+
"The essay highlights its importance at length.",
|
|
1093
|
+
"This emphasizes the significance of early testing."
|
|
1094
|
+
],
|
|
1095
|
+
examples_ok: [
|
|
1096
|
+
"Replace each space with an underscore.",
|
|
1097
|
+
"Variable names use underscores instead of dashes.",
|
|
1098
|
+
"The underscore character separates words in snake case.",
|
|
1099
|
+
"Prefix private methods with a leading underscore.",
|
|
1100
|
+
"Ruby numeric literals accept underscores for readability.",
|
|
1101
|
+
"Two underscores mark a dunder method in Python.",
|
|
1102
|
+
"She highlights the key line in yellow.",
|
|
1103
|
+
"The paper emphasizes the method, not the results.",
|
|
1104
|
+
# A paragraph break never welds the noun onto the next sentence.
|
|
1105
|
+
"Numbers accept underscores\n\nThe next section covers floats."
|
|
1106
|
+
],
|
|
1107
|
+
rationale: "Models reach for 'underscore' as an all-purpose emphasis verb -- findings " \
|
|
1108
|
+
"underscore, outages underscore -- asserting significance without earning it. " \
|
|
1109
|
+
"Sincere journalistic and academic use exists, hence info: a flag means the " \
|
|
1110
|
+
"move is present, not that it's slop. The character noun never takes the " \
|
|
1111
|
+
"verb's frame and stays out."
|
|
344
1112
|
),
|
|
345
1113
|
Rule.new(
|
|
346
1114
|
id: "rich-tapestry",
|
|
@@ -397,8 +1165,8 @@ module Sloplint
|
|
|
397
1165
|
"copula, which is the specific shape models overuse. Correlative 'not only… " \
|
|
398
1166
|
"but' joining two verb phrases or clauses, without a preceding copula, is " \
|
|
399
1167
|
"ordinary and common in formal human prose; requiring the escalation word " \
|
|
400
|
-
"keeps those out. 'not because
|
|
401
|
-
"
|
|
1168
|
+
"keeps those out. The same logic covers 'not because A, but because B': the " \
|
|
1169
|
+
"escalation word is what marks it deliberate."
|
|
402
1170
|
),
|
|
403
1171
|
Rule.new(
|
|
404
1172
|
id: "not-x-but-y",
|
|
@@ -464,9 +1232,9 @@ module Sloplint
|
|
|
464
1232
|
],
|
|
465
1233
|
rationale: "The bare 'is not A but B' corrective is the 'not just… but' move with the " \
|
|
466
1234
|
"escalation word dropped, and models reach for it constantly. It is the " \
|
|
467
|
-
"noisiest rule in the catalog by design: a loose version scored 174 hits
|
|
468
|
-
"
|
|
469
|
-
"
|
|
1235
|
+
"noisiest rule in the catalog by design: a loose version scored 174 hits " \
|
|
1236
|
+
"before narrowing, cut to 15 here -- but not all 15 are correctives. Some " \
|
|
1237
|
+
"are concessions with an elided subject (Walden's " \
|
|
470
1238
|
"'It was not lonely, but made all the earth lonely beneath it'), which no " \
|
|
471
1239
|
"surface pattern can tell apart from the real thing. Hence info: a flag here " \
|
|
472
1240
|
"means 'this has the shape', not 'this is slop'."
|
|
@@ -536,6 +1304,60 @@ module Sloplint
|
|
|
536
1304
|
"habit than most human writing settles into."
|
|
537
1305
|
),
|
|
538
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
|
+
),
|
|
539
1361
|
# ── hedging ───────────────────────────────────────────────────────────
|
|
540
1362
|
Rule.new(
|
|
541
1363
|
id: "vague-attribution",
|
data/lib/sloplint/version.rb
CHANGED