sloplint 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +263 -1
- data/README.md +34 -7
- data/docs/SPEC.md +37 -5
- data/exe/sloplint +13 -0
- data/lib/sloplint/cli.rb +30 -8
- data/lib/sloplint/rules.rb +1807 -15
- data/lib/sloplint/version.rb +1 -1
- metadata +4 -4
- data/bin/sloplint +0 -5
data/lib/sloplint/rules.rb
CHANGED
|
@@ -16,6 +16,43 @@ module Sloplint
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# A paragraph break: a newline, a line holding nothing but spaces (a
|
|
20
|
+
# non-breaking space among them, since the editors that emit curly
|
|
21
|
+
# apostrophes emit those), and another newline.
|
|
22
|
+
PARA_BREAK = /\r?\n[ \t\u00A0]*\r?\n/
|
|
23
|
+
|
|
24
|
+
# A gap that may hard-wrap but never crosses a paragraph break. A
|
|
25
|
+
# non-breaking space is a gap too. thats-the-whole, is-the-whole-x and
|
|
26
|
+
# WHOLE_CLOSERS share it.
|
|
27
|
+
WRAP_GAP = /(?:[ \t\u00A0]|(?!#{PARA_BREAK})\r?\n)/
|
|
28
|
+
|
|
29
|
+
# The nouns "that's the whole N" closes on. thats-the-whole owns the
|
|
30
|
+
# demonstrative form and is-the-whole-x yields it, so both patterns
|
|
31
|
+
# interpolate this one fragment and the lists cannot drift. "value" and
|
|
32
|
+
# "fix" must end the sentence or the paragraph, or run into one of the
|
|
33
|
+
# words a closer trails off on (a preposition, a pronoun, a determiner,
|
|
34
|
+
# "right", "though", "now"), because "value chain", "value-add" and
|
|
35
|
+
# "fix list" name things. The older nouns compound too ("game plan") and
|
|
36
|
+
# ship as
|
|
37
|
+
# they always have; only the two new ones were probed for it. The list
|
|
38
|
+
# of tails is an allowlist and stays one: a blocklist of compound heads
|
|
39
|
+
# would widen every time a new compound turned up. A hyphen or an
|
|
40
|
+
# apostrophe ends the closer only after a gap, since with no gap it is
|
|
41
|
+
# part of the compound; an opening delimiter after a gap (an emphasis
|
|
42
|
+
# marker, a backtick, a quote, a bracket) is not an end either, since it
|
|
43
|
+
# opens the next word ("value *chain*", "fix `list`"). Under --markdown
|
|
44
|
+
# a code span is blanked to spaces and the tail after it reads as the
|
|
45
|
+
# closer's; that is the blanking's cost, not this one's. A numbered list
|
|
46
|
+
# item on the next line ends the closer like a bullet does. Prepositions
|
|
47
|
+
# stay tails although "value at risk" and "value for money" name things;
|
|
48
|
+
# a tail list is not the place to enumerate compounds. The
|
|
49
|
+
# gap may hard-wrap but never crosses a paragraph break, so
|
|
50
|
+
# "value\nchain" is still the compound, and a heading, which ends at a
|
|
51
|
+
# blank line, still ends on the noun. The character classes are
|
|
52
|
+
# Unicode-aware, so a non-breaking space is a space and an accented
|
|
53
|
+
# letter is a letter.
|
|
54
|
+
WHOLE_CLOSERS = /point|game|thing|deal|story|ballgame|ball#{WRAP_GAP}+game|(?:value|fix)(?=[^[:word:][:space:]'’-]|#{WRAP_GAP}+(?:[^[:word:][:space:]*_`"'‘“(\[{~]|\d+[.)][ \t])|#{WRAP_GAP}*(?:\z|#{PARA_BREAK}|(?:of|to|for|in|on|at|with|here|there|behind|right|though|now|anyway|really|from|over|after|and|but|so|as|that|which|if|when|because|since|unless|until|once|while|where|i|we|you|he|she|it|they|the|a|an|this|these|those|every|any|my|our|your|his|her|their|its)\b))/i
|
|
55
|
+
|
|
19
56
|
RULES = [
|
|
20
57
|
# ── rhetorical-tic ────────────────────────────────────────────────────
|
|
21
58
|
Rule.new(
|
|
@@ -83,13 +120,229 @@ module Sloplint
|
|
|
83
120
|
id: "thats-the-whole",
|
|
84
121
|
category: "rhetorical-tic",
|
|
85
122
|
severity: "warning",
|
|
86
|
-
|
|
123
|
+
# The nouns are WHOLE_CLOSERS. "value" and "fix" are the closers agents
|
|
124
|
+
# write in technical prose ("That's the whole fix"), where the
|
|
125
|
+
# contraction keeps them out of is-the-whole-x, which sees only "is".
|
|
126
|
+
# Either apostrophe counts; editors emit the curly one.
|
|
127
|
+
pattern: /\b(?:that|this)(?:['’]s|#{WRAP_GAP}+is)#{WRAP_GAP}+the#{WRAP_GAP}+whole#{WRAP_GAP}+(?:#{WHOLE_CLOSERS})\b/i,
|
|
87
128
|
message: '"That\'s the whole point/game/…" is a stock LLM closer.',
|
|
88
129
|
suggestion: "Say the point directly instead of announcing it.",
|
|
89
|
-
examples_bad: [
|
|
90
|
-
|
|
130
|
+
examples_bad: [
|
|
131
|
+
"That's the whole point.",
|
|
132
|
+
"That's the whole value of a typed error.",
|
|
133
|
+
"That's the whole fix; the cache was already right.",
|
|
134
|
+
"That’s the whole value here.",
|
|
135
|
+
"That's the whole fix right there.",
|
|
136
|
+
"That's the whole fix that was needed.",
|
|
137
|
+
"That's the whole fix the reviewer asked for.",
|
|
138
|
+
# A blank line holding only a non-breaking space is still blank.
|
|
139
|
+
"## That's the whole fix\n\u00A0\nApply it.",
|
|
140
|
+
# A dash or a bullet after a gap ends the closer; only a glued
|
|
141
|
+
# hyphen joins a compound.
|
|
142
|
+
"That's the whole fix -- the cache was already right.",
|
|
143
|
+
"- Cache key was stale.\n- That's the whole fix\n- Tests pass.",
|
|
144
|
+
"2. That's the whole fix\n3. Tests pass.",
|
|
145
|
+
# A heading ends on the noun with no full stop.
|
|
146
|
+
"## That's the whole fix\n\nApply it and rerun the suite."
|
|
147
|
+
],
|
|
148
|
+
examples_ok: [
|
|
149
|
+
"This is the whole cake.",
|
|
150
|
+
# An unlisted noun stays clean, however closer-shaped the sentence.
|
|
151
|
+
"That's the whole history of the case.",
|
|
152
|
+
# "value" and "fix" running on into a compound name a thing.
|
|
153
|
+
"That's the whole value chain, end to end.",
|
|
154
|
+
"That's the whole value\nchain, end to end.",
|
|
155
|
+
# The gap is a non-breaking space.
|
|
156
|
+
"That's the whole value\u00A0chain, end to end.",
|
|
157
|
+
"That's the whole value *chain*, end to end.",
|
|
158
|
+
"That's the whole fix `list` for the release.",
|
|
159
|
+
"That's the whole fix (list) for the release.",
|
|
160
|
+
"That's the whole value-add of the consultant.",
|
|
161
|
+
"That's the whole value's worth.",
|
|
162
|
+
"That's the whole fix list for the release.",
|
|
163
|
+
# A paragraph break is not a gap, inside the two-word noun or
|
|
164
|
+
# before any noun.
|
|
165
|
+
"That's the whole ball\n\ngame.",
|
|
166
|
+
"That's the whole\n\npoint."
|
|
167
|
+
],
|
|
91
168
|
rationale: "The 'that's the whole X' flourish is a model tic for landing a paragraph."
|
|
92
169
|
),
|
|
170
|
+
Rule.new(
|
|
171
|
+
id: "is-the-whole-x",
|
|
172
|
+
category: "rhetorical-tic",
|
|
173
|
+
severity: "info",
|
|
174
|
+
# The generic form of thats-the-whole: a subject, then "is the whole /
|
|
175
|
+
# real / actual / entire N" with N from a closed abstract list. "That
|
|
176
|
+
# periodicity is the whole tell.", "Consistency is the real test." The
|
|
177
|
+
# match opens on the subject's last word, so the note points at the
|
|
178
|
+
# sentence and not at a space. Two older rules own two exact shapes,
|
|
179
|
+
# and those are yielded so nothing is reported twice: "that/this is
|
|
180
|
+
# the whole N" for every N in WHOLE_CLOSERS to thats-the-whole (the
|
|
181
|
+
# lookahead interpolates the same fragment), and "is the entire
|
|
182
|
+
# point/game/thing/deal/story" to is-the-entire; every
|
|
183
|
+
# other subject and noun flags here, so "This is the real test." is
|
|
184
|
+
# not lost. A question is not a closer, so an interrogative subject
|
|
185
|
+
# (what, which, who, where, when, how) is out. "only" is left out
|
|
186
|
+
# because "is the only thing" is ordinary speech; "deal", "thing" and
|
|
187
|
+
# "cost" because "the real deal", "the real thing" and "the whole cost"
|
|
188
|
+
# are idioms or quantities. The noun may not run on into a compound
|
|
189
|
+
# ("problem-solver"). Gaps may cross a hard-wrapped newline but never
|
|
190
|
+
# a paragraph break. Ships at info because "the real question" and
|
|
191
|
+
# "the whole point" are also how people talk.
|
|
192
|
+
pattern: /(?<![\w'’-])
|
|
193
|
+
(?!(?:that|this)#{WRAP_GAP}+(?:is)#{WRAP_GAP}+(?:the)#{WRAP_GAP}+(?:whole)#{WRAP_GAP}+(?:#{WHOLE_CLOSERS})(?![\w'’-]))
|
|
194
|
+
(?!(?:what|which|who|where|when|how)(?![\w'’-]))
|
|
195
|
+
[\w'’-]+#{WRAP_GAP}+(?:is)#{WRAP_GAP}+(?:the)#{WRAP_GAP}+
|
|
196
|
+
(?:(?:whole|real|actual)#{WRAP_GAP}+(?:tell|point|game|story|trick|question|problem|issue|lesson|job|work|move|test|signal|difference|answer|risk|goal|reason|pattern|insight|takeaway|shift|bet|win|catch|gap|bottleneck|value|skill|challenge|fix)
|
|
197
|
+
|entire #{WRAP_GAP}+(?!(?:point|game|thing|deal|story)(?![\w'’-]))(?:tell|point|game|story|trick|question|problem|issue|lesson|job|work|move|test|signal|difference|answer|risk|goal|reason|pattern|insight|takeaway|shift|bet|win|catch|gap|bottleneck|value|skill|challenge|fix))(?![\w'’-])/ix,
|
|
198
|
+
message: '"… is the whole/real N" is a stock LLM closer.',
|
|
199
|
+
suggestion: "Say the point directly instead of ranking it.",
|
|
200
|
+
examples_bad: [
|
|
201
|
+
"That periodicity is the whole tell.",
|
|
202
|
+
"Consistency is the real test.",
|
|
203
|
+
"Getting the handoff right is the actual work.",
|
|
204
|
+
# Not thats-the-whole's nouns, so not yielded.
|
|
205
|
+
"This is the real test.",
|
|
206
|
+
"That is the actual problem.",
|
|
207
|
+
# "entire" with a noun is-the-entire does not own.
|
|
208
|
+
"Timing is the entire tell.",
|
|
209
|
+
# A hard-wrapped closer survives one newline.
|
|
210
|
+
"Consistency\nis the real test."
|
|
211
|
+
],
|
|
212
|
+
examples_ok: [
|
|
213
|
+
# Left to thats-the-whole, an old noun and a widened one.
|
|
214
|
+
"That is the whole point.",
|
|
215
|
+
"That is the whole fix.",
|
|
216
|
+
# Left to is-the-entire.
|
|
217
|
+
"Timing is the entire game.",
|
|
218
|
+
# "only" is ordinary speech.
|
|
219
|
+
"Sleep is the only thing that helps.",
|
|
220
|
+
"The real question was never asked.",
|
|
221
|
+
# Idioms and quantities.
|
|
222
|
+
"Clojure is the real deal, and so is the REPL.",
|
|
223
|
+
"The 1962 recording is the real thing.",
|
|
224
|
+
"The remaining balance is the whole cost of the repair.",
|
|
225
|
+
# A compound with a listed noun.
|
|
226
|
+
"She is the real problem-solver on the team.",
|
|
227
|
+
# A question is not a closer.
|
|
228
|
+
"What is the real difference between the two plans?",
|
|
229
|
+
"Nobody knows what is the actual cost of storage.",
|
|
230
|
+
# A paragraph break is not a gap.
|
|
231
|
+
"Consistency\n\nis the real test."
|
|
232
|
+
],
|
|
233
|
+
rationale: "Ranking a claim as 'the whole point' or 'the real test' is how a model " \
|
|
234
|
+
"lands a paragraph without adding to it. People say it too, so one is a " \
|
|
235
|
+
"question; several in a draft should be read as a warning."
|
|
236
|
+
),
|
|
237
|
+
Rule.new(
|
|
238
|
+
id: "bare-equative",
|
|
239
|
+
category: "rhetorical-tic",
|
|
240
|
+
severity: "info",
|
|
241
|
+
# A sentence that opens on an abstract head noun and equates it with
|
|
242
|
+
# something: "The tell here is the periodicity.", "The problem is not
|
|
243
|
+
# the tool.", "The lesson is the handoff." The head noun list is closed
|
|
244
|
+
# and holds only nouns that cannot name an object, the same list
|
|
245
|
+
# the-x-is-the-x uses: "the key is the brass thing on the hook" and
|
|
246
|
+
# "the cost is the price of the ticket" are definitions that tell the
|
|
247
|
+
# reader something. The sentence must start on "The", at a sentence
|
|
248
|
+
# start or after a list marker, and the copula ("is", "is not",
|
|
249
|
+
# "isn't") must be followed by "the" and a lowercase word, so a
|
|
250
|
+
# predicate adjective ("The problem is real"), an indefinite ("The
|
|
251
|
+
# answer is a mess"), a pointing complement ("the same", "the one",
|
|
252
|
+
# "the first"), and a proper noun ("the Slack thread") are all out.
|
|
253
|
+
# Gaps may cross a hard-wrapped newline but never a paragraph break.
|
|
254
|
+
# Ships at info: "The problem is the cost" is how people write too, at
|
|
255
|
+
# about four per million words on Hacker News; it is the density that
|
|
256
|
+
# tells.
|
|
257
|
+
pattern: /(?:^|(?<=[.!?])[ \t]{1,2})(?:[-*+•][ \t]+|\d+[.)][ \t]+)?\KThe(?:[ \t]|\r?\n(?!\s*\n))+
|
|
258
|
+
(?:tell|point|question|problem|issue|lesson|difference|trick|move|risk|goal|reason|pattern|insight|takeaway|shift|bet|catch|gap|bottleneck|failure|mistake|secret|magic|challenge|tension|trap)(?:[ \t]|\r?\n(?!\s*\n))+
|
|
259
|
+
(?:here(?:[ \t]|\r?\n(?!\s*\n))+)?is(?:n['’]t|(?:[ \t]|\r?\n(?!\s*\n))+not)?(?:[ \t]|\r?\n(?!\s*\n))+ the(?:[ \t]|\r?\n(?!\s*\n))+(?!(?:one|same|only|first|last|best|worst|next|other|latter|former)\b)(?-i:(?=[a-z]))/x,
|
|
260
|
+
message: '"The X is the Y." is the AI definitional equative.',
|
|
261
|
+
suggestion: "Say what the thing does or why it matters, instead of what it equals.",
|
|
262
|
+
examples_bad: [
|
|
263
|
+
"The tell here is the periodicity.",
|
|
264
|
+
"The lesson is the handoff, not the tool.",
|
|
265
|
+
"The problem is not the tool. It is the habit.",
|
|
266
|
+
"The problem isn't the handoff.",
|
|
267
|
+
# The pair's usual habitat.
|
|
268
|
+
"- The point is the cadence.",
|
|
269
|
+
# A hard-wrapped equative survives one newline.
|
|
270
|
+
"The problem is the\ncost."
|
|
271
|
+
],
|
|
272
|
+
examples_ok: [
|
|
273
|
+
"The problem is real.",
|
|
274
|
+
"The answer is a mess of caveats.",
|
|
275
|
+
# Not at a sentence start.
|
|
276
|
+
"We think the problem is the handoff.",
|
|
277
|
+
# Concrete heads are definitions.
|
|
278
|
+
"The key is the brass thing on the hook.",
|
|
279
|
+
"The cost is the price of the ticket plus tax.",
|
|
280
|
+
# Pointing complements.
|
|
281
|
+
"The story is the one she told last week.",
|
|
282
|
+
"The reason is the same: space.",
|
|
283
|
+
"The point is the first item on the list.",
|
|
284
|
+
# A proper noun.
|
|
285
|
+
"The tell is the Slack thread.",
|
|
286
|
+
# A paragraph break is not a gap.
|
|
287
|
+
"The problem is the\n\ncost."
|
|
288
|
+
],
|
|
289
|
+
rationale: "Opening on an abstract noun and equating it with a second noun phrase " \
|
|
290
|
+
"states a diagnosis as a definition, which sounds settled and explains " \
|
|
291
|
+
"nothing. People write the shape too, so one is a question; several in " \
|
|
292
|
+
"a draft should be read as a warning."
|
|
293
|
+
),
|
|
294
|
+
Rule.new(
|
|
295
|
+
id: "epistrophe",
|
|
296
|
+
category: "rhetorical-tic",
|
|
297
|
+
severity: "info",
|
|
298
|
+
default_on: false,
|
|
299
|
+
# Two clauses that end on the same two-word phrase, the second closing
|
|
300
|
+
# the sentence: "built for one desk, and almost no job is done at one
|
|
301
|
+
# desk." Two backreferences catch the repeat, one per word, so the
|
|
302
|
+
# phrase may be hard-wrapped at either occurrence. The phrase may not
|
|
303
|
+
# open on an article, since "in the report, and … in the report" is
|
|
304
|
+
# ordinary; its second word must be four letters or more; and the
|
|
305
|
+
# second clause is five to sixty characters with no internal
|
|
306
|
+
# punctuation, opening on a word, with whitespace runs capped at two
|
|
307
|
+
# spaces (or a hard wrap with a small indent), so a code span or URL
|
|
308
|
+
# blanked by --markdown cannot weld a false repeat. Off by default:
|
|
309
|
+
# this is a named figure that Emerson and Marcus Aurelius use on
|
|
310
|
+
# purpose, and on Hacker News most hits are ordinary phrase reuse
|
|
311
|
+
# ("what we said, not what the minutes say we said"), so the rate is
|
|
312
|
+
# above what an info rule should carry. Select it when a draft is
|
|
313
|
+
# suspected of leaning on it.
|
|
314
|
+
pattern: /\b((?!the\b|a\b|an\b)[\w'’-]+)(?:[ \t]|\r?\n(?!\s*\n)[ \t]{0,4})+([\w'’-]{4,}),(?:[ \t]{1,2}|\r?\n(?!\s*\n)[ \t]{0,4})(?:(?:and|but)(?:[ \t]|\r?\n(?!\s*\n)[ \t]{0,4})+)?
|
|
315
|
+
(?=\w)(?:[^.,;:!?\n\s]|(?<![ \t])[ \t]{1,2}(?![ \t])|\r?\n(?!\s*\n)[ \t]{0,4}){5,60}?\b\1(?:[ \t]|\r?\n(?!\s*\n)[ \t]{0,4})+\2[.!?]/ix,
|
|
316
|
+
message: "Two clauses ending on the same phrase (epistrophe) read as AI cadence.",
|
|
317
|
+
suggestion: "Vary the second ending, or cut the repeat.",
|
|
318
|
+
examples_bad: [
|
|
319
|
+
"The tool was built for one desk, and almost no job is done at one desk.",
|
|
320
|
+
"They wanted a shared file, but nobody would maintain a shared file.",
|
|
321
|
+
# A hard-wrapped repeat survives one newline, in either clause.
|
|
322
|
+
"The tool was built for one desk, and almost no job\nis done at one desk.",
|
|
323
|
+
"The tool was built for one\ndesk, and almost no job is done at one desk."
|
|
324
|
+
],
|
|
325
|
+
examples_ok: [
|
|
326
|
+
"The tool was built for one desk, and almost no job is done alone.",
|
|
327
|
+
# A repeat across a sentence boundary is two sentences.
|
|
328
|
+
"They wanted a shared file. Nobody would maintain a shared file.",
|
|
329
|
+
# An article-led phrase is ordinary repetition.
|
|
330
|
+
"It was in the report, and the numbers were in the report.",
|
|
331
|
+
# The second word must be four letters or more.
|
|
332
|
+
"I like the blue one, and she likes the blue one.",
|
|
333
|
+
# The second clause is at most sixty characters.
|
|
334
|
+
"The tool was built for one desk, and almost no job anywhere in the whole company across all of its many offices is done at one desk.",
|
|
335
|
+
# No internal punctuation in the second clause.
|
|
336
|
+
"The tool was built for one desk, and, as it happens, no job is done at one desk.",
|
|
337
|
+
# Blanked text cannot make the second clause.
|
|
338
|
+
"It was tuned for one desk, and on one desk."
|
|
339
|
+
],
|
|
340
|
+
rationale: "Ending consecutive clauses on the same phrase is a figure of emphasis, " \
|
|
341
|
+
"and a model reaches for it whenever it wants a sentence to land. People " \
|
|
342
|
+
"use it too, and much of what the pattern catches is plain phrase reuse, " \
|
|
343
|
+
"which is why the rule is off by default; when selected, several in a " \
|
|
344
|
+
"draft should be read as a warning."
|
|
345
|
+
),
|
|
93
346
|
Rule.new(
|
|
94
347
|
id: "did-not-x-did-not-y",
|
|
95
348
|
category: "rhetorical-tic",
|
|
@@ -102,6 +355,385 @@ module Sloplint
|
|
|
102
355
|
examples_ok: ["He didn't know the answer."],
|
|
103
356
|
rationale: "Repeated negated-verb parallelism is a signature model cadence."
|
|
104
357
|
),
|
|
358
|
+
Rule.new(
|
|
359
|
+
id: "from-x-to-y-chain",
|
|
360
|
+
category: "rhetorical-tic",
|
|
361
|
+
severity: "warning",
|
|
362
|
+
# Two or more "from X to Y" spans in a row, comma-separated: "from
|
|
363
|
+
# private notes to shared files, from personal memory to team context".
|
|
364
|
+
# Each span is "from", one to three words, "to", one to three words; the
|
|
365
|
+
# comma between spans is the evidence of authored parallelism, as in
|
|
366
|
+
# no-x-no-y, so a chain never crosses a sentence boundary. Every operand
|
|
367
|
+
# must open with a letter, so a list of ranges ("from 1990 to 1995, from
|
|
368
|
+
# 1997 to 2001", "from 9 to 5") is not a chain. The last span is bounded
|
|
369
|
+
# too: its Y must run into punctuation, a conjunction or preposition, a
|
|
370
|
+
# wh-word, or the end of the text, so the excerpt never carries the
|
|
371
|
+
# opening of the next clause.
|
|
372
|
+
#
|
|
373
|
+
# Two human shapes share the surface and are skipped. The relay, where
|
|
374
|
+
# each span starts where the last one ended ("from the egg to the worm,
|
|
375
|
+
# from the worm to the fly"), traces a sequence: the second "from"
|
|
376
|
+
# repeating the first "to" drops the note. The reduplication, "from
|
|
377
|
+
# hummock to hummock, from root to root", describes motion: a span whose
|
|
378
|
+
# two ends are the same words drops it. Both backreferences must close
|
|
379
|
+
# on a whole operand, so "work-life" is not a relay of "work" and "code
|
|
380
|
+
# review" is not a reduplication of "code".
|
|
381
|
+
#
|
|
382
|
+
# A list of concrete mappings ("from MySQL to Postgres, from Redis to
|
|
383
|
+
# Memcached") has the same surface and flags. That is an accepted cost:
|
|
384
|
+
# the shape is one hit in 2.4M words of Hacker News, and the note says
|
|
385
|
+
# what to check.
|
|
386
|
+
pattern: /\bfrom\s+(?=[a-z])(?:[\w'-]+\s+){1,3}to\s+(?=[a-z])(?:[\w'-]+\s+){0,2}[\w'-]+
|
|
387
|
+
(?:,\s+(?:and\s+)?from\s+(?=[a-z])(?:[\w'-]+\s+){1,3}to\s+(?=[a-z])(?:[\w'-]+\s+){0,2}[\w'-]+)+
|
|
388
|
+
(?=[^\w\s'-]|\s+(?:and|or|but|in|on|at|by|with|within|over|across|as|when|while|which|that|so|because|what|how|why|who)\b|\s*\z)/ix,
|
|
389
|
+
message: '"from X to Y, from X to Y" chain (%{count} spans) reads as AI cadence.',
|
|
390
|
+
suggestion: "Keep one span, or name the things instead of sweeping across them.",
|
|
391
|
+
# Span heads only, so a "from" inside an operand is not a span.
|
|
392
|
+
count_group: /(?:\A|,\s+(?:and\s+)?)from\b/i,
|
|
393
|
+
skip: [
|
|
394
|
+
# The relay: "to the worm, from the worm".
|
|
395
|
+
/\bto\s+(?:[\w'-]+\s+){0,2}([\w'-]+),\s+(?:and\s+)?from\s+(?:[\w'-]+\s+){0,2}\1(?![\w'-])/i,
|
|
396
|
+
# The reduplication: "from hummock to hummock".
|
|
397
|
+
/\bfrom\s+(?:the\s+)?((?:[\w'-]+\s+){0,2}[\w'-]+)\s+to\s+(?:the\s+)?\1(?=,|\s*\z)/i
|
|
398
|
+
],
|
|
399
|
+
examples_bad: [
|
|
400
|
+
"The change is the move from scattered notes to one shared file, from habit to written rules, from solo effort to a team that can carry it.",
|
|
401
|
+
"We went from guessing to measuring, from hoping to knowing.",
|
|
402
|
+
"The plan takes them from the pilot to the rollout, and from the memo to the audit.",
|
|
403
|
+
# The last span may run into a clause, as long as a joining word starts it.
|
|
404
|
+
"We went from guessing to measuring, from hoping to knowing in a single quarter.",
|
|
405
|
+
# Not a relay: "work-life" is not "work".
|
|
406
|
+
"We went from rest to work, from work-life balance to burnout.",
|
|
407
|
+
# Not a reduplication: "code review" is not "code".
|
|
408
|
+
"We moved from code to code review, from guessing to measuring.",
|
|
409
|
+
# A "from" inside an operand is not a span head.
|
|
410
|
+
"The shift from revenue from ads to revenue from subscriptions, from hoping to knowing."
|
|
411
|
+
],
|
|
412
|
+
examples_ok: [
|
|
413
|
+
"The train runs from Boston to New York.",
|
|
414
|
+
# Pride and Prejudice (Austen, public domain): the relay.
|
|
415
|
+
"it jumps from admiration to love, from love to matrimony, in a moment.",
|
|
416
|
+
# Walden (Thoreau, public domain): the reduplication.
|
|
417
|
+
"jumping from hummock to hummock, from willow root to willow root, when the wild river valley",
|
|
418
|
+
# A span longer than three words on either side is a clause, not an item.
|
|
419
|
+
"He drove from the coast to the mountains in a day, and from there the road was easy.",
|
|
420
|
+
# Two spans in separate sentences never chain.
|
|
421
|
+
"She moved from Ohio to Maine. From there she wrote to him weekly.",
|
|
422
|
+
# Ranges are not spans.
|
|
423
|
+
"He served on the board from 1990 to 1995, from 1997 to 2001, and from 2005 to 2009.",
|
|
424
|
+
"We are open from 9 to 5, from Monday to Friday."
|
|
425
|
+
],
|
|
426
|
+
rationale: "Stacked 'from X to Y' spans are a model's way of gesturing at a whole " \
|
|
427
|
+
"transformation without describing any of it; each span names two poles and " \
|
|
428
|
+
"nothing between them. Human prose stacks the phrase for a sequence (each span " \
|
|
429
|
+
"picking up where the last ended) or for motion (the same word at both ends), " \
|
|
430
|
+
"and both of those are skipped. What remains is rare in careful writing; a list " \
|
|
431
|
+
"of concrete mappings (tools migrated, units converted) shares the shape and is " \
|
|
432
|
+
"the case to check before acting."
|
|
433
|
+
),
|
|
434
|
+
Rule.new(
|
|
435
|
+
id: "one-x-one-y",
|
|
436
|
+
category: "rhetorical-tic",
|
|
437
|
+
severity: "warning",
|
|
438
|
+
# Three or more "one X" items in a comma chain that stands on its own:
|
|
439
|
+
# "One owner, one repository, one weekly prune." or, after a colon,
|
|
440
|
+
# "the setup is narrow: one reviewer, one queue, one deadline". The
|
|
441
|
+
# determiner is the cadence; the comma is the evidence of authored
|
|
442
|
+
# parallelism, as in no-x-no-y.
|
|
443
|
+
#
|
|
444
|
+
# The chain must open a sentence or follow a colon, semicolon or dash.
|
|
445
|
+
# That is the narrowing that separates the drumbeat from counting: "the
|
|
446
|
+
# flat has one bedroom, one bathroom, one balcony" and "add one egg, one
|
|
447
|
+
# onion, one carrot" hang off a verb, and there the word is doing
|
|
448
|
+
# arithmetic. Each item is "one" plus one or two letter-led words (never
|
|
449
|
+
# "and"/"or"), so an enumeration over numbers is not a chain; the
|
|
450
|
+
# Oxford comma is optional after the first link; a gap may cross a
|
|
451
|
+
# hard-wrapped newline but never a paragraph break; and the last item
|
|
452
|
+
# must run into punctuation, a joining word, or the end of the text, so
|
|
453
|
+
# the excerpt never carries the opening of the next clause. The
|
|
454
|
+
# distributive "one for you, one for me, one for the pot" is skipped at
|
|
455
|
+
# any length, and a pair never flags: two is distribution, three is a
|
|
456
|
+
# drumbeat.
|
|
457
|
+
#
|
|
458
|
+
# Verse keeps the shape on purpose ("One face, one voice, one habit, and
|
|
459
|
+
# two persons"), and that is an accepted cost.
|
|
460
|
+
pattern: /(?:^|(?<=[.!?:;—–])[ \t]{1,2})\K
|
|
461
|
+
one[ \t]+(?=[a-z])[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?=[a-z])[\w'’-]+)?
|
|
462
|
+
,(?:[ \t]|\r?\n(?!\s*\n))+(?:and(?:[ \t]|\r?\n(?!\s*\n))+)?
|
|
463
|
+
one[ \t]+(?=[a-z])[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?=[a-z])[\w'’-]+)?
|
|
464
|
+
(?:(?:,(?:[ \t]|\r?\n(?!\s*\n))+(?:and(?:[ \t]|\r?\n(?!\s*\n))+)?|(?:[ \t]|\r?\n(?!\s*\n))+and(?:[ \t]|\r?\n(?!\s*\n))+)
|
|
465
|
+
one[ \t]+(?=[a-z])[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?=[a-z])[\w'’-]+)?)+
|
|
466
|
+
(?=[^\w\s'’-]|\s+(?:and|or|but|in|on|at|by|with|for|to|of|from|per|each|that|which|who|when|where|so|because|is|are|was|were)\b|\s*\z)/ix,
|
|
467
|
+
message: '"one X, one Y, one Z" chain (%{count} items) reads as AI cadence.',
|
|
468
|
+
suggestion: "Cut the chain or make it one plain sentence.",
|
|
469
|
+
# Item heads only, so a "one" inside an item is not an item.
|
|
470
|
+
count_group: /(?:\A|,\s+(?:and\s+)?|\s+and\s+)one\b/i,
|
|
471
|
+
# The distributive frame, at any length.
|
|
472
|
+
skip: [/(?:\bone\s+for\b[\s\S]*?){3}/i],
|
|
473
|
+
examples_bad: [
|
|
474
|
+
"The setup is deliberately narrow: one reviewer, one queue, one deadline.",
|
|
475
|
+
"One owner, one repository, one weekly prune.",
|
|
476
|
+
# No Oxford comma.
|
|
477
|
+
"One name, one number and one date.",
|
|
478
|
+
# A curly apostrophe is a word character here.
|
|
479
|
+
"One team’s lead, one owner, one date.",
|
|
480
|
+
# A hard-wrapped chain survives one newline.
|
|
481
|
+
"One owner, one repository,\none weekly prune."
|
|
482
|
+
],
|
|
483
|
+
examples_ok: [
|
|
484
|
+
"One for you, one for me.",
|
|
485
|
+
"One for you, one for me, one for the pot.",
|
|
486
|
+
"We keep three bins: one for 2019, one for 2020, one for 2021.",
|
|
487
|
+
"One of them left, and the other one stayed.",
|
|
488
|
+
# After a verb the word is counting.
|
|
489
|
+
"The flat has one bedroom, one bathroom, one balcony.",
|
|
490
|
+
"Add one egg, one onion, one carrot and simmer for an hour.",
|
|
491
|
+
"The season ended with one win, one loss, one draw.",
|
|
492
|
+
# Essays: First Series (Emerson, public domain): not at a clause start.
|
|
493
|
+
"Not for nothing one face, one character, one fact, makes much impression on him",
|
|
494
|
+
# Items in separate sentences never chain.
|
|
495
|
+
"One person wrote it in the morning. One person read it after lunch. One person filed it at the end of the day.",
|
|
496
|
+
# A paragraph break ends the chain.
|
|
497
|
+
"He carried one bag, one coat,\n\nOne question remained unanswered."
|
|
498
|
+
],
|
|
499
|
+
rationale: "A drumbeat of 'one' items is a model's way of making a setup sound " \
|
|
500
|
+
"spare and inevitable; the word does no counting, it sets a rhythm. " \
|
|
501
|
+
"The chain has to stand on its own, at a sentence start or after a " \
|
|
502
|
+
"colon, because after a verb the word is counting and the shape is " \
|
|
503
|
+
"ordinary. Careful writers distribute in pairs and rarely stack three."
|
|
504
|
+
),
|
|
505
|
+
Rule.new(
|
|
506
|
+
id: "and-what-it-should",
|
|
507
|
+
category: "rhetorical-tic",
|
|
508
|
+
severity: "warning",
|
|
509
|
+
# The elliptical tail: "List what the assistant knows about the client,
|
|
510
|
+
# and what it should." The second "what" clause borrows its verb from the
|
|
511
|
+
# first and ends on a bare modal or a negated auxiliary, so the sentence
|
|
512
|
+
# closes on a contrast it never states. The comma, the conjunction, and
|
|
513
|
+
# the full stop right after the modal are all required; "and what it
|
|
514
|
+
# should do" is a complete clause and does not flag. The affirmative
|
|
515
|
+
# copula and do-verb ("and what he does.", "and what it is.") are left
|
|
516
|
+
# out: those are complete clauses with a main verb, not ellipsis. A
|
|
517
|
+
# question mark is left out too, because an interrogative licenses the
|
|
518
|
+
# ellipsis ("what does it cover, and what doesn't it?"). Gaps may cross
|
|
519
|
+
# a hard-wrapped newline but never a paragraph break.
|
|
520
|
+
pattern: /,(?:[ \t]|\r?\n(?!\s*\n))+(?:and|but|or)(?:[ \t]|\r?\n(?!\s*\n))+what(?:[ \t]|\r?\n(?!\s*\n))+(?:it|they|you|we|he|she|one|i)(?:[ \t]|\r?\n(?!\s*\n))+
|
|
521
|
+
(?:(?:should|could|would|must|can|will|might|may)(?:(?:[ \t]|\r?\n(?!\s*\n))+not|n['’]t)?
|
|
522
|
+
|cannot|(?:does|did|is|was|has|have|had|are|were)(?:(?:[ \t]|\r?\n(?!\s*\n))+not|n['’]t))[.!]/ix,
|
|
523
|
+
message: '"…, and what it should." is the AI elliptical tail.',
|
|
524
|
+
suggestion: "Finish the clause, or cut it.",
|
|
525
|
+
examples_bad: [
|
|
526
|
+
"List what the assistant knows about the client, and what it should.",
|
|
527
|
+
"List what the tool does, and what it doesn't.",
|
|
528
|
+
"List what the tool does, and what it does not.",
|
|
529
|
+
"Say what the team decided, but what it couldn't.",
|
|
530
|
+
"List what I know about the client, and what I should.",
|
|
531
|
+
"List what we cover, and what we haven't.",
|
|
532
|
+
# A hard-wrapped tail survives one newline.
|
|
533
|
+
"List what the model knows about the client,\nand what it should."
|
|
534
|
+
],
|
|
535
|
+
examples_ok: [
|
|
536
|
+
"List what the assistant knows about the client, and what it should know.",
|
|
537
|
+
"He asked what it was, and what it should be called.",
|
|
538
|
+
"Nobody knew what it cost or what it should.",
|
|
539
|
+
"She wrote down what she saw. And what she should have seen, she added later.",
|
|
540
|
+
# A main verb is a complete clause.
|
|
541
|
+
"It is not what he says, but what he does.",
|
|
542
|
+
"They knew what he did, and what he was.",
|
|
543
|
+
# A question licenses the ellipsis.
|
|
544
|
+
"Who decides what the policy covers, and what it doesn't?",
|
|
545
|
+
# A paragraph break is not a comma.
|
|
546
|
+
"List what the assistant knows,\n\nand what it should."
|
|
547
|
+
],
|
|
548
|
+
rationale: "Ending on a bare modal makes the reader supply the verb and the " \
|
|
549
|
+
"contrast, which reads as poise in a model and as an unfinished sentence " \
|
|
550
|
+
"in a person. Careful writers finish the clause."
|
|
551
|
+
),
|
|
552
|
+
Rule.new(
|
|
553
|
+
id: "abstract-lives-in",
|
|
554
|
+
category: "rhetorical-tic",
|
|
555
|
+
severity: "info",
|
|
556
|
+
# An abstraction given an address: "the craft that lives between the two
|
|
557
|
+
# desks", "its context lives in a folder nobody else can open", "the
|
|
558
|
+
# value sits in the follow-up". The subject list is closed and abstract,
|
|
559
|
+
# so a person, a dog, or a house living or sitting somewhere never
|
|
560
|
+
# matches, and a capitalised subject is skipped, so the messenger app
|
|
561
|
+
# Signal sitting in the middle of a relay is not the figure (at the cost
|
|
562
|
+
# of a sentence-initial "Context lives in", which is rare).
|
|
563
|
+
#
|
|
564
|
+
# Two prepositions are left out. "with" marks responsibility ("the
|
|
565
|
+
# decision sits with the board"), and "at" marks a quantity ("the value
|
|
566
|
+
# sits at ten million"); "at the intersection of" belongs to
|
|
567
|
+
# intersection-of. Two verbs are left out too: "lies in" ("the problem
|
|
568
|
+
# lies in the assumption") and "resides in" are ordinary English for
|
|
569
|
+
# where a fault or an authority is, at any register.
|
|
570
|
+
#
|
|
571
|
+
# Ships at info. The same shape states where information literally is
|
|
572
|
+
# ("the knowledge lives in our heads", "the instructions live in the
|
|
573
|
+
# README"), and a sample of pre-2022 Hacker News biased toward the
|
|
574
|
+
# construction turns up a few of those per million words, all human.
|
|
575
|
+
# One flag is a question; a draft that keeps giving ideas addresses
|
|
576
|
+
# should be read as a warning.
|
|
577
|
+
pattern: /\b(?:work|context|knowledge|answer|value|truth|problem|risk|decision|instructions?|memory|leverage|power|magic|difference|opportunity|insight|advantage|gap|signal|nuance|meaning|tension|friction|complexity|craft|skill|expertise)(?:[ \t]|\r?\n(?!\s*\n))+
|
|
578
|
+
(?:that(?:[ \t]|\r?\n(?!\s*\n))+|which(?:[ \t]|\r?\n(?!\s*\n))+)?(?:lives?|lived|sits?|sat)(?:[ \t]|\r?\n(?!\s*\n))+(?:in|between|inside|outside|beneath|behind|under|underneath)\b/ix,
|
|
579
|
+
message: "An abstraction that lives/sits somewhere is an AI figure.",
|
|
580
|
+
suggestion: "Say who does the work, or where the thing actually is.",
|
|
581
|
+
# A capitalised subject is a proper noun.
|
|
582
|
+
skip: [/\A[A-Z]/],
|
|
583
|
+
examples_bad: [
|
|
584
|
+
"The craft that lives between the two desks is where the handoff fails.",
|
|
585
|
+
"Its context lives in a folder nobody else can open.",
|
|
586
|
+
"The real value sits in the follow-up, not the meeting."
|
|
587
|
+
],
|
|
588
|
+
examples_ok: [
|
|
589
|
+
"She lives in Boston and sits between us at dinner.",
|
|
590
|
+
# Responsibility, not location.
|
|
591
|
+
"The decision sits with the board.",
|
|
592
|
+
"The risk lives with the buyer once the goods ship.",
|
|
593
|
+
# A quantity, not a location.
|
|
594
|
+
"The value sits at ten million dollars a life.",
|
|
595
|
+
# "on" is not in the list.
|
|
596
|
+
"The knowledge lived on a server in the basement.",
|
|
597
|
+
# An intervening noun breaks the figure.
|
|
598
|
+
"The knowledge base lived in the basement.",
|
|
599
|
+
# A proper noun.
|
|
600
|
+
"Signal sits in the middle of the relay.",
|
|
601
|
+
# "lies in" is ordinary English for where a fault is.
|
|
602
|
+
"The problem lies in the assumption.",
|
|
603
|
+
"He put the answer in the margin and sat between the two of them."
|
|
604
|
+
],
|
|
605
|
+
rationale: "Giving an idea a location ('the value sits in', 'the work lives " \
|
|
606
|
+
"between') lets a writer sound precise about where something is without " \
|
|
607
|
+
"saying who does it or what it is. Models reach for it constantly, but " \
|
|
608
|
+
"people use the same shape to say where information literally is, so " \
|
|
609
|
+
"one flag is a question; a draft that keeps giving ideas addresses should " \
|
|
610
|
+
"be read as a warning."
|
|
611
|
+
),
|
|
612
|
+
Rule.new(
|
|
613
|
+
id: "the-x-is-the-x",
|
|
614
|
+
category: "rhetorical-tic",
|
|
615
|
+
severity: "warning",
|
|
616
|
+
# The repeated-head equative: "the reason it holds up is the reason the
|
|
617
|
+
# other half happens", "the problem with A is the problem with B". The
|
|
618
|
+
# same abstract head noun sits on both sides of the copula, caught by a
|
|
619
|
+
# backreference, so the sentence equates two things by declaring them
|
|
620
|
+
# the same kind of thing.
|
|
621
|
+
#
|
|
622
|
+
# The noun list is closed and holds only heads that cannot name a
|
|
623
|
+
# specific object: reason, problem, question, lesson and their kin.
|
|
624
|
+
# "key", "cost", "value", "unit", "fix" and the like are out, because
|
|
625
|
+
# "the key to the front door is the key on the red fob" is an identity
|
|
626
|
+
# statement that tells the reader something. The clause between the
|
|
627
|
+
# two heads is capped at fifty characters and may not hold a comma,
|
|
628
|
+
# semicolon or colon, so the two heads sit in one clause and an earlier
|
|
629
|
+
# "the cost was low, but shipping is the cost" never pairs; it may
|
|
630
|
+
# cross a hard-wrapped newline. The second head must be followed by a
|
|
631
|
+
# preposition, determiner, quantifier, pronoun, plural noun, or
|
|
632
|
+
# punctuation, so a compound ("the answer key") is not a repeat. The
|
|
633
|
+
# bare form ("the reason is the reason we came") and the contracted
|
|
634
|
+
# negation ("isn't the reason") both count. The sentence-initial
|
|
635
|
+
# capital is not required, so "and the reason … is the reason …"
|
|
636
|
+
# flags too.
|
|
637
|
+
pattern: /\bthe\s+(reason|problem|question|lesson|difference|trick|move|goal|tell|pattern|insight|takeaway|shift|bet|catch|bottleneck|issue|game|cause|failure|magic|challenge|tension|irony|paradox|trap)\b
|
|
638
|
+
(?:[^.!?;:,\n]|\r?\n(?!\s*\n)){1,50}?\bis(?:n['’]t|\s+not)?\s+the\s+\1
|
|
639
|
+
(?=[,.;:!?]|\s+(?:of|for|with|in|on|to|at|behind|about|that|which|why|here|there|the|a|an|this|these|those|my|our|your|their|its|his|her|it|they|we|you|i|he|she|every|each|most|many|some|any|no|nobody|everyone|people|[a-z]+s)\b)/ix,
|
|
640
|
+
message: '"The X … is the X …" equates by repeating the head noun.',
|
|
641
|
+
suggestion: "Say what the second thing is, not that it is the same kind of thing.",
|
|
642
|
+
examples_bad: [
|
|
643
|
+
"The reason it holds up is the reason the other half happens.",
|
|
644
|
+
"The problem with the tool is the problem with the team.",
|
|
645
|
+
"In practice the question for them is not the question for us.",
|
|
646
|
+
"The problem with the tool isn't the problem with the team.",
|
|
647
|
+
# The bare form.
|
|
648
|
+
"The reason is the reason we came.",
|
|
649
|
+
# A quantifier or a plural noun may follow the second head.
|
|
650
|
+
"The reason people stay is the reason people leave.",
|
|
651
|
+
"The problem with onboarding is the problem every team has.",
|
|
652
|
+
"The lesson from the outage is the lesson most teams skip.",
|
|
653
|
+
# A hard-wrapped clause survives one newline.
|
|
654
|
+
"The reason\nit holds is the reason it fails."
|
|
655
|
+
],
|
|
656
|
+
examples_ok: [
|
|
657
|
+
"Rain is the reason we stayed home.",
|
|
658
|
+
"Its cost is the reason we came late.",
|
|
659
|
+
# Across a sentence boundary is two sentences.
|
|
660
|
+
"The reason is simple. It is the reason we left.",
|
|
661
|
+
# A compound noun is not a repeated head.
|
|
662
|
+
"The answer to the first question is the answer key, not a guess.",
|
|
663
|
+
# A semicolon or a comma is a boundary: the heads must share a clause.
|
|
664
|
+
"Process is the issue; community process is the issue we can fix.",
|
|
665
|
+
"The cost was low, but shipping is the cost we forgot.",
|
|
666
|
+
"The reason was never clear, but timing is the reason it failed.",
|
|
667
|
+
# Concrete heads are identity statements, not tautologies.
|
|
668
|
+
"The key to the front door is the key on the red fob.",
|
|
669
|
+
"The cost of shipping is the cost of the box plus postage."
|
|
670
|
+
],
|
|
671
|
+
rationale: "Repeating the head noun across the copula asserts an identity between " \
|
|
672
|
+
"two things while naming neither; it sounds like a diagnosis and " \
|
|
673
|
+
"delivers a tautology. Careful writers say what the second thing is."
|
|
674
|
+
),
|
|
675
|
+
Rule.new(
|
|
676
|
+
id: "same-determiner-chain",
|
|
677
|
+
category: "rhetorical-tic",
|
|
678
|
+
severity: "info",
|
|
679
|
+
# The quiet cousin of one-x-one-y and no-x-no-y: three or more items in
|
|
680
|
+
# a comma chain that all open on the same determiner or quantifier,
|
|
681
|
+
# caught with a backreference: "every faculty, every thought, every
|
|
682
|
+
# emotion", "your inbox, your calendar, your task list", "more work,
|
|
683
|
+
# more meetings, more email". "one" and "no" are left to their own
|
|
684
|
+
# rules. The narrative possessives (my, his, her, their, its) are left
|
|
685
|
+
# out: "his fame, his position, his life" is every novelist's, and it
|
|
686
|
+
# doubled the human rate. Ships at info because this is a rhetorical
|
|
687
|
+
# device humans own too, Emerson above all; a model uses it the way it
|
|
688
|
+
# uses the others, as a default cadence, and several in one draft is
|
|
689
|
+
# the tell. The Oxford comma is optional after the first link, a gap may
|
|
690
|
+
# cross a hard-wrapped newline but never a paragraph break, and the last
|
|
691
|
+
# item must run into punctuation, a joining word, a modal or common
|
|
692
|
+
# verb, or the end of the text. A verb or adverb the list does not know
|
|
693
|
+
# can still be swallowed as the last item's second word ("every deploy
|
|
694
|
+
# today"); the count is right and the over-reach is one word. The
|
|
695
|
+
# letter-led guards are case-sensitive on purpose: under /i they would
|
|
696
|
+
# let "New York, New Jersey, New Hampshire" through as a chain.
|
|
697
|
+
pattern: /\b(every|each|your|our|more|less|fewer|same|any|another|zero|new|real|true)[ \t]+(?-i:(?=[a-z]))[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?-i:(?=[a-z]))[\w'’-]+)?
|
|
698
|
+
,(?:[ \t]|\r?\n(?!\s*\n))+(?:and(?:[ \t]|\r?\n(?!\s*\n))+)?
|
|
699
|
+
\1[ \t]+(?-i:(?=[a-z]))[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?-i:(?=[a-z]))[\w'’-]+)?
|
|
700
|
+
(?:(?:,(?:[ \t]|\r?\n(?!\s*\n))+(?:and(?:[ \t]|\r?\n(?!\s*\n))+)?|(?:[ \t]|\r?\n(?!\s*\n))+and(?:[ \t]|\r?\n(?!\s*\n))+)
|
|
701
|
+
\1[ \t]+(?-i:(?=[a-z]))[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?-i:(?=[a-z]))[\w'’-]+)?)+
|
|
702
|
+
(?=[^\w\s'’-]|\s+(?:and|or|but|in|on|at|by|with|for|to|of|from|per|than|that|which|who|when|where|so|because|is|are|was|were|will|would|can|could|should|must|may|might|has|have|had|do|does|did|all|every|each|need|needs|make|makes|bring|brings|matter|matters|today|now|then|here|there)\b|\s*\z)/ix,
|
|
703
|
+
message: 'Repeated-determiner chain (%{count} items) reads as AI cadence.',
|
|
704
|
+
suggestion: "Cut the chain or make it one plain sentence.",
|
|
705
|
+
count_group: /(?:\A|,\s+(?:and\s+)?|\s+and\s+)(?:every|each|your|our|more|less|fewer|same|any|another|zero|new|real|true)\b/i,
|
|
706
|
+
examples_bad: [
|
|
707
|
+
"It touches every file, every branch, every deploy.",
|
|
708
|
+
"You get more work, more meetings, and more email.",
|
|
709
|
+
"Your inbox, your calendar, your task list.",
|
|
710
|
+
# No Oxford comma.
|
|
711
|
+
"It touches every file, every branch and every deploy.",
|
|
712
|
+
# A chain as the subject of a clause.
|
|
713
|
+
"Every test, every lint, every build must pass.",
|
|
714
|
+
"You get more work, more meetings, and more email every day.",
|
|
715
|
+
# A hard-wrapped chain survives one newline.
|
|
716
|
+
"Every file, every branch,\nevery deploy."
|
|
717
|
+
],
|
|
718
|
+
examples_ok: [
|
|
719
|
+
"Every file and every branch was checked.",
|
|
720
|
+
# Two items is a pair, not a chain.
|
|
721
|
+
"Every file, every branch.",
|
|
722
|
+
# The determiner must repeat; a mixed list is a list.
|
|
723
|
+
"Every file, each branch, and some deploys.",
|
|
724
|
+
"We bought more bread, some milk, and a dozen eggs.",
|
|
725
|
+
# A paragraph break ends the chain.
|
|
726
|
+
"Every file, every branch,\n\nEvery deploy was checked twice.",
|
|
727
|
+
# Narrative possessives are left out.
|
|
728
|
+
"His coat, his hat, his gloves.",
|
|
729
|
+
# Proper nouns are not a chain.
|
|
730
|
+
"We visited New York, New Jersey, and New Hampshire."
|
|
731
|
+
],
|
|
732
|
+
rationale: "A comma chain of items opening on the same determiner is a drumbeat, " \
|
|
733
|
+
"and a model falls into it as a default cadence. It is also a device " \
|
|
734
|
+
"careful writers use on purpose, so one is a question, not a verdict; " \
|
|
735
|
+
"when a draft carries several, read the family as a warning."
|
|
736
|
+
),
|
|
105
737
|
Rule.new(
|
|
106
738
|
id: "dont-verb-it",
|
|
107
739
|
category: "rhetorical-tic",
|
|
@@ -175,25 +807,50 @@ module Sloplint
|
|
|
175
807
|
Rule.new(
|
|
176
808
|
id: "cleanly",
|
|
177
809
|
category: "rhetorical-tic",
|
|
178
|
-
severity: "
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
|
|
810
|
+
severity: "info",
|
|
811
|
+
# The bare adverb was the whole rule, and the engineering idioms were
|
|
812
|
+
# flagged on purpose. Technical prose says that was the wrong call: a
|
|
813
|
+
# patch that applies cleanly, a build that compiles cleanly and a gear
|
|
814
|
+
# leg that separates cleanly are all checkable facts with a visible
|
|
815
|
+
# failure state, which is the opposite of rating a fit you haven't
|
|
816
|
+
# shown. The original probe -- 25 Gutenberg texts, no modern manner
|
|
817
|
+
# adverb found -- could not have caught this, because Victorian novels
|
|
818
|
+
# have no builds; the same corpus read as engineering ("clearly and
|
|
819
|
+
# cleanly drawn in pencil", Machine Drawing and Design, 1890) has it.
|
|
820
|
+
#
|
|
821
|
+
# What is left is the partition frame: something divided into parts, or
|
|
822
|
+
# mapped onto another scheme, and rated before the reader sees the
|
|
823
|
+
# parts. The clause-final form ("the objection breaks down cleanly, and
|
|
824
|
+
# neither half survives") is the same tell and is left out.
|
|
825
|
+
#
|
|
826
|
+
# The frame is not the sense, and this rule ships at info because of it.
|
|
827
|
+
# "The argument splits cleanly into two parts" and "the gear retracted
|
|
828
|
+
# cleanly into the well" are one pattern apart only in their subject,
|
|
829
|
+
# and a regex cannot see the subject -- the same limit that keeps the
|
|
830
|
+
# animacy verbs out of trailing-significance-participle. The preposition
|
|
831
|
+
# buys the common physical cases ("separated cleanly from", "cleanly
|
|
832
|
+
# compiled", "cleanly drawn") and nothing more, so a flag here is a
|
|
833
|
+
# question for the agent reading it, not a verdict.
|
|
834
|
+
pattern: /\bcleanly\s+(?:in(?:to|\s+(?:two|three|half))|onto)\b/i,
|
|
185
835
|
message: '"Cleanly" rates the fit instead of showing it.',
|
|
186
836
|
suggestion: "Cut the adverb, or say what actually lined up.",
|
|
187
837
|
examples_bad: [
|
|
188
838
|
"The argument splits cleanly into two parts.",
|
|
189
|
-
"The patch applies cleanly to main.",
|
|
190
839
|
"The new taxonomy maps cleanly onto the old one.",
|
|
191
|
-
"The objection
|
|
840
|
+
"The objection divides cleanly in half, and neither side survives."
|
|
192
841
|
],
|
|
193
842
|
examples_ok: [
|
|
194
843
|
"The branch merged without conflicts.",
|
|
195
844
|
"She wiped the counter clean.",
|
|
196
|
-
"The build finished with no warnings."
|
|
845
|
+
"The build finished with no warnings.",
|
|
846
|
+
# The engineering idiom the rule used to flag on purpose. A patch that
|
|
847
|
+
# applies cleanly either did or did not; there is nothing being rated.
|
|
848
|
+
"The patch applies cleanly to main.",
|
|
849
|
+
# Wording follows NTSB AAR-14/01 and NASA SEL-84-101 (US government
|
|
850
|
+
# works) and An Introduction to Machine Drawing and Design (1890).
|
|
851
|
+
"The main landing gear separated cleanly from the airplane.",
|
|
852
|
+
"The code should be cleanly compiled beforehand.",
|
|
853
|
+
"Your answers should be clearly and cleanly drawn in pencil."
|
|
197
854
|
],
|
|
198
855
|
rationale: "The adverb rates the join instead of showing it, and it rates before the " \
|
|
199
856
|
"reader has anything to check. Real material resists -- the leftover case, " \
|
|
@@ -516,7 +1173,64 @@ module Sloplint
|
|
|
516
1173
|
id: "exact-exactly",
|
|
517
1174
|
category: "rhetorical-tic",
|
|
518
1175
|
severity: "info",
|
|
519
|
-
|
|
1176
|
+
# Rewritten after 2.2M words of technical prose left ~111 of 125 hits
|
|
1177
|
+
# false. The shape was the problem, not the entries: the pattern matched
|
|
1178
|
+
# "exact" everywhere and subtracted an allow-list, and an open pattern
|
|
1179
|
+
# with a growing exception list can never close over a common word.
|
|
1180
|
+
# Machining and investigation prose puts "exact" in front of anything an
|
|
1181
|
+
# instrument reads -- diameter, pitch angle, CPU time, blade setting,
|
|
1182
|
+
# five threads, perpendicular, on center -- so each addition bought one
|
|
1183
|
+
# false positive and risked silencing the cliche sitting behind it.
|
|
1184
|
+
#
|
|
1185
|
+
# So the rule now matches the tell instead, and the tell is a small
|
|
1186
|
+
# closed set of frames: a demonstrative copula carrying the intensifier
|
|
1187
|
+
# ("that's exactly", "this is exactly the kind of"), the bare evaluative
|
|
1188
|
+
# complement ("exactly right"), the deictic noun ("exactly the point",
|
|
1189
|
+
# "the exact problem"), and a knowing verb plus a wh-word ("I know
|
|
1190
|
+
# exactly why"). Everything else is silent by construction rather than
|
|
1191
|
+
# by exception.
|
|
1192
|
+
#
|
|
1193
|
+
# Two recall losses, both deliberate. "exactly the right words" is the
|
|
1194
|
+
# tell and "reamed to exactly the right size" is a measurement, and they
|
|
1195
|
+
# differ only in their subject, which a regex cannot see -- so that
|
|
1196
|
+
# frame stays out unless a copula carries it. And "determined exactly
|
|
1197
|
+
# when" is precise where "know exactly why" is filler, so only the
|
|
1198
|
+
# knowing verbs take the wh-branch.
|
|
1199
|
+
pattern: /\b(?:
|
|
1200
|
+
# "That's exactly ...", "This is exactly the kind of ...".
|
|
1201
|
+
# The complement is open here, so it needs the checkable
|
|
1202
|
+
# allow-list back as a guard, or the copula re-admits every
|
|
1203
|
+
# measurement the rest of the rule was rewritten to drop:
|
|
1204
|
+
# "That's the exact same design", "It was exactly noon".
|
|
1205
|
+
# "here" and "there" are not subjects: the existential
|
|
1206
|
+
# "there is exactly one solution" is a count, so checkable.
|
|
1207
|
+
(?:that|this|it|these|those|which)
|
|
1208
|
+
(?:'s|\u2019s|\s+(?:is|was|are|were))\s+
|
|
1209
|
+
(?:the\s+)?exact(?:ly)?\b
|
|
1210
|
+
(?!\s*(?:
|
|
1211
|
+
(?:the\s+)?
|
|
1212
|
+
(?:same|opposite|science|change|replica|cop(?:y|ies)|
|
|
1213
|
+
location|coordinates|way)\b
|
|
1214
|
+
|[$\d]|noon\b|midnight\b|o'?\s*clock\b
|
|
1215
|
+
|(?:one|two|three|four|five|six|seven|eight|nine|ten|
|
|
1216
|
+
eleven|twelve|dozen|twenty|thirty|fifty|hundred)\b
|
|
1217
|
+
|(?:the\s+|a\s+|an\s+)?(?:[\w-]+\s+){0,2}
|
|
1218
|
+
(?:diameter|radius|size|dimensions?|length|width|
|
|
1219
|
+
height|depth|thickness|weight|mass|volume|
|
|
1220
|
+
angle|pitch|temperature|pressure|speed|altitude|
|
|
1221
|
+
position|distance|clearance|tolerance)\b))
|
|
1222
|
+
# "exactly right", "exactly backwards"
|
|
1223
|
+
| exactly\s+(?:right|wrong|backwards|so)\b
|
|
1224
|
+
# "exactly the point", "the exact problem"
|
|
1225
|
+
| exactly\s+the\s+
|
|
1226
|
+
(?:point|problem|issue|reason|kind|sort|type|thing|
|
|
1227
|
+
question|shape|word)\b
|
|
1228
|
+
| the\s+exact\s+
|
|
1229
|
+
(?:point|problem|issue|reason|thing|question|shape)\b
|
|
1230
|
+
# "I know exactly why this happened"
|
|
1231
|
+
| know(?:s|n)?\s+exactly\s+(?:why|what|how|who)\b
|
|
1232
|
+
| knew\s+exactly\s+(?:why|what|how|who)\b
|
|
1233
|
+
)/ix,
|
|
520
1234
|
message: '"exact/exactly" is reflexive emphasis unless it names something checkable.',
|
|
521
1235
|
suggestion: "Cut it, or replace with the number, name, or match it's supposed to be precise about.",
|
|
522
1236
|
examples_bad: [
|
|
@@ -539,7 +1253,25 @@ module Sloplint
|
|
|
539
1253
|
"The train left at exactly noon.",
|
|
540
1254
|
"They agreed to meet at exactly midnight.",
|
|
541
1255
|
"The meeting starts at exactly 3 o'clock.",
|
|
542
|
-
"She has exacting standards for her students."
|
|
1256
|
+
"She has exacting standards for her students.",
|
|
1257
|
+
# A measured quantity and a determined fact, pinning the two branches
|
|
1258
|
+
# added above. Wording follows "Turning and Boring" (1919, public
|
|
1259
|
+
# domain by date) and NTSB AAR-04/01 (US government work).
|
|
1260
|
+
"The soft jaws are bored to the exact diameter of the finished rim.",
|
|
1261
|
+
"Investigators could not determine the exact blade pitch angle.",
|
|
1262
|
+
"The bore is finished with a reamer to exactly the right size and taper.",
|
|
1263
|
+
"It could not be determined exactly when the fire began.",
|
|
1264
|
+
# The demonstrative copula leaves its complement open, so each of the
|
|
1265
|
+
# checkable uses is pinned again in that frame -- review found the
|
|
1266
|
+
# branch re-admitting the whole allow-list it was meant to replace.
|
|
1267
|
+
"That's the exact same design we shipped last year.",
|
|
1268
|
+
"These are the exact coordinates of the wreck.",
|
|
1269
|
+
"It was exactly noon when the whistle blew.",
|
|
1270
|
+
"This is the exact replica of the ship.",
|
|
1271
|
+
# Existential "there"/"here" carry a count, which is checkable.
|
|
1272
|
+
"There is exactly one solution to the equation.",
|
|
1273
|
+
"There are exactly 24 hours in a day.",
|
|
1274
|
+
"Here is the exact temperature at the time of the failure."
|
|
543
1275
|
],
|
|
544
1276
|
rationale: "Models reach for 'exact/exactly' as filler emphasis on a claim with nothing to " \
|
|
545
1277
|
"check; it earns its place only next to a number, a name, or a stated identity."
|
|
@@ -594,6 +1326,293 @@ module Sloplint
|
|
|
594
1326
|
"structure. Models borrow it as a metaphor for anything important, which just " \
|
|
595
1327
|
"restates the sentence's importance without saying what actually holds it up."
|
|
596
1328
|
),
|
|
1329
|
+
Rule.new(
|
|
1330
|
+
id: "intersection-of",
|
|
1331
|
+
category: "rhetorical-tic",
|
|
1332
|
+
severity: "warning",
|
|
1333
|
+
# "at" is not load-bearing -- "explores the intersection of art and
|
|
1334
|
+
# technology" is the same move -- so the anchor is "the intersection of"
|
|
1335
|
+
# and the two guards carry the whole burden of separating the literal
|
|
1336
|
+
# senses. A street corner names its streets, and a street name is
|
|
1337
|
+
# Capitalized-then-lowercase ("Elm", "Broadway", "Highway 12"), so the
|
|
1338
|
+
# positive lookahead demands the next word be lowercase or an all-caps
|
|
1339
|
+
# acronym -- "AI", "UX", "HCI" are the metaphor's favourite operands and
|
|
1340
|
+
# no street is spelled that way. Geometry and set arithmetic name their
|
|
1341
|
+
# operands ("the two curves", "the ranges", "both key sets"), so the
|
|
1342
|
+
# negative lookahead drops a literal noun found within two words of
|
|
1343
|
+
# "of". Two words, not three, keeps "art and city streets" from reaching
|
|
1344
|
+
# "streets" and silencing a real hit. No /i on the whole pattern: it
|
|
1345
|
+
# would make [a-z] match capitals and undo the first guard, so the
|
|
1346
|
+
# case-insensitive parts are inline (?i:...) groups instead.
|
|
1347
|
+
#
|
|
1348
|
+
# Three additions after the rule met accident reports and reference
|
|
1349
|
+
# documentation, where every hit was literal and each broke a guard.
|
|
1350
|
+
# Airfield surfaces (runway, taxiway, apron) are as literal as a street
|
|
1351
|
+
# and are written lowercase, so the first guard was actively admitting
|
|
1352
|
+
# them. A matrix cell is the same literal sense as a set intersection,
|
|
1353
|
+
# so rows and columns join the geometry list. And the all-caps
|
|
1354
|
+
# allowance, added for "AI"/"UX"/"HCI" on the premise that no street is
|
|
1355
|
+
# spelled that way, was admitting American route designators. The
|
|
1356
|
+
# trailing (?!-\d) on that branch drops a route number ("US-27A"), and
|
|
1357
|
+
# one more lookahead drops a quadrant plus a house number ("NE 140th
|
|
1358
|
+
# Court"). Street-type nouns are deliberately NOT added to the literal
|
|
1359
|
+
# list: "court", "drive" and "place" are ordinary abstract nouns, and
|
|
1360
|
+
# the two-word window would reach them as the second operand and
|
|
1361
|
+
# silence "the intersection of memory and place".
|
|
1362
|
+
pattern: /\b[Tt]he\s+intersection\s+of\b
|
|
1363
|
+
(?!\s+(?:[\w-]+\s+){0,2}
|
|
1364
|
+
(?i:sets?|lines?|curves?|planes?|circles?|spheres?|axes|
|
|
1365
|
+
rays?|segments?|arcs?|orbits?|streets?|avenues?|
|
|
1366
|
+
roads?|highways?|routes?|tracks?|corridors?|paths?|
|
|
1367
|
+
boulevards?|lanes?|arrays?|lists?|ranges?|
|
|
1368
|
+
collections?|keys?|vectors?|matrices|polygons?|
|
|
1369
|
+
rectangles?|intervals?|data|datasets?|
|
|
1370
|
+
runways?|taxiways?|taxilanes?|aprons?|
|
|
1371
|
+
rows?|columns?|cells?)\b)
|
|
1372
|
+
(?!\s+[A-Z]{1,3}\s+\d)
|
|
1373
|
+
(?=\s+(?:[a-z]|[A-Z]{2,}\b(?!-\d)))/x,
|
|
1374
|
+
message: '"the intersection of X and Y" outside streets or geometry is borrowed positioning.',
|
|
1375
|
+
suggestion: "Say what the work does, or name the two things it takes from each.",
|
|
1376
|
+
examples_bad: [
|
|
1377
|
+
"Her work sits at the intersection of art and technology.",
|
|
1378
|
+
"Her book explores the intersection of art and technology.",
|
|
1379
|
+
"We operate at the intersection of AI and healthcare.",
|
|
1380
|
+
"The product lives at the intersection of design and engineering.",
|
|
1381
|
+
"At the intersection of policy and practice, nothing moves quickly.",
|
|
1382
|
+
"The intersection of grief and comedy is where this essay lands.",
|
|
1383
|
+
"The role sits at the intersection of the marketing and product teams."
|
|
1384
|
+
],
|
|
1385
|
+
examples_ok: [
|
|
1386
|
+
"The accident happened at the intersection of Elm Street and Oak Avenue.",
|
|
1387
|
+
"Turn left at the intersection of Main and Fifth.",
|
|
1388
|
+
"The bus stops at the intersection of Broadway and 42nd Street.",
|
|
1389
|
+
"A hydrant stands at the intersection of Elm and Willow.",
|
|
1390
|
+
"The house sits at the intersection of Highway 12 and County Road 8.",
|
|
1391
|
+
"The intersection of Elm and Willow was closed for repaving.",
|
|
1392
|
+
"The solution lies at the intersection of the two curves.",
|
|
1393
|
+
"Draw a point at the intersection of the lines.",
|
|
1394
|
+
"The point at the intersection of two circles is equidistant.",
|
|
1395
|
+
"He stood at the intersection of five streets and could not choose.",
|
|
1396
|
+
"The village grew up at the intersection of several old roads.",
|
|
1397
|
+
"Snow piled up at the intersection of the roads below.",
|
|
1398
|
+
"Compute the intersection of the two arrays.",
|
|
1399
|
+
"The query returns the intersection of both key sets.",
|
|
1400
|
+
"The intersection of the ranges is empty.",
|
|
1401
|
+
# Airfield surfaces, route designators and matrix cells, each pinning
|
|
1402
|
+
# one of the three guards added above. Wording follows NTSB AAR-16/02,
|
|
1403
|
+
# AAR-14/01 and HAR-17/02 and the NASA Software Safety Guidebook
|
|
1404
|
+
# (NASA-GB-8719.13) -- US government works, public domain.
|
|
1405
|
+
"The airplane crossed the intersection of runway 13 with runway 4.",
|
|
1406
|
+
"Firefighters gathered near the intersection of taxiway N and taxiway F.",
|
|
1407
|
+
"The crash occurred at the intersection of US-27A and NE 140th Court.",
|
|
1408
|
+
"Traffic was heaviest at the intersection of NE 140th Court.",
|
|
1409
|
+
"The square at the intersection of a row and a column contains a code."
|
|
1410
|
+
],
|
|
1411
|
+
rationale: "A street corner and a set diagram are the phrase's literal homes. Everywhere " \
|
|
1412
|
+
"else it claims a position without doing the work of one: the writer sits " \
|
|
1413
|
+
"between two fields and says nothing about either. Models open bios and " \
|
|
1414
|
+
"pitches with it because it sounds like a thesis while committing to nothing."
|
|
1415
|
+
),
|
|
1416
|
+
Rule.new(
|
|
1417
|
+
id: "impact-verb",
|
|
1418
|
+
category: "rhetorical-tic",
|
|
1419
|
+
severity: "warning",
|
|
1420
|
+
# "impact" and "impacts" are also nouns, so they only count as verbs
|
|
1421
|
+
# behind an auxiliary or a subject pronoun. One optional pronoun may sit
|
|
1422
|
+
# between the auxiliary and the verb ("does this impact the date");
|
|
1423
|
+
# allowing a determiner in that slot instead would swallow "will the
|
|
1424
|
+
# impact be permanent", so the copula lookahead below backs it up.
|
|
1425
|
+
# "impacted"/"impacting" are verbal on their own except for the medical
|
|
1426
|
+
# and soil sense: the forward lookahead catches the attributive form
|
|
1427
|
+
# ("an impacted wisdom tooth"), but the predicate form ("the tooth was
|
|
1428
|
+
# impacted") needs the noun and copula pulled into the match, because
|
|
1429
|
+
# skip: only ever sees matched text, never surrounding context. Same
|
|
1430
|
+
# reason load-bearing does it, and a lookbehind that wide trips the
|
|
1431
|
+
# Onigmo bug documented there. The trailing (?!-) is load-bearing too:
|
|
1432
|
+
# every other guard is spelled with \s+, so "will impact-test the
|
|
1433
|
+
# housing" walks straight past them.
|
|
1434
|
+
#
|
|
1435
|
+
# Accident reports found two holes. "to impact" was in the auxiliary
|
|
1436
|
+
# list as an infinitive marker, so it also matched the preposition in
|
|
1437
|
+
# "4 seconds prior to impact" and "time to impact"; that branch now
|
|
1438
|
+
# stands on its own and requires a following object. And "impacted" in
|
|
1439
|
+
# those reports is usually one object hitting another -- an airplane
|
|
1440
|
+
# impacts terrain, debris impacts a wing -- which is the collision noun
|
|
1441
|
+
# the rule already excludes, just on the other side of the verb, so the
|
|
1442
|
+
# struck-object list mirrors it.
|
|
1443
|
+
pattern: /\b(?:
|
|
1444
|
+
(?:will|would|can|could|may|might|shall|should|must|does|
|
|
1445
|
+
do|did|doesn't|don't|didn't|won't|helps?|helped)\s+
|
|
1446
|
+
(?:(?:it|this|that|they|we|you)\s+)?
|
|
1447
|
+
impacts?
|
|
1448
|
+
| to\s+impacts?
|
|
1449
|
+
(?=\s+(?!and\b|or\b|but\b|in\b|on\b|at\b|of\b|for\b|from\b|
|
|
1450
|
+
with\b|during\b|after\b|before\b|than\b)\w)
|
|
1451
|
+
| (?:it|this|that|which|he|she)\s+impacts
|
|
1452
|
+
| (?:they|we|you)\s+impact
|
|
1453
|
+
| (?:(?:tooth|teeth|molars?|bowels?|colon|fractures?|soils?)\s+
|
|
1454
|
+
(?:is|are|was|were)\s+)?
|
|
1455
|
+
impact(?:ed|ing)
|
|
1456
|
+
)\b
|
|
1457
|
+
(?!-)
|
|
1458
|
+
(?!\s+(?:be|been|is|are|was|were|of|on|has|have|had)\b)
|
|
1459
|
+
(?!\s+(?:tooth|teeth|molars?|wisdom|canines?|bowels?|colon|
|
|
1460
|
+
stool|feces|fecal|fractures?|soils?|snow|ice|sediment|
|
|
1461
|
+
gravel|earwax|cerumen)\b)
|
|
1462
|
+
(?!\s+(?:the\s+|a\s+|an\s+)?(?:[\w-]+\s+)?
|
|
1463
|
+
(?:terrain|ground|seabed|seawall|treetops|trees?|
|
|
1464
|
+
grove|thicket|hillside|embankment|berm|
|
|
1465
|
+
escarpment|ridgeline|cliff|
|
|
1466
|
+
runway|taxiway|apron|tarmac|guardrail|
|
|
1467
|
+
fuselage|nacelle|airframe|empennage|rotor|
|
|
1468
|
+
windshield|bulkhead|revetment|abutment|
|
|
1469
|
+
wing|wingtip|stabilizer|landing\s+gear)\b)
|
|
1470
|
+
(?!\s+(?:statements?|assessments?|reports?|studies|study|
|
|
1471
|
+
analys[ie]s|evaluations?|factors?|ratings?|scores?|
|
|
1472
|
+
investing|investors?|funds?|bonds?|craters?|
|
|
1473
|
+
wrench(?:es)?|drivers?|sockets?|printers?|sprinklers?|
|
|
1474
|
+
resistan(?:t|ce)|tested|testing|velocit(?:y|ies)|
|
|
1475
|
+
forces?|energy|load(?:s|ing)?|zones?|points?|players?|
|
|
1476
|
+
sites?|angles?|damage|absorbers?)\b)/ix,
|
|
1477
|
+
skip: [/\A(?:tooth|teeth|molars?|bowels?|colon|fractures?|soils?)\s+
|
|
1478
|
+
(?:is|are|was|were)\s+impacted/ix],
|
|
1479
|
+
message: '"impact" as a verb hides which way something moved and by how much.',
|
|
1480
|
+
suggestion: 'Name the verb: cut, delayed, doubled, broke, raised. Or use "affected".',
|
|
1481
|
+
examples_bad: [
|
|
1482
|
+
"The outage impacted about four thousand accounts.",
|
|
1483
|
+
"Changing the default will impact every downstream job.",
|
|
1484
|
+
"How does this impact the release date?",
|
|
1485
|
+
"Rising rates impacted our hiring plan.",
|
|
1486
|
+
"The migration impacted performance across the board.",
|
|
1487
|
+
"This impacts every downstream job.",
|
|
1488
|
+
"The new policy is impacting our margins."
|
|
1489
|
+
],
|
|
1490
|
+
examples_ok: [
|
|
1491
|
+
"The impact crushed the front bumper.",
|
|
1492
|
+
"The crater marks the point of impact.",
|
|
1493
|
+
"Torque the bolts with an impact wrench.",
|
|
1494
|
+
"The meteor impact left a ring of debris.",
|
|
1495
|
+
"The parachute reduces impact force on landing.",
|
|
1496
|
+
"The county filed an environmental impact statement.",
|
|
1497
|
+
"The fund runs an impact investing strategy.",
|
|
1498
|
+
"The journal reports a five-year impact factor.",
|
|
1499
|
+
"He bought an impact driver for the deck job.",
|
|
1500
|
+
"She is an impact player off the bench.",
|
|
1501
|
+
"She had an impacted wisdom tooth removed.",
|
|
1502
|
+
"The tooth was impacted and had to come out.",
|
|
1503
|
+
"The soil was impacted by years of heavy machinery.",
|
|
1504
|
+
"An impacted fracture heals without displacement.",
|
|
1505
|
+
"Will the impact be permanent?",
|
|
1506
|
+
"Consultants will impact-test the housing next week.",
|
|
1507
|
+
# One object striking another, and the preposition that was reading as
|
|
1508
|
+
# an infinitive. Wording follows NTSB AAR-06/03 and AAR-14/01 and the
|
|
1509
|
+
# Columbia Accident Investigation Board report -- US government works.
|
|
1510
|
+
"The airplane impacted terrain about 300 feet north of the threshold.",
|
|
1511
|
+
"The foam debris impacted the left wing shortly after separation.",
|
|
1512
|
+
"The stick shaker activated 4 seconds prior to impact.",
|
|
1513
|
+
"The chart plots time to impact in seconds."
|
|
1514
|
+
],
|
|
1515
|
+
rationale: "As a verb, 'impact' reports that something changed while withholding the " \
|
|
1516
|
+
"direction and the size. The specific verb -- slowed, doubled, broke -- " \
|
|
1517
|
+
"carries what the sentence is missing, and 'affected' covers the rest. " \
|
|
1518
|
+
"Models reach for it because it sounds consequential at no cost."
|
|
1519
|
+
),
|
|
1520
|
+
Rule.new(
|
|
1521
|
+
id: "impact-noun-vague",
|
|
1522
|
+
category: "puffery",
|
|
1523
|
+
severity: "warning",
|
|
1524
|
+
# Only the puffed shapes: an intensity adjective, or make/have plus an
|
|
1525
|
+
# article. "the impact of X" is left to impact-noun-bare, which sits at
|
|
1526
|
+
# info because it is the standard word in research prose. "positive" and
|
|
1527
|
+
# "negative" stay out of the adjective list on purpose -- they name a
|
|
1528
|
+
# direction, which is more than the intensity words do. "statistically"
|
|
1529
|
+
# is pulled into the match as an optional leading word so skip: can see
|
|
1530
|
+
# it; a statistically significant impact is a finding, not puffery, and
|
|
1531
|
+
# skip: never sees text outside the matched span.
|
|
1532
|
+
pattern: /\b(?:statistically\s+)?
|
|
1533
|
+
(?:
|
|
1534
|
+
(?:significant|real|meaningful|lasting|massive|huge|profound|
|
|
1535
|
+
big|major|tremendous|enormous|outsized|considerable|
|
|
1536
|
+
substantial|genuine|tangible|immense|incredible|
|
|
1537
|
+
remarkable)\s+
|
|
1538
|
+
| (?:mak(?:e|es|ing)|made|hav(?:e|es|ing)|has|had)\s+
|
|
1539
|
+
(?:a|an|real|some)\s+
|
|
1540
|
+
)
|
|
1541
|
+
impacts?\b(?!-)
|
|
1542
|
+
(?!\s+(?:statements?|assessments?|reports?|studies|study|
|
|
1543
|
+
analys[ie]s|evaluations?|factors?|ratings?|scores?|
|
|
1544
|
+
investing|investors?|funds?|bonds?|craters?|
|
|
1545
|
+
wrench(?:es)?|drivers?|sockets?|printers?|sprinklers?|
|
|
1546
|
+
resistan(?:t|ce)|tested|testing|velocit(?:y|ies)|
|
|
1547
|
+
forces?|energy|load(?:s|ing)?|zones?|points?|players?|
|
|
1548
|
+
sites?|angles?|damage|absorbers?)\b)/ix,
|
|
1549
|
+
skip: [/\Astatistically\s/i],
|
|
1550
|
+
message: '"significant/real/big impact" and "make an impact" inflate a claim without stating it.',
|
|
1551
|
+
suggestion: "Say what changed and by how much, or cut the sentence.",
|
|
1552
|
+
examples_bad: [
|
|
1553
|
+
"The change had a significant impact on conversion.",
|
|
1554
|
+
"This will have real impact for the team.",
|
|
1555
|
+
"The rewrite made a big impact on load times.",
|
|
1556
|
+
"The launch had a profound impact on morale.",
|
|
1557
|
+
"We want to make an impact this quarter."
|
|
1558
|
+
],
|
|
1559
|
+
examples_ok: [
|
|
1560
|
+
"The study found a statistically significant impact on mortality.",
|
|
1561
|
+
"The impact crushed the front bumper.",
|
|
1562
|
+
"The helmet failed at high-impact loading.",
|
|
1563
|
+
"Impact-resistant polycarbonate replaced the glass.",
|
|
1564
|
+
"The blast impact zone extended two hundred metres.",
|
|
1565
|
+
"That impact rating exceeds the standard.",
|
|
1566
|
+
"Is this impact reversible?"
|
|
1567
|
+
],
|
|
1568
|
+
rationale: "The adjective does the work the sentence should have done: 'significant' and " \
|
|
1569
|
+
"'real' assert that a change mattered without naming it or sizing it. " \
|
|
1570
|
+
"'Make an impact' is the same move with the noun left bare."
|
|
1571
|
+
),
|
|
1572
|
+
Rule.new(
|
|
1573
|
+
id: "impact-noun-bare",
|
|
1574
|
+
category: "rhetorical-tic",
|
|
1575
|
+
severity: "info",
|
|
1576
|
+
# Two shapes, each needing its own anchor: a measuring verb in front, or
|
|
1577
|
+
# "of" behind. That is what keeps the collision sense clear without a
|
|
1578
|
+
# list of collision verbs -- "the impact crushed the front bumper" has
|
|
1579
|
+
# neither anchor, and "the point of impact" runs the other way round.
|
|
1580
|
+
# The verb stems end in \w*, not \w+: with \w+ the rule misses the bare
|
|
1581
|
+
# imperative "Consider the impact before you merge".
|
|
1582
|
+
pattern: /\b(?:
|
|
1583
|
+
(?:measur|assess|consider|understand|understood|evaluat|
|
|
1584
|
+
gaug|weigh|quantif|track|examin|explor|maximi[sz])\w*\s+
|
|
1585
|
+
(?:the|its|their|our|this|that)\s+impacts?
|
|
1586
|
+
| the\s+impacts?\s+of
|
|
1587
|
+
)\b(?!-)
|
|
1588
|
+
(?!\s+(?:statements?|assessments?|reports?|studies|study|
|
|
1589
|
+
analys[ie]s|evaluations?|factors?|ratings?|scores?|
|
|
1590
|
+
investing|investors?|funds?|bonds?|craters?|
|
|
1591
|
+
wrench(?:es)?|drivers?|sockets?|printers?|sprinklers?|
|
|
1592
|
+
resistan(?:t|ce)|tested|testing|velocit(?:y|ies)|
|
|
1593
|
+
forces?|energy|load(?:s|ing)?|zones?|points?|players?|
|
|
1594
|
+
sites?|angles?|damage|absorbers?)\b)/ix,
|
|
1595
|
+
message: '"the impact of X" defers naming what actually changed.',
|
|
1596
|
+
suggestion: "Name the change itself, or the number that shows it.",
|
|
1597
|
+
examples_bad: [
|
|
1598
|
+
"We measured the impact of the new onboarding flow.",
|
|
1599
|
+
"Consider the impact before you merge.",
|
|
1600
|
+
"The team is still assessing the impact.",
|
|
1601
|
+
"The impact of the change is hard to quantify."
|
|
1602
|
+
],
|
|
1603
|
+
examples_ok: [
|
|
1604
|
+
"The impact crushed the front bumper.",
|
|
1605
|
+
"The crater marks the point of impact.",
|
|
1606
|
+
"The meteor impact left a ring of debris.",
|
|
1607
|
+
"Crews measured the impact crater at dawn.",
|
|
1608
|
+
"The panel reviewed the impact assessment before the vote.",
|
|
1609
|
+
"Torque the bolts with an impact wrench.",
|
|
1610
|
+
"Will the impact be permanent?"
|
|
1611
|
+
],
|
|
1612
|
+
rationale: "This one sits at info because 'the impact of X on Y' is the ordinary word in " \
|
|
1613
|
+
"research and policy writing, not a tell. Elsewhere it postpones the sentence: " \
|
|
1614
|
+
"the writer announces that an effect exists and stops before naming it."
|
|
1615
|
+
),
|
|
597
1616
|
Rule.new(
|
|
598
1617
|
id: "thats-how-x",
|
|
599
1618
|
category: "rhetorical-tic",
|
|
@@ -948,6 +1967,136 @@ module Sloplint
|
|
|
948
1967
|
"promises a confession that the sentence after it rarely delivers. It " \
|
|
949
1968
|
"costs the writer nothing and reads as intimacy the reader did not earn."
|
|
950
1969
|
),
|
|
1970
|
+
Rule.new(
|
|
1971
|
+
id: "honestly",
|
|
1972
|
+
category: "rhetorical-tic",
|
|
1973
|
+
severity: "warning",
|
|
1974
|
+
# The manner adverb, the way "cleanly" is the manner adverb: hung on a
|
|
1975
|
+
# subject that cannot be honest. Two guards, no verb list.
|
|
1976
|
+
#
|
|
1977
|
+
# Terminal position does most of the work. The adverb must close on a
|
|
1978
|
+
# full stop, comma, semicolon or exclamation mark, which is what keeps
|
|
1979
|
+
# the ordinary intensifier out -- "I honestly think", "I honestly don't
|
|
1980
|
+
# know" have the word mid-clause and never match. Sentence-initial
|
|
1981
|
+
# "Honestly," needs no guard of its own: a full stop cannot match \w+,
|
|
1982
|
+
# so the sentence before it can never donate its last word.
|
|
1983
|
+
#
|
|
1984
|
+
# The guard list covers the slot immediately before the adverb, which is
|
|
1985
|
+
# where the discourse marker lives. "And honestly," and "Quite honestly,"
|
|
1986
|
+
# are how people write, and both die there.
|
|
1987
|
+
#
|
|
1988
|
+
# There is no animacy test, because no regex sees animacy. "She answered
|
|
1989
|
+
# honestly." matches, and so does the sentence-final hedge people write
|
|
1990
|
+
# in casual comments ("it's beyond boring honestly"). Both are the known
|
|
1991
|
+
# cost of anchoring on position alone; see rationale for the rate.
|
|
1992
|
+
pattern: /\b(?!(?:and|but|or|nor|yet|so|then|though|although|because|if
|
|
1993
|
+
|quite|more|most|very|really|pretty|fairly|too|also|just
|
|
1994
|
+
|only|now|again|well|i|we|you|he|she|they|it)\b)
|
|
1995
|
+
\w+\b[ \t]+honestly[ \t]*(?=[.,;!])/ix,
|
|
1996
|
+
message: '"Honestly" in the manner slot claims good faith the reader cannot check.',
|
|
1997
|
+
suggestion: "Cut the adverb, or show the thing that makes it honest.",
|
|
1998
|
+
examples_bad: [
|
|
1999
|
+
"These two rows compare honestly.",
|
|
2000
|
+
"The numbers add up honestly.",
|
|
2001
|
+
"The taxonomy maps honestly, which is what the table is for.",
|
|
2002
|
+
"The benchmark reports honestly, which is the whole point.",
|
|
2003
|
+
"It reads honestly; that is what carries the piece."
|
|
2004
|
+
],
|
|
2005
|
+
examples_ok: [
|
|
2006
|
+
# Mid-clause: the ordinary intensifier, and the terminal guard drops it.
|
|
2007
|
+
"I honestly do not know.",
|
|
2008
|
+
"She honestly believes it will work.",
|
|
2009
|
+
# The discourse marker, which the guard list drops.
|
|
2010
|
+
"And honestly, I forgot the deadline.",
|
|
2011
|
+
"Quite honestly, the meeting ran long.",
|
|
2012
|
+
"But honestly, nobody minded."
|
|
2013
|
+
],
|
|
2014
|
+
rationale: "The adverb rates the good faith of the writing instead of showing anything " \
|
|
2015
|
+
"the reader can check, and a table or a benchmark has no good faith to rate. " \
|
|
2016
|
+
"Position is the only anchor, so ordinary English gets caught too: a dialogue " \
|
|
2017
|
+
"tag (\"said Isabel honestly\") and the sentence-final hedge of casual speech " \
|
|
2018
|
+
"(\"it's beyond boring honestly\"). Both are rare -- twice in 3.65M words of " \
|
|
2019
|
+
"public-domain prose, ten times in 6.07M words of pre-2022 Hacker News."
|
|
2020
|
+
),
|
|
2021
|
+
Rule.new(
|
|
2022
|
+
id: "honest-x",
|
|
2023
|
+
category: "rhetorical-tic",
|
|
2024
|
+
severity: "warning",
|
|
2025
|
+
# The noun list is short on purpose, and the words left out are the
|
|
2026
|
+
# point. "An honest answer", "an honest assessment" and "an honest
|
|
2027
|
+
# account" are ordinary English about people -- six hits between them in
|
|
2028
|
+
# 6.07M words of Hacker News, every one of them a person being truthful
|
|
2029
|
+
# rather than a writer praising their own framing -- so they are out.
|
|
2030
|
+
# "man", "mistake", "living", "work" and "opinion" never went in.
|
|
2031
|
+
#
|
|
2032
|
+
# What ships is the set that only ever appears in front of the writer's
|
|
2033
|
+
# own construction, and none of it appears in 9.7M words of either
|
|
2034
|
+
# corpus. "most" and "more" are excluded from the modifier slot so the
|
|
2035
|
+
# superlative belongs to most-honest-x alone.
|
|
2036
|
+
pattern: /\b(?:an|the|one|this|that)[ \t]+(?:(?!most\b|more\b)\w+[ \t]+)?
|
|
2037
|
+
honest[ \t]+(?:\w+[ \t]+)?
|
|
2038
|
+
(?:comparison|framing|formulation|accounting|abstraction|mapping
|
|
2039
|
+
|through-line)\b/ix,
|
|
2040
|
+
message: '"An honest comparison / the honest framing" praises the writing, not the thing.',
|
|
2041
|
+
suggestion: "Cut the adjective and make the comparison; the reader decides if it is honest.",
|
|
2042
|
+
examples_bad: [
|
|
2043
|
+
"That gives us an honest comparison of the two.",
|
|
2044
|
+
"The honest framing is that nobody knew.",
|
|
2045
|
+
"This is an honest accounting of what the delay cost.",
|
|
2046
|
+
"What we want is an honest mapping from one schema to the other."
|
|
2047
|
+
],
|
|
2048
|
+
examples_ok: [
|
|
2049
|
+
# The idioms, which is where "honest" lives in ordinary prose.
|
|
2050
|
+
"He was an honest man.",
|
|
2051
|
+
"It was an honest mistake.",
|
|
2052
|
+
"She made an honest living.",
|
|
2053
|
+
"That is an honest day's work.",
|
|
2054
|
+
# A person being truthful, which is not a claim about the writing.
|
|
2055
|
+
"How could you expect an honest answer to a question like that?",
|
|
2056
|
+
# The superlative belongs to most-honest-x.
|
|
2057
|
+
"The most honest framing is that we guessed."
|
|
2058
|
+
],
|
|
2059
|
+
rationale: "\"Honest\" in front of a framing or a comparison is the writer approving " \
|
|
2060
|
+
"their own work, the same move \"clean\" makes one shelf over. The reader " \
|
|
2061
|
+
"cannot check it and it costs nothing to assert. The narrow noun list is what " \
|
|
2062
|
+
"separates it from the ordinary sense: the shipped pattern flags nothing in " \
|
|
2063
|
+
"3.65M words of public-domain prose or 6.07M words of pre-2022 Hacker News."
|
|
2064
|
+
),
|
|
2065
|
+
Rule.new(
|
|
2066
|
+
id: "most-honest-x",
|
|
2067
|
+
category: "rhetorical-tic",
|
|
2068
|
+
severity: "warning",
|
|
2069
|
+
# The superlative frame carries the tell on its own, so this noun list is
|
|
2070
|
+
# wider than honest-x's -- "the most honest assessment" is self-ranking
|
|
2071
|
+
# in a way "an honest assessment" is not. The list still has to keep out
|
|
2072
|
+
# the ordinary superlative about a person, which is what "the most honest
|
|
2073
|
+
# politician" is, so no human nouns go in. Nothing in either corpus
|
|
2074
|
+
# matches. "way to" gets its own branch, mirroring cleanest-x.
|
|
2075
|
+
pattern: /\bmost[ \t]+honest[ \t]+(?:(?!way\b)\w+[ \t]+){0,2}
|
|
2076
|
+
(?:comparison|framing|formulation|accounting|assessment|appraisal
|
|
2077
|
+
|reading|account|answer|version|summary|take|signal|abstraction
|
|
2078
|
+
|mapping|through-line)\b
|
|
2079
|
+
|\bmost[ \t]+honest[ \t]+way[ \t]+to[ \t]+
|
|
2080
|
+
(?:say|put|frame|state|describe|phrase|think[ \t]+about)\b/ix,
|
|
2081
|
+
message: '"The most honest framing…" ranks your own claim for the reader.',
|
|
2082
|
+
suggestion: "Drop the ranking and make the claim; the reader grades it.",
|
|
2083
|
+
examples_bad: [
|
|
2084
|
+
"The most honest framing is that we guessed.",
|
|
2085
|
+
"That is the most honest way to put it.",
|
|
2086
|
+
"The most honest reading of the data is duller than the headline.",
|
|
2087
|
+
"This is the most honest summary anyone offered."
|
|
2088
|
+
],
|
|
2089
|
+
examples_ok: [
|
|
2090
|
+
# The ordinary superlative, about a person.
|
|
2091
|
+
"He is the most honest person I know.",
|
|
2092
|
+
"She is the most honest politician in the state.",
|
|
2093
|
+
# "way to" outside the speech verbs.
|
|
2094
|
+
"This is the most honest way to earn a living."
|
|
2095
|
+
],
|
|
2096
|
+
rationale: "Self-ranking: the writer tells the reader which of their own claims is the " \
|
|
2097
|
+
"candid one, which is the reader's job. The superlative also implies the rest " \
|
|
2098
|
+
"of the piece was less honest, and the sentence after it never says which part."
|
|
2099
|
+
),
|
|
951
2100
|
Rule.new(
|
|
952
2101
|
id: "genuinely",
|
|
953
2102
|
category: "rhetorical-tic",
|
|
@@ -1002,6 +2151,111 @@ module Sloplint
|
|
|
1002
2151
|
"was never raised. It reads as reassurance addressed to nobody, and the " \
|
|
1003
2152
|
"paragraph is the same without it."
|
|
1004
2153
|
),
|
|
2154
|
+
Rule.new(
|
|
2155
|
+
id: "and-nothing-else",
|
|
2156
|
+
category: "rhetorical-tic",
|
|
2157
|
+
severity: "warning",
|
|
2158
|
+
# This rule is deliberately wide, and the cost is known. There is no verb
|
|
2159
|
+
# list and no imperative requirement, so the only narrowing is structural
|
|
2160
|
+
# -- which means the pattern cannot tell a leaked instruction from the
|
|
2161
|
+
# ordinary English construction it borrows. "Darkness there, and nothing
|
|
2162
|
+
# more!" matches, and so does any sentence built the same way. That is a
|
|
2163
|
+
# recall-over-precision choice, not an oversight; see rationale.
|
|
2164
|
+
#
|
|
2165
|
+
# Three structural guards, each pinned by an examples_ok fixture. The
|
|
2166
|
+
# tail must CLOSE the sentence, so "nothing more than a rumour" and
|
|
2167
|
+
# "nothing else could explain it" never match. A comma must introduce
|
|
2168
|
+
# it, which keeps "there was nothing more to say" out. And the bare
|
|
2169
|
+
# "no more" branch additionally requires "and": a large share of the
|
|
2170
|
+
# ordinary-prose hits are the amount sense ("two years, no more",
|
|
2171
|
+
# "no less, no more"), and requiring the conjunction drops them without
|
|
2172
|
+
# losing the closer the rule is after.
|
|
2173
|
+
#
|
|
2174
|
+
# A closing quote or bracket may sit between the tail and the period so
|
|
2175
|
+
# a quoted sentence still matches. "?" is not accepted as the terminal
|
|
2176
|
+
# mark -- "and nothing more?" asks a question, a different act. Token
|
|
2177
|
+
# gaps cross a hard-wrapped line but never a paragraph break.
|
|
2178
|
+
pattern: /,(?:[ \t]|\r?\n(?![ \t]*\r?\n))+
|
|
2179
|
+
(?:(?:and(?:[ \t]|\r?\n(?![ \t]*\r?\n))+)?
|
|
2180
|
+
nothing(?:[ \t]|\r?\n(?![ \t]*\r?\n))+(?:else|more|further)
|
|
2181
|
+
|and(?:[ \t]|\r?\n(?![ \t]*\r?\n))+no(?:[ \t]|\r?\n(?![ \t]*\r?\n))+more)
|
|
2182
|
+
[ \t]*(?=["'”’)\]]*[.!])/ix,
|
|
2183
|
+
message: "Trailing \"and nothing else\" restates what the sentence already said.",
|
|
2184
|
+
suggestion: "Cut the tail. If the exclusion is the point, say what was excluded.",
|
|
2185
|
+
examples_bad: [
|
|
2186
|
+
"Return the JSON, and nothing else.",
|
|
2187
|
+
"The endpoint gives back the id, and nothing more.",
|
|
2188
|
+
"Reply with the number, nothing else.",
|
|
2189
|
+
"Print the filename, nothing further.",
|
|
2190
|
+
"Hand over the receipt, and no more."
|
|
2191
|
+
],
|
|
2192
|
+
examples_ok: [
|
|
2193
|
+
# The tail does not close the sentence.
|
|
2194
|
+
"It was nothing more than a rumour.",
|
|
2195
|
+
"The delay was down to the weather, and nothing else could explain it.",
|
|
2196
|
+
# No comma introduces it.
|
|
2197
|
+
"There was nothing more to say after that.",
|
|
2198
|
+
"Nothing else matters once the deadline passes.",
|
|
2199
|
+
# The amount sense, which is why bare "no more" needs the conjunction.
|
|
2200
|
+
"The lease runs two years, no more.",
|
|
2201
|
+
"He asked for a fair share, no more.",
|
|
2202
|
+
# A question is a different act.
|
|
2203
|
+
"Is there anything else, or nothing more?"
|
|
2204
|
+
],
|
|
2205
|
+
rationale: "A model told to return one thing and nothing else carries the phrasing into " \
|
|
2206
|
+
"the prose it writes afterwards, where the exclusion adds nothing -- the " \
|
|
2207
|
+
"sentence already named what it covers. The catch is that English has always " \
|
|
2208
|
+
"used this tail, so the rule fires on careful writing too: twice in 1.92M " \
|
|
2209
|
+
"words of public-domain prose, once in 461k words of pre-2022 Hacker News, and " \
|
|
2210
|
+
"on every refrain in \"The Raven\". Expect to dismiss it on fiction and on " \
|
|
2211
|
+
"quoted verse."
|
|
2212
|
+
),
|
|
2213
|
+
Rule.new(
|
|
2214
|
+
id: "nothing-else-frag",
|
|
2215
|
+
category: "rhetorical-tic",
|
|
2216
|
+
severity: "warning",
|
|
2217
|
+
# The same exclusion as its own sentence: "Return the JSON. Nothing
|
|
2218
|
+
# else." Built on the no-x-no-y-frag template -- the fragment must start
|
|
2219
|
+
# at a sentence boundary and the separator is at most two spaces or one
|
|
2220
|
+
# hard-wrapped newline, so a blanked code span under --markdown cannot
|
|
2221
|
+
# weld two distant sentences together, and a paragraph break stops it.
|
|
2222
|
+
#
|
|
2223
|
+
# Two guards past that. The fragment must open with a capital letter,
|
|
2224
|
+
# and the preceding mark cannot be a semicolon: both keep out the
|
|
2225
|
+
# continuation sense
|
|
2226
|
+
# ("keep pulling; nothing more"), which is one clause, not a closer.
|
|
2227
|
+
# And the fragment must be the whole sentence -- "Nothing else mattered."
|
|
2228
|
+
# has a verb and never matches. "No more." is left out entirely; as a
|
|
2229
|
+
# standalone sentence it is ordinary speech, and no-x-no-y-frag already
|
|
2230
|
+
# takes the chained form.
|
|
2231
|
+
pattern: /(?<=[.!?])(?:[ \t]{1,2}|\r?\n(?!\s*\n)[ \t]*)\K
|
|
2232
|
+
(?:And[ \t]+nothing|Nothing)[ \t]+(?:else|more|further)[ \t]*[.!]/x,
|
|
2233
|
+
message: "\"Nothing else.\" as a closer restates what the sentence already said.",
|
|
2234
|
+
suggestion: "Cut the fragment, or fold the exclusion into the sentence before it.",
|
|
2235
|
+
examples_bad: [
|
|
2236
|
+
"Return the JSON. Nothing else.",
|
|
2237
|
+
"Give me the number. Nothing more.",
|
|
2238
|
+
"Answer with the file name. And nothing else.",
|
|
2239
|
+
"Print the total. Nothing further."
|
|
2240
|
+
],
|
|
2241
|
+
examples_ok: [
|
|
2242
|
+
# A verb follows, so the fragment is a sentence about something.
|
|
2243
|
+
"The tests passed. Nothing else mattered that afternoon.",
|
|
2244
|
+
# The continuation sense: one clause, joined by a semicolon.
|
|
2245
|
+
"Keep pulling; nothing more.",
|
|
2246
|
+
# A paragraph break is not a sentence separator.
|
|
2247
|
+
"The kiln runs hot.\n\nNothing more.",
|
|
2248
|
+
# Moby-Dick (public domain): the lower-case continuation the capital
|
|
2249
|
+
# letter guard is there to exclude.
|
|
2250
|
+
"and so on; nothing more."
|
|
2251
|
+
],
|
|
2252
|
+
rationale: "The fragment form of the same leaked instruction, and it reads the same way: " \
|
|
2253
|
+
"a second sentence that only says the first one was complete. It ships at " \
|
|
2254
|
+
"warning rather than info because the capital letter and the whole-sentence " \
|
|
2255
|
+
"requirement keep it off the continuation sense, which is where ordinary prose " \
|
|
2256
|
+
"puts the phrase: one hit in 1.92M words of public-domain prose and one in " \
|
|
2257
|
+
"461k words of pre-2022 Hacker News."
|
|
2258
|
+
),
|
|
1005
2259
|
# ── puffery ───────────────────────────────────────────────────────────
|
|
1006
2260
|
Rule.new(
|
|
1007
2261
|
id: "puffery-words",
|
|
@@ -1239,6 +2493,149 @@ module Sloplint
|
|
|
1239
2493
|
"surface pattern can tell apart from the real thing. Hence info: a flag here " \
|
|
1240
2494
|
"means 'this has the shape', not 'this is slop'."
|
|
1241
2495
|
),
|
|
2496
|
+
Rule.new(
|
|
2497
|
+
id: "isnt-x-its-y",
|
|
2498
|
+
category: "structure",
|
|
2499
|
+
severity: "info",
|
|
2500
|
+
# The corrective with the conjunction dropped: one clause rejects a
|
|
2501
|
+
# description, the next supplies the replacement through a second copula
|
|
2502
|
+
# ("It isn't the tool. It's the habit."). Every copula tense is covered,
|
|
2503
|
+
# and the two clauses may be joined by a period, semicolon, comma, or dash.
|
|
2504
|
+
#
|
|
2505
|
+
# The narrowing sits in the two complement slots, and it is lexical class
|
|
2506
|
+
# rather than vocabulary: neither slot may open with a pronoun, a
|
|
2507
|
+
# possessive, a preposition ("about" excepted, since "not about X, it's
|
|
2508
|
+
# about Y" is the same move), a degree word, or one of the predicate
|
|
2509
|
+
# adjectives that make the second clause a comment instead of a
|
|
2510
|
+
# replacement ("It isn't ready. It's close."). The article is matched
|
|
2511
|
+
# atomically so the guard cannot be skipped by backtracking past it. Those
|
|
2512
|
+
# guards cut a loose version from 19 hits to 4 over 889k words of
|
|
2513
|
+
# 19th-century fiction.
|
|
2514
|
+
#
|
|
2515
|
+
# What survives is the reason for info. An adjective outside the list
|
|
2516
|
+
# still slips through, and some hits are the same shape written by a
|
|
2517
|
+
# person -- "was not the wife; it was the children" is Conan Doyle. A flag
|
|
2518
|
+
# here says the sentence has the frame, not that a model wrote it.
|
|
2519
|
+
pattern: /(?:\b(?:is|are|was|were)\s+not
|
|
2520
|
+
|\b(?:is|are|was|were)n['’]t
|
|
2521
|
+
|\b(?:it|that|this|there)['’]s\s+not
|
|
2522
|
+
|\b(?:they|these|those|we|you)['’]re\s+not)
|
|
2523
|
+
(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2524
|
+
(?>(?:a\s+|an\s+|the\s+)?)
|
|
2525
|
+
(?!so\b|just\b|only\b|merely\b|simply\b|solely\b|even\b|yet\b|quite\b|very\b
|
|
2526
|
+
|too\b|all\b|always\b|never\b|often\b|also\b|enough\b|really\b|actually\b
|
|
2527
|
+
|entirely\b|ready\b|close\b|done\b|easy\b|hard\b|clear\b|simple\b
|
|
2528
|
+
|possible\b|likely\b|true\b|false\b|fine\b|good\b|bad\b|better\b|worse\b
|
|
2529
|
+
|obvious\b|important\b|different\b|not\b|no\b|nothing\b|there\b|here\b
|
|
2530
|
+
|what\b|who\b|how\b|why\b|when\b|where\b|because\b|i\b|me\b|he\b|him\b
|
|
2531
|
+
|she\b|her\b|it\b|we\b|us\b|you\b|they\b|them\b|that\b|this\b|these\b
|
|
2532
|
+
|those\b|his\b|their\b|its\b|my\b|your\b|our\b|in\b|on\b|at\b|of\b|to\b
|
|
2533
|
+
|for\b|from\b|with\b|as\b|by\b|toward\b|towards\b|into\b|over\b|under\b
|
|
2534
|
+
|through\b|against\b|upon\b|within\b|without\b)
|
|
2535
|
+
[\w'’-]+
|
|
2536
|
+
[^.!?;\n]{0,40}?
|
|
2537
|
+
(?:[.!?;]|[ \t]*[—–]|,)
|
|
2538
|
+
(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2539
|
+
(?:it|this|that|these|those|they)
|
|
2540
|
+
(?:['’]s|['’]re|\s+is|\s+are|\s+was|\s+were)
|
|
2541
|
+
(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2542
|
+
(?>(?:a\s+|an\s+|the\s+)?)
|
|
2543
|
+
(?!so\b|just\b|only\b|merely\b|simply\b|solely\b|even\b|yet\b|quite\b|very\b
|
|
2544
|
+
|too\b|all\b|always\b|never\b|often\b|also\b|enough\b|really\b|actually\b
|
|
2545
|
+
|entirely\b|ready\b|close\b|done\b|easy\b|hard\b|clear\b|simple\b
|
|
2546
|
+
|possible\b|likely\b|true\b|false\b|fine\b|good\b|bad\b|better\b|worse\b
|
|
2547
|
+
|obvious\b|important\b|different\b|not\b|no\b|nothing\b|there\b|here\b
|
|
2548
|
+
|what\b|who\b|how\b|why\b|when\b|where\b|because\b|i\b|me\b|he\b|him\b
|
|
2549
|
+
|she\b|her\b|it\b|we\b|us\b|you\b|they\b|them\b|that\b|this\b|these\b
|
|
2550
|
+
|those\b|his\b|their\b|its\b|my\b|your\b|our\b|in\b|on\b|at\b|of\b|to\b
|
|
2551
|
+
|for\b|from\b|with\b|as\b|by\b|toward\b|towards\b|into\b|over\b|under\b
|
|
2552
|
+
|through\b|against\b|upon\b|within\b|without\b)
|
|
2553
|
+
[\w'’-]+/ix,
|
|
2554
|
+
message: '"isn\'t X, it\'s Y" corrects a description nobody offered.',
|
|
2555
|
+
suggestion: "State the second half on its own; drop the rejected one.",
|
|
2556
|
+
examples_bad: [
|
|
2557
|
+
"It isn't the tool. It's the habit.",
|
|
2558
|
+
"This wasn't a setback, it was a setup for the next release.",
|
|
2559
|
+
"The delay wasn't the network. It was the retry loop.",
|
|
2560
|
+
"Those aren't the metrics, they're the vanity numbers.",
|
|
2561
|
+
"This isn't failure. It's iteration.",
|
|
2562
|
+
"They're not customers; they are partners.",
|
|
2563
|
+
"These weren't accidents. They were choices."
|
|
2564
|
+
],
|
|
2565
|
+
examples_ok: [
|
|
2566
|
+
# Predicate adjectives on both sides: a comment, not a replacement.
|
|
2567
|
+
"It isn't ready. It's close.",
|
|
2568
|
+
# Escalation word: that shape belongs to not-just-x-but-y.
|
|
2569
|
+
"It isn't only the cost. It is the delay too.",
|
|
2570
|
+
# The second clause needs a pronoun subject and a copula.
|
|
2571
|
+
"It wasn't the alarm that woke me. The dog did.",
|
|
2572
|
+
# The two clauses must be adjacent.
|
|
2573
|
+
"It isn't the heat. Everyone says so, and they have said so for years. It's the humidity.",
|
|
2574
|
+
# Prepositional complements are ordinary contrast.
|
|
2575
|
+
"The message was not to him; it was to the clerk.",
|
|
2576
|
+
# Pronoun complement.
|
|
2577
|
+
"It was not me who called; it was the neighbour.",
|
|
2578
|
+
# The guard holds after the article, which is matched atomically.
|
|
2579
|
+
"The fire was not out; it was the only light left.",
|
|
2580
|
+
# A paragraph break ends the frame.
|
|
2581
|
+
"The plan was not the problem.\n\nIt was the schedule."
|
|
2582
|
+
],
|
|
2583
|
+
rationale: "The frame rejects a description nobody proposed, then supplies the true " \
|
|
2584
|
+
"one, so the sentence sounds like a correction while correcting nobody. It " \
|
|
2585
|
+
"is 'not A but B' with the conjunction dropped and the second half promoted " \
|
|
2586
|
+
"to its own clause, which is the form models reach for most. Ordinary prose " \
|
|
2587
|
+
"contrasts two things this way too, so the rule ships at info: it reports " \
|
|
2588
|
+
"the shape, not a verdict."
|
|
2589
|
+
),
|
|
2590
|
+
Rule.new(
|
|
2591
|
+
id: "not-by-x-but-by-y",
|
|
2592
|
+
category: "structure",
|
|
2593
|
+
severity: "info",
|
|
2594
|
+
# The corrective built on a repeated preposition: "not by A, but by B",
|
|
2595
|
+
# "not from A but from B". The copula rules above cannot see it because
|
|
2596
|
+
# nothing precedes "not" but the verb or a dash, so the anchor here is
|
|
2597
|
+
# the preposition itself, which must repeat after "but". That repetition
|
|
2598
|
+
# is the whole narrowing: a different preposition on the B side is a
|
|
2599
|
+
# concession or an afterthought, not a correction.
|
|
2600
|
+
#
|
|
2601
|
+
# A is capped at six words and B may not open with a pronoun. Neither
|
|
2602
|
+
# guard removes a false positive -- every corpus hit is the real shape
|
|
2603
|
+
# -- they only keep the match from running across a sentence.
|
|
2604
|
+
pattern: /\bnot(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2605
|
+
(?<prep>by|for|from|with|about|in|on|at|to|of|through|because(?:[ \t]|\r?\n(?!\s*\n))+of|out(?:[ \t]|\r?\n(?!\s*\n))+of)
|
|
2606
|
+
(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2607
|
+
(?:[\w'’-]+(?:[ \t]|\r?\n(?!\s*\n))+){0,5}[\w'’-]+
|
|
2608
|
+
,?(?:[ \t]|\r?\n(?!\s*\n))+but(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2609
|
+
(?:rather(?:[ \t]|\r?\n(?!\s*\n))+)?
|
|
2610
|
+
\k<prep>(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2611
|
+
(?!that\b|this\b|it\b|him\b|her\b|them\b|me\b|us\b|you\b|which\b|whom\b|what\b)
|
|
2612
|
+
(?:a\s+|an\s+|the\s+)?[\w'’-]+/ix,
|
|
2613
|
+
message: '"not by A, but by B" is the corrective frame on a repeated preposition.',
|
|
2614
|
+
suggestion: "Say what it was by; drop the 'not by… but by' frame.",
|
|
2615
|
+
examples_bad: [
|
|
2616
|
+
"We won not by luck but by preparation.",
|
|
2617
|
+
"The gain came not from the model, but from the data.",
|
|
2618
|
+
"Judge it not on the demo but on the deployment.",
|
|
2619
|
+
"Revenue grew this quarter — not by a rounding error, but by a margin that survives any cut of the data."
|
|
2620
|
+
],
|
|
2621
|
+
examples_ok: [
|
|
2622
|
+
# The preposition changes: a contrast, not a correction.
|
|
2623
|
+
"He came not for the money but with an apology.",
|
|
2624
|
+
# B-side pronoun.
|
|
2625
|
+
"She wrote not to him but to them.",
|
|
2626
|
+
# A capped at six words.
|
|
2627
|
+
"The deal closed not in the long slow grind of the seventh week, but in an afternoon.",
|
|
2628
|
+
# "not only" belongs to the escalation rules, and the B preposition here would not repeat anyway.
|
|
2629
|
+
"It was not only for the money.",
|
|
2630
|
+
# A paragraph break ends the frame.
|
|
2631
|
+
"We won not by luck\n\nBut by then it hardly mattered."
|
|
2632
|
+
],
|
|
2633
|
+
rationale: "The 'not A but B' corrective with the copula swapped for a repeated " \
|
|
2634
|
+
"preposition, which is how a model corrects a claim about means or " \
|
|
2635
|
+
"cause ('not by luck, but by design'). People write it too -- Thoreau " \
|
|
2636
|
+
"and Melville both lean on it -- so the rule ships at info. It reports " \
|
|
2637
|
+
"the shape; a human reader decides whether it earned its place."
|
|
2638
|
+
),
|
|
1242
2639
|
Rule.new(
|
|
1243
2640
|
id: "rule-of-three",
|
|
1244
2641
|
category: "structure",
|
|
@@ -1261,6 +2658,401 @@ module Sloplint
|
|
|
1261
2658
|
# then...", elliptical legal "shall... then..."). It was also the most
|
|
1262
2659
|
# expensive rule in the catalog at 65% of scan time on 1MB. Per
|
|
1263
2660
|
# CLAUDE.md: some tells can't be regexes; this was one.
|
|
2661
|
+
Rule.new(
|
|
2662
|
+
id: "everyone-nobody",
|
|
2663
|
+
category: "structure",
|
|
2664
|
+
severity: "warning",
|
|
2665
|
+
# The comma-spliced antithesis on quantifier subjects: "Everyone wants
|
|
2666
|
+
# the dashboard, nobody maintains it." One clause opens on
|
|
2667
|
+
# everyone/everybody, the other on nobody/no one/none or "one N"
|
|
2668
|
+
# ("everyone may pitch, one editor decides"), joined by a bare comma, and
|
|
2669
|
+
# the second clause closes the sentence. The comma splice is the
|
|
2670
|
+
# evidence: with "and" or "but" it is an ordinary sentence, and with a
|
|
2671
|
+
# period it is two. The first subject must open a clause (one or two
|
|
2672
|
+
# spaces after a stop, as elsewhere in the catalog), the two subjects
|
|
2673
|
+
# must differ in polarity, so the anaphoric "nobody is on my side,
|
|
2674
|
+
# nobody takes part with me" is not a hinge, and each clause is at most
|
|
2675
|
+
# eighty characters with no newline, so a hinge never crosses a
|
|
2676
|
+
# paragraph break and the comma gap is at most two spaces.
|
|
2677
|
+
#
|
|
2678
|
+
# The "one N" and "none" subjects need guards, because the comma slot
|
|
2679
|
+
# also holds phrases that are not clauses. "one of them", "one by one",
|
|
2680
|
+
# "one per ticket", and a measure ("one hour before the talk") are out;
|
|
2681
|
+
# so is a capitalised "One" mid-sentence, a proper noun, which the
|
|
2682
|
+
# case-sensitive lookahead catches under /i. "none of which" is a
|
|
2683
|
+
# relative clause and "none louder than" a comparative, and both are
|
|
2684
|
+
# out.
|
|
2685
|
+
pattern: /(?:^|(?<=[.!?:;])[ \t]{1,2})\K
|
|
2686
|
+
(?:(?:everyone|everybody)[ \t]+[^,.;:!?\n]{2,80},[ \t]{1,2}
|
|
2687
|
+
(?:nobody|no[ \t]+one|none(?![ \t]+(?:of|more|less|so)\b|[ \t]+\w+er\b)
|
|
2688
|
+
|one[ \t]+(?!of\b|by\b|per\b|(?:hour|minute|second|day|week|month|year|dollar|cent|mile|foot|inch|pound|kilo|metre|meter)s?\b)(?-i:(?=[a-z]))[\w'’-]+)
|
|
2689
|
+
|(?:nobody|no[ \t]+one)[ \t]+[^,.;:!?\n]{2,80},[ \t]{1,2}
|
|
2690
|
+
(?:everyone|everybody|one[ \t]+(?!of\b|by\b|per\b|(?:hour|minute|second|day|week|month|year|dollar|cent|mile|foot|inch|pound|kilo|metre|meter)s?\b)(?-i:(?=[a-z]))[\w'’-]+))
|
|
2691
|
+
[ \t]+[^,.;:!?\n]{2,80}[.;!?]/ix,
|
|
2692
|
+
message: '"Everyone X, nobody Y." is the AI antithesis hinge.',
|
|
2693
|
+
suggestion: "Say which one is the problem, in its own sentence.",
|
|
2694
|
+
examples_bad: [
|
|
2695
|
+
"Everyone wants the dashboard, nobody maintains it.",
|
|
2696
|
+
"Run it like a newsroom desk: everyone may pitch, one editor decides.",
|
|
2697
|
+
"Nobody owns the file, everyone edits it.",
|
|
2698
|
+
"Everyone wanted the job, none applied.",
|
|
2699
|
+
# Two spaces after the stop still open a clause.
|
|
2700
|
+
"Ship it. Everyone wants the dashboard, nobody maintains it.",
|
|
2701
|
+
# Clauses may run to eighty characters.
|
|
2702
|
+
"Everyone talks about observability in the abstract, nobody wants to own the pager rotation."
|
|
2703
|
+
],
|
|
2704
|
+
examples_ok: [
|
|
2705
|
+
# A conjunction makes it a sentence, not a hinge.
|
|
2706
|
+
"Everyone left early, and nobody noticed.",
|
|
2707
|
+
# Pride and Prejudice (Austen, public domain): the same subject twice is
|
|
2708
|
+
# anaphora, not antithesis.
|
|
2709
|
+
"Nobody is on my side, nobody takes part with me;",
|
|
2710
|
+
# The second clause must close the sentence.
|
|
2711
|
+
"Everyone who came, nobody excepted, signed the book.",
|
|
2712
|
+
# A period is two sentences.
|
|
2713
|
+
"Everyone wants the dashboard. Nobody maintains it.",
|
|
2714
|
+
# "one of", "one by one", "one per", a measure, and a proper noun are
|
|
2715
|
+
# not a second subject.
|
|
2716
|
+
"Everyone signed up, one of them dropped out later.",
|
|
2717
|
+
"Nobody moved for a moment, one by one they stood up.",
|
|
2718
|
+
"Everybody got a ticket, one per person at the gate.",
|
|
2719
|
+
"Everyone arrived by noon, one hour before the talk.",
|
|
2720
|
+
"Nobody stirred in the hall, One Direction played on the radio.",
|
|
2721
|
+
# A relative clause and a comparative are not a second subject.
|
|
2722
|
+
"Everyone brought a dish, none of which we actually ate.",
|
|
2723
|
+
"Everyone in the room laughed, none louder than the author himself.",
|
|
2724
|
+
# A clause over eighty characters is a sentence of its own.
|
|
2725
|
+
"Everyone who has ever tried to keep a dashboard alive through two reorganisations and a migration knows the cost, nobody maintains it.",
|
|
2726
|
+
# A paragraph break is not a comma.
|
|
2727
|
+
"Everyone wants the dashboard,\n\nnobody maintains it."
|
|
2728
|
+
],
|
|
2729
|
+
rationale: "The everyone/nobody hinge states a whole diagnosis as a balanced pair of " \
|
|
2730
|
+
"clauses, and the balance is what makes it sound settled. Models reach for " \
|
|
2731
|
+
"it to close a setup; careful writers join the clauses with a conjunction " \
|
|
2732
|
+
"or give the problem its own sentence."
|
|
2733
|
+
),
|
|
2734
|
+
Rule.new(
|
|
2735
|
+
id: "np-fragment-and",
|
|
2736
|
+
category: "structure",
|
|
2737
|
+
severity: "info",
|
|
2738
|
+
# A whole sentence that is two noun phrases and an "and": "A named owner
|
|
2739
|
+
# and a quarterly review." It is the fix half of a model's
|
|
2740
|
+
# problem-then-fix pair, with the verb left for the reader to supply.
|
|
2741
|
+
# The sentence must open on A/An/One at a sentence start or after a
|
|
2742
|
+
# list marker (the pair's usual habitat is a bulleted list), the second
|
|
2743
|
+
# phrase must open on a/an/one, each phrase is one to three words, and
|
|
2744
|
+
# nothing else may be in the sentence. No auxiliary or modal may appear
|
|
2745
|
+
# anywhere in it, contractions included, so "A man and a woman were
|
|
2746
|
+
# there." and "A man and a woman aren't here." never match.
|
|
2747
|
+
#
|
|
2748
|
+
# Ships at info, and this is why: a lexical verb is invisible to the
|
|
2749
|
+
# pattern, so "A car and a truck collided." has the same shape and
|
|
2750
|
+
# flags. The corpora say that sentence is rare (one hit in 1.25M words
|
|
2751
|
+
# of public-domain prose, most of it narrative), but it is a complete
|
|
2752
|
+
# sentence, and nothing a regex can see separates it from the fragment.
|
|
2753
|
+
pattern: /(?:^|(?<=[.!?])[ \t]{1,2})(?:[-*+•][ \t]+|\d+[.)][ \t]+)?\K(?:A|An|One)(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2754
|
+
(?:(?!(?:(?:is|are|was|were|be|been|being|am|has|have|had|having|does|do|did|can|could|will|would|shall|should|must|may|might|ought|ain)(?![\w'’-])|\w+n['’]t(?![\w'’-])))[\w'’-]+(?:[ \t]|\r?\n(?!\s*\n))+){0,2}(?!(?:(?:is|are|was|were|be|been|being|am|has|have|had|having|does|do|did|can|could|will|would|shall|should|must|may|might|ought|ain)(?![\w'’-])|\w+n['’]t(?![\w'’-])))[\w'’-]+,?(?:[ \t]|\r?\n(?!\s*\n))+and(?:[ \t]|\r?\n(?!\s*\n))+(?:a|an|one)(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2755
|
+
(?:(?!(?:(?:is|are|was|were|be|been|being|am|has|have|had|having|does|do|did|can|could|will|would|shall|should|must|may|might|ought|ain)(?![\w'’-])|\w+n['’]t(?![\w'’-])))[\w'’-]+(?:(?:[ \t]|\r?\n(?!\s*\n))+|(?=\.))){1,3}\.(?=\s|\z)/x,
|
|
2756
|
+
message: '"A X and a Y." as a whole sentence is an AI fragment.',
|
|
2757
|
+
suggestion: "Give the sentence a verb, or fold it into the one before.",
|
|
2758
|
+
examples_bad: [
|
|
2759
|
+
"Nobody checked it after launch. A named owner and a quarterly review.",
|
|
2760
|
+
"One merger and a version-controlled folder.",
|
|
2761
|
+
"An owner and a deadline.",
|
|
2762
|
+
# The pair's usual habitat.
|
|
2763
|
+
"- A named owner and a quarterly review.",
|
|
2764
|
+
"1. A named owner and a quarterly review.",
|
|
2765
|
+
# A comma before "and" is still the pair.
|
|
2766
|
+
"A named owner, and a quarterly review."
|
|
2767
|
+
],
|
|
2768
|
+
examples_ok: [
|
|
2769
|
+
"A man and a woman were waiting at the door.",
|
|
2770
|
+
"A dog and a cat can share a house.",
|
|
2771
|
+
# Contractions and the rarer auxiliaries are auxiliaries too.
|
|
2772
|
+
"A man and a woman aren't here.",
|
|
2773
|
+
"A boy and a girl shall meet.",
|
|
2774
|
+
"A boy and a girl ought to know.",
|
|
2775
|
+
# Not at a sentence start.
|
|
2776
|
+
"We hired a designer and an engineer.",
|
|
2777
|
+
# More than three words on a side is a clause, not a label.
|
|
2778
|
+
"A long walk down to the river and a swim before breakfast.",
|
|
2779
|
+
# Only "and" between two phrases; a list is not the pair.
|
|
2780
|
+
"A hammer, a saw, and a level."
|
|
2781
|
+
],
|
|
2782
|
+
rationale: "Two noun phrases and an 'and', standing as a sentence, is how a model " \
|
|
2783
|
+
"hands over a fix without committing to a verb: the reader supplies " \
|
|
2784
|
+
"'you need' or 'add'. A short sentence with a plain verb ('A car and a " \
|
|
2785
|
+
"truck collided.') has the same shape and cannot be told apart, which is " \
|
|
2786
|
+
"why this is a question rather than a verdict; a draft full of them, " \
|
|
2787
|
+
"especially as list items, should be read as a warning."
|
|
2788
|
+
),
|
|
2789
|
+
Rule.new(
|
|
2790
|
+
id: "quip-question",
|
|
2791
|
+
category: "structure",
|
|
2792
|
+
severity: "info",
|
|
2793
|
+
# The verbless question that opens a pitch: "No invite?", "New to the
|
|
2794
|
+
# tool?", "Still stuck?", "Ready to start?". It must start a sentence,
|
|
2795
|
+
# open on one of a short list of words, run one to four more words, and
|
|
2796
|
+
# end on the question mark, with no auxiliary or contraction anywhere,
|
|
2797
|
+
# so a real question ("Not what you expected?" flags, "Is it new?" does
|
|
2798
|
+
# not) stays out. "Need" and "Want" are not on the list: "Need help?"
|
|
2799
|
+
# is a question with its verb elided, not a verbless one. Gaps may
|
|
2800
|
+
# cross a hard-wrapped newline but never a paragraph break. Ships at
|
|
2801
|
+
# info: dialogue and forum replies ask the same shape of a person, and
|
|
2802
|
+
# one is a question, not a verdict.
|
|
2803
|
+
pattern: /(?:^|(?<=[.!?])[ \t]{1,2})\K(?:No|New|Still|Not|Already|Ready|Curious|Unsure|Stuck|Tired|Confused|Worried)(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2804
|
+
(?:(?!(?:is|are|was|were|has|have|had|do|does|did|can|could|will|would|should|must|may|might|am|[\w]+n['’]t)\b)[\w'’-]+(?:[ \t]|\r?\n(?!\s*\n))+){0,3}(?!(?:is|are|was|were|has|have|had|do|does|did|can|could|will|would|should|must|may|might|am|[\w]+n['’]t)\b)[\w'’-]+[ \t]*\?/x,
|
|
2805
|
+
message: "A verbless opening question is a marketing-copy tell.",
|
|
2806
|
+
suggestion: "Ask it as a sentence, or state what follows without the setup.",
|
|
2807
|
+
examples_bad: [
|
|
2808
|
+
"No invite? Start a team of your own.",
|
|
2809
|
+
"New to the tool? Read the guide first.",
|
|
2810
|
+
"Still stuck after that? Ask in the channel.",
|
|
2811
|
+
"Ready to start?",
|
|
2812
|
+
"Not what you expected?",
|
|
2813
|
+
# A hard-wrapped quip survives one newline.
|
|
2814
|
+
"Still\nstuck? Ask in the channel."
|
|
2815
|
+
],
|
|
2816
|
+
examples_ok: [
|
|
2817
|
+
"Is it new?",
|
|
2818
|
+
"No, they aren't?",
|
|
2819
|
+
"Not sure if it will work?",
|
|
2820
|
+
# Not at a sentence start.
|
|
2821
|
+
"She asked, Still unsure?",
|
|
2822
|
+
# An elided verb is a question, not a quip.
|
|
2823
|
+
"Need help?",
|
|
2824
|
+
"Want the short version?",
|
|
2825
|
+
# Too long to be a quip.
|
|
2826
|
+
"Still waiting for the last batch of reviews from the other team?",
|
|
2827
|
+
# A paragraph break ends it, and the question mark stays on the line.
|
|
2828
|
+
"Not sure\n\nwhat happened here?",
|
|
2829
|
+
"Not sure what happened\n?"
|
|
2830
|
+
],
|
|
2831
|
+
rationale: "A one-line question with no verb is the hook of a landing page, and a " \
|
|
2832
|
+
"model reaches for it to open any section. People ask the same shape in " \
|
|
2833
|
+
"replies, of a product or a person, so one flag is a question; several " \
|
|
2834
|
+
"in a draft should be read as a warning."
|
|
2835
|
+
),
|
|
2836
|
+
Rule.new(
|
|
2837
|
+
id: "mic-drop-closer",
|
|
2838
|
+
category: "structure",
|
|
2839
|
+
severity: "info",
|
|
2840
|
+
# The kicker: a sentence of at least sixty characters, then a closer of
|
|
2841
|
+
# two to eight words that ends the paragraph and opens on a quantifier
|
|
2842
|
+
# ("Nothing here needs a new login.", "Most teams end up
|
|
2843
|
+
# with two.", "Then find out whether it paid off."). The long sentence
|
|
2844
|
+
# must start a sentence itself, so the scan is linear, and \K drops it
|
|
2845
|
+
# from the match so the note points at the closer. The closer must be
|
|
2846
|
+
# the last thing in the paragraph: a blank line or the end of the text
|
|
2847
|
+
# must follow, so the same sentence mid-paragraph, or a bullet followed
|
|
2848
|
+
# by another bullet, is just a sentence. Both sentences may cross a
|
|
2849
|
+
# hard-wrapped newline. Whitespace inside the long sentence is capped at
|
|
2850
|
+
# two spaces a run, so a URL or code span blanked by --markdown cannot
|
|
2851
|
+
# manufacture the sixty characters. The gap between the two sentences
|
|
2852
|
+
# is one or two spaces, and the closer needs at least two words.
|
|
2853
|
+
#
|
|
2854
|
+
# "That", "This" and "It" were in the opener list and are out. They are
|
|
2855
|
+
# not quantifiers, and procedural writing ends a step with one as a
|
|
2856
|
+
# matter of course: every hit in 2.25M words of engineering prose was a
|
|
2857
|
+
# bare demonstrative closing a paragraph after a long sentence -- "This
|
|
2858
|
+
# completes the roughing operations.", "This is the normal running
|
|
2859
|
+
# position." The quantifiers carry the tell; the demonstratives are
|
|
2860
|
+
# ordinary, and no examples_bad used one.
|
|
2861
|
+
#
|
|
2862
|
+
# Ships at info, and the rationale says why: people end paragraphs this
|
|
2863
|
+
# way too, at about 150 per million words on Hacker News. One is
|
|
2864
|
+
# nothing. A draft where most paragraphs end this way is the tell, and
|
|
2865
|
+
# an agent that sees the flag repeat should read the family as a
|
|
2866
|
+
# warning.
|
|
2867
|
+
# The long-sentence prefix is wrapped in an atomic group. Its branches
|
|
2868
|
+
# cannot match [.!?], so the greedy run always ends at the first
|
|
2869
|
+
# sentence-ending punctuation or paragraph break and no shorter partition
|
|
2870
|
+
# can ever satisfy the [.!?] that follows -- backtracking into it only
|
|
2871
|
+
# ever fails. Without (?>...) it fails slowly: PDF-extracted prose with
|
|
2872
|
+
# long unpunctuated stretches (the Columbia report has an 864-character
|
|
2873
|
+
# one) sent this into catastrophic backtracking, 62 seconds for a 2 KB
|
|
2874
|
+
# window and no completion on the 1.1 MB document.
|
|
2875
|
+
pattern: /(?:^|(?<=[.!?])[ \t]{1,2})(?>(?:[^.!?\n\s]|(?<![ \t])[ \t]{1,2}(?![ \t])|\r?\n(?!\s*\n)[ \t]*){60,})[.!?][ \t]{1,2}\K
|
|
2876
|
+
(?:Nothing|Most|None|Everything|Everyone|Nobody|Then|Neither|Both)
|
|
2877
|
+
(?:,?(?:[ \t]|\r?\n(?!\s*\n))+[\w'’-]+){1,7}[.!?](?=[ \t]*(?:\r?\n[ \t]*(?:\r?\n|\z)|\z))/x,
|
|
2878
|
+
message: "A short quantifier-led closer after a long sentence is the AI kicker.",
|
|
2879
|
+
suggestion: "Cut the closer, or move the claim to the front of the paragraph.",
|
|
2880
|
+
examples_bad: [
|
|
2881
|
+
"Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. Nothing here needs a new login.",
|
|
2882
|
+
"Keep a private notebook for the drafts where taste matters, and a shared one for the work that passes between desks. Most teams end up with two.",
|
|
2883
|
+
"Ship the shared notebook to a team that has agreed on the owner, the folder, and the first task it will hold. Then find out whether it paid off.\n\nNext week: the audit.",
|
|
2884
|
+
# Both sentences may be hard-wrapped.
|
|
2885
|
+
"Each step can be done in the app, pasted into whichever\nassistant the company allows, or run the way the team\nalready works. Nothing here\nneeds a new login.\n"
|
|
2886
|
+
],
|
|
2887
|
+
examples_ok: [
|
|
2888
|
+
# No long sentence before it.
|
|
2889
|
+
"Nothing here needs a new login.",
|
|
2890
|
+
# Not the end of the paragraph, wrapped or not.
|
|
2891
|
+
"Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. Nothing here needs a new login. The prompts are in the appendix.",
|
|
2892
|
+
"Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. Nothing here needs a new login.\nThe prompts are in the appendix.",
|
|
2893
|
+
# A closer that does not open on the list.
|
|
2894
|
+
"Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. We kept the prompts short.",
|
|
2895
|
+
# Too long to be a kicker.
|
|
2896
|
+
"Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. Most of the teams we spoke to ended up using a mix of two of them.",
|
|
2897
|
+
# Too short to be a kicker.
|
|
2898
|
+
"Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. Nothing.",
|
|
2899
|
+
# More than two spaces is not a sentence gap.
|
|
2900
|
+
"Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. Nothing here needs a new login.",
|
|
2901
|
+
# A bullet followed by another bullet is not a paragraph end.
|
|
2902
|
+
"- Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. Nothing here breaks.\n- We moved the deploy script into the repo so the on-call rota could run it. Nothing here breaks.\n- The rest is in the appendix and needs no change from anyone on the team.\n",
|
|
2903
|
+
# Blanked text cannot make the long sentence.
|
|
2904
|
+
"See here. Nothing here breaks.\n",
|
|
2905
|
+
# A bare demonstrative closing a step is how procedural writing ends
|
|
2906
|
+
# a paragraph. Wording follows Turning and Boring (1919) and
|
|
2907
|
+
# Aviation Engines (1917), both public domain by date.
|
|
2908
|
+
"The cutting tools are set to the dimensions required for the finished work, and the stops are locked. This completes the roughing operations.",
|
|
2909
|
+
"The magneto is protected from oil and grit by a cover that is easy to remove for service. This means prolonged life for the magneto."
|
|
2910
|
+
],
|
|
2911
|
+
rationale: "A short sentence after a long one borrows emphasis from the contrast, " \
|
|
2912
|
+
"and a model spends that emphasis at the end of nearly every paragraph, " \
|
|
2913
|
+
"restating the point it has just made. People write the shape too, at " \
|
|
2914
|
+
"about 150 per million words, so one flag means nothing; a draft " \
|
|
2915
|
+
"where the flag repeats paragraph after paragraph should be read as a " \
|
|
2916
|
+
"warning, and the fix is usually to delete the closer outright."
|
|
2917
|
+
),
|
|
2918
|
+
Rule.new(
|
|
2919
|
+
id: "short-run",
|
|
2920
|
+
category: "structure",
|
|
2921
|
+
severity: "info",
|
|
2922
|
+
# Three consecutive sentences of thirty characters or fewer, each
|
|
2923
|
+
# opening on a letter and closing on a full stop, with no quotation
|
|
2924
|
+
# mark or digit in any of them: "Nobody used it. A named owner. Then a
|
|
2925
|
+
# review." The run must start at a real boundary (the text, a blank
|
|
2926
|
+
# line, a line ending on a stop, or a stop and one or two spaces), so
|
|
2927
|
+
# the short tail of a hard-wrapped long sentence never opens one; it
|
|
2928
|
+
# may cross a hard-wrapped newline but not a paragraph break, and the
|
|
2929
|
+
# indent after a newline is at most four spaces, so a URL or code span
|
|
2930
|
+
# blanked by --markdown cannot weld two sentences. A list marker may
|
|
2931
|
+
# open the run, but consecutive bullets are a list, not staccato.
|
|
2932
|
+
#
|
|
2933
|
+
# Dialogue is excluded by the boundary: a closing quote after the stop
|
|
2934
|
+
# is not a space. A quotation mark inside a sentence is excluded by the
|
|
2935
|
+
# class. A sentence with a digit in it is data, and a run holding a
|
|
2936
|
+
# multi-letter abbreviation ("Prof.", "Dept.") is dropped, since the
|
|
2937
|
+
# abbreviation is not a sentence end.
|
|
2938
|
+
#
|
|
2939
|
+
# Three narrowings after 2.25M words of technical prose, all of them the
|
|
2940
|
+
# same mistake: reading document furniture as sentences.
|
|
2941
|
+
#
|
|
2942
|
+
# A "sentence" ending on a lone letter is a list label, not a sentence.
|
|
2943
|
+
# The guard used to cover capitals only, for initials ("Alan W."), so
|
|
2944
|
+
# lettered enumeration walked through it -- "Acronym e. OpNom f. Hazard
|
|
2945
|
+
# System record number g." is one form field, read as three sentences.
|
|
2946
|
+
# No English sentence ends on a bare letter, either case, so the guard
|
|
2947
|
+
# now covers both.
|
|
2948
|
+
#
|
|
2949
|
+
# Every sentence in the run must hold a space. A one-word "sentence" is
|
|
2950
|
+
# an abbreviation, and a citation line is nothing else: "Natl. Inst.
|
|
2951
|
+
# Stand. Technol." was a clean three-sentence run under the old shape.
|
|
2952
|
+
#
|
|
2953
|
+
# Every sentence opens on a capital. Lowercase-initial runs are speech
|
|
2954
|
+
# transcribed, not prose written: a cockpit voice recorder transcript
|
|
2955
|
+
# ("we all get gas. we go to divert to Albany. we pick up a load.") has
|
|
2956
|
+
# the staccato shape exactly, and an accident report carries pages of
|
|
2957
|
+
# it. A sentence in edited prose starts with a capital.
|
|
2958
|
+
#
|
|
2959
|
+
# And the run may not step over a list marker -- see MARKER below. Questions and exclamations are
|
|
2960
|
+
# left out because a run of them is a different device.
|
|
2961
|
+
#
|
|
2962
|
+
# Ships at info. A staccato run is a device people use on purpose, at
|
|
2963
|
+
# about thirty per million words on Hacker News, so one flag is a
|
|
2964
|
+
# question. A draft that keeps doing it is the tell, and an agent that
|
|
2965
|
+
# sees the flag repeat should read the family as a warning.
|
|
2966
|
+
# MARKER is the list furniture this rule has to see in order to ignore
|
|
2967
|
+
# it: a bullet glyph, the literal "o" that plain-text technical
|
|
2968
|
+
# documents use as one, a numbered item, and a lettered item. It opens a
|
|
2969
|
+
# run (a bullet may hold staccato) but may never sit *inside* one --
|
|
2970
|
+
# three bullets in a row are a list, which is what the comment above
|
|
2971
|
+
# always intended and the pattern did not enforce.
|
|
2972
|
+
pattern: /(?:\A|(?<=\n\n)|(?<=[.!?])[ \t]{0,2}\r?\n|(?<=[.!?])[ \t]{1,2})[ \t]{0,4}
|
|
2973
|
+
(?:[-*+•][ \t]+|o[ \t]+(?=[A-Z])|\d+[.)][ \t]+|[A-Za-z][.)][ \t]+(?=[A-Z]))?\K
|
|
2974
|
+
(?:[A-Za-z][^.!?\n"“”0-9]{3,29}(?<![^A-Za-z'’][A-Za-z])\.
|
|
2975
|
+
(?:[ \t]{1,2}|\r?\n(?!\s*\n)[ \t]{0,4})
|
|
2976
|
+
(?![-*+•][ \t]|o[ \t]+[A-Z]|\d+[.)][ \t]|[A-Za-z][.)][ \t]+[A-Z])){2}
|
|
2977
|
+
[A-Za-z][^.!?\n"“”0-9]{3,29}(?<![^A-Za-z'’][A-Za-z])\.(?=\s|\z)/x,
|
|
2978
|
+
message: "A run of three short sentences reads as AI staccato.",
|
|
2979
|
+
suggestion: "Join two of them, or give one of them a subordinate clause.",
|
|
2980
|
+
# Two whole-run exclusions, because each is a property of the run and
|
|
2981
|
+
# not of any one sentence in it. Three single-word "sentences" in a row
|
|
2982
|
+
# is a citation line ("Natl. Inst. Stand. Technol."), never staccato --
|
|
2983
|
+
# one single-word sentence is the archetypal kicker and stays. Three
|
|
2984
|
+
# lowercase-led sentences in a row is transcribed speech ("we all get
|
|
2985
|
+
# gas. we go to divert to Albany."); a run that reaches a capital
|
|
2986
|
+
# anywhere is prose, so identifier-initial writing ("npm was slow. git
|
|
2987
|
+
# blame helped. We moved on.") is untouched.
|
|
2988
|
+
skip: [
|
|
2989
|
+
/\b(?:Corp|Prof|Dept|Sept|Univ|Assn|approx|misc|cont|Ave|Blvd|Fig|Est|Inc|Ltd|vol|etc|Mrs|Mr|Ms|Dr|St|Jr|Sr|No)\./,
|
|
2990
|
+
/\A\S+\.[ \t\r\n]+\S+\.[ \t\r\n]+\S+\.\z/,
|
|
2991
|
+
/\A[a-z][^.!?]*\.\s+[a-z][^.!?]*\.\s+[a-z][^.!?]*\.\z/
|
|
2992
|
+
],
|
|
2993
|
+
examples_bad: [
|
|
2994
|
+
"Nobody used it. A named owner. Then a review.",
|
|
2995
|
+
"The draft sat on one desk. Nobody else saw it. So it never shipped.",
|
|
2996
|
+
"Drafts only at first. Widen after a month. Two people sign off.",
|
|
2997
|
+
# A hard-wrapped run survives one newline.
|
|
2998
|
+
"Nobody used it. A named owner.\nThen a review.",
|
|
2999
|
+
# A bullet may hold a run.
|
|
3000
|
+
"- Nobody used it. A named owner. Then a review.",
|
|
3001
|
+
# An acronym ends a sentence; only a lone initial does not.
|
|
3002
|
+
"Nobody used it. We shipped it to QA. Then a review.",
|
|
3003
|
+
# A one-word sentence is the kicker, not an abbreviation, so only a
|
|
3004
|
+
# run made entirely of them is excluded.
|
|
3005
|
+
"The fix landed. Agreed. We moved on.",
|
|
3006
|
+
# A sentence may end on a possessive; that is not a bare letter.
|
|
3007
|
+
"We shipped a fix. The bug was Ana's. Nobody cared.",
|
|
3008
|
+
# Identifier-initial prose is the register this tool is aimed at, so a
|
|
3009
|
+
# run only counts as transcript when nothing in it reaches a capital.
|
|
3010
|
+
"git blame helped. npm was slow. We moved on."
|
|
3011
|
+
],
|
|
3012
|
+
examples_ok: [
|
|
3013
|
+
"Nobody used it. A named owner and a quarterly review that the whole team can see.",
|
|
3014
|
+
# Dialogue.
|
|
3015
|
+
"\"Go now.\" \"I will.\" \"Then go.\"",
|
|
3016
|
+
# A quotation mark inside a sentence.
|
|
3017
|
+
"He said \"go\" today. Then a review. So it ended.",
|
|
3018
|
+
# A paragraph break ends the run.
|
|
3019
|
+
"Nobody used it. A named owner.\n\nThen a review.",
|
|
3020
|
+
# Questions and exclamations are a different device.
|
|
3021
|
+
"Who owns it? Nobody. Who checks it? Nobody.",
|
|
3022
|
+
# Two short sentences are a pair.
|
|
3023
|
+
"Ship it. Then find out if it was worth the effort and the wait.",
|
|
3024
|
+
# One sentence over thirty characters breaks the run.
|
|
3025
|
+
"Nobody used it. A named owner reviewed the draft again. Then a review.",
|
|
3026
|
+
# Numbers are data, and an initial is not a sentence end.
|
|
3027
|
+
"Hold cash. Expected value: 1000. Expected tax: none.",
|
|
3028
|
+
"Alan W. Prosser loves officer Kane. Nobody else does.",
|
|
3029
|
+
# An abbreviation is not a sentence end.
|
|
3030
|
+
"She wrote to Prof. Ellis at the Dept. Nobody replied to her.",
|
|
3031
|
+
# The tail of a hard-wrapped long sentence does not open a run.
|
|
3032
|
+
"The build had been red since Tuesday and nobody could say quite why, so we\nbisected it. Then we found the flake. It was a clock skew.",
|
|
3033
|
+
# Blanked text after a newline cannot weld two sentences.
|
|
3034
|
+
"Nobody used it. A named owner.\n Then a review.",
|
|
3035
|
+
# Consecutive bullets are a list.
|
|
3036
|
+
"- Fast setup.\n- No config.\n- Free tier.\n",
|
|
3037
|
+
# Document furniture, not staccato. Wording follows a NASA hazard
|
|
3038
|
+
# report form, a NIST publication citation line and a NIST control
|
|
3039
|
+
# enumeration -- US government works.
|
|
3040
|
+
"Acronym e. OpNom f. Hazard System record number g.",
|
|
3041
|
+
"Phone q. Fax r. e-mail s.",
|
|
3042
|
+
"Natl. Inst. Stand. Technol.",
|
|
3043
|
+
"What type of event occurred; b. When the event occurred; c. Where the event occurred; d.",
|
|
3044
|
+
"o Shop was not a clean area. o Lighting was not adequate. o Space was limited.",
|
|
3045
|
+
# Consecutive bullets are a list, whatever each one says.
|
|
3046
|
+
"- Nobody used it here.\n- A named owner was set.\n- Then a review happened.",
|
|
3047
|
+
# Transcribed speech has the staccato shape and is not prose.
|
|
3048
|
+
# Wording follows an NTSB cockpit voice recorder transcript.
|
|
3049
|
+
"we all get gas. we go to divert to Albany. we pick up a load."
|
|
3050
|
+
],
|
|
3051
|
+
rationale: "Short sentences in a row borrow force from their rhythm, and a model " \
|
|
3052
|
+
"falls into the rhythm whenever it wants to sound decisive. People do it " \
|
|
3053
|
+
"on purpose, about thirty times per million words, so one flag is a " \
|
|
3054
|
+
"question; a draft where the flag repeats should be read as a warning."
|
|
3055
|
+
),
|
|
1264
3056
|
Rule.new(
|
|
1265
3057
|
id: "em-dash",
|
|
1266
3058
|
category: "structure",
|