sloplint 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +329 -1
- data/README.md +38 -10
- data/docs/SPEC.md +48 -10
- data/exe/sloplint +13 -0
- data/lib/sloplint/cli.rb +45 -12
- data/lib/sloplint/engine.rb +7 -8
- data/lib/sloplint/rules.rb +2273 -22
- data/lib/sloplint/version.rb +1 -1
- metadata +4 -4
- data/bin/sloplint +0 -5
data/lib/sloplint/rules.rb
CHANGED
|
@@ -16,6 +16,72 @@ 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
|
+
|
|
56
|
+
# Nouns that only ever name the writer's own construction -- never a
|
|
57
|
+
# concrete object, a person, or an idiom -- and that all four of
|
|
58
|
+
# clean-x, cleanest-x, honest-x and most-honest-x already matched before
|
|
59
|
+
# this constant existed. "comparison" and "through-line" read the same way
|
|
60
|
+
# but are each missing from one rule's original list (clean-x never had
|
|
61
|
+
# "comparison", cleanest-x never had "through-line"), so both stay out of
|
|
62
|
+
# this constant and inline in the three rules that carry them -- putting
|
|
63
|
+
# either one here would add a match the fourth rule never had.
|
|
64
|
+
WRITERS_OWN_CONSTRUCTION_NOUNS = "framing|formulation|mapping|abstraction"
|
|
65
|
+
|
|
66
|
+
# A sentence of at least sixty characters, ending in sentence-ending
|
|
67
|
+
# punctuation followed by one or two spaces -- the setup mic-drop-closer's
|
|
68
|
+
# kicker needs before it looks for the closer. The atomic group (?>...) is
|
|
69
|
+
# load-bearing against catastrophic backtracking; see the comment on
|
|
70
|
+
# mic-drop-closer's pattern for the incident that made it necessary.
|
|
71
|
+
SENTENCE_OF_SIXTY_CHARACTERS_ENDING_IN_PUNCTUATION_AND_SPACE =
|
|
72
|
+
/(?:^|(?<=[.!?])[ \t]{1,2})(?>(?:[^.!?\n\s]|(?<![ \t])[ \t]{1,2}(?![ \t])|\r?\n(?!\s*\n)[ \t]*){60,})[.!?][ \t]{1,2}/
|
|
73
|
+
|
|
74
|
+
# real-x-real-y interpolates this fragment after each of its two "real"s, so
|
|
75
|
+
# a later narrowing can only ever apply to both at once -- two copies typed
|
|
76
|
+
# out separately could drift, one gaining an exception the other never
|
|
77
|
+
# sees. Function words that only ever continue a predicative "is real",
|
|
78
|
+
# then the fixed senses common in this register that are never the
|
|
79
|
+
# doubled-intensifier tell: real time, real-world, the math sense (real
|
|
80
|
+
# numbers/roots), real user (monitoring), real estate, real money.
|
|
81
|
+
REAL_X_REAL_Y_EXCLUDED_NEXT_WORD =
|
|
82
|
+
"and|but|or|nor|yet|too|enough|itself|indeed|which|that|here|there|so" \
|
|
83
|
+
"|time|world|numbers?|roots?|money|estate|user"
|
|
84
|
+
|
|
19
85
|
RULES = [
|
|
20
86
|
# ── rhetorical-tic ────────────────────────────────────────────────────
|
|
21
87
|
Rule.new(
|
|
@@ -83,13 +149,313 @@ module Sloplint
|
|
|
83
149
|
id: "thats-the-whole",
|
|
84
150
|
category: "rhetorical-tic",
|
|
85
151
|
severity: "warning",
|
|
86
|
-
|
|
152
|
+
# The nouns are WHOLE_CLOSERS. "value" and "fix" are the closers agents
|
|
153
|
+
# write in technical prose ("That's the whole fix"), where the
|
|
154
|
+
# contraction keeps them out of is-the-whole-x, which sees only "is".
|
|
155
|
+
# Either apostrophe counts; editors emit the curly one.
|
|
156
|
+
pattern: /\b(?:that|this)(?:['’]s|#{WRAP_GAP}+is)#{WRAP_GAP}+the#{WRAP_GAP}+whole#{WRAP_GAP}+(?:#{WHOLE_CLOSERS})\b/i,
|
|
87
157
|
message: '"That\'s the whole point/game/…" is a stock LLM closer.',
|
|
88
158
|
suggestion: "Say the point directly instead of announcing it.",
|
|
89
|
-
examples_bad: [
|
|
90
|
-
|
|
159
|
+
examples_bad: [
|
|
160
|
+
"That's the whole point.",
|
|
161
|
+
"That's the whole value of a typed error.",
|
|
162
|
+
"That's the whole fix; the cache was already right.",
|
|
163
|
+
"That’s the whole value here.",
|
|
164
|
+
"That's the whole fix right there.",
|
|
165
|
+
"That's the whole fix that was needed.",
|
|
166
|
+
"That's the whole fix the reviewer asked for.",
|
|
167
|
+
# A blank line holding only a non-breaking space is still blank.
|
|
168
|
+
"## That's the whole fix\n\u00A0\nApply it.",
|
|
169
|
+
# A dash or a bullet after a gap ends the closer; only a glued
|
|
170
|
+
# hyphen joins a compound.
|
|
171
|
+
"That's the whole fix -- the cache was already right.",
|
|
172
|
+
"- Cache key was stale.\n- That's the whole fix\n- Tests pass.",
|
|
173
|
+
"2. That's the whole fix\n3. Tests pass.",
|
|
174
|
+
# A heading ends on the noun with no full stop.
|
|
175
|
+
"## That's the whole fix\n\nApply it and rerun the suite."
|
|
176
|
+
],
|
|
177
|
+
examples_ok: [
|
|
178
|
+
"This is the whole cake.",
|
|
179
|
+
# An unlisted noun stays clean, however closer-shaped the sentence.
|
|
180
|
+
"That's the whole history of the case.",
|
|
181
|
+
# "value" and "fix" running on into a compound name a thing.
|
|
182
|
+
"That's the whole value chain, end to end.",
|
|
183
|
+
"That's the whole value\nchain, end to end.",
|
|
184
|
+
# The gap is a non-breaking space.
|
|
185
|
+
"That's the whole value\u00A0chain, end to end.",
|
|
186
|
+
"That's the whole value *chain*, end to end.",
|
|
187
|
+
"That's the whole fix `list` for the release.",
|
|
188
|
+
"That's the whole fix (list) for the release.",
|
|
189
|
+
"That's the whole value-add of the consultant.",
|
|
190
|
+
"That's the whole value's worth.",
|
|
191
|
+
"That's the whole fix list for the release.",
|
|
192
|
+
# A paragraph break is not a gap, inside the two-word noun or
|
|
193
|
+
# before any noun.
|
|
194
|
+
"That's the whole ball\n\ngame.",
|
|
195
|
+
"That's the whole\n\npoint."
|
|
196
|
+
],
|
|
91
197
|
rationale: "The 'that's the whole X' flourish is a model tic for landing a paragraph."
|
|
92
198
|
),
|
|
199
|
+
Rule.new(
|
|
200
|
+
id: "is-the-whole-x",
|
|
201
|
+
category: "rhetorical-tic",
|
|
202
|
+
severity: "info",
|
|
203
|
+
# The generic form of thats-the-whole: a subject, then "is the whole /
|
|
204
|
+
# real / actual / entire N" with N from a closed abstract list. "That
|
|
205
|
+
# periodicity is the whole tell.", "Consistency is the real test." The
|
|
206
|
+
# match opens on the subject's last word, so the note points at the
|
|
207
|
+
# sentence and not at a space. Two older rules own two exact shapes,
|
|
208
|
+
# and those are yielded so nothing is reported twice: "that/this is
|
|
209
|
+
# the whole N" for every N in WHOLE_CLOSERS to thats-the-whole (the
|
|
210
|
+
# lookahead interpolates the same fragment), and "is the entire
|
|
211
|
+
# point/game/thing/deal/story" to is-the-entire; every
|
|
212
|
+
# other subject and noun flags here, so "This is the real test." is
|
|
213
|
+
# not lost. A question is not a closer, so an interrogative subject
|
|
214
|
+
# (what, which, who, where, when, how) is out. "only" is left out
|
|
215
|
+
# because "is the only thing" is ordinary speech; "deal", "thing" and
|
|
216
|
+
# "cost" because "the real deal", "the real thing" and "the whole cost"
|
|
217
|
+
# are idioms or quantities. The noun may not run on into a compound
|
|
218
|
+
# ("problem-solver"). Gaps may cross a hard-wrapped newline but never
|
|
219
|
+
# a paragraph break. Ships at info because "the real question" and
|
|
220
|
+
# "the whole point" are also how people talk.
|
|
221
|
+
pattern: /(?<![\w'’-])
|
|
222
|
+
(?!(?:that|this)#{WRAP_GAP}+(?:is)#{WRAP_GAP}+(?:the)#{WRAP_GAP}+(?:whole)#{WRAP_GAP}+(?:#{WHOLE_CLOSERS})(?![\w'’-]))
|
|
223
|
+
(?!(?:what|which|who|where|when|how)(?![\w'’-]))
|
|
224
|
+
[\w'’-]+#{WRAP_GAP}+(?:is)#{WRAP_GAP}+(?:the)#{WRAP_GAP}+
|
|
225
|
+
(?:(?: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)
|
|
226
|
+
|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,
|
|
227
|
+
message: '"… is the whole/real N" is a stock LLM closer.',
|
|
228
|
+
suggestion: "Say the point directly instead of ranking it.",
|
|
229
|
+
examples_bad: [
|
|
230
|
+
"That periodicity is the whole tell.",
|
|
231
|
+
"Consistency is the real test.",
|
|
232
|
+
"Getting the handoff right is the actual work.",
|
|
233
|
+
# Not thats-the-whole's nouns, so not yielded.
|
|
234
|
+
"This is the real test.",
|
|
235
|
+
"That is the actual problem.",
|
|
236
|
+
# "entire" with a noun is-the-entire does not own.
|
|
237
|
+
"Timing is the entire tell.",
|
|
238
|
+
# A hard-wrapped closer survives one newline.
|
|
239
|
+
"Consistency\nis the real test."
|
|
240
|
+
],
|
|
241
|
+
examples_ok: [
|
|
242
|
+
# Left to thats-the-whole, an old noun and a widened one.
|
|
243
|
+
"That is the whole point.",
|
|
244
|
+
"That is the whole fix.",
|
|
245
|
+
# Left to is-the-entire.
|
|
246
|
+
"Timing is the entire game.",
|
|
247
|
+
# "only" is ordinary speech.
|
|
248
|
+
"Sleep is the only thing that helps.",
|
|
249
|
+
"The real question was never asked.",
|
|
250
|
+
# Idioms and quantities.
|
|
251
|
+
"Clojure is the real deal, and so is the REPL.",
|
|
252
|
+
"The 1962 recording is the real thing.",
|
|
253
|
+
"The remaining balance is the whole cost of the repair.",
|
|
254
|
+
# A compound with a listed noun.
|
|
255
|
+
"She is the real problem-solver on the team.",
|
|
256
|
+
# A question is not a closer.
|
|
257
|
+
"What is the real difference between the two plans?",
|
|
258
|
+
"Nobody knows what is the actual cost of storage.",
|
|
259
|
+
# A paragraph break is not a gap.
|
|
260
|
+
"Consistency\n\nis the real test."
|
|
261
|
+
],
|
|
262
|
+
rationale: "Ranking a claim as 'the whole point' or 'the real test' is how a model " \
|
|
263
|
+
"lands a paragraph without adding to it. People say it too, so one is a " \
|
|
264
|
+
"question; several in a draft should be read as a warning."
|
|
265
|
+
),
|
|
266
|
+
Rule.new(
|
|
267
|
+
id: "bare-equative",
|
|
268
|
+
category: "rhetorical-tic",
|
|
269
|
+
severity: "info",
|
|
270
|
+
# A sentence that opens on an abstract head noun and equates it with
|
|
271
|
+
# something: "The tell here is the periodicity.", "The problem is not
|
|
272
|
+
# the tool.", "The lesson is the handoff." The head noun list is closed
|
|
273
|
+
# and holds only nouns that cannot name an object, the same list
|
|
274
|
+
# the-x-is-the-x uses: "the key is the brass thing on the hook" and
|
|
275
|
+
# "the cost is the price of the ticket" are definitions that tell the
|
|
276
|
+
# reader something. The sentence must start on "The", at a sentence
|
|
277
|
+
# start or after a list marker, and the copula ("is", "is not",
|
|
278
|
+
# "isn't") must be followed by "the" and a lowercase word, so a
|
|
279
|
+
# predicate adjective ("The problem is real"), an indefinite ("The
|
|
280
|
+
# answer is a mess"), a pointing complement ("the same", "the one",
|
|
281
|
+
# "the first"), and a proper noun ("the Slack thread") are all out.
|
|
282
|
+
# Gaps may cross a hard-wrapped newline but never a paragraph break.
|
|
283
|
+
# Ships at info: "The problem is the cost" is how people write too, at
|
|
284
|
+
# about four per million words on Hacker News; it is the density that
|
|
285
|
+
# tells.
|
|
286
|
+
pattern: /(?:^|(?<=[.!?])[ \t]{1,2})(?:[-*+•][ \t]+|\d+[.)][ \t]+)?\KThe(?:[ \t]|\r?\n(?!\s*\n))+
|
|
287
|
+
(?: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))+
|
|
288
|
+
(?: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,
|
|
289
|
+
message: '"The X is the Y." is the AI definitional equative.',
|
|
290
|
+
suggestion: "Say what the thing does or why it matters, instead of what it equals.",
|
|
291
|
+
examples_bad: [
|
|
292
|
+
"The tell here is the periodicity.",
|
|
293
|
+
"The lesson is the handoff, not the tool.",
|
|
294
|
+
"The problem is not the tool. It is the habit.",
|
|
295
|
+
"The problem isn't the handoff.",
|
|
296
|
+
# The pair's usual habitat.
|
|
297
|
+
"- The point is the cadence.",
|
|
298
|
+
# A hard-wrapped equative survives one newline.
|
|
299
|
+
"The problem is the\ncost."
|
|
300
|
+
],
|
|
301
|
+
examples_ok: [
|
|
302
|
+
"The problem is real.",
|
|
303
|
+
"The answer is a mess of caveats.",
|
|
304
|
+
# Not at a sentence start.
|
|
305
|
+
"We think the problem is the handoff.",
|
|
306
|
+
# Concrete heads are definitions.
|
|
307
|
+
"The key is the brass thing on the hook.",
|
|
308
|
+
"The cost is the price of the ticket plus tax.",
|
|
309
|
+
# Pointing complements.
|
|
310
|
+
"The story is the one she told last week.",
|
|
311
|
+
"The reason is the same: space.",
|
|
312
|
+
"The point is the first item on the list.",
|
|
313
|
+
# A proper noun.
|
|
314
|
+
"The tell is the Slack thread.",
|
|
315
|
+
# A paragraph break is not a gap.
|
|
316
|
+
"The problem is the\n\ncost."
|
|
317
|
+
],
|
|
318
|
+
rationale: "Opening on an abstract noun and equating it with a second noun phrase " \
|
|
319
|
+
"states a diagnosis as a definition, which sounds settled and explains " \
|
|
320
|
+
"nothing. People write the shape too, so one is a question; several in " \
|
|
321
|
+
"a draft should be read as a warning."
|
|
322
|
+
),
|
|
323
|
+
Rule.new(
|
|
324
|
+
id: "epistrophe",
|
|
325
|
+
category: "rhetorical-tic",
|
|
326
|
+
severity: "info",
|
|
327
|
+
default_on: false,
|
|
328
|
+
# Two clauses that end on the same two-word phrase, the second closing
|
|
329
|
+
# the sentence: "built for one desk, and almost no job is done at one
|
|
330
|
+
# desk." Two backreferences catch the repeat, one per word, so the
|
|
331
|
+
# phrase may be hard-wrapped at either occurrence. The phrase may not
|
|
332
|
+
# open on an article, since "in the report, and … in the report" is
|
|
333
|
+
# ordinary; its second word must be four letters or more; and the
|
|
334
|
+
# second clause is five to sixty characters with no internal
|
|
335
|
+
# punctuation, opening on a word, with whitespace runs capped at two
|
|
336
|
+
# spaces (or a hard wrap with a small indent), so a code span or URL
|
|
337
|
+
# blanked by --markdown cannot weld a false repeat. Off by default:
|
|
338
|
+
# this is a named figure that Emerson and Marcus Aurelius use on
|
|
339
|
+
# purpose, and on Hacker News most hits are ordinary phrase reuse
|
|
340
|
+
# ("what we said, not what the minutes say we said"), so the rate is
|
|
341
|
+
# above what an info rule should carry. Select it when a draft is
|
|
342
|
+
# suspected of leaning on it.
|
|
343
|
+
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})+)?
|
|
344
|
+
(?=\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,
|
|
345
|
+
message: "Two clauses ending on the same phrase (epistrophe) read as AI cadence.",
|
|
346
|
+
suggestion: "Vary the second ending, or cut the repeat.",
|
|
347
|
+
examples_bad: [
|
|
348
|
+
"The tool was built for one desk, and almost no job is done at one desk.",
|
|
349
|
+
"They wanted a shared file, but nobody would maintain a shared file.",
|
|
350
|
+
# A hard-wrapped repeat survives one newline, in either clause.
|
|
351
|
+
"The tool was built for one desk, and almost no job\nis done at one desk.",
|
|
352
|
+
"The tool was built for one\ndesk, and almost no job is done at one desk."
|
|
353
|
+
],
|
|
354
|
+
examples_ok: [
|
|
355
|
+
"The tool was built for one desk, and almost no job is done alone.",
|
|
356
|
+
# A repeat across a sentence boundary is two sentences.
|
|
357
|
+
"They wanted a shared file. Nobody would maintain a shared file.",
|
|
358
|
+
# An article-led phrase is ordinary repetition.
|
|
359
|
+
"It was in the report, and the numbers were in the report.",
|
|
360
|
+
# The second word must be four letters or more.
|
|
361
|
+
"I like the blue one, and she likes the blue one.",
|
|
362
|
+
# The second clause is at most sixty characters.
|
|
363
|
+
"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.",
|
|
364
|
+
# No internal punctuation in the second clause.
|
|
365
|
+
"The tool was built for one desk, and, as it happens, no job is done at one desk.",
|
|
366
|
+
# Blanked text cannot make the second clause.
|
|
367
|
+
"It was tuned for one desk, and on one desk."
|
|
368
|
+
],
|
|
369
|
+
rationale: "Ending consecutive clauses on the same phrase is a figure of emphasis, " \
|
|
370
|
+
"and a model reaches for it whenever it wants a sentence to land. People " \
|
|
371
|
+
"use it too, and much of what the pattern catches is plain phrase reuse, " \
|
|
372
|
+
"which is why the rule is off by default; when selected, several in a " \
|
|
373
|
+
"draft should be read as a warning."
|
|
374
|
+
),
|
|
375
|
+
Rule.new(
|
|
376
|
+
id: "phrase-echo",
|
|
377
|
+
category: "rhetorical-tic",
|
|
378
|
+
severity: "info",
|
|
379
|
+
default_on: false,
|
|
380
|
+
# The same three words again a few paragraphs on. Three consecutive
|
|
381
|
+
# words, each four characters or more and lowercase-led, one of them
|
|
382
|
+
# six letters with nothing but letters, and the same three again
|
|
383
|
+
# within about 400 words. That is the whole defence, and it is
|
|
384
|
+
# structural: length drops the function-word runs, the all-letters
|
|
385
|
+
# word drops the contractions and hyphenated compounds that would
|
|
386
|
+
# otherwise pass on characters alone, case drops the names and
|
|
387
|
+
# headings. The gaps inside the phrase are capped at two spaces, as
|
|
388
|
+
# in epistrophe, so a code span blanked by --markdown cannot weld two
|
|
389
|
+
# words into a phrase.
|
|
390
|
+
#
|
|
391
|
+
# The window is a lazy walk of word steps in an atomic group, so
|
|
392
|
+
# nothing backtracks, and it counts Unicode words, since `\w` is
|
|
393
|
+
# ASCII in Ruby. Each gap in the walk is capped at 80 non-word
|
|
394
|
+
# characters and refuses to cross into a list item or a table row:
|
|
395
|
+
# a blanked code fence, a rule of dashes or a bullet is a wall, not a
|
|
396
|
+
# step. The repeat sits in a lookahead so the match, and the excerpt,
|
|
397
|
+
# is the first occurrence alone rather than the whole span; the
|
|
398
|
+
# suggestion says so. The repeat may not open on a quote mark, a
|
|
399
|
+
# backtick, an emphasis marker, a hyphen or a table bar: a quoted
|
|
400
|
+
# self-repeat is deliberate, and "re-shared" is not "shared". One
|
|
401
|
+
# backreference per word so either occurrence may be hard-wrapped.
|
|
402
|
+
#
|
|
403
|
+
# Off by default: in reference prose the rate runs to thousands per
|
|
404
|
+
# million words, all of it terms of art and running epithets.
|
|
405
|
+
pattern: /(?<![\p{Word}'’-])
|
|
406
|
+
(?=(?:[\w'’-]+(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})){0,2}[a-z]{6,}(?![\w'’-]))
|
|
407
|
+
([a-z][\w'’-]{3,})(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})
|
|
408
|
+
([a-z][\w'’-]{3,})(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})
|
|
409
|
+
([a-z][\w'’-]{3,})\b
|
|
410
|
+
(?=(?>(?:(?!\r?\n[ \t]*(?:[-*+•|]|\d+[.)])[ \t])\P{Word}){1,80}\p{Word}+){0,400}?
|
|
411
|
+
(?:(?!\r?\n[ \t]*(?:[-*+•|]|\d+[.)])[ \t])[^\p{Word}"“‘'`*|-]){1,80}
|
|
412
|
+
\1(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})
|
|
413
|
+
\2(?:[ \t ]{1,2}|\r?\n(?![ \t ]*\r?\n)[ \t ]{0,4})
|
|
414
|
+
\3\b)/x,
|
|
415
|
+
message: "Three-word phrase that comes back within a few hundred words -- a model reusing its own output.",
|
|
416
|
+
suggestion: "This is the first use and the repeat is ahead. Reword the repeat, unless the phrase is a term the reader needs to see again.",
|
|
417
|
+
examples_bad: [
|
|
418
|
+
"The shortest honest answer that came out of the review was a list.\n\nWhen you write back, the shortest honest answer you can send is the list.",
|
|
419
|
+
"We keep a shared review checklist in the repo, and it is short. Everyone who opens a pull request edits the shared review checklist first.",
|
|
420
|
+
# A hard-wrapped first occurrence.
|
|
421
|
+
"We keep a shared review\nchecklist in the repo. Everyone edits the shared review checklist first.",
|
|
422
|
+
# The last word inside the window.
|
|
423
|
+
"We keep a shared review checklist. #{"word " * 399}The shared review checklist is short."
|
|
424
|
+
],
|
|
425
|
+
examples_ok: [
|
|
426
|
+
# Function words fall out on length.
|
|
427
|
+
"In order to ship we cut scope, and in order to ship again we cut it more.",
|
|
428
|
+
# Four-letter words alone are not enough; one word must be six letters, letters only.
|
|
429
|
+
"It would have been better, and it would have been faster.",
|
|
430
|
+
"It couldn't have been worse, and it couldn't have been better.",
|
|
431
|
+
"We saw every top-ten list here, and every top-ten list there.",
|
|
432
|
+
# Proper nouns and Title Case headings fall out on case.
|
|
433
|
+
"Grand Central Station has one, and Grand Central Station wants two.",
|
|
434
|
+
"Incident Response Plan\n\nThe Incident Response Plan covers the first hour.",
|
|
435
|
+
# The repeat is beyond the window, in words, in non-ASCII words, or past a wall of dashes.
|
|
436
|
+
"We keep a shared review checklist. #{"word " * 400}The shared review checklist is short.",
|
|
437
|
+
"We keep a shared review checklist. #{"слово " * 400}The shared review checklist is short.",
|
|
438
|
+
"We keep a shared review checklist.\n\n#{"-" * 100}\n\nThe shared review checklist is short.",
|
|
439
|
+
# A quoted repeat is deliberate, whatever the quote mark.
|
|
440
|
+
"The shared review checklist is new. He wrote \"shared review checklist\" on the board.",
|
|
441
|
+
"The shared review checklist is new. He wrote 'shared review checklist' on the board.",
|
|
442
|
+
"The shared review checklist is new. He wrote `shared review checklist` on the board.",
|
|
443
|
+
# A different word form is a different phrase, and so is a hyphenated compound.
|
|
444
|
+
"The shared review checklist grew, and then both shared review checklists grew.",
|
|
445
|
+
"The auto-generated review checklist was long. The hand-generated review checklist was longer.",
|
|
446
|
+
"We use a shared review checklist daily. Nobody re-shared review checklist edits.",
|
|
447
|
+
# A run of spaces where --markdown blanked a code span does not weld a phrase.
|
|
448
|
+
"We keep a shared review checklist here. Everyone edits the shared review checklist first.",
|
|
449
|
+
# List items and table rows are furniture, not prose.
|
|
450
|
+
"- shared review checklist covers pull requests\n- shared review checklist covers deploys",
|
|
451
|
+
"1. Update the shared config file.\n2. Restart the shared config file watcher.",
|
|
452
|
+
"| Task | Status |\n|---|---|\n| A | needs manual review |\n| B | needs manual review |"
|
|
453
|
+
],
|
|
454
|
+
rationale: "A model reuses a phrase it has just minted because its own recent output is " \
|
|
455
|
+
"the likeliest continuation, so the same three words turn up again a few " \
|
|
456
|
+
"paragraphs on, doing no new work. Terms of art repeat too, and the pattern " \
|
|
457
|
+
"cannot tell a coined phrase from a name, so the rule is off by default."
|
|
458
|
+
),
|
|
93
459
|
Rule.new(
|
|
94
460
|
id: "did-not-x-did-not-y",
|
|
95
461
|
category: "rhetorical-tic",
|
|
@@ -102,6 +468,385 @@ module Sloplint
|
|
|
102
468
|
examples_ok: ["He didn't know the answer."],
|
|
103
469
|
rationale: "Repeated negated-verb parallelism is a signature model cadence."
|
|
104
470
|
),
|
|
471
|
+
Rule.new(
|
|
472
|
+
id: "from-x-to-y-chain",
|
|
473
|
+
category: "rhetorical-tic",
|
|
474
|
+
severity: "warning",
|
|
475
|
+
# Two or more "from X to Y" spans in a row, comma-separated: "from
|
|
476
|
+
# private notes to shared files, from personal memory to team context".
|
|
477
|
+
# Each span is "from", one to three words, "to", one to three words; the
|
|
478
|
+
# comma between spans is the evidence of authored parallelism, as in
|
|
479
|
+
# no-x-no-y, so a chain never crosses a sentence boundary. Every operand
|
|
480
|
+
# must open with a letter, so a list of ranges ("from 1990 to 1995, from
|
|
481
|
+
# 1997 to 2001", "from 9 to 5") is not a chain. The last span is bounded
|
|
482
|
+
# too: its Y must run into punctuation, a conjunction or preposition, a
|
|
483
|
+
# wh-word, or the end of the text, so the excerpt never carries the
|
|
484
|
+
# opening of the next clause.
|
|
485
|
+
#
|
|
486
|
+
# Two human shapes share the surface and are skipped. The relay, where
|
|
487
|
+
# each span starts where the last one ended ("from the egg to the worm,
|
|
488
|
+
# from the worm to the fly"), traces a sequence: the second "from"
|
|
489
|
+
# repeating the first "to" drops the note. The reduplication, "from
|
|
490
|
+
# hummock to hummock, from root to root", describes motion: a span whose
|
|
491
|
+
# two ends are the same words drops it. Both backreferences must close
|
|
492
|
+
# on a whole operand, so "work-life" is not a relay of "work" and "code
|
|
493
|
+
# review" is not a reduplication of "code".
|
|
494
|
+
#
|
|
495
|
+
# A list of concrete mappings ("from MySQL to Postgres, from Redis to
|
|
496
|
+
# Memcached") has the same surface and flags. That is an accepted cost:
|
|
497
|
+
# the shape is one hit in 2.4M words of Hacker News, and the note says
|
|
498
|
+
# what to check.
|
|
499
|
+
pattern: /\bfrom\s+(?=[a-z])(?:[\w'-]+\s+){1,3}to\s+(?=[a-z])(?:[\w'-]+\s+){0,2}[\w'-]+
|
|
500
|
+
(?:,\s+(?:and\s+)?from\s+(?=[a-z])(?:[\w'-]+\s+){1,3}to\s+(?=[a-z])(?:[\w'-]+\s+){0,2}[\w'-]+)+
|
|
501
|
+
(?=[^\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,
|
|
502
|
+
message: '"from X to Y, from X to Y" chain (%{count} spans) reads as AI cadence.',
|
|
503
|
+
suggestion: "Keep one span, or name the things instead of sweeping across them.",
|
|
504
|
+
# Span heads only, so a "from" inside an operand is not a span.
|
|
505
|
+
count_group: /(?:\A|,\s+(?:and\s+)?)from\b/i,
|
|
506
|
+
skip: [
|
|
507
|
+
# The relay: "to the worm, from the worm".
|
|
508
|
+
/\bto\s+(?:[\w'-]+\s+){0,2}([\w'-]+),\s+(?:and\s+)?from\s+(?:[\w'-]+\s+){0,2}\1(?![\w'-])/i,
|
|
509
|
+
# The reduplication: "from hummock to hummock".
|
|
510
|
+
/\bfrom\s+(?:the\s+)?((?:[\w'-]+\s+){0,2}[\w'-]+)\s+to\s+(?:the\s+)?\1(?=,|\s*\z)/i
|
|
511
|
+
],
|
|
512
|
+
examples_bad: [
|
|
513
|
+
"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.",
|
|
514
|
+
"We went from guessing to measuring, from hoping to knowing.",
|
|
515
|
+
"The plan takes them from the pilot to the rollout, and from the memo to the audit.",
|
|
516
|
+
# The last span may run into a clause, as long as a joining word starts it.
|
|
517
|
+
"We went from guessing to measuring, from hoping to knowing in a single quarter.",
|
|
518
|
+
# Not a relay: "work-life" is not "work".
|
|
519
|
+
"We went from rest to work, from work-life balance to burnout.",
|
|
520
|
+
# Not a reduplication: "code review" is not "code".
|
|
521
|
+
"We moved from code to code review, from guessing to measuring.",
|
|
522
|
+
# A "from" inside an operand is not a span head.
|
|
523
|
+
"The shift from revenue from ads to revenue from subscriptions, from hoping to knowing."
|
|
524
|
+
],
|
|
525
|
+
examples_ok: [
|
|
526
|
+
"The train runs from Boston to New York.",
|
|
527
|
+
# Pride and Prejudice (Austen, public domain): the relay.
|
|
528
|
+
"it jumps from admiration to love, from love to matrimony, in a moment.",
|
|
529
|
+
# Walden (Thoreau, public domain): the reduplication.
|
|
530
|
+
"jumping from hummock to hummock, from willow root to willow root, when the wild river valley",
|
|
531
|
+
# A span longer than three words on either side is a clause, not an item.
|
|
532
|
+
"He drove from the coast to the mountains in a day, and from there the road was easy.",
|
|
533
|
+
# Two spans in separate sentences never chain.
|
|
534
|
+
"She moved from Ohio to Maine. From there she wrote to him weekly.",
|
|
535
|
+
# Ranges are not spans.
|
|
536
|
+
"He served on the board from 1990 to 1995, from 1997 to 2001, and from 2005 to 2009.",
|
|
537
|
+
"We are open from 9 to 5, from Monday to Friday."
|
|
538
|
+
],
|
|
539
|
+
rationale: "Stacked 'from X to Y' spans are a model's way of gesturing at a whole " \
|
|
540
|
+
"transformation without describing any of it; each span names two poles and " \
|
|
541
|
+
"nothing between them. Human prose stacks the phrase for a sequence (each span " \
|
|
542
|
+
"picking up where the last ended) or for motion (the same word at both ends), " \
|
|
543
|
+
"and both of those are skipped. What remains is rare in careful writing; a list " \
|
|
544
|
+
"of concrete mappings (tools migrated, units converted) shares the shape and is " \
|
|
545
|
+
"the case to check before acting."
|
|
546
|
+
),
|
|
547
|
+
Rule.new(
|
|
548
|
+
id: "one-x-one-y",
|
|
549
|
+
category: "rhetorical-tic",
|
|
550
|
+
severity: "warning",
|
|
551
|
+
# Three or more "one X" items in a comma chain that stands on its own:
|
|
552
|
+
# "One owner, one repository, one weekly prune." or, after a colon,
|
|
553
|
+
# "the setup is narrow: one reviewer, one queue, one deadline". The
|
|
554
|
+
# determiner is the cadence; the comma is the evidence of authored
|
|
555
|
+
# parallelism, as in no-x-no-y.
|
|
556
|
+
#
|
|
557
|
+
# The chain must open a sentence or follow a colon, semicolon or dash.
|
|
558
|
+
# That is the narrowing that separates the drumbeat from counting: "the
|
|
559
|
+
# flat has one bedroom, one bathroom, one balcony" and "add one egg, one
|
|
560
|
+
# onion, one carrot" hang off a verb, and there the word is doing
|
|
561
|
+
# arithmetic. Each item is "one" plus one or two letter-led words (never
|
|
562
|
+
# "and"/"or"), so an enumeration over numbers is not a chain; the
|
|
563
|
+
# Oxford comma is optional after the first link; a gap may cross a
|
|
564
|
+
# hard-wrapped newline but never a paragraph break; and the last item
|
|
565
|
+
# must run into punctuation, a joining word, or the end of the text, so
|
|
566
|
+
# the excerpt never carries the opening of the next clause. The
|
|
567
|
+
# distributive "one for you, one for me, one for the pot" is skipped at
|
|
568
|
+
# any length, and a pair never flags: two is distribution, three is a
|
|
569
|
+
# drumbeat.
|
|
570
|
+
#
|
|
571
|
+
# Verse keeps the shape on purpose ("One face, one voice, one habit, and
|
|
572
|
+
# two persons"), and that is an accepted cost.
|
|
573
|
+
pattern: /(?:^|(?<=[.!?:;—–])[ \t]{1,2})\K
|
|
574
|
+
one[ \t]+(?=[a-z])[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?=[a-z])[\w'’-]+)?
|
|
575
|
+
,(?:[ \t]|\r?\n(?!\s*\n))+(?:and(?:[ \t]|\r?\n(?!\s*\n))+)?
|
|
576
|
+
one[ \t]+(?=[a-z])[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?=[a-z])[\w'’-]+)?
|
|
577
|
+
(?:(?:,(?:[ \t]|\r?\n(?!\s*\n))+(?:and(?:[ \t]|\r?\n(?!\s*\n))+)?|(?:[ \t]|\r?\n(?!\s*\n))+and(?:[ \t]|\r?\n(?!\s*\n))+)
|
|
578
|
+
one[ \t]+(?=[a-z])[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?=[a-z])[\w'’-]+)?)+
|
|
579
|
+
(?=[^\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,
|
|
580
|
+
message: '"one X, one Y, one Z" chain (%{count} items) reads as AI cadence.',
|
|
581
|
+
suggestion: "Cut the chain or make it one plain sentence.",
|
|
582
|
+
# Item heads only, so a "one" inside an item is not an item.
|
|
583
|
+
count_group: /(?:\A|,\s+(?:and\s+)?|\s+and\s+)one\b/i,
|
|
584
|
+
# The distributive frame, at any length.
|
|
585
|
+
skip: [/(?:\bone\s+for\b[\s\S]*?){3}/i],
|
|
586
|
+
examples_bad: [
|
|
587
|
+
"The setup is deliberately narrow: one reviewer, one queue, one deadline.",
|
|
588
|
+
"One owner, one repository, one weekly prune.",
|
|
589
|
+
# No Oxford comma.
|
|
590
|
+
"One name, one number and one date.",
|
|
591
|
+
# A curly apostrophe is a word character here.
|
|
592
|
+
"One team’s lead, one owner, one date.",
|
|
593
|
+
# A hard-wrapped chain survives one newline.
|
|
594
|
+
"One owner, one repository,\none weekly prune."
|
|
595
|
+
],
|
|
596
|
+
examples_ok: [
|
|
597
|
+
"One for you, one for me.",
|
|
598
|
+
"One for you, one for me, one for the pot.",
|
|
599
|
+
"We keep three bins: one for 2019, one for 2020, one for 2021.",
|
|
600
|
+
"One of them left, and the other one stayed.",
|
|
601
|
+
# After a verb the word is counting.
|
|
602
|
+
"The flat has one bedroom, one bathroom, one balcony.",
|
|
603
|
+
"Add one egg, one onion, one carrot and simmer for an hour.",
|
|
604
|
+
"The season ended with one win, one loss, one draw.",
|
|
605
|
+
# Essays: First Series (Emerson, public domain): not at a clause start.
|
|
606
|
+
"Not for nothing one face, one character, one fact, makes much impression on him",
|
|
607
|
+
# Items in separate sentences never chain.
|
|
608
|
+
"One person wrote it in the morning. One person read it after lunch. One person filed it at the end of the day.",
|
|
609
|
+
# A paragraph break ends the chain.
|
|
610
|
+
"He carried one bag, one coat,\n\nOne question remained unanswered."
|
|
611
|
+
],
|
|
612
|
+
rationale: "A drumbeat of 'one' items is a model's way of making a setup sound " \
|
|
613
|
+
"spare and inevitable; the word does no counting, it sets a rhythm. " \
|
|
614
|
+
"The chain has to stand on its own, at a sentence start or after a " \
|
|
615
|
+
"colon, because after a verb the word is counting and the shape is " \
|
|
616
|
+
"ordinary. Careful writers distribute in pairs and rarely stack three."
|
|
617
|
+
),
|
|
618
|
+
Rule.new(
|
|
619
|
+
id: "and-what-it-should",
|
|
620
|
+
category: "rhetorical-tic",
|
|
621
|
+
severity: "warning",
|
|
622
|
+
# The elliptical tail: "List what the assistant knows about the client,
|
|
623
|
+
# and what it should." The second "what" clause borrows its verb from the
|
|
624
|
+
# first and ends on a bare modal or a negated auxiliary, so the sentence
|
|
625
|
+
# closes on a contrast it never states. The comma, the conjunction, and
|
|
626
|
+
# the full stop right after the modal are all required; "and what it
|
|
627
|
+
# should do" is a complete clause and does not flag. The affirmative
|
|
628
|
+
# copula and do-verb ("and what he does.", "and what it is.") are left
|
|
629
|
+
# out: those are complete clauses with a main verb, not ellipsis. A
|
|
630
|
+
# question mark is left out too, because an interrogative licenses the
|
|
631
|
+
# ellipsis ("what does it cover, and what doesn't it?"). Gaps may cross
|
|
632
|
+
# a hard-wrapped newline but never a paragraph break.
|
|
633
|
+
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))+
|
|
634
|
+
(?:(?:should|could|would|must|can|will|might|may)(?:(?:[ \t]|\r?\n(?!\s*\n))+not|n['’]t)?
|
|
635
|
+
|cannot|(?:does|did|is|was|has|have|had|are|were)(?:(?:[ \t]|\r?\n(?!\s*\n))+not|n['’]t))[.!]/ix,
|
|
636
|
+
message: '"…, and what it should." is the AI elliptical tail.',
|
|
637
|
+
suggestion: "Finish the clause, or cut it.",
|
|
638
|
+
examples_bad: [
|
|
639
|
+
"List what the assistant knows about the client, and what it should.",
|
|
640
|
+
"List what the tool does, and what it doesn't.",
|
|
641
|
+
"List what the tool does, and what it does not.",
|
|
642
|
+
"Say what the team decided, but what it couldn't.",
|
|
643
|
+
"List what I know about the client, and what I should.",
|
|
644
|
+
"List what we cover, and what we haven't.",
|
|
645
|
+
# A hard-wrapped tail survives one newline.
|
|
646
|
+
"List what the model knows about the client,\nand what it should."
|
|
647
|
+
],
|
|
648
|
+
examples_ok: [
|
|
649
|
+
"List what the assistant knows about the client, and what it should know.",
|
|
650
|
+
"He asked what it was, and what it should be called.",
|
|
651
|
+
"Nobody knew what it cost or what it should.",
|
|
652
|
+
"She wrote down what she saw. And what she should have seen, she added later.",
|
|
653
|
+
# A main verb is a complete clause.
|
|
654
|
+
"It is not what he says, but what he does.",
|
|
655
|
+
"They knew what he did, and what he was.",
|
|
656
|
+
# A question licenses the ellipsis.
|
|
657
|
+
"Who decides what the policy covers, and what it doesn't?",
|
|
658
|
+
# A paragraph break is not a comma.
|
|
659
|
+
"List what the assistant knows,\n\nand what it should."
|
|
660
|
+
],
|
|
661
|
+
rationale: "Ending on a bare modal makes the reader supply the verb and the " \
|
|
662
|
+
"contrast, which reads as poise in a model and as an unfinished sentence " \
|
|
663
|
+
"in a person. Careful writers finish the clause."
|
|
664
|
+
),
|
|
665
|
+
Rule.new(
|
|
666
|
+
id: "abstract-lives-in",
|
|
667
|
+
category: "rhetorical-tic",
|
|
668
|
+
severity: "info",
|
|
669
|
+
# An abstraction given an address: "the craft that lives between the two
|
|
670
|
+
# desks", "its context lives in a folder nobody else can open", "the
|
|
671
|
+
# value sits in the follow-up". The subject list is closed and abstract,
|
|
672
|
+
# so a person, a dog, or a house living or sitting somewhere never
|
|
673
|
+
# matches, and a capitalised subject is skipped, so the messenger app
|
|
674
|
+
# Signal sitting in the middle of a relay is not the figure (at the cost
|
|
675
|
+
# of a sentence-initial "Context lives in", which is rare).
|
|
676
|
+
#
|
|
677
|
+
# Two prepositions are left out. "with" marks responsibility ("the
|
|
678
|
+
# decision sits with the board"), and "at" marks a quantity ("the value
|
|
679
|
+
# sits at ten million"); "at the intersection of" belongs to
|
|
680
|
+
# intersection-of. Two verbs are left out too: "lies in" ("the problem
|
|
681
|
+
# lies in the assumption") and "resides in" are ordinary English for
|
|
682
|
+
# where a fault or an authority is, at any register.
|
|
683
|
+
#
|
|
684
|
+
# Ships at info. The same shape states where information literally is
|
|
685
|
+
# ("the knowledge lives in our heads", "the instructions live in the
|
|
686
|
+
# README"), and a sample of pre-2022 Hacker News biased toward the
|
|
687
|
+
# construction turns up a few of those per million words, all human.
|
|
688
|
+
# One flag is a question; a draft that keeps giving ideas addresses
|
|
689
|
+
# should be read as a warning.
|
|
690
|
+
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))+
|
|
691
|
+
(?: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,
|
|
692
|
+
message: "An abstraction that lives/sits somewhere is an AI figure.",
|
|
693
|
+
suggestion: "Say who does the work, or where the thing actually is.",
|
|
694
|
+
# A capitalised subject is a proper noun.
|
|
695
|
+
skip: [/\A[A-Z]/],
|
|
696
|
+
examples_bad: [
|
|
697
|
+
"The craft that lives between the two desks is where the handoff fails.",
|
|
698
|
+
"Its context lives in a folder nobody else can open.",
|
|
699
|
+
"The real value sits in the follow-up, not the meeting."
|
|
700
|
+
],
|
|
701
|
+
examples_ok: [
|
|
702
|
+
"She lives in Boston and sits between us at dinner.",
|
|
703
|
+
# Responsibility, not location.
|
|
704
|
+
"The decision sits with the board.",
|
|
705
|
+
"The risk lives with the buyer once the goods ship.",
|
|
706
|
+
# A quantity, not a location.
|
|
707
|
+
"The value sits at ten million dollars a life.",
|
|
708
|
+
# "on" is not in the list.
|
|
709
|
+
"The knowledge lived on a server in the basement.",
|
|
710
|
+
# An intervening noun breaks the figure.
|
|
711
|
+
"The knowledge base lived in the basement.",
|
|
712
|
+
# A proper noun.
|
|
713
|
+
"Signal sits in the middle of the relay.",
|
|
714
|
+
# "lies in" is ordinary English for where a fault is.
|
|
715
|
+
"The problem lies in the assumption.",
|
|
716
|
+
"He put the answer in the margin and sat between the two of them."
|
|
717
|
+
],
|
|
718
|
+
rationale: "Giving an idea a location ('the value sits in', 'the work lives " \
|
|
719
|
+
"between') lets a writer sound precise about where something is without " \
|
|
720
|
+
"saying who does it or what it is. Models reach for it constantly, but " \
|
|
721
|
+
"people use the same shape to say where information literally is, so " \
|
|
722
|
+
"one flag is a question; a draft that keeps giving ideas addresses should " \
|
|
723
|
+
"be read as a warning."
|
|
724
|
+
),
|
|
725
|
+
Rule.new(
|
|
726
|
+
id: "the-x-is-the-x",
|
|
727
|
+
category: "rhetorical-tic",
|
|
728
|
+
severity: "warning",
|
|
729
|
+
# The repeated-head equative: "the reason it holds up is the reason the
|
|
730
|
+
# other half happens", "the problem with A is the problem with B". The
|
|
731
|
+
# same abstract head noun sits on both sides of the copula, caught by a
|
|
732
|
+
# backreference, so the sentence equates two things by declaring them
|
|
733
|
+
# the same kind of thing.
|
|
734
|
+
#
|
|
735
|
+
# The noun list is closed and holds only heads that cannot name a
|
|
736
|
+
# specific object: reason, problem, question, lesson and their kin.
|
|
737
|
+
# "key", "cost", "value", "unit", "fix" and the like are out, because
|
|
738
|
+
# "the key to the front door is the key on the red fob" is an identity
|
|
739
|
+
# statement that tells the reader something. The clause between the
|
|
740
|
+
# two heads is capped at fifty characters and may not hold a comma,
|
|
741
|
+
# semicolon or colon, so the two heads sit in one clause and an earlier
|
|
742
|
+
# "the cost was low, but shipping is the cost" never pairs; it may
|
|
743
|
+
# cross a hard-wrapped newline. The second head must be followed by a
|
|
744
|
+
# preposition, determiner, quantifier, pronoun, plural noun, or
|
|
745
|
+
# punctuation, so a compound ("the answer key") is not a repeat. The
|
|
746
|
+
# bare form ("the reason is the reason we came") and the contracted
|
|
747
|
+
# negation ("isn't the reason") both count. The sentence-initial
|
|
748
|
+
# capital is not required, so "and the reason … is the reason …"
|
|
749
|
+
# flags too.
|
|
750
|
+
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
|
|
751
|
+
(?:[^.!?;:,\n]|\r?\n(?!\s*\n)){1,50}?\bis(?:n['’]t|\s+not)?\s+the\s+\1
|
|
752
|
+
(?=[,.;:!?]|\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,
|
|
753
|
+
message: '"The X … is the X …" equates by repeating the head noun.',
|
|
754
|
+
suggestion: "Say what the second thing is, not that it is the same kind of thing.",
|
|
755
|
+
examples_bad: [
|
|
756
|
+
"The reason it holds up is the reason the other half happens.",
|
|
757
|
+
"The problem with the tool is the problem with the team.",
|
|
758
|
+
"In practice the question for them is not the question for us.",
|
|
759
|
+
"The problem with the tool isn't the problem with the team.",
|
|
760
|
+
# The bare form.
|
|
761
|
+
"The reason is the reason we came.",
|
|
762
|
+
# A quantifier or a plural noun may follow the second head.
|
|
763
|
+
"The reason people stay is the reason people leave.",
|
|
764
|
+
"The problem with onboarding is the problem every team has.",
|
|
765
|
+
"The lesson from the outage is the lesson most teams skip.",
|
|
766
|
+
# A hard-wrapped clause survives one newline.
|
|
767
|
+
"The reason\nit holds is the reason it fails."
|
|
768
|
+
],
|
|
769
|
+
examples_ok: [
|
|
770
|
+
"Rain is the reason we stayed home.",
|
|
771
|
+
"Its cost is the reason we came late.",
|
|
772
|
+
# Across a sentence boundary is two sentences.
|
|
773
|
+
"The reason is simple. It is the reason we left.",
|
|
774
|
+
# A compound noun is not a repeated head.
|
|
775
|
+
"The answer to the first question is the answer key, not a guess.",
|
|
776
|
+
# A semicolon or a comma is a boundary: the heads must share a clause.
|
|
777
|
+
"Process is the issue; community process is the issue we can fix.",
|
|
778
|
+
"The cost was low, but shipping is the cost we forgot.",
|
|
779
|
+
"The reason was never clear, but timing is the reason it failed.",
|
|
780
|
+
# Concrete heads are identity statements, not tautologies.
|
|
781
|
+
"The key to the front door is the key on the red fob.",
|
|
782
|
+
"The cost of shipping is the cost of the box plus postage."
|
|
783
|
+
],
|
|
784
|
+
rationale: "Repeating the head noun across the copula asserts an identity between " \
|
|
785
|
+
"two things while naming neither; it sounds like a diagnosis and " \
|
|
786
|
+
"delivers a tautology. Careful writers say what the second thing is."
|
|
787
|
+
),
|
|
788
|
+
Rule.new(
|
|
789
|
+
id: "same-determiner-chain",
|
|
790
|
+
category: "rhetorical-tic",
|
|
791
|
+
severity: "info",
|
|
792
|
+
# The quiet cousin of one-x-one-y and no-x-no-y: three or more items in
|
|
793
|
+
# a comma chain that all open on the same determiner or quantifier,
|
|
794
|
+
# caught with a backreference: "every faculty, every thought, every
|
|
795
|
+
# emotion", "your inbox, your calendar, your task list", "more work,
|
|
796
|
+
# more meetings, more email". "one" and "no" are left to their own
|
|
797
|
+
# rules. The narrative possessives (my, his, her, their, its) are left
|
|
798
|
+
# out: "his fame, his position, his life" is every novelist's, and it
|
|
799
|
+
# doubled the human rate. Ships at info because this is a rhetorical
|
|
800
|
+
# device humans own too, Emerson above all; a model uses it the way it
|
|
801
|
+
# uses the others, as a default cadence, and several in one draft is
|
|
802
|
+
# the tell. The Oxford comma is optional after the first link, a gap may
|
|
803
|
+
# cross a hard-wrapped newline but never a paragraph break, and the last
|
|
804
|
+
# item must run into punctuation, a joining word, a modal or common
|
|
805
|
+
# verb, or the end of the text. A verb or adverb the list does not know
|
|
806
|
+
# can still be swallowed as the last item's second word ("every deploy
|
|
807
|
+
# today"); the count is right and the over-reach is one word. The
|
|
808
|
+
# letter-led guards are case-sensitive on purpose: under /i they would
|
|
809
|
+
# let "New York, New Jersey, New Hampshire" through as a chain.
|
|
810
|
+
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'’-]+)?
|
|
811
|
+
,(?:[ \t]|\r?\n(?!\s*\n))+(?:and(?:[ \t]|\r?\n(?!\s*\n))+)?
|
|
812
|
+
\1[ \t]+(?-i:(?=[a-z]))[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?-i:(?=[a-z]))[\w'’-]+)?
|
|
813
|
+
(?:(?:,(?:[ \t]|\r?\n(?!\s*\n))+(?:and(?:[ \t]|\r?\n(?!\s*\n))+)?|(?:[ \t]|\r?\n(?!\s*\n))+and(?:[ \t]|\r?\n(?!\s*\n))+)
|
|
814
|
+
\1[ \t]+(?-i:(?=[a-z]))[\w'’-]+(?:[ \t]+(?!and\b|or\b)(?-i:(?=[a-z]))[\w'’-]+)?)+
|
|
815
|
+
(?=[^\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,
|
|
816
|
+
message: 'Repeated-determiner chain (%{count} items) reads as AI cadence.',
|
|
817
|
+
suggestion: "Cut the chain or make it one plain sentence.",
|
|
818
|
+
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,
|
|
819
|
+
examples_bad: [
|
|
820
|
+
"It touches every file, every branch, every deploy.",
|
|
821
|
+
"You get more work, more meetings, and more email.",
|
|
822
|
+
"Your inbox, your calendar, your task list.",
|
|
823
|
+
# No Oxford comma.
|
|
824
|
+
"It touches every file, every branch and every deploy.",
|
|
825
|
+
# A chain as the subject of a clause.
|
|
826
|
+
"Every test, every lint, every build must pass.",
|
|
827
|
+
"You get more work, more meetings, and more email every day.",
|
|
828
|
+
# A hard-wrapped chain survives one newline.
|
|
829
|
+
"Every file, every branch,\nevery deploy."
|
|
830
|
+
],
|
|
831
|
+
examples_ok: [
|
|
832
|
+
"Every file and every branch was checked.",
|
|
833
|
+
# Two items is a pair, not a chain.
|
|
834
|
+
"Every file, every branch.",
|
|
835
|
+
# The determiner must repeat; a mixed list is a list.
|
|
836
|
+
"Every file, each branch, and some deploys.",
|
|
837
|
+
"We bought more bread, some milk, and a dozen eggs.",
|
|
838
|
+
# A paragraph break ends the chain.
|
|
839
|
+
"Every file, every branch,\n\nEvery deploy was checked twice.",
|
|
840
|
+
# Narrative possessives are left out.
|
|
841
|
+
"His coat, his hat, his gloves.",
|
|
842
|
+
# Proper nouns are not a chain.
|
|
843
|
+
"We visited New York, New Jersey, and New Hampshire."
|
|
844
|
+
],
|
|
845
|
+
rationale: "A comma chain of items opening on the same determiner is a drumbeat, " \
|
|
846
|
+
"and a model falls into it as a default cadence. It is also a device " \
|
|
847
|
+
"careful writers use on purpose, so one is a question, not a verdict; " \
|
|
848
|
+
"when a draft carries several, read the family as a warning."
|
|
849
|
+
),
|
|
105
850
|
Rule.new(
|
|
106
851
|
id: "dont-verb-it",
|
|
107
852
|
category: "rhetorical-tic",
|
|
@@ -175,25 +920,50 @@ module Sloplint
|
|
|
175
920
|
Rule.new(
|
|
176
921
|
id: "cleanly",
|
|
177
922
|
category: "rhetorical-tic",
|
|
178
|
-
severity: "
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
|
|
923
|
+
severity: "info",
|
|
924
|
+
# The bare adverb was the whole rule, and the engineering idioms were
|
|
925
|
+
# flagged on purpose. Technical prose says that was the wrong call: a
|
|
926
|
+
# patch that applies cleanly, a build that compiles cleanly and a gear
|
|
927
|
+
# leg that separates cleanly are all checkable facts with a visible
|
|
928
|
+
# failure state, which is the opposite of rating a fit you haven't
|
|
929
|
+
# shown. The original probe -- 25 Gutenberg texts, no modern manner
|
|
930
|
+
# adverb found -- could not have caught this, because Victorian novels
|
|
931
|
+
# have no builds; the same corpus read as engineering ("clearly and
|
|
932
|
+
# cleanly drawn in pencil", Machine Drawing and Design, 1890) has it.
|
|
933
|
+
#
|
|
934
|
+
# What is left is the partition frame: something divided into parts, or
|
|
935
|
+
# mapped onto another scheme, and rated before the reader sees the
|
|
936
|
+
# parts. The clause-final form ("the objection breaks down cleanly, and
|
|
937
|
+
# neither half survives") is the same tell and is left out.
|
|
938
|
+
#
|
|
939
|
+
# The frame is not the sense, and this rule ships at info because of it.
|
|
940
|
+
# "The argument splits cleanly into two parts" and "the gear retracted
|
|
941
|
+
# cleanly into the well" are one pattern apart only in their subject,
|
|
942
|
+
# and a regex cannot see the subject -- the same limit that keeps the
|
|
943
|
+
# animacy verbs out of trailing-significance-participle. The preposition
|
|
944
|
+
# buys the common physical cases ("separated cleanly from", "cleanly
|
|
945
|
+
# compiled", "cleanly drawn") and nothing more, so a flag here is a
|
|
946
|
+
# question for the agent reading it, not a verdict.
|
|
947
|
+
pattern: /\bcleanly\s+(?:in(?:to|\s+(?:two|three|half))|onto)\b/i,
|
|
185
948
|
message: '"Cleanly" rates the fit instead of showing it.',
|
|
186
949
|
suggestion: "Cut the adverb, or say what actually lined up.",
|
|
187
950
|
examples_bad: [
|
|
188
951
|
"The argument splits cleanly into two parts.",
|
|
189
|
-
"The patch applies cleanly to main.",
|
|
190
952
|
"The new taxonomy maps cleanly onto the old one.",
|
|
191
|
-
"The objection
|
|
953
|
+
"The objection divides cleanly in half, and neither side survives."
|
|
192
954
|
],
|
|
193
955
|
examples_ok: [
|
|
194
956
|
"The branch merged without conflicts.",
|
|
195
957
|
"She wiped the counter clean.",
|
|
196
|
-
"The build finished with no warnings."
|
|
958
|
+
"The build finished with no warnings.",
|
|
959
|
+
# The engineering idiom the rule used to flag on purpose. A patch that
|
|
960
|
+
# applies cleanly either did or did not; there is nothing being rated.
|
|
961
|
+
"The patch applies cleanly to main.",
|
|
962
|
+
# Wording follows NTSB AAR-14/01 and NASA SEL-84-101 (US government
|
|
963
|
+
# works) and An Introduction to Machine Drawing and Design (1890).
|
|
964
|
+
"The main landing gear separated cleanly from the airplane.",
|
|
965
|
+
"The code should be cleanly compiled beforehand.",
|
|
966
|
+
"Your answers should be clearly and cleanly drawn in pencil."
|
|
197
967
|
],
|
|
198
968
|
rationale: "The adverb rates the join instead of showing it, and it rates before the " \
|
|
199
969
|
"reader has anything to check. Real material resists -- the leftover case, " \
|
|
@@ -235,9 +1005,9 @@ module Sloplint
|
|
|
235
1005
|
# in front of a speech verb, and the concrete-capable nouns (cut, line,
|
|
236
1006
|
# version, split) are left out entirely.
|
|
237
1007
|
pattern: /\bcleanest\s+(?:\w+\s+){0,2}
|
|
238
|
-
(
|
|
239
|
-
|
|
|
240
|
-
|
|
|
1008
|
+
(?:#{WRITERS_OWN_CONSTRUCTION_NOUNS}|comparison
|
|
1009
|
+
|statement|account|argument|idea|definition|summary
|
|
1010
|
+
|reading|take|point|story|explanation|distinction)\b
|
|
241
1011
|
|\bcleanest\s+way\s+to\s+(?:say|put|frame|state|describe|phrase|express
|
|
242
1012
|
|think\s+about)\b/ix,
|
|
243
1013
|
message: '"The cleanest framing/way to put it…" ranks your own claim for the reader.',
|
|
@@ -265,7 +1035,7 @@ module Sloplint
|
|
|
265
1035
|
# "break" is admitted only in "clean break between", never bare, because
|
|
266
1036
|
# "make a clean break with the past" is an idiom and not a tell.
|
|
267
1037
|
pattern: /\b(?:a|the|one)\s+(?:\w+\s+)?clean\s+(?:\w+\s+)?
|
|
268
|
-
(
|
|
1038
|
+
(?:#{WRITERS_OWN_CONSTRUCTION_NOUNS}|distinction|through-line
|
|
269
1039
|
|story|answer|argument|split|divide)\b
|
|
270
1040
|
|\bclean\s+(?:line|break|split)\s+between\b/ix,
|
|
271
1041
|
message: '"A clean abstraction / clean framing" praises the idea instead of showing it.',
|
|
@@ -340,6 +1110,104 @@ module Sloplint
|
|
|
340
1110
|
"are three words apart and identical on the surface. An agent reading the flag " \
|
|
341
1111
|
"has the rest of the sentence to judge; the pattern alone doesn't."
|
|
342
1112
|
),
|
|
1113
|
+
Rule.new(
|
|
1114
|
+
id: "real-x-real-y",
|
|
1115
|
+
category: "rhetorical-tic",
|
|
1116
|
+
severity: "info",
|
|
1117
|
+
# Ships at info, not warning: the only two hits the probe found in
|
|
1118
|
+
# 1.28M words were both false positives, and one corpus of one
|
|
1119
|
+
# register is thin evidence next to honestly, which sits at warning
|
|
1120
|
+
# on 9.7M words across two. A doubled "real" is also defensible on
|
|
1121
|
+
# its own ("No real financial loss, but a real failure") in a way
|
|
1122
|
+
# the warning rules around it are not. The agent reading the flag
|
|
1123
|
+
# decides.
|
|
1124
|
+
#
|
|
1125
|
+
# "If it can trigger real API calls or hold real credentials, it needs
|
|
1126
|
+
# the same rigor." -- the same intensifier, said twice in one sentence,
|
|
1127
|
+
# each time in front of a different noun. Bare "real" is an ordinary
|
|
1128
|
+
# word (a real number, real time, a real problem) and cannot be flagged
|
|
1129
|
+
# on its own; the narrowing here is repetition, not a noun list --
|
|
1130
|
+
# requiring two attributive uses in one sentence, naming two different
|
|
1131
|
+
# things, is what tells the doubled intensifier apart from plain
|
|
1132
|
+
# English, and it needs no list of nouns to do it.
|
|
1133
|
+
#
|
|
1134
|
+
# "Attributive" is enforced by requiring "real" to run straight into
|
|
1135
|
+
# the word it modifies: a space, then a letter, with no comma or
|
|
1136
|
+
# conjunction between. That already keeps the predicative use out
|
|
1137
|
+
# ("The risk is real, and it is growing" -- its own rule, above) since
|
|
1138
|
+
# a predicate "real" is followed by punctuation or "and", never
|
|
1139
|
+
# directly by the next noun.
|
|
1140
|
+
#
|
|
1141
|
+
# A hyphen touching "real" on either side takes it out of the running:
|
|
1142
|
+
# "real-time" and "real-world" are compounds, one modifier, not two
|
|
1143
|
+
# independent uses of the intensifier, and "non-real" is a negation,
|
|
1144
|
+
# not the intensifier at all. (?<!-) and (?!-) drop all three.
|
|
1145
|
+
#
|
|
1146
|
+
# The second "real" must name a word the first one didn't -- a
|
|
1147
|
+
# backreference, so "a real risk ... that real risk" (the same thing,
|
|
1148
|
+
# referred to twice) is ordinary reference, not the tic. Two mentions
|
|
1149
|
+
# only look alike when they're both new: two different real things.
|
|
1150
|
+
#
|
|
1151
|
+
# The closed list after each "real" (REAL_X_REAL_Y_EXCLUDED_NEXT_WORD,
|
|
1152
|
+
# above) is the handful of fixed senses that are common in this
|
|
1153
|
+
# register and are never the tell, plus the function words that only
|
|
1154
|
+
# ever continue a predicative "is real". This gives up "real user"
|
|
1155
|
+
# outside "real user monitoring" and any doubled "real name"/"real
|
|
1156
|
+
# names" pairing that differs only by a plural -- known, deliberate
|
|
1157
|
+
# recall losses rather than a growing list.
|
|
1158
|
+
#
|
|
1159
|
+
# The gap between the two "real"s crosses a hard-wrapped line the same
|
|
1160
|
+
# way WRAP_GAP does (a newline is fine unless it opens a paragraph
|
|
1161
|
+
# break, PARA_BREAK, since a non-breaking space alone on the line in
|
|
1162
|
+
# between still reads as a blank line to the editors that emit one),
|
|
1163
|
+
# but never a sentence-ending mark, so the two "real"s must fall in
|
|
1164
|
+
# one sentence.
|
|
1165
|
+
pattern: /
|
|
1166
|
+
(?<!-)\breal(?!-)[ \t]+
|
|
1167
|
+
(?!(?:#{REAL_X_REAL_Y_EXCLUDED_NEXT_WORD})\b)
|
|
1168
|
+
(?>([a-z][\w'’-]*))\b
|
|
1169
|
+
(?:(?!\breal\b)[^.!?\n]|(?!#{PARA_BREAK})\r?\n){1,200}?
|
|
1170
|
+
(?<!-)\breal(?!-)[ \t]+
|
|
1171
|
+
(?!(?:#{REAL_X_REAL_Y_EXCLUDED_NEXT_WORD})\b)
|
|
1172
|
+
(?!\1\b)
|
|
1173
|
+
(?>[a-z][\w'’-]*)\b
|
|
1174
|
+
/ix,
|
|
1175
|
+
message: 'Doubled "real" ("real X … real Y") repeats the intensifier for emphasis.',
|
|
1176
|
+
suggestion: "Cut one 'real,' or say what actually makes each thing real -- a name, a number, a log line.",
|
|
1177
|
+
examples_bad: [
|
|
1178
|
+
"If it can trigger real API calls or hold real credentials, it needs the same rigor.",
|
|
1179
|
+
"The demo used real customer data and exposed real financial records to the whole team.",
|
|
1180
|
+
"This isn't a mockup; it hits a real database and charges a real credit card.",
|
|
1181
|
+
"The incident caused real financial losses and real reputational damage to the company."
|
|
1182
|
+
],
|
|
1183
|
+
examples_ok: [
|
|
1184
|
+
# The second "real" must name something the first didn't.
|
|
1185
|
+
"The system exposes a real risk, and that real risk must be tracked.",
|
|
1186
|
+
# A hyphen on either side makes it a compound, not two uses.
|
|
1187
|
+
"The dashboard shows real-time metrics and real-time alerts.",
|
|
1188
|
+
# "real world" is on the closed list even unhyphenated.
|
|
1189
|
+
"In the real world this rarely happens, and the real world rewards patience.",
|
|
1190
|
+
# The math sense, both nouns.
|
|
1191
|
+
"The equation has two real roots and no real numbers outside that range.",
|
|
1192
|
+
# "real user" (monitoring) and a hyphenated compound together.
|
|
1193
|
+
"The tool combines real user monitoring with real-time dashboards.",
|
|
1194
|
+
# Two more closed-list nouns.
|
|
1195
|
+
"She invested in real estate and lost real money.",
|
|
1196
|
+
# "non-real" is a negation, not a second use of the intensifier.
|
|
1197
|
+
"The audit flagged real deployment risk but no non-real anomalies.",
|
|
1198
|
+
# Only one "real" in the sentence.
|
|
1199
|
+
"This is a real problem worth solving.",
|
|
1200
|
+
# A paragraph break -- even one where the blank line holds only a
|
|
1201
|
+
# non-breaking space -- ends the sentence; the two paragraphs are
|
|
1202
|
+
# never joined into one hit.
|
|
1203
|
+
"This plan needs real signoff\n \nbefore it touches real production data."
|
|
1204
|
+
],
|
|
1205
|
+
rationale: "\"Real\" is the plainest way to say a thing isn't fake or hypothetical, and it " \
|
|
1206
|
+
"only needs saying once a sentence -- naming it again in front of a second noun " \
|
|
1207
|
+
"doesn't add information, it repeats the reassurance. A person defending a claim " \
|
|
1208
|
+
"from two directions in the same breath writes the two facts and lets one " \
|
|
1209
|
+
"\"real\" cover both."
|
|
1210
|
+
),
|
|
343
1211
|
Rule.new(
|
|
344
1212
|
id: "the-punchline-is",
|
|
345
1213
|
category: "rhetorical-tic",
|
|
@@ -516,7 +1384,64 @@ module Sloplint
|
|
|
516
1384
|
id: "exact-exactly",
|
|
517
1385
|
category: "rhetorical-tic",
|
|
518
1386
|
severity: "info",
|
|
519
|
-
|
|
1387
|
+
# Rewritten after 2.2M words of technical prose left ~111 of 125 hits
|
|
1388
|
+
# false. The shape was the problem, not the entries: the pattern matched
|
|
1389
|
+
# "exact" everywhere and subtracted an allow-list, and an open pattern
|
|
1390
|
+
# with a growing exception list can never close over a common word.
|
|
1391
|
+
# Machining and investigation prose puts "exact" in front of anything an
|
|
1392
|
+
# instrument reads -- diameter, pitch angle, CPU time, blade setting,
|
|
1393
|
+
# five threads, perpendicular, on center -- so each addition bought one
|
|
1394
|
+
# false positive and risked silencing the cliche sitting behind it.
|
|
1395
|
+
#
|
|
1396
|
+
# So the rule now matches the tell instead, and the tell is a small
|
|
1397
|
+
# closed set of frames: a demonstrative copula carrying the intensifier
|
|
1398
|
+
# ("that's exactly", "this is exactly the kind of"), the bare evaluative
|
|
1399
|
+
# complement ("exactly right"), the deictic noun ("exactly the point",
|
|
1400
|
+
# "the exact problem"), and a knowing verb plus a wh-word ("I know
|
|
1401
|
+
# exactly why"). Everything else is silent by construction rather than
|
|
1402
|
+
# by exception.
|
|
1403
|
+
#
|
|
1404
|
+
# Two recall losses, both deliberate. "exactly the right words" is the
|
|
1405
|
+
# tell and "reamed to exactly the right size" is a measurement, and they
|
|
1406
|
+
# differ only in their subject, which a regex cannot see -- so that
|
|
1407
|
+
# frame stays out unless a copula carries it. And "determined exactly
|
|
1408
|
+
# when" is precise where "know exactly why" is filler, so only the
|
|
1409
|
+
# knowing verbs take the wh-branch.
|
|
1410
|
+
pattern: /\b(?:
|
|
1411
|
+
# "That's exactly ...", "This is exactly the kind of ...".
|
|
1412
|
+
# The complement is open here, so it needs the checkable
|
|
1413
|
+
# allow-list back as a guard, or the copula re-admits every
|
|
1414
|
+
# measurement the rest of the rule was rewritten to drop:
|
|
1415
|
+
# "That's the exact same design", "It was exactly noon".
|
|
1416
|
+
# "here" and "there" are not subjects: the existential
|
|
1417
|
+
# "there is exactly one solution" is a count, so checkable.
|
|
1418
|
+
(?:that|this|it|these|those|which)
|
|
1419
|
+
(?:'s|\u2019s|\s+(?:is|was|are|were))\s+
|
|
1420
|
+
(?:the\s+)?exact(?:ly)?\b
|
|
1421
|
+
(?!\s*(?:
|
|
1422
|
+
(?:the\s+)?
|
|
1423
|
+
(?:same|opposite|science|change|replica|cop(?:y|ies)|
|
|
1424
|
+
location|coordinates|way)\b
|
|
1425
|
+
|[$\d]|noon\b|midnight\b|o'?\s*clock\b
|
|
1426
|
+
|(?:one|two|three|four|five|six|seven|eight|nine|ten|
|
|
1427
|
+
eleven|twelve|dozen|twenty|thirty|fifty|hundred)\b
|
|
1428
|
+
|(?:the\s+|a\s+|an\s+)?(?:[\w-]+\s+){0,2}
|
|
1429
|
+
(?:diameter|radius|size|dimensions?|length|width|
|
|
1430
|
+
height|depth|thickness|weight|mass|volume|
|
|
1431
|
+
angle|pitch|temperature|pressure|speed|altitude|
|
|
1432
|
+
position|distance|clearance|tolerance)\b))
|
|
1433
|
+
# "exactly right", "exactly backwards"
|
|
1434
|
+
| exactly\s+(?:right|wrong|backwards|so)\b
|
|
1435
|
+
# "exactly the point", "the exact problem"
|
|
1436
|
+
| exactly\s+the\s+
|
|
1437
|
+
(?:point|problem|issue|reason|kind|sort|type|thing|
|
|
1438
|
+
question|shape|word)\b
|
|
1439
|
+
| the\s+exact\s+
|
|
1440
|
+
(?:point|problem|issue|reason|thing|question|shape)\b
|
|
1441
|
+
# "I know exactly why this happened"
|
|
1442
|
+
| know(?:s|n)?\s+exactly\s+(?:why|what|how|who)\b
|
|
1443
|
+
| knew\s+exactly\s+(?:why|what|how|who)\b
|
|
1444
|
+
)/ix,
|
|
520
1445
|
message: '"exact/exactly" is reflexive emphasis unless it names something checkable.',
|
|
521
1446
|
suggestion: "Cut it, or replace with the number, name, or match it's supposed to be precise about.",
|
|
522
1447
|
examples_bad: [
|
|
@@ -539,7 +1464,25 @@ module Sloplint
|
|
|
539
1464
|
"The train left at exactly noon.",
|
|
540
1465
|
"They agreed to meet at exactly midnight.",
|
|
541
1466
|
"The meeting starts at exactly 3 o'clock.",
|
|
542
|
-
"She has exacting standards for her students."
|
|
1467
|
+
"She has exacting standards for her students.",
|
|
1468
|
+
# A measured quantity and a determined fact, pinning the two branches
|
|
1469
|
+
# added above. Wording follows "Turning and Boring" (1919, public
|
|
1470
|
+
# domain by date) and NTSB AAR-04/01 (US government work).
|
|
1471
|
+
"The soft jaws are bored to the exact diameter of the finished rim.",
|
|
1472
|
+
"Investigators could not determine the exact blade pitch angle.",
|
|
1473
|
+
"The bore is finished with a reamer to exactly the right size and taper.",
|
|
1474
|
+
"It could not be determined exactly when the fire began.",
|
|
1475
|
+
# The demonstrative copula leaves its complement open, so each of the
|
|
1476
|
+
# checkable uses is pinned again in that frame -- review found the
|
|
1477
|
+
# branch re-admitting the whole allow-list it was meant to replace.
|
|
1478
|
+
"That's the exact same design we shipped last year.",
|
|
1479
|
+
"These are the exact coordinates of the wreck.",
|
|
1480
|
+
"It was exactly noon when the whistle blew.",
|
|
1481
|
+
"This is the exact replica of the ship.",
|
|
1482
|
+
# Existential "there"/"here" carry a count, which is checkable.
|
|
1483
|
+
"There is exactly one solution to the equation.",
|
|
1484
|
+
"There are exactly 24 hours in a day.",
|
|
1485
|
+
"Here is the exact temperature at the time of the failure."
|
|
543
1486
|
],
|
|
544
1487
|
rationale: "Models reach for 'exact/exactly' as filler emphasis on a claim with nothing to " \
|
|
545
1488
|
"check; it earns its place only next to a number, a name, or a stated identity."
|
|
@@ -594,6 +1537,293 @@ module Sloplint
|
|
|
594
1537
|
"structure. Models borrow it as a metaphor for anything important, which just " \
|
|
595
1538
|
"restates the sentence's importance without saying what actually holds it up."
|
|
596
1539
|
),
|
|
1540
|
+
Rule.new(
|
|
1541
|
+
id: "intersection-of",
|
|
1542
|
+
category: "rhetorical-tic",
|
|
1543
|
+
severity: "warning",
|
|
1544
|
+
# "at" is not load-bearing -- "explores the intersection of art and
|
|
1545
|
+
# technology" is the same move -- so the anchor is "the intersection of"
|
|
1546
|
+
# and the two guards carry the whole burden of separating the literal
|
|
1547
|
+
# senses. A street corner names its streets, and a street name is
|
|
1548
|
+
# Capitalized-then-lowercase ("Elm", "Broadway", "Highway 12"), so the
|
|
1549
|
+
# positive lookahead demands the next word be lowercase or an all-caps
|
|
1550
|
+
# acronym -- "AI", "UX", "HCI" are the metaphor's favourite operands and
|
|
1551
|
+
# no street is spelled that way. Geometry and set arithmetic name their
|
|
1552
|
+
# operands ("the two curves", "the ranges", "both key sets"), so the
|
|
1553
|
+
# negative lookahead drops a literal noun found within two words of
|
|
1554
|
+
# "of". Two words, not three, keeps "art and city streets" from reaching
|
|
1555
|
+
# "streets" and silencing a real hit. No /i on the whole pattern: it
|
|
1556
|
+
# would make [a-z] match capitals and undo the first guard, so the
|
|
1557
|
+
# case-insensitive parts are inline (?i:...) groups instead.
|
|
1558
|
+
#
|
|
1559
|
+
# Three additions after the rule met accident reports and reference
|
|
1560
|
+
# documentation, where every hit was literal and each broke a guard.
|
|
1561
|
+
# Airfield surfaces (runway, taxiway, apron) are as literal as a street
|
|
1562
|
+
# and are written lowercase, so the first guard was actively admitting
|
|
1563
|
+
# them. A matrix cell is the same literal sense as a set intersection,
|
|
1564
|
+
# so rows and columns join the geometry list. And the all-caps
|
|
1565
|
+
# allowance, added for "AI"/"UX"/"HCI" on the premise that no street is
|
|
1566
|
+
# spelled that way, was admitting American route designators. The
|
|
1567
|
+
# trailing (?!-\d) on that branch drops a route number ("US-27A"), and
|
|
1568
|
+
# one more lookahead drops a quadrant plus a house number ("NE 140th
|
|
1569
|
+
# Court"). Street-type nouns are deliberately NOT added to the literal
|
|
1570
|
+
# list: "court", "drive" and "place" are ordinary abstract nouns, and
|
|
1571
|
+
# the two-word window would reach them as the second operand and
|
|
1572
|
+
# silence "the intersection of memory and place".
|
|
1573
|
+
pattern: /\b[Tt]he\s+intersection\s+of\b
|
|
1574
|
+
(?!\s+(?:[\w-]+\s+){0,2}
|
|
1575
|
+
(?i:sets?|lines?|curves?|planes?|circles?|spheres?|axes|
|
|
1576
|
+
rays?|segments?|arcs?|orbits?|streets?|avenues?|
|
|
1577
|
+
roads?|highways?|routes?|tracks?|corridors?|paths?|
|
|
1578
|
+
boulevards?|lanes?|arrays?|lists?|ranges?|
|
|
1579
|
+
collections?|keys?|vectors?|matrices|polygons?|
|
|
1580
|
+
rectangles?|intervals?|data|datasets?|
|
|
1581
|
+
runways?|taxiways?|taxilanes?|aprons?|
|
|
1582
|
+
rows?|columns?|cells?)\b)
|
|
1583
|
+
(?!\s+[A-Z]{1,3}\s+\d)
|
|
1584
|
+
(?=\s+(?:[a-z]|[A-Z]{2,}\b(?!-\d)))/x,
|
|
1585
|
+
message: '"the intersection of X and Y" outside streets or geometry is borrowed positioning.',
|
|
1586
|
+
suggestion: "Say what the work does, or name the two things it takes from each.",
|
|
1587
|
+
examples_bad: [
|
|
1588
|
+
"Her work sits at the intersection of art and technology.",
|
|
1589
|
+
"Her book explores the intersection of art and technology.",
|
|
1590
|
+
"We operate at the intersection of AI and healthcare.",
|
|
1591
|
+
"The product lives at the intersection of design and engineering.",
|
|
1592
|
+
"At the intersection of policy and practice, nothing moves quickly.",
|
|
1593
|
+
"The intersection of grief and comedy is where this essay lands.",
|
|
1594
|
+
"The role sits at the intersection of the marketing and product teams."
|
|
1595
|
+
],
|
|
1596
|
+
examples_ok: [
|
|
1597
|
+
"The accident happened at the intersection of Elm Street and Oak Avenue.",
|
|
1598
|
+
"Turn left at the intersection of Main and Fifth.",
|
|
1599
|
+
"The bus stops at the intersection of Broadway and 42nd Street.",
|
|
1600
|
+
"A hydrant stands at the intersection of Elm and Willow.",
|
|
1601
|
+
"The house sits at the intersection of Highway 12 and County Road 8.",
|
|
1602
|
+
"The intersection of Elm and Willow was closed for repaving.",
|
|
1603
|
+
"The solution lies at the intersection of the two curves.",
|
|
1604
|
+
"Draw a point at the intersection of the lines.",
|
|
1605
|
+
"The point at the intersection of two circles is equidistant.",
|
|
1606
|
+
"He stood at the intersection of five streets and could not choose.",
|
|
1607
|
+
"The village grew up at the intersection of several old roads.",
|
|
1608
|
+
"Snow piled up at the intersection of the roads below.",
|
|
1609
|
+
"Compute the intersection of the two arrays.",
|
|
1610
|
+
"The query returns the intersection of both key sets.",
|
|
1611
|
+
"The intersection of the ranges is empty.",
|
|
1612
|
+
# Airfield surfaces, route designators and matrix cells, each pinning
|
|
1613
|
+
# one of the three guards added above. Wording follows NTSB AAR-16/02,
|
|
1614
|
+
# AAR-14/01 and HAR-17/02 and the NASA Software Safety Guidebook
|
|
1615
|
+
# (NASA-GB-8719.13) -- US government works, public domain.
|
|
1616
|
+
"The airplane crossed the intersection of runway 13 with runway 4.",
|
|
1617
|
+
"Firefighters gathered near the intersection of taxiway N and taxiway F.",
|
|
1618
|
+
"The crash occurred at the intersection of US-27A and NE 140th Court.",
|
|
1619
|
+
"Traffic was heaviest at the intersection of NE 140th Court.",
|
|
1620
|
+
"The square at the intersection of a row and a column contains a code."
|
|
1621
|
+
],
|
|
1622
|
+
rationale: "A street corner and a set diagram are the phrase's literal homes. Everywhere " \
|
|
1623
|
+
"else it claims a position without doing the work of one: the writer sits " \
|
|
1624
|
+
"between two fields and says nothing about either. Models open bios and " \
|
|
1625
|
+
"pitches with it because it sounds like a thesis while committing to nothing."
|
|
1626
|
+
),
|
|
1627
|
+
Rule.new(
|
|
1628
|
+
id: "impact-verb",
|
|
1629
|
+
category: "rhetorical-tic",
|
|
1630
|
+
severity: "warning",
|
|
1631
|
+
# "impact" and "impacts" are also nouns, so they only count as verbs
|
|
1632
|
+
# behind an auxiliary or a subject pronoun. One optional pronoun may sit
|
|
1633
|
+
# between the auxiliary and the verb ("does this impact the date");
|
|
1634
|
+
# allowing a determiner in that slot instead would swallow "will the
|
|
1635
|
+
# impact be permanent", so the copula lookahead below backs it up.
|
|
1636
|
+
# "impacted"/"impacting" are verbal on their own except for the medical
|
|
1637
|
+
# and soil sense: the forward lookahead catches the attributive form
|
|
1638
|
+
# ("an impacted wisdom tooth"), but the predicate form ("the tooth was
|
|
1639
|
+
# impacted") needs the noun and copula pulled into the match, because
|
|
1640
|
+
# skip: only ever sees matched text, never surrounding context. Same
|
|
1641
|
+
# reason load-bearing does it, and a lookbehind that wide trips the
|
|
1642
|
+
# Onigmo bug documented there. The trailing (?!-) is load-bearing too:
|
|
1643
|
+
# every other guard is spelled with \s+, so "will impact-test the
|
|
1644
|
+
# housing" walks straight past them.
|
|
1645
|
+
#
|
|
1646
|
+
# Accident reports found two holes. "to impact" was in the auxiliary
|
|
1647
|
+
# list as an infinitive marker, so it also matched the preposition in
|
|
1648
|
+
# "4 seconds prior to impact" and "time to impact"; that branch now
|
|
1649
|
+
# stands on its own and requires a following object. And "impacted" in
|
|
1650
|
+
# those reports is usually one object hitting another -- an airplane
|
|
1651
|
+
# impacts terrain, debris impacts a wing -- which is the collision noun
|
|
1652
|
+
# the rule already excludes, just on the other side of the verb, so the
|
|
1653
|
+
# struck-object list mirrors it.
|
|
1654
|
+
pattern: /\b(?:
|
|
1655
|
+
(?:will|would|can|could|may|might|shall|should|must|does|
|
|
1656
|
+
do|did|doesn't|don't|didn't|won't|helps?|helped)\s+
|
|
1657
|
+
(?:(?:it|this|that|they|we|you)\s+)?
|
|
1658
|
+
impacts?
|
|
1659
|
+
| to\s+impacts?
|
|
1660
|
+
(?=\s+(?!and\b|or\b|but\b|in\b|on\b|at\b|of\b|for\b|from\b|
|
|
1661
|
+
with\b|during\b|after\b|before\b|than\b)\w)
|
|
1662
|
+
| (?:it|this|that|which|he|she)\s+impacts
|
|
1663
|
+
| (?:they|we|you)\s+impact
|
|
1664
|
+
| (?:(?:tooth|teeth|molars?|bowels?|colon|fractures?|soils?)\s+
|
|
1665
|
+
(?:is|are|was|were)\s+)?
|
|
1666
|
+
impact(?:ed|ing)
|
|
1667
|
+
)\b
|
|
1668
|
+
(?!-)
|
|
1669
|
+
(?!\s+(?:be|been|is|are|was|were|of|on|has|have|had)\b)
|
|
1670
|
+
(?!\s+(?:tooth|teeth|molars?|wisdom|canines?|bowels?|colon|
|
|
1671
|
+
stool|feces|fecal|fractures?|soils?|snow|ice|sediment|
|
|
1672
|
+
gravel|earwax|cerumen)\b)
|
|
1673
|
+
(?!\s+(?:the\s+|a\s+|an\s+)?(?:[\w-]+\s+)?
|
|
1674
|
+
(?:terrain|ground|seabed|seawall|treetops|trees?|
|
|
1675
|
+
grove|thicket|hillside|embankment|berm|
|
|
1676
|
+
escarpment|ridgeline|cliff|
|
|
1677
|
+
runway|taxiway|apron|tarmac|guardrail|
|
|
1678
|
+
fuselage|nacelle|airframe|empennage|rotor|
|
|
1679
|
+
windshield|bulkhead|revetment|abutment|
|
|
1680
|
+
wing|wingtip|stabilizer|landing\s+gear)\b)
|
|
1681
|
+
(?!\s+(?:statements?|assessments?|reports?|studies|study|
|
|
1682
|
+
analys[ie]s|evaluations?|factors?|ratings?|scores?|
|
|
1683
|
+
investing|investors?|funds?|bonds?|craters?|
|
|
1684
|
+
wrench(?:es)?|drivers?|sockets?|printers?|sprinklers?|
|
|
1685
|
+
resistan(?:t|ce)|tested|testing|velocit(?:y|ies)|
|
|
1686
|
+
forces?|energy|load(?:s|ing)?|zones?|points?|players?|
|
|
1687
|
+
sites?|angles?|damage|absorbers?)\b)/ix,
|
|
1688
|
+
skip: [/\A(?:tooth|teeth|molars?|bowels?|colon|fractures?|soils?)\s+
|
|
1689
|
+
(?:is|are|was|were)\s+impacted/ix],
|
|
1690
|
+
message: '"impact" as a verb hides which way something moved and by how much.',
|
|
1691
|
+
suggestion: 'Name the verb: cut, delayed, doubled, broke, raised. Or use "affected".',
|
|
1692
|
+
examples_bad: [
|
|
1693
|
+
"The outage impacted about four thousand accounts.",
|
|
1694
|
+
"Changing the default will impact every downstream job.",
|
|
1695
|
+
"How does this impact the release date?",
|
|
1696
|
+
"Rising rates impacted our hiring plan.",
|
|
1697
|
+
"The migration impacted performance across the board.",
|
|
1698
|
+
"This impacts every downstream job.",
|
|
1699
|
+
"The new policy is impacting our margins."
|
|
1700
|
+
],
|
|
1701
|
+
examples_ok: [
|
|
1702
|
+
"The impact crushed the front bumper.",
|
|
1703
|
+
"The crater marks the point of impact.",
|
|
1704
|
+
"Torque the bolts with an impact wrench.",
|
|
1705
|
+
"The meteor impact left a ring of debris.",
|
|
1706
|
+
"The parachute reduces impact force on landing.",
|
|
1707
|
+
"The county filed an environmental impact statement.",
|
|
1708
|
+
"The fund runs an impact investing strategy.",
|
|
1709
|
+
"The journal reports a five-year impact factor.",
|
|
1710
|
+
"He bought an impact driver for the deck job.",
|
|
1711
|
+
"She is an impact player off the bench.",
|
|
1712
|
+
"She had an impacted wisdom tooth removed.",
|
|
1713
|
+
"The tooth was impacted and had to come out.",
|
|
1714
|
+
"The soil was impacted by years of heavy machinery.",
|
|
1715
|
+
"An impacted fracture heals without displacement.",
|
|
1716
|
+
"Will the impact be permanent?",
|
|
1717
|
+
"Consultants will impact-test the housing next week.",
|
|
1718
|
+
# One object striking another, and the preposition that was reading as
|
|
1719
|
+
# an infinitive. Wording follows NTSB AAR-06/03 and AAR-14/01 and the
|
|
1720
|
+
# Columbia Accident Investigation Board report -- US government works.
|
|
1721
|
+
"The airplane impacted terrain about 300 feet north of the threshold.",
|
|
1722
|
+
"The foam debris impacted the left wing shortly after separation.",
|
|
1723
|
+
"The stick shaker activated 4 seconds prior to impact.",
|
|
1724
|
+
"The chart plots time to impact in seconds."
|
|
1725
|
+
],
|
|
1726
|
+
rationale: "As a verb, 'impact' reports that something changed while withholding the " \
|
|
1727
|
+
"direction and the size. The specific verb -- slowed, doubled, broke -- " \
|
|
1728
|
+
"carries what the sentence is missing, and 'affected' covers the rest. " \
|
|
1729
|
+
"Models reach for it because it sounds consequential at no cost."
|
|
1730
|
+
),
|
|
1731
|
+
Rule.new(
|
|
1732
|
+
id: "impact-noun-vague",
|
|
1733
|
+
category: "puffery",
|
|
1734
|
+
severity: "warning",
|
|
1735
|
+
# Only the puffed shapes: an intensity adjective, or make/have plus an
|
|
1736
|
+
# article. "the impact of X" is left to impact-noun-bare, which sits at
|
|
1737
|
+
# info because it is the standard word in research prose. "positive" and
|
|
1738
|
+
# "negative" stay out of the adjective list on purpose -- they name a
|
|
1739
|
+
# direction, which is more than the intensity words do. "statistically"
|
|
1740
|
+
# is pulled into the match as an optional leading word so skip: can see
|
|
1741
|
+
# it; a statistically significant impact is a finding, not puffery, and
|
|
1742
|
+
# skip: never sees text outside the matched span.
|
|
1743
|
+
pattern: /\b(?:statistically\s+)?
|
|
1744
|
+
(?:
|
|
1745
|
+
(?:significant|real|meaningful|lasting|massive|huge|profound|
|
|
1746
|
+
big|major|tremendous|enormous|outsized|considerable|
|
|
1747
|
+
substantial|genuine|tangible|immense|incredible|
|
|
1748
|
+
remarkable)\s+
|
|
1749
|
+
| (?:mak(?:e|es|ing)|made|hav(?:e|es|ing)|has|had)\s+
|
|
1750
|
+
(?:a|an|real|some)\s+
|
|
1751
|
+
)
|
|
1752
|
+
impacts?\b(?!-)
|
|
1753
|
+
(?!\s+(?:statements?|assessments?|reports?|studies|study|
|
|
1754
|
+
analys[ie]s|evaluations?|factors?|ratings?|scores?|
|
|
1755
|
+
investing|investors?|funds?|bonds?|craters?|
|
|
1756
|
+
wrench(?:es)?|drivers?|sockets?|printers?|sprinklers?|
|
|
1757
|
+
resistan(?:t|ce)|tested|testing|velocit(?:y|ies)|
|
|
1758
|
+
forces?|energy|load(?:s|ing)?|zones?|points?|players?|
|
|
1759
|
+
sites?|angles?|damage|absorbers?)\b)/ix,
|
|
1760
|
+
skip: [/\Astatistically\s/i],
|
|
1761
|
+
message: '"significant/real/big impact" and "make an impact" inflate a claim without stating it.',
|
|
1762
|
+
suggestion: "Say what changed and by how much, or cut the sentence.",
|
|
1763
|
+
examples_bad: [
|
|
1764
|
+
"The change had a significant impact on conversion.",
|
|
1765
|
+
"This will have real impact for the team.",
|
|
1766
|
+
"The rewrite made a big impact on load times.",
|
|
1767
|
+
"The launch had a profound impact on morale.",
|
|
1768
|
+
"We want to make an impact this quarter."
|
|
1769
|
+
],
|
|
1770
|
+
examples_ok: [
|
|
1771
|
+
"The study found a statistically significant impact on mortality.",
|
|
1772
|
+
"The impact crushed the front bumper.",
|
|
1773
|
+
"The helmet failed at high-impact loading.",
|
|
1774
|
+
"Impact-resistant polycarbonate replaced the glass.",
|
|
1775
|
+
"The blast impact zone extended two hundred metres.",
|
|
1776
|
+
"That impact rating exceeds the standard.",
|
|
1777
|
+
"Is this impact reversible?"
|
|
1778
|
+
],
|
|
1779
|
+
rationale: "The adjective does the work the sentence should have done: 'significant' and " \
|
|
1780
|
+
"'real' assert that a change mattered without naming it or sizing it. " \
|
|
1781
|
+
"'Make an impact' is the same move with the noun left bare."
|
|
1782
|
+
),
|
|
1783
|
+
Rule.new(
|
|
1784
|
+
id: "impact-noun-bare",
|
|
1785
|
+
category: "rhetorical-tic",
|
|
1786
|
+
severity: "info",
|
|
1787
|
+
# Two shapes, each needing its own anchor: a measuring verb in front, or
|
|
1788
|
+
# "of" behind. That is what keeps the collision sense clear without a
|
|
1789
|
+
# list of collision verbs -- "the impact crushed the front bumper" has
|
|
1790
|
+
# neither anchor, and "the point of impact" runs the other way round.
|
|
1791
|
+
# The verb stems end in \w*, not \w+: with \w+ the rule misses the bare
|
|
1792
|
+
# imperative "Consider the impact before you merge".
|
|
1793
|
+
pattern: /\b(?:
|
|
1794
|
+
(?:measur|assess|consider|understand|understood|evaluat|
|
|
1795
|
+
gaug|weigh|quantif|track|examin|explor|maximi[sz])\w*\s+
|
|
1796
|
+
(?:the|its|their|our|this|that)\s+impacts?
|
|
1797
|
+
| the\s+impacts?\s+of
|
|
1798
|
+
)\b(?!-)
|
|
1799
|
+
(?!\s+(?:statements?|assessments?|reports?|studies|study|
|
|
1800
|
+
analys[ie]s|evaluations?|factors?|ratings?|scores?|
|
|
1801
|
+
investing|investors?|funds?|bonds?|craters?|
|
|
1802
|
+
wrench(?:es)?|drivers?|sockets?|printers?|sprinklers?|
|
|
1803
|
+
resistan(?:t|ce)|tested|testing|velocit(?:y|ies)|
|
|
1804
|
+
forces?|energy|load(?:s|ing)?|zones?|points?|players?|
|
|
1805
|
+
sites?|angles?|damage|absorbers?)\b)/ix,
|
|
1806
|
+
message: '"the impact of X" defers naming what actually changed.',
|
|
1807
|
+
suggestion: "Name the change itself, or the number that shows it.",
|
|
1808
|
+
examples_bad: [
|
|
1809
|
+
"We measured the impact of the new onboarding flow.",
|
|
1810
|
+
"Consider the impact before you merge.",
|
|
1811
|
+
"The team is still assessing the impact.",
|
|
1812
|
+
"The impact of the change is hard to quantify."
|
|
1813
|
+
],
|
|
1814
|
+
examples_ok: [
|
|
1815
|
+
"The impact crushed the front bumper.",
|
|
1816
|
+
"The crater marks the point of impact.",
|
|
1817
|
+
"The meteor impact left a ring of debris.",
|
|
1818
|
+
"Crews measured the impact crater at dawn.",
|
|
1819
|
+
"The panel reviewed the impact assessment before the vote.",
|
|
1820
|
+
"Torque the bolts with an impact wrench.",
|
|
1821
|
+
"Will the impact be permanent?"
|
|
1822
|
+
],
|
|
1823
|
+
rationale: "This one sits at info because 'the impact of X on Y' is the ordinary word in " \
|
|
1824
|
+
"research and policy writing, not a tell. Elsewhere it postpones the sentence: " \
|
|
1825
|
+
"the writer announces that an effect exists and stops before naming it."
|
|
1826
|
+
),
|
|
597
1827
|
Rule.new(
|
|
598
1828
|
id: "thats-how-x",
|
|
599
1829
|
category: "rhetorical-tic",
|
|
@@ -948,6 +2178,136 @@ module Sloplint
|
|
|
948
2178
|
"promises a confession that the sentence after it rarely delivers. It " \
|
|
949
2179
|
"costs the writer nothing and reads as intimacy the reader did not earn."
|
|
950
2180
|
),
|
|
2181
|
+
Rule.new(
|
|
2182
|
+
id: "honestly",
|
|
2183
|
+
category: "rhetorical-tic",
|
|
2184
|
+
severity: "warning",
|
|
2185
|
+
# The manner adverb, the way "cleanly" is the manner adverb: hung on a
|
|
2186
|
+
# subject that cannot be honest. Two guards, no verb list.
|
|
2187
|
+
#
|
|
2188
|
+
# Terminal position does most of the work. The adverb must close on a
|
|
2189
|
+
# full stop, comma, semicolon or exclamation mark, which is what keeps
|
|
2190
|
+
# the ordinary intensifier out -- "I honestly think", "I honestly don't
|
|
2191
|
+
# know" have the word mid-clause and never match. Sentence-initial
|
|
2192
|
+
# "Honestly," needs no guard of its own: a full stop cannot match \w+,
|
|
2193
|
+
# so the sentence before it can never donate its last word.
|
|
2194
|
+
#
|
|
2195
|
+
# The guard list covers the slot immediately before the adverb, which is
|
|
2196
|
+
# where the discourse marker lives. "And honestly," and "Quite honestly,"
|
|
2197
|
+
# are how people write, and both die there.
|
|
2198
|
+
#
|
|
2199
|
+
# There is no animacy test, because no regex sees animacy. "She answered
|
|
2200
|
+
# honestly." matches, and so does the sentence-final hedge people write
|
|
2201
|
+
# in casual comments ("it's beyond boring honestly"). Both are the known
|
|
2202
|
+
# cost of anchoring on position alone; see rationale for the rate.
|
|
2203
|
+
pattern: /\b(?!(?:and|but|or|nor|yet|so|then|though|although|because|if
|
|
2204
|
+
|quite|more|most|very|really|pretty|fairly|too|also|just
|
|
2205
|
+
|only|now|again|well|i|we|you|he|she|they|it)\b)
|
|
2206
|
+
\w+\b[ \t]+honestly[ \t]*(?=[.,;!])/ix,
|
|
2207
|
+
message: '"Honestly" in the manner slot claims good faith the reader cannot check.',
|
|
2208
|
+
suggestion: "Cut the adverb, or show the thing that makes it honest.",
|
|
2209
|
+
examples_bad: [
|
|
2210
|
+
"These two rows compare honestly.",
|
|
2211
|
+
"The numbers add up honestly.",
|
|
2212
|
+
"The taxonomy maps honestly, which is what the table is for.",
|
|
2213
|
+
"The benchmark reports honestly, which is the whole point.",
|
|
2214
|
+
"It reads honestly; that is what carries the piece."
|
|
2215
|
+
],
|
|
2216
|
+
examples_ok: [
|
|
2217
|
+
# Mid-clause: the ordinary intensifier, and the terminal guard drops it.
|
|
2218
|
+
"I honestly do not know.",
|
|
2219
|
+
"She honestly believes it will work.",
|
|
2220
|
+
# The discourse marker, which the guard list drops.
|
|
2221
|
+
"And honestly, I forgot the deadline.",
|
|
2222
|
+
"Quite honestly, the meeting ran long.",
|
|
2223
|
+
"But honestly, nobody minded."
|
|
2224
|
+
],
|
|
2225
|
+
rationale: "The adverb rates the good faith of the writing instead of showing anything " \
|
|
2226
|
+
"the reader can check, and a table or a benchmark has no good faith to rate. " \
|
|
2227
|
+
"Position is the only anchor, so ordinary English gets caught too: a dialogue " \
|
|
2228
|
+
"tag (\"said Isabel honestly\") and the sentence-final hedge of casual speech " \
|
|
2229
|
+
"(\"it's beyond boring honestly\"). Both are rare -- twice in 3.65M words of " \
|
|
2230
|
+
"public-domain prose, ten times in 6.07M words of pre-2022 Hacker News."
|
|
2231
|
+
),
|
|
2232
|
+
Rule.new(
|
|
2233
|
+
id: "honest-x",
|
|
2234
|
+
category: "rhetorical-tic",
|
|
2235
|
+
severity: "warning",
|
|
2236
|
+
# The noun list is short on purpose, and the words left out are the
|
|
2237
|
+
# point. "An honest answer", "an honest assessment" and "an honest
|
|
2238
|
+
# account" are ordinary English about people -- six hits between them in
|
|
2239
|
+
# 6.07M words of Hacker News, every one of them a person being truthful
|
|
2240
|
+
# rather than a writer praising their own framing -- so they are out.
|
|
2241
|
+
# "man", "mistake", "living", "work" and "opinion" never went in.
|
|
2242
|
+
#
|
|
2243
|
+
# What ships is the set that only ever appears in front of the writer's
|
|
2244
|
+
# own construction, and none of it appears in 9.7M words of either
|
|
2245
|
+
# corpus. "most" and "more" are excluded from the modifier slot so the
|
|
2246
|
+
# superlative belongs to most-honest-x alone.
|
|
2247
|
+
pattern: /\b(?:an|the|one|this|that)[ \t]+(?:(?!most\b|more\b)\w+[ \t]+)?
|
|
2248
|
+
honest[ \t]+(?:\w+[ \t]+)?
|
|
2249
|
+
(?:#{WRITERS_OWN_CONSTRUCTION_NOUNS}|comparison|accounting|through-line)\b/ix,
|
|
2250
|
+
message: '"An honest comparison / the honest framing" praises the writing, not the thing.',
|
|
2251
|
+
suggestion: "Cut the adjective and make the comparison; the reader decides if it is honest.",
|
|
2252
|
+
examples_bad: [
|
|
2253
|
+
"That gives us an honest comparison of the two.",
|
|
2254
|
+
"The honest framing is that nobody knew.",
|
|
2255
|
+
"This is an honest accounting of what the delay cost.",
|
|
2256
|
+
"What we want is an honest mapping from one schema to the other."
|
|
2257
|
+
],
|
|
2258
|
+
examples_ok: [
|
|
2259
|
+
# The idioms, which is where "honest" lives in ordinary prose.
|
|
2260
|
+
"He was an honest man.",
|
|
2261
|
+
"It was an honest mistake.",
|
|
2262
|
+
"She made an honest living.",
|
|
2263
|
+
"That is an honest day's work.",
|
|
2264
|
+
# A person being truthful, which is not a claim about the writing.
|
|
2265
|
+
"How could you expect an honest answer to a question like that?",
|
|
2266
|
+
# The superlative belongs to most-honest-x.
|
|
2267
|
+
"The most honest framing is that we guessed."
|
|
2268
|
+
],
|
|
2269
|
+
rationale: "\"Honest\" in front of a framing or a comparison is the writer approving " \
|
|
2270
|
+
"their own work, the same move \"clean\" makes one shelf over. The reader " \
|
|
2271
|
+
"cannot check it and it costs nothing to assert. The narrow noun list is what " \
|
|
2272
|
+
"separates it from the ordinary sense: the shipped pattern flags nothing in " \
|
|
2273
|
+
"3.65M words of public-domain prose or 6.07M words of pre-2022 Hacker News."
|
|
2274
|
+
),
|
|
2275
|
+
Rule.new(
|
|
2276
|
+
id: "most-honest-x",
|
|
2277
|
+
category: "rhetorical-tic",
|
|
2278
|
+
severity: "warning",
|
|
2279
|
+
# The superlative frame carries the tell on its own, so this noun list is
|
|
2280
|
+
# wider than honest-x's -- "the most honest assessment" is self-ranking
|
|
2281
|
+
# in a way "an honest assessment" is not. The list still has to keep out
|
|
2282
|
+
# the ordinary superlative about a person, which is what "the most honest
|
|
2283
|
+
# politician" is, so no human nouns go in. Nothing in either corpus
|
|
2284
|
+
# matches. "way to" gets its own branch, mirroring cleanest-x.
|
|
2285
|
+
pattern: /\bmost[ \t]+honest[ \t]+(?:(?!way\b)\w+[ \t]+){0,2}
|
|
2286
|
+
(?:#{WRITERS_OWN_CONSTRUCTION_NOUNS}|comparison
|
|
2287
|
+
|accounting|assessment|appraisal
|
|
2288
|
+
|reading|account|answer|version|summary|take|signal
|
|
2289
|
+
|through-line)\b
|
|
2290
|
+
|\bmost[ \t]+honest[ \t]+way[ \t]+to[ \t]+
|
|
2291
|
+
(?:say|put|frame|state|describe|phrase|think[ \t]+about)\b/ix,
|
|
2292
|
+
message: '"The most honest framing…" ranks your own claim for the reader.',
|
|
2293
|
+
suggestion: "Drop the ranking and make the claim; the reader grades it.",
|
|
2294
|
+
examples_bad: [
|
|
2295
|
+
"The most honest framing is that we guessed.",
|
|
2296
|
+
"That is the most honest way to put it.",
|
|
2297
|
+
"The most honest reading of the data is duller than the headline.",
|
|
2298
|
+
"This is the most honest summary anyone offered."
|
|
2299
|
+
],
|
|
2300
|
+
examples_ok: [
|
|
2301
|
+
# The ordinary superlative, about a person.
|
|
2302
|
+
"He is the most honest person I know.",
|
|
2303
|
+
"She is the most honest politician in the state.",
|
|
2304
|
+
# "way to" outside the speech verbs.
|
|
2305
|
+
"This is the most honest way to earn a living."
|
|
2306
|
+
],
|
|
2307
|
+
rationale: "Self-ranking: the writer tells the reader which of their own claims is the " \
|
|
2308
|
+
"candid one, which is the reader's job. The superlative also implies the rest " \
|
|
2309
|
+
"of the piece was less honest, and the sentence after it never says which part."
|
|
2310
|
+
),
|
|
951
2311
|
Rule.new(
|
|
952
2312
|
id: "genuinely",
|
|
953
2313
|
category: "rhetorical-tic",
|
|
@@ -977,6 +2337,72 @@ module Sloplint
|
|
|
977
2337
|
"nominally free -- and nothing in the sentence marks which use is which, " \
|
|
978
2338
|
"which is why this one runs only when you ask for it."
|
|
979
2339
|
),
|
|
2340
|
+
Rule.new(
|
|
2341
|
+
id: "actually-not-x",
|
|
2342
|
+
category: "rhetorical-tic",
|
|
2343
|
+
severity: "warning",
|
|
2344
|
+
# Two markers that each correct the reader, doubled up in one clause:
|
|
2345
|
+
# the adverb and the trailing "…, not X". Bare "actually" is not the
|
|
2346
|
+
# tell and is not matched, because "the build actually failed on the
|
|
2347
|
+
# second run" names which run and that is a fact with something behind
|
|
2348
|
+
# it to check.
|
|
2349
|
+
#
|
|
2350
|
+
# The narrowing is one structural constraint, not a word list: the
|
|
2351
|
+
# comma in front of "not" has to be the first comma of its clause and
|
|
2352
|
+
# has to follow a word. A writer correcting an assumption the reader
|
|
2353
|
+
# does hold names it first, and that setup is either a fronted clause
|
|
2354
|
+
# with a comma after it ("Despite the name, …") or a parenthetical
|
|
2355
|
+
# whose closing bracket the comma follows. No earlier comma means
|
|
2356
|
+
# nothing in the clause set the alternative up. A clause starts at the
|
|
2357
|
+
# beginning of the text, at a full stop, question mark, exclamation
|
|
2358
|
+
# mark, semicolon or colon, or at a line break.
|
|
2359
|
+
#
|
|
2360
|
+
# Nothing crosses a line break, so the rule cannot weld two rows of a
|
|
2361
|
+
# table or two items of a list into one correction. The deliberate
|
|
2362
|
+
# misses: a hard-wrapped correction, and a correction whose clause
|
|
2363
|
+
# carries an earlier comma of any kind, an apposition ("The report,
|
|
2364
|
+
# filed late, says the id is actually a byte string, not text.")
|
|
2365
|
+
# included. No pattern tells an apposition from a fronted setup, and
|
|
2366
|
+
# the rule would rather say nothing than guess.
|
|
2367
|
+
pattern: /(?:\A|(?<=[.;:!?\n]))[^.;:!?,\n]{0,200}\K
|
|
2368
|
+
\bactually\b[^.;:!?,\n]{0,120}(?<=[\p{Word}]),[ \t]*not\b/ix,
|
|
2369
|
+
message: '"Actually …, not X" corrects an assumption the text never offered.',
|
|
2370
|
+
suggestion: "State the fact plainly, or name the belief the correction answers.",
|
|
2371
|
+
examples_bad: [
|
|
2372
|
+
"The disclosure actually covered two incidents, not one.",
|
|
2373
|
+
"The identifier is actually a byte string, not text.",
|
|
2374
|
+
"It actually reads the file at startup, not on the first request.",
|
|
2375
|
+
# A colon opens a clause, so the correction after it is still the tell.
|
|
2376
|
+
"Note: the endpoint actually returns two fields, not one."
|
|
2377
|
+
],
|
|
2378
|
+
examples_ok: [
|
|
2379
|
+
# The bare adverb, which names which run failed. Not matched.
|
|
2380
|
+
"The build actually failed on the second run.",
|
|
2381
|
+
# The setup sits in a fronted clause, so the correction answers
|
|
2382
|
+
# something the reader was given. The earlier comma drops both.
|
|
2383
|
+
"Despite the name, the identifier is actually a byte string, not text.",
|
|
2384
|
+
"Although the name suggests otherwise, it is actually a byte string, not text.",
|
|
2385
|
+
# The correction on its own, with no adverb in front of it.
|
|
2386
|
+
"The run that failed was the second, not the first.",
|
|
2387
|
+
# Negation in front of the adverb with no trailing correction. This
|
|
2388
|
+
# was the other candidate shape; every real instance of it carried
|
|
2389
|
+
# its own setup ("normally reserved but not actually registered"),
|
|
2390
|
+
# so it was rejected, and this fixture pins the rejection.
|
|
2391
|
+
"The provider was not actually billed for the usage.",
|
|
2392
|
+
# The correction's comma closes a parenthesis, and the assumption
|
|
2393
|
+
# being corrected is inside it.
|
|
2394
|
+
"The party actually sending the mail (which we assumed was the relay), not the sender.",
|
|
2395
|
+
# Two rows of a table, not a wrapped sentence. The rule never crosses
|
|
2396
|
+
# a line break, so it cannot join them.
|
|
2397
|
+
"Actually running processes\nThreads, not processes"
|
|
2398
|
+
],
|
|
2399
|
+
rationale: "The adverb and the trailing \"not X\" both correct the reader, and a writer " \
|
|
2400
|
+
"needs one or the other, never both. Doubling them marks a contrast against " \
|
|
2401
|
+
"an alternative the reader was never offered: nothing said one incident, so " \
|
|
2402
|
+
"\"not one\" answers nobody. The setup can also sit in the sentence before, " \
|
|
2403
|
+
"which no pattern sees, so a flag on a correction that a previous sentence " \
|
|
2404
|
+
"genuinely set up is the known cost."
|
|
2405
|
+
),
|
|
980
2406
|
Rule.new(
|
|
981
2407
|
id: "and-thats-fine",
|
|
982
2408
|
category: "rhetorical-tic",
|
|
@@ -1002,6 +2428,111 @@ module Sloplint
|
|
|
1002
2428
|
"was never raised. It reads as reassurance addressed to nobody, and the " \
|
|
1003
2429
|
"paragraph is the same without it."
|
|
1004
2430
|
),
|
|
2431
|
+
Rule.new(
|
|
2432
|
+
id: "and-nothing-else",
|
|
2433
|
+
category: "rhetorical-tic",
|
|
2434
|
+
severity: "warning",
|
|
2435
|
+
# This rule is deliberately wide, and the cost is known. There is no verb
|
|
2436
|
+
# list and no imperative requirement, so the only narrowing is structural
|
|
2437
|
+
# -- which means the pattern cannot tell a leaked instruction from the
|
|
2438
|
+
# ordinary English construction it borrows. "Darkness there, and nothing
|
|
2439
|
+
# more!" matches, and so does any sentence built the same way. That is a
|
|
2440
|
+
# recall-over-precision choice, not an oversight; see rationale.
|
|
2441
|
+
#
|
|
2442
|
+
# Three structural guards, each pinned by an examples_ok fixture. The
|
|
2443
|
+
# tail must CLOSE the sentence, so "nothing more than a rumour" and
|
|
2444
|
+
# "nothing else could explain it" never match. A comma must introduce
|
|
2445
|
+
# it, which keeps "there was nothing more to say" out. And the bare
|
|
2446
|
+
# "no more" branch additionally requires "and": a large share of the
|
|
2447
|
+
# ordinary-prose hits are the amount sense ("two years, no more",
|
|
2448
|
+
# "no less, no more"), and requiring the conjunction drops them without
|
|
2449
|
+
# losing the closer the rule is after.
|
|
2450
|
+
#
|
|
2451
|
+
# A closing quote or bracket may sit between the tail and the period so
|
|
2452
|
+
# a quoted sentence still matches. "?" is not accepted as the terminal
|
|
2453
|
+
# mark -- "and nothing more?" asks a question, a different act. Token
|
|
2454
|
+
# gaps cross a hard-wrapped line but never a paragraph break.
|
|
2455
|
+
pattern: /,(?:[ \t]|\r?\n(?![ \t]*\r?\n))+
|
|
2456
|
+
(?:(?:and(?:[ \t]|\r?\n(?![ \t]*\r?\n))+)?
|
|
2457
|
+
nothing(?:[ \t]|\r?\n(?![ \t]*\r?\n))+(?:else|more|further)
|
|
2458
|
+
|and(?:[ \t]|\r?\n(?![ \t]*\r?\n))+no(?:[ \t]|\r?\n(?![ \t]*\r?\n))+more)
|
|
2459
|
+
[ \t]*(?=["'”’)\]]*[.!])/ix,
|
|
2460
|
+
message: "Trailing \"and nothing else\" restates what the sentence already said.",
|
|
2461
|
+
suggestion: "Cut the tail. If the exclusion is the point, say what was excluded.",
|
|
2462
|
+
examples_bad: [
|
|
2463
|
+
"Return the JSON, and nothing else.",
|
|
2464
|
+
"The endpoint gives back the id, and nothing more.",
|
|
2465
|
+
"Reply with the number, nothing else.",
|
|
2466
|
+
"Print the filename, nothing further.",
|
|
2467
|
+
"Hand over the receipt, and no more."
|
|
2468
|
+
],
|
|
2469
|
+
examples_ok: [
|
|
2470
|
+
# The tail does not close the sentence.
|
|
2471
|
+
"It was nothing more than a rumour.",
|
|
2472
|
+
"The delay was down to the weather, and nothing else could explain it.",
|
|
2473
|
+
# No comma introduces it.
|
|
2474
|
+
"There was nothing more to say after that.",
|
|
2475
|
+
"Nothing else matters once the deadline passes.",
|
|
2476
|
+
# The amount sense, which is why bare "no more" needs the conjunction.
|
|
2477
|
+
"The lease runs two years, no more.",
|
|
2478
|
+
"He asked for a fair share, no more.",
|
|
2479
|
+
# A question is a different act.
|
|
2480
|
+
"Is there anything else, or nothing more?"
|
|
2481
|
+
],
|
|
2482
|
+
rationale: "A model told to return one thing and nothing else carries the phrasing into " \
|
|
2483
|
+
"the prose it writes afterwards, where the exclusion adds nothing -- the " \
|
|
2484
|
+
"sentence already named what it covers. The catch is that English has always " \
|
|
2485
|
+
"used this tail, so the rule fires on careful writing too: twice in 1.92M " \
|
|
2486
|
+
"words of public-domain prose, once in 461k words of pre-2022 Hacker News, and " \
|
|
2487
|
+
"on every refrain in \"The Raven\". Expect to dismiss it on fiction and on " \
|
|
2488
|
+
"quoted verse."
|
|
2489
|
+
),
|
|
2490
|
+
Rule.new(
|
|
2491
|
+
id: "nothing-else-frag",
|
|
2492
|
+
category: "rhetorical-tic",
|
|
2493
|
+
severity: "warning",
|
|
2494
|
+
# The same exclusion as its own sentence: "Return the JSON. Nothing
|
|
2495
|
+
# else." Built on the no-x-no-y-frag template -- the fragment must start
|
|
2496
|
+
# at a sentence boundary and the separator is at most two spaces or one
|
|
2497
|
+
# hard-wrapped newline, so a blanked code span under --markdown cannot
|
|
2498
|
+
# weld two distant sentences together, and a paragraph break stops it.
|
|
2499
|
+
#
|
|
2500
|
+
# Two guards past that. The fragment must open with a capital letter,
|
|
2501
|
+
# and the preceding mark cannot be a semicolon: both keep out the
|
|
2502
|
+
# continuation sense
|
|
2503
|
+
# ("keep pulling; nothing more"), which is one clause, not a closer.
|
|
2504
|
+
# And the fragment must be the whole sentence -- "Nothing else mattered."
|
|
2505
|
+
# has a verb and never matches. "No more." is left out entirely; as a
|
|
2506
|
+
# standalone sentence it is ordinary speech, and no-x-no-y-frag already
|
|
2507
|
+
# takes the chained form.
|
|
2508
|
+
pattern: /(?<=[.!?])(?:[ \t]{1,2}|\r?\n(?!\s*\n)[ \t]*)\K
|
|
2509
|
+
(?:And[ \t]+nothing|Nothing)[ \t]+(?:else|more|further)[ \t]*[.!]/x,
|
|
2510
|
+
message: "\"Nothing else.\" as a closer restates what the sentence already said.",
|
|
2511
|
+
suggestion: "Cut the fragment, or fold the exclusion into the sentence before it.",
|
|
2512
|
+
examples_bad: [
|
|
2513
|
+
"Return the JSON. Nothing else.",
|
|
2514
|
+
"Give me the number. Nothing more.",
|
|
2515
|
+
"Answer with the file name. And nothing else.",
|
|
2516
|
+
"Print the total. Nothing further."
|
|
2517
|
+
],
|
|
2518
|
+
examples_ok: [
|
|
2519
|
+
# A verb follows, so the fragment is a sentence about something.
|
|
2520
|
+
"The tests passed. Nothing else mattered that afternoon.",
|
|
2521
|
+
# The continuation sense: one clause, joined by a semicolon.
|
|
2522
|
+
"Keep pulling; nothing more.",
|
|
2523
|
+
# A paragraph break is not a sentence separator.
|
|
2524
|
+
"The kiln runs hot.\n\nNothing more.",
|
|
2525
|
+
# Moby-Dick (public domain): the lower-case continuation the capital
|
|
2526
|
+
# letter guard is there to exclude.
|
|
2527
|
+
"and so on; nothing more."
|
|
2528
|
+
],
|
|
2529
|
+
rationale: "The fragment form of the same leaked instruction, and it reads the same way: " \
|
|
2530
|
+
"a second sentence that only says the first one was complete. It ships at " \
|
|
2531
|
+
"warning rather than info because the capital letter and the whole-sentence " \
|
|
2532
|
+
"requirement keep it off the continuation sense, which is where ordinary prose " \
|
|
2533
|
+
"puts the phrase: one hit in 1.92M words of public-domain prose and one in " \
|
|
2534
|
+
"461k words of pre-2022 Hacker News."
|
|
2535
|
+
),
|
|
1005
2536
|
# ── puffery ───────────────────────────────────────────────────────────
|
|
1006
2537
|
Rule.new(
|
|
1007
2538
|
id: "puffery-words",
|
|
@@ -1239,17 +2770,166 @@ module Sloplint
|
|
|
1239
2770
|
"surface pattern can tell apart from the real thing. Hence info: a flag here " \
|
|
1240
2771
|
"means 'this has the shape', not 'this is slop'."
|
|
1241
2772
|
),
|
|
2773
|
+
Rule.new(
|
|
2774
|
+
id: "isnt-x-its-y",
|
|
2775
|
+
category: "structure",
|
|
2776
|
+
severity: "info",
|
|
2777
|
+
# The corrective with the conjunction dropped: one clause rejects a
|
|
2778
|
+
# description, the next supplies the replacement through a second copula
|
|
2779
|
+
# ("It isn't the tool. It's the habit."). Every copula tense is covered,
|
|
2780
|
+
# and the two clauses may be joined by a period, semicolon, comma, or dash.
|
|
2781
|
+
#
|
|
2782
|
+
# The narrowing sits in the two complement slots, and it is lexical class
|
|
2783
|
+
# rather than vocabulary: neither slot may open with a pronoun, a
|
|
2784
|
+
# possessive, a preposition ("about" excepted, since "not about X, it's
|
|
2785
|
+
# about Y" is the same move), a degree word, or one of the predicate
|
|
2786
|
+
# adjectives that make the second clause a comment instead of a
|
|
2787
|
+
# replacement ("It isn't ready. It's close."). The article is matched
|
|
2788
|
+
# atomically so the guard cannot be skipped by backtracking past it. Those
|
|
2789
|
+
# guards cut a loose version from 19 hits to 4 over 889k words of
|
|
2790
|
+
# 19th-century fiction.
|
|
2791
|
+
#
|
|
2792
|
+
# What survives is the reason for info. An adjective outside the list
|
|
2793
|
+
# still slips through, and some hits are the same shape written by a
|
|
2794
|
+
# person -- "was not the wife; it was the children" is Conan Doyle. A flag
|
|
2795
|
+
# here says the sentence has the frame, not that a model wrote it.
|
|
2796
|
+
pattern: /(?:\b(?:is|are|was|were)\s+not
|
|
2797
|
+
|\b(?:is|are|was|were)n['’]t
|
|
2798
|
+
|\b(?:it|that|this|there)['’]s\s+not
|
|
2799
|
+
|\b(?:they|these|those|we|you)['’]re\s+not)
|
|
2800
|
+
(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2801
|
+
(?>(?:a\s+|an\s+|the\s+)?)
|
|
2802
|
+
(?!so\b|just\b|only\b|merely\b|simply\b|solely\b|even\b|yet\b|quite\b|very\b
|
|
2803
|
+
|too\b|all\b|always\b|never\b|often\b|also\b|enough\b|really\b|actually\b
|
|
2804
|
+
|entirely\b|ready\b|close\b|done\b|easy\b|hard\b|clear\b|simple\b
|
|
2805
|
+
|possible\b|likely\b|true\b|false\b|fine\b|good\b|bad\b|better\b|worse\b
|
|
2806
|
+
|obvious\b|important\b|different\b|not\b|no\b|nothing\b|there\b|here\b
|
|
2807
|
+
|what\b|who\b|how\b|why\b|when\b|where\b|because\b|i\b|me\b|he\b|him\b
|
|
2808
|
+
|she\b|her\b|it\b|we\b|us\b|you\b|they\b|them\b|that\b|this\b|these\b
|
|
2809
|
+
|those\b|his\b|their\b|its\b|my\b|your\b|our\b|in\b|on\b|at\b|of\b|to\b
|
|
2810
|
+
|for\b|from\b|with\b|as\b|by\b|toward\b|towards\b|into\b|over\b|under\b
|
|
2811
|
+
|through\b|against\b|upon\b|within\b|without\b)
|
|
2812
|
+
[\w'’-]+
|
|
2813
|
+
[^.!?;\n]{0,40}?
|
|
2814
|
+
(?:[.!?;]|[ \t]*[—–]|,)
|
|
2815
|
+
(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2816
|
+
(?:it|this|that|these|those|they)
|
|
2817
|
+
(?:['’]s|['’]re|\s+is|\s+are|\s+was|\s+were)
|
|
2818
|
+
(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2819
|
+
(?>(?:a\s+|an\s+|the\s+)?)
|
|
2820
|
+
(?!so\b|just\b|only\b|merely\b|simply\b|solely\b|even\b|yet\b|quite\b|very\b
|
|
2821
|
+
|too\b|all\b|always\b|never\b|often\b|also\b|enough\b|really\b|actually\b
|
|
2822
|
+
|entirely\b|ready\b|close\b|done\b|easy\b|hard\b|clear\b|simple\b
|
|
2823
|
+
|possible\b|likely\b|true\b|false\b|fine\b|good\b|bad\b|better\b|worse\b
|
|
2824
|
+
|obvious\b|important\b|different\b|not\b|no\b|nothing\b|there\b|here\b
|
|
2825
|
+
|what\b|who\b|how\b|why\b|when\b|where\b|because\b|i\b|me\b|he\b|him\b
|
|
2826
|
+
|she\b|her\b|it\b|we\b|us\b|you\b|they\b|them\b|that\b|this\b|these\b
|
|
2827
|
+
|those\b|his\b|their\b|its\b|my\b|your\b|our\b|in\b|on\b|at\b|of\b|to\b
|
|
2828
|
+
|for\b|from\b|with\b|as\b|by\b|toward\b|towards\b|into\b|over\b|under\b
|
|
2829
|
+
|through\b|against\b|upon\b|within\b|without\b)
|
|
2830
|
+
[\w'’-]+/ix,
|
|
2831
|
+
message: '"isn\'t X, it\'s Y" corrects a description nobody offered.',
|
|
2832
|
+
suggestion: "State the second half on its own; drop the rejected one.",
|
|
2833
|
+
examples_bad: [
|
|
2834
|
+
"It isn't the tool. It's the habit.",
|
|
2835
|
+
"This wasn't a setback, it was a setup for the next release.",
|
|
2836
|
+
"The delay wasn't the network. It was the retry loop.",
|
|
2837
|
+
"Those aren't the metrics, they're the vanity numbers.",
|
|
2838
|
+
"This isn't failure. It's iteration.",
|
|
2839
|
+
"They're not customers; they are partners.",
|
|
2840
|
+
"These weren't accidents. They were choices."
|
|
2841
|
+
],
|
|
2842
|
+
examples_ok: [
|
|
2843
|
+
# Predicate adjectives on both sides: a comment, not a replacement.
|
|
2844
|
+
"It isn't ready. It's close.",
|
|
2845
|
+
# Escalation word: that shape belongs to not-just-x-but-y.
|
|
2846
|
+
"It isn't only the cost. It is the delay too.",
|
|
2847
|
+
# The second clause needs a pronoun subject and a copula.
|
|
2848
|
+
"It wasn't the alarm that woke me. The dog did.",
|
|
2849
|
+
# The two clauses must be adjacent.
|
|
2850
|
+
"It isn't the heat. Everyone says so, and they have said so for years. It's the humidity.",
|
|
2851
|
+
# Prepositional complements are ordinary contrast.
|
|
2852
|
+
"The message was not to him; it was to the clerk.",
|
|
2853
|
+
# Pronoun complement.
|
|
2854
|
+
"It was not me who called; it was the neighbour.",
|
|
2855
|
+
# The guard holds after the article, which is matched atomically.
|
|
2856
|
+
"The fire was not out; it was the only light left.",
|
|
2857
|
+
# A paragraph break ends the frame.
|
|
2858
|
+
"The plan was not the problem.\n\nIt was the schedule."
|
|
2859
|
+
],
|
|
2860
|
+
rationale: "The frame rejects a description nobody proposed, then supplies the true " \
|
|
2861
|
+
"one, so the sentence sounds like a correction while correcting nobody. It " \
|
|
2862
|
+
"is 'not A but B' with the conjunction dropped and the second half promoted " \
|
|
2863
|
+
"to its own clause, which is the form models reach for most. Ordinary prose " \
|
|
2864
|
+
"contrasts two things this way too, so the rule ships at info: it reports " \
|
|
2865
|
+
"the shape, not a verdict."
|
|
2866
|
+
),
|
|
2867
|
+
Rule.new(
|
|
2868
|
+
id: "not-by-x-but-by-y",
|
|
2869
|
+
category: "structure",
|
|
2870
|
+
severity: "info",
|
|
2871
|
+
# The corrective built on a repeated preposition: "not by A, but by B",
|
|
2872
|
+
# "not from A but from B". The copula rules above cannot see it because
|
|
2873
|
+
# nothing precedes "not" but the verb or a dash, so the anchor here is
|
|
2874
|
+
# the preposition itself, which must repeat after "but". That repetition
|
|
2875
|
+
# is the whole narrowing: a different preposition on the B side is a
|
|
2876
|
+
# concession or an afterthought, not a correction.
|
|
2877
|
+
#
|
|
2878
|
+
# A is capped at six words and B may not open with a pronoun. Neither
|
|
2879
|
+
# guard removes a false positive -- every corpus hit is the real shape
|
|
2880
|
+
# -- they only keep the match from running across a sentence.
|
|
2881
|
+
pattern: /\bnot(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2882
|
+
(?<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)
|
|
2883
|
+
(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2884
|
+
(?:[\w'’-]+(?:[ \t]|\r?\n(?!\s*\n))+){0,5}[\w'’-]+
|
|
2885
|
+
,?(?:[ \t]|\r?\n(?!\s*\n))+but(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2886
|
+
(?:rather(?:[ \t]|\r?\n(?!\s*\n))+)?
|
|
2887
|
+
\k<prep>(?:[ \t]|\r?\n(?!\s*\n))+
|
|
2888
|
+
(?!that\b|this\b|it\b|him\b|her\b|them\b|me\b|us\b|you\b|which\b|whom\b|what\b)
|
|
2889
|
+
(?:a\s+|an\s+|the\s+)?[\w'’-]+/ix,
|
|
2890
|
+
message: '"not by A, but by B" is the corrective frame on a repeated preposition.',
|
|
2891
|
+
suggestion: "Say what it was by; drop the 'not by… but by' frame.",
|
|
2892
|
+
examples_bad: [
|
|
2893
|
+
"We won not by luck but by preparation.",
|
|
2894
|
+
"The gain came not from the model, but from the data.",
|
|
2895
|
+
"Judge it not on the demo but on the deployment.",
|
|
2896
|
+
"Revenue grew this quarter — not by a rounding error, but by a margin that survives any cut of the data."
|
|
2897
|
+
],
|
|
2898
|
+
examples_ok: [
|
|
2899
|
+
# The preposition changes: a contrast, not a correction.
|
|
2900
|
+
"He came not for the money but with an apology.",
|
|
2901
|
+
# B-side pronoun.
|
|
2902
|
+
"She wrote not to him but to them.",
|
|
2903
|
+
# A capped at six words.
|
|
2904
|
+
"The deal closed not in the long slow grind of the seventh week, but in an afternoon.",
|
|
2905
|
+
# "not only" belongs to the escalation rules, and the B preposition here would not repeat anyway.
|
|
2906
|
+
"It was not only for the money.",
|
|
2907
|
+
# A paragraph break ends the frame.
|
|
2908
|
+
"We won not by luck\n\nBut by then it hardly mattered."
|
|
2909
|
+
],
|
|
2910
|
+
rationale: "The 'not A but B' corrective with the copula swapped for a repeated " \
|
|
2911
|
+
"preposition, which is how a model corrects a claim about means or " \
|
|
2912
|
+
"cause ('not by luck, but by design'). People write it too -- Thoreau " \
|
|
2913
|
+
"and Melville both lean on it -- so the rule ships at info. It reports " \
|
|
2914
|
+
"the shape; a human reader decides whether it earned its place."
|
|
2915
|
+
),
|
|
1242
2916
|
Rule.new(
|
|
1243
2917
|
id: "rule-of-three",
|
|
1244
2918
|
category: "structure",
|
|
1245
2919
|
severity: "info",
|
|
1246
2920
|
default_on: false,
|
|
1247
2921
|
pattern: /\b[\w'-]+,\s+[\w'-]+,\s+(?:and\s+)?[\w'-]+[.!?]/,
|
|
1248
|
-
message: "Three
|
|
2922
|
+
message: "Three single words in a comma series closing a sentence (heuristic; high false-positive).",
|
|
1249
2923
|
suggestion: "Fine in moderation; watch for the AI habit of ending on triplets.",
|
|
1250
2924
|
examples_bad: ["It was fast, cheap, and simple."],
|
|
1251
|
-
examples_ok: [
|
|
1252
|
-
|
|
2925
|
+
examples_ok: [
|
|
2926
|
+
"We met on Tuesday afternoon.",
|
|
2927
|
+
# Phrasal items do not match; the closing two slots take one word each.
|
|
2928
|
+
"It was very fast, very cheap, and very simple."
|
|
2929
|
+
],
|
|
2930
|
+
rationale: "Rule-of-three endings are a model habit, but humans use them too — off by default. " \
|
|
2931
|
+
"The closing two items must be single words; a triad of phrases is three ordinary " \
|
|
2932
|
+
"list items to a regex, so those are left unflagged rather than guessed at."
|
|
1253
2933
|
),
|
|
1254
2934
|
# clause-triad-then was cut. The pattern (comma-clause, comma-clause,
|
|
1255
2935
|
# "then" clause) had no way to require the clauses actually be parallel
|
|
@@ -1261,6 +2941,485 @@ module Sloplint
|
|
|
1261
2941
|
# then...", elliptical legal "shall... then..."). It was also the most
|
|
1262
2942
|
# expensive rule in the catalog at 65% of scan time on 1MB. Per
|
|
1263
2943
|
# CLAUDE.md: some tells can't be regexes; this was one.
|
|
2944
|
+
Rule.new(
|
|
2945
|
+
id: "everyone-nobody",
|
|
2946
|
+
category: "structure",
|
|
2947
|
+
severity: "warning",
|
|
2948
|
+
# The comma-spliced antithesis on quantifier subjects: "Everyone wants
|
|
2949
|
+
# the dashboard, nobody maintains it." One clause opens on
|
|
2950
|
+
# everyone/everybody, the other on nobody/no one/none or "one N"
|
|
2951
|
+
# ("everyone may pitch, one editor decides"), joined by a bare comma, and
|
|
2952
|
+
# the second clause closes the sentence. The comma splice is the
|
|
2953
|
+
# evidence: with "and" or "but" it is an ordinary sentence, and with a
|
|
2954
|
+
# period it is two. The first subject must open a clause (one or two
|
|
2955
|
+
# spaces after a stop, as elsewhere in the catalog), the two subjects
|
|
2956
|
+
# must differ in polarity, so the anaphoric "nobody is on my side,
|
|
2957
|
+
# nobody takes part with me" is not a hinge, and each clause is at most
|
|
2958
|
+
# eighty characters with no newline, so a hinge never crosses a
|
|
2959
|
+
# paragraph break and the comma gap is at most two spaces.
|
|
2960
|
+
#
|
|
2961
|
+
# The "one N" and "none" subjects need guards, because the comma slot
|
|
2962
|
+
# also holds phrases that are not clauses. "one of them", "one by one",
|
|
2963
|
+
# "one per ticket", and a measure ("one hour before the talk") are out;
|
|
2964
|
+
# so is a capitalised "One" mid-sentence, a proper noun, which the
|
|
2965
|
+
# case-sensitive lookahead catches under /i. "none of which" is a
|
|
2966
|
+
# relative clause and "none louder than" a comparative, and both are
|
|
2967
|
+
# out.
|
|
2968
|
+
pattern: /(?:^|(?<=[.!?:;])[ \t]{1,2})\K
|
|
2969
|
+
(?:(?:everyone|everybody)[ \t]+[^,.;:!?\n]{2,80},[ \t]{1,2}
|
|
2970
|
+
(?:nobody|no[ \t]+one|none(?![ \t]+(?:of|more|less|so)\b|[ \t]+\w+er\b)
|
|
2971
|
+
|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'’-]+)
|
|
2972
|
+
|(?:nobody|no[ \t]+one)[ \t]+[^,.;:!?\n]{2,80},[ \t]{1,2}
|
|
2973
|
+
(?: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'’-]+))
|
|
2974
|
+
[ \t]+[^,.;:!?\n]{2,80}[.;!?]/ix,
|
|
2975
|
+
message: '"Everyone X, nobody Y." is the AI antithesis hinge.',
|
|
2976
|
+
suggestion: "Say which one is the problem, in its own sentence.",
|
|
2977
|
+
examples_bad: [
|
|
2978
|
+
"Everyone wants the dashboard, nobody maintains it.",
|
|
2979
|
+
"Run it like a newsroom desk: everyone may pitch, one editor decides.",
|
|
2980
|
+
"Nobody owns the file, everyone edits it.",
|
|
2981
|
+
"Everyone wanted the job, none applied.",
|
|
2982
|
+
# Two spaces after the stop still open a clause.
|
|
2983
|
+
"Ship it. Everyone wants the dashboard, nobody maintains it.",
|
|
2984
|
+
# Clauses may run to eighty characters.
|
|
2985
|
+
"Everyone talks about observability in the abstract, nobody wants to own the pager rotation."
|
|
2986
|
+
],
|
|
2987
|
+
examples_ok: [
|
|
2988
|
+
# A conjunction makes it a sentence, not a hinge.
|
|
2989
|
+
"Everyone left early, and nobody noticed.",
|
|
2990
|
+
# Pride and Prejudice (Austen, public domain): the same subject twice is
|
|
2991
|
+
# anaphora, not antithesis.
|
|
2992
|
+
"Nobody is on my side, nobody takes part with me;",
|
|
2993
|
+
# The second clause must close the sentence.
|
|
2994
|
+
"Everyone who came, nobody excepted, signed the book.",
|
|
2995
|
+
# A period is two sentences.
|
|
2996
|
+
"Everyone wants the dashboard. Nobody maintains it.",
|
|
2997
|
+
# "one of", "one by one", "one per", a measure, and a proper noun are
|
|
2998
|
+
# not a second subject.
|
|
2999
|
+
"Everyone signed up, one of them dropped out later.",
|
|
3000
|
+
"Nobody moved for a moment, one by one they stood up.",
|
|
3001
|
+
"Everybody got a ticket, one per person at the gate.",
|
|
3002
|
+
"Everyone arrived by noon, one hour before the talk.",
|
|
3003
|
+
"Nobody stirred in the hall, One Direction played on the radio.",
|
|
3004
|
+
# A relative clause and a comparative are not a second subject.
|
|
3005
|
+
"Everyone brought a dish, none of which we actually ate.",
|
|
3006
|
+
"Everyone in the room laughed, none louder than the author himself.",
|
|
3007
|
+
# A clause over eighty characters is a sentence of its own.
|
|
3008
|
+
"Everyone who has ever tried to keep a dashboard alive through two reorganisations and a migration knows the cost, nobody maintains it.",
|
|
3009
|
+
# A paragraph break is not a comma.
|
|
3010
|
+
"Everyone wants the dashboard,\n\nnobody maintains it."
|
|
3011
|
+
],
|
|
3012
|
+
rationale: "The everyone/nobody hinge states a whole diagnosis as a balanced pair of " \
|
|
3013
|
+
"clauses, and the balance is what makes it sound settled. Models reach for " \
|
|
3014
|
+
"it to close a setup; careful writers join the clauses with a conjunction " \
|
|
3015
|
+
"or give the problem its own sentence."
|
|
3016
|
+
),
|
|
3017
|
+
Rule.new(
|
|
3018
|
+
id: "np-fragment-and",
|
|
3019
|
+
category: "structure",
|
|
3020
|
+
severity: "info",
|
|
3021
|
+
# A whole sentence that is two noun phrases and an "and": "A named owner
|
|
3022
|
+
# and a quarterly review." It is the fix half of a model's
|
|
3023
|
+
# problem-then-fix pair, with the verb left for the reader to supply.
|
|
3024
|
+
# The sentence must open on A/An/One at a sentence start or after a
|
|
3025
|
+
# list marker (the pair's usual habitat is a bulleted list), the second
|
|
3026
|
+
# phrase must open on a/an/one, each phrase is one to three words, and
|
|
3027
|
+
# nothing else may be in the sentence. No auxiliary or modal may appear
|
|
3028
|
+
# anywhere in it, contractions included, so "A man and a woman were
|
|
3029
|
+
# there." and "A man and a woman aren't here." never match.
|
|
3030
|
+
#
|
|
3031
|
+
# Ships at info, and this is why: a lexical verb is invisible to the
|
|
3032
|
+
# pattern, so "A car and a truck collided." has the same shape and
|
|
3033
|
+
# flags. The corpora say that sentence is rare (one hit in 1.25M words
|
|
3034
|
+
# of public-domain prose, most of it narrative), but it is a complete
|
|
3035
|
+
# sentence, and nothing a regex can see separates it from the fragment.
|
|
3036
|
+
pattern: /(?:^|(?<=[.!?])[ \t]{1,2})(?:[-*+•][ \t]+|\d+[.)][ \t]+)?\K(?:A|An|One)(?:[ \t]|\r?\n(?!\s*\n))+
|
|
3037
|
+
(?:(?!(?:(?: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))+
|
|
3038
|
+
(?:(?!(?:(?: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,
|
|
3039
|
+
message: '"A X and a Y." as a whole sentence is an AI fragment.',
|
|
3040
|
+
suggestion: "Give the sentence a verb, or fold it into the one before.",
|
|
3041
|
+
examples_bad: [
|
|
3042
|
+
"Nobody checked it after launch. A named owner and a quarterly review.",
|
|
3043
|
+
"One merger and a version-controlled folder.",
|
|
3044
|
+
"An owner and a deadline.",
|
|
3045
|
+
# The pair's usual habitat.
|
|
3046
|
+
"- A named owner and a quarterly review.",
|
|
3047
|
+
"1. A named owner and a quarterly review.",
|
|
3048
|
+
# A comma before "and" is still the pair.
|
|
3049
|
+
"A named owner, and a quarterly review."
|
|
3050
|
+
],
|
|
3051
|
+
examples_ok: [
|
|
3052
|
+
"A man and a woman were waiting at the door.",
|
|
3053
|
+
"A dog and a cat can share a house.",
|
|
3054
|
+
# Contractions and the rarer auxiliaries are auxiliaries too.
|
|
3055
|
+
"A man and a woman aren't here.",
|
|
3056
|
+
"A boy and a girl shall meet.",
|
|
3057
|
+
"A boy and a girl ought to know.",
|
|
3058
|
+
# Not at a sentence start.
|
|
3059
|
+
"We hired a designer and an engineer.",
|
|
3060
|
+
# More than three words on a side is a clause, not a label.
|
|
3061
|
+
"A long walk down to the river and a swim before breakfast.",
|
|
3062
|
+
# Only "and" between two phrases; a list is not the pair.
|
|
3063
|
+
"A hammer, a saw, and a level."
|
|
3064
|
+
],
|
|
3065
|
+
rationale: "Two noun phrases and an 'and', standing as a sentence, is how a model " \
|
|
3066
|
+
"hands over a fix without committing to a verb: the reader supplies " \
|
|
3067
|
+
"'you need' or 'add'. A short sentence with a plain verb ('A car and a " \
|
|
3068
|
+
"truck collided.') has the same shape and cannot be told apart, which is " \
|
|
3069
|
+
"why this is a question rather than a verdict; a draft full of them, " \
|
|
3070
|
+
"especially as list items, should be read as a warning."
|
|
3071
|
+
),
|
|
3072
|
+
Rule.new(
|
|
3073
|
+
id: "quip-question",
|
|
3074
|
+
category: "structure",
|
|
3075
|
+
severity: "info",
|
|
3076
|
+
# The verbless question that opens a pitch: "No invite?", "New to the
|
|
3077
|
+
# tool?", "Still stuck?", "Ready to start?". It must start a sentence,
|
|
3078
|
+
# open on one of a short list of words, run one to four more words, and
|
|
3079
|
+
# end on the question mark, with no auxiliary or contraction anywhere,
|
|
3080
|
+
# so a real question ("Not what you expected?" flags, "Is it new?" does
|
|
3081
|
+
# not) stays out. "Need" and "Want" are not on the list: "Need help?"
|
|
3082
|
+
# is a question with its verb elided, not a verbless one. Gaps may
|
|
3083
|
+
# cross a hard-wrapped newline but never a paragraph break. Ships at
|
|
3084
|
+
# info: dialogue and forum replies ask the same shape of a person, and
|
|
3085
|
+
# one is a question, not a verdict.
|
|
3086
|
+
pattern: /(?:^|(?<=[.!?])[ \t]{1,2})\K(?:No|New|Still|Not|Already|Ready|Curious|Unsure|Stuck|Tired|Confused|Worried)(?:[ \t]|\r?\n(?!\s*\n))+
|
|
3087
|
+
(?:(?!(?: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,
|
|
3088
|
+
message: "A verbless opening question is a marketing-copy tell.",
|
|
3089
|
+
suggestion: "Ask it as a sentence, or state what follows without the setup.",
|
|
3090
|
+
examples_bad: [
|
|
3091
|
+
"No invite? Start a team of your own.",
|
|
3092
|
+
"New to the tool? Read the guide first.",
|
|
3093
|
+
"Still stuck after that? Ask in the channel.",
|
|
3094
|
+
"Ready to start?",
|
|
3095
|
+
"Not what you expected?",
|
|
3096
|
+
# A hard-wrapped quip survives one newline.
|
|
3097
|
+
"Still\nstuck? Ask in the channel."
|
|
3098
|
+
],
|
|
3099
|
+
examples_ok: [
|
|
3100
|
+
"Is it new?",
|
|
3101
|
+
"No, they aren't?",
|
|
3102
|
+
"Not sure if it will work?",
|
|
3103
|
+
# Not at a sentence start.
|
|
3104
|
+
"She asked, Still unsure?",
|
|
3105
|
+
# An elided verb is a question, not a quip.
|
|
3106
|
+
"Need help?",
|
|
3107
|
+
"Want the short version?",
|
|
3108
|
+
# Too long to be a quip.
|
|
3109
|
+
"Still waiting for the last batch of reviews from the other team?",
|
|
3110
|
+
# A paragraph break ends it, and the question mark stays on the line.
|
|
3111
|
+
"Not sure\n\nwhat happened here?",
|
|
3112
|
+
"Not sure what happened\n?"
|
|
3113
|
+
],
|
|
3114
|
+
rationale: "A one-line question with no verb is the hook of a landing page, and a " \
|
|
3115
|
+
"model reaches for it to open any section. People ask the same shape in " \
|
|
3116
|
+
"replies, of a product or a person, so one flag is a question; several " \
|
|
3117
|
+
"in a draft should be read as a warning."
|
|
3118
|
+
),
|
|
3119
|
+
Rule.new(
|
|
3120
|
+
id: "mic-drop-closer",
|
|
3121
|
+
category: "structure",
|
|
3122
|
+
severity: "info",
|
|
3123
|
+
# The kicker: a sentence of at least sixty characters, then a closer of
|
|
3124
|
+
# two to eight words that ends the paragraph and opens on a quantifier
|
|
3125
|
+
# ("Nothing here needs a new login.", "Most teams end up
|
|
3126
|
+
# with two.", "Then find out whether it paid off."). The long sentence
|
|
3127
|
+
# must start a sentence itself, so the scan is linear, and \K drops it
|
|
3128
|
+
# from the match so the note points at the closer. The closer must be
|
|
3129
|
+
# the last thing in the paragraph: a blank line or the end of the text
|
|
3130
|
+
# must follow, so the same sentence mid-paragraph, or a bullet followed
|
|
3131
|
+
# by another bullet, is just a sentence. Both sentences may cross a
|
|
3132
|
+
# hard-wrapped newline. Whitespace inside the long sentence is capped at
|
|
3133
|
+
# two spaces a run, so a URL or code span blanked by --markdown cannot
|
|
3134
|
+
# manufacture the sixty characters. The gap between the two sentences
|
|
3135
|
+
# is one or two spaces, and the closer needs at least two words.
|
|
3136
|
+
#
|
|
3137
|
+
# "That", "This" and "It" were in the opener list and are out. They are
|
|
3138
|
+
# not quantifiers, and procedural writing ends a step with one as a
|
|
3139
|
+
# matter of course: every hit in 2.25M words of engineering prose was a
|
|
3140
|
+
# bare demonstrative closing a paragraph after a long sentence -- "This
|
|
3141
|
+
# completes the roughing operations.", "This is the normal running
|
|
3142
|
+
# position." The quantifiers carry the tell; the demonstratives are
|
|
3143
|
+
# ordinary, and no examples_bad used one.
|
|
3144
|
+
#
|
|
3145
|
+
# Ships at info, and the rationale says why: people end paragraphs this
|
|
3146
|
+
# way too, at about 150 per million words on Hacker News. One is
|
|
3147
|
+
# nothing. A draft where most paragraphs end this way is the tell, and
|
|
3148
|
+
# an agent that sees the flag repeat should read the family as a
|
|
3149
|
+
# warning.
|
|
3150
|
+
# The long-sentence prefix is wrapped in an atomic group. Its branches
|
|
3151
|
+
# cannot match [.!?], so the greedy run always ends at the first
|
|
3152
|
+
# sentence-ending punctuation or paragraph break and no shorter partition
|
|
3153
|
+
# can ever satisfy the [.!?] that follows -- backtracking into it only
|
|
3154
|
+
# ever fails. Without (?>...) it fails slowly: PDF-extracted prose with
|
|
3155
|
+
# long unpunctuated stretches (the Columbia report has an 864-character
|
|
3156
|
+
# one) sent this into catastrophic backtracking, 62 seconds for a 2 KB
|
|
3157
|
+
# window and no completion on the 1.1 MB document.
|
|
3158
|
+
pattern: /#{SENTENCE_OF_SIXTY_CHARACTERS_ENDING_IN_PUNCTUATION_AND_SPACE}\K
|
|
3159
|
+
(?:Nothing|Most|None|Everything|Everyone|Nobody|Then|Neither|Both)
|
|
3160
|
+
(?:,?(?:[ \t]|\r?\n(?!\s*\n))+[\w'’-]+){1,7}[.!?](?=[ \t]*(?:\r?\n[ \t]*(?:\r?\n|\z)|\z))/x,
|
|
3161
|
+
message: "A short quantifier-led closer after a long sentence is the AI kicker.",
|
|
3162
|
+
suggestion: "Cut the closer, or move the claim to the front of the paragraph.",
|
|
3163
|
+
examples_bad: [
|
|
3164
|
+
"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.",
|
|
3165
|
+
"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.",
|
|
3166
|
+
"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.",
|
|
3167
|
+
# Both sentences may be hard-wrapped.
|
|
3168
|
+
"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"
|
|
3169
|
+
],
|
|
3170
|
+
examples_ok: [
|
|
3171
|
+
# No long sentence before it.
|
|
3172
|
+
"Nothing here needs a new login.",
|
|
3173
|
+
# Not the end of the paragraph, wrapped or not.
|
|
3174
|
+
"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.",
|
|
3175
|
+
"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.",
|
|
3176
|
+
# A closer that does not open on the list.
|
|
3177
|
+
"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.",
|
|
3178
|
+
# Too long to be a kicker.
|
|
3179
|
+
"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.",
|
|
3180
|
+
# Too short to be a kicker.
|
|
3181
|
+
"Each step can be done in the app, pasted into whichever assistant the company allows, or run the way the team already works. Nothing.",
|
|
3182
|
+
# More than two spaces is not a sentence gap.
|
|
3183
|
+
"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.",
|
|
3184
|
+
# A bullet followed by another bullet is not a paragraph end.
|
|
3185
|
+
"- 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",
|
|
3186
|
+
# Blanked text cannot make the long sentence.
|
|
3187
|
+
"See here. Nothing here breaks.\n",
|
|
3188
|
+
# A bare demonstrative closing a step is how procedural writing ends
|
|
3189
|
+
# a paragraph. Wording follows Turning and Boring (1919) and
|
|
3190
|
+
# Aviation Engines (1917), both public domain by date.
|
|
3191
|
+
"The cutting tools are set to the dimensions required for the finished work, and the stops are locked. This completes the roughing operations.",
|
|
3192
|
+
"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."
|
|
3193
|
+
],
|
|
3194
|
+
rationale: "A short sentence after a long one borrows emphasis from the contrast, " \
|
|
3195
|
+
"and a model spends that emphasis at the end of nearly every paragraph, " \
|
|
3196
|
+
"restating the point it has just made. People write the shape too, at " \
|
|
3197
|
+
"about 150 per million words, so one flag means nothing; a draft " \
|
|
3198
|
+
"where the flag repeats paragraph after paragraph should be read as a " \
|
|
3199
|
+
"warning, and the fix is usually to delete the closer outright."
|
|
3200
|
+
),
|
|
3201
|
+
Rule.new(
|
|
3202
|
+
id: "ellipsis-closer",
|
|
3203
|
+
category: "structure",
|
|
3204
|
+
severity: "info",
|
|
3205
|
+
# The same long-sentence-then-short-closer shape as mic-drop-closer,
|
|
3206
|
+
# but the tell lives in the verb, not the subject. mic-drop-closer's
|
|
3207
|
+
# closer opens on a quantifier and keeps a full verb with its object
|
|
3208
|
+
# ("Nothing here needs a new login."). This one carries no verb at
|
|
3209
|
+
# all: the closer's verb phrase has been elided down to the bare
|
|
3210
|
+
# auxiliary that would have introduced it, and the object it
|
|
3211
|
+
# promised never arrives -- "They found an exposed dashboard and
|
|
3212
|
+
# asked the agent running on it to hand over its own key. The agent
|
|
3213
|
+
# did." Because the tell is the missing verb, this rule needs no
|
|
3214
|
+
# subject list the way mic-drop-closer does, and "That", "This" and
|
|
3215
|
+
# "It" are not excluded here the way they are there: "This completes
|
|
3216
|
+
# the roughing operations." ends on a full verb with an object and
|
|
3217
|
+
# never matches (there is no auxiliary at the sentence's end), while
|
|
3218
|
+
# "This did." would, on the same subject mic-drop-closer had to bar.
|
|
3219
|
+
#
|
|
3220
|
+
# The closer is a one-to-three word subject running straight into the
|
|
3221
|
+
# bare auxiliary and a period, and, same as mic-drop-closer, it must
|
|
3222
|
+
# be the last thing in the paragraph -- a blank line or the end of
|
|
3223
|
+
# the text follows it. A closing quotation mark after the period is
|
|
3224
|
+
# not blank, so quoted dialogue never satisfies this and is excluded
|
|
3225
|
+
# the same way short-run excludes it. A negative lookahead drops any
|
|
3226
|
+
# closer that still holds "what", "that", "which", "who", "why" or
|
|
3227
|
+
# "how", because those mark a subordinate clause supplying its own
|
|
3228
|
+
# complement rather than an elided one: "Nobody knew who did." asks
|
|
3229
|
+
# "who did it", which the subject cap alone does not catch, since
|
|
3230
|
+
# "Nobody knew who" is itself only three words.
|
|
3231
|
+
#
|
|
3232
|
+
# Reuses mic-drop-closer's long-sentence prefix rather than pasting a
|
|
3233
|
+
# second copy of it; see that constant's comment for the atomic group
|
|
3234
|
+
# that keeps the scan linear.
|
|
3235
|
+
#
|
|
3236
|
+
# The negated form is bolted on with "n't" for every auxiliary except
|
|
3237
|
+
# two irregular ones: "can" already ends in n, so its negation is
|
|
3238
|
+
# "can't", not "cann't", and "will" negates to the different stem
|
|
3239
|
+
# "won't" rather than "willn't". Both are spelled out rather than
|
|
3240
|
+
# built by suffix, so the two most common contractions in the list
|
|
3241
|
+
# are not silently unmatchable.
|
|
3242
|
+
pattern: /#{SENTENCE_OF_SIXTY_CHARACTERS_ENDING_IN_PUNCTUATION_AND_SPACE}\K
|
|
3243
|
+
(?![^.!?\n]*\b(?:what|that|which|who|why|how)\b)
|
|
3244
|
+
[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[ \t]+
|
|
3245
|
+
(?:(?:did|does|do|was|were|is|are|had|has|could|would|should|might|must)(?:n['’]t)?
|
|
3246
|
+
|can(?:['’]t)?|will|won['’]t)\.
|
|
3247
|
+
(?=[ \t]*(?:\r?\n[ \t]*(?:\r?\n|\z)|\z))/x,
|
|
3248
|
+
message: "A closer that ends on a bare auxiliary is the AI verb-phrase-ellipsis kicker.",
|
|
3249
|
+
suggestion: "Cut the closer, or say what actually happened.",
|
|
3250
|
+
examples_bad: [
|
|
3251
|
+
"They found an exposed dashboard and asked the agent running on it to hand over its own key. The agent did.",
|
|
3252
|
+
"The team spent three weeks arguing about whether the migration was worth the downtime it would cost the on-call rotation. It wasn't.",
|
|
3253
|
+
"The reviewer asked whether a contractor with read access to the shared drive could still open the finance folder after the offboarding ran. She could.",
|
|
3254
|
+
"He asked whether the on-call engineer had actually paged the second responder before escalating past the fifteen-minute window. She had.",
|
|
3255
|
+
# The two irregular negated forms, pinned so a later rewrite of the
|
|
3256
|
+
# suffix can't quietly drop them again.
|
|
3257
|
+
"They double-checked whether the fallback path could still serve read traffic once the primary region failed over during the drill. It can't.",
|
|
3258
|
+
"The team hoped the migration window would close before the seasonal freight peak began overwhelming the warehouse systems. It won't."
|
|
3259
|
+
],
|
|
3260
|
+
examples_ok: [
|
|
3261
|
+
# Not the end of the paragraph.
|
|
3262
|
+
"They found an exposed dashboard and asked the agent running on it to hand over its own key. The agent did. We logged the incident and rotated the key within the hour.",
|
|
3263
|
+
# A subordinate clause, not an elided one -- caught by the wh-word guard.
|
|
3264
|
+
"The team spent three weeks arguing about whether the migration was worth the downtime it would cost the on-call rotation. That is what it did.",
|
|
3265
|
+
# No long sentence in front of it.
|
|
3266
|
+
"The agent did.",
|
|
3267
|
+
# A subject longer than three words -- the cap, not the wh-guard, excludes it.
|
|
3268
|
+
"The engineer who had been paged in the middle of the night finally agreed with what the on-call reviewer had been saying for the better part of an hour about the rollback plan. The whole team already did.",
|
|
3269
|
+
# A full verb with an object, not a bare auxiliary.
|
|
3270
|
+
"Each step already passed local review before it reached the pipeline that runs on every push to the shared branch. It worked.",
|
|
3271
|
+
# A bare demonstrative closing a step is how procedural writing ends
|
|
3272
|
+
# a paragraph, and it ends on a full verb, not an auxiliary. Wording
|
|
3273
|
+
# follows Turning and Boring (1919) and Aviation Engines (1917),
|
|
3274
|
+
# both public domain by date; mic-drop-closer carries the same two
|
|
3275
|
+
# examples for the same reason.
|
|
3276
|
+
"The cutting tools are set to the dimensions required for the finished work, and the stops are locked. This completes the roughing operations.",
|
|
3277
|
+
"The magneto is protected from oil and grit by a cover that is easy to remove for service. This means prolonged life for the magneto."
|
|
3278
|
+
],
|
|
3279
|
+
rationale: "The closer withholds exactly the thing the long setup built toward -- the " \
|
|
3280
|
+
"verb and its object are gone, and only the bare confirmation that something " \
|
|
3281
|
+
"happened is left standing. Ships at info: plenty of ordinary writing drops " \
|
|
3282
|
+
"the verb the same way when it confirms an expectation, and one flag proves " \
|
|
3283
|
+
"nothing; a draft where it repeats is the tell."
|
|
3284
|
+
),
|
|
3285
|
+
Rule.new(
|
|
3286
|
+
id: "short-run",
|
|
3287
|
+
category: "structure",
|
|
3288
|
+
severity: "info",
|
|
3289
|
+
# Three consecutive sentences of thirty characters or fewer, each
|
|
3290
|
+
# opening on a letter and closing on a full stop, with no quotation
|
|
3291
|
+
# mark or digit in any of them: "Nobody used it. A named owner. Then a
|
|
3292
|
+
# review." The run must start at a real boundary (the text, a blank
|
|
3293
|
+
# line, a line ending on a stop, or a stop and one or two spaces), so
|
|
3294
|
+
# the short tail of a hard-wrapped long sentence never opens one; it
|
|
3295
|
+
# may cross a hard-wrapped newline but not a paragraph break, and the
|
|
3296
|
+
# indent after a newline is at most four spaces, so a URL or code span
|
|
3297
|
+
# blanked by --markdown cannot weld two sentences. A list marker may
|
|
3298
|
+
# open the run, but consecutive bullets are a list, not staccato.
|
|
3299
|
+
#
|
|
3300
|
+
# Dialogue is excluded by the boundary: a closing quote after the stop
|
|
3301
|
+
# is not a space. A quotation mark inside a sentence is excluded by the
|
|
3302
|
+
# class. A sentence with a digit in it is data, and a run holding a
|
|
3303
|
+
# multi-letter abbreviation ("Prof.", "Dept.") is dropped, since the
|
|
3304
|
+
# abbreviation is not a sentence end.
|
|
3305
|
+
#
|
|
3306
|
+
# Three narrowings after 2.25M words of technical prose, all of them the
|
|
3307
|
+
# same mistake: reading document furniture as sentences.
|
|
3308
|
+
#
|
|
3309
|
+
# A "sentence" ending on a lone letter is a list label, not a sentence.
|
|
3310
|
+
# The guard used to cover capitals only, for initials ("Alan W."), so
|
|
3311
|
+
# lettered enumeration walked through it -- "Acronym e. OpNom f. Hazard
|
|
3312
|
+
# System record number g." is one form field, read as three sentences.
|
|
3313
|
+
# No English sentence ends on a bare letter, either case, so the guard
|
|
3314
|
+
# now covers both.
|
|
3315
|
+
#
|
|
3316
|
+
# Every sentence in the run must hold a space. A one-word "sentence" is
|
|
3317
|
+
# an abbreviation, and a citation line is nothing else: "Natl. Inst.
|
|
3318
|
+
# Stand. Technol." was a clean three-sentence run under the old shape.
|
|
3319
|
+
#
|
|
3320
|
+
# Every sentence opens on a capital. Lowercase-initial runs are speech
|
|
3321
|
+
# transcribed, not prose written: a cockpit voice recorder transcript
|
|
3322
|
+
# ("we all get gas. we go to divert to Albany. we pick up a load.") has
|
|
3323
|
+
# the staccato shape exactly, and an accident report carries pages of
|
|
3324
|
+
# it. A sentence in edited prose starts with a capital.
|
|
3325
|
+
#
|
|
3326
|
+
# And the run may not step over a list marker -- see MARKER below. Questions and exclamations are
|
|
3327
|
+
# left out because a run of them is a different device.
|
|
3328
|
+
#
|
|
3329
|
+
# Ships at info. A staccato run is a device people use on purpose, at
|
|
3330
|
+
# about thirty per million words on Hacker News, so one flag is a
|
|
3331
|
+
# question. A draft that keeps doing it is the tell, and an agent that
|
|
3332
|
+
# sees the flag repeat should read the family as a warning.
|
|
3333
|
+
# MARKER is the list furniture this rule has to see in order to ignore
|
|
3334
|
+
# it: a bullet glyph, the literal "o" that plain-text technical
|
|
3335
|
+
# documents use as one, a numbered item, and a lettered item. It opens a
|
|
3336
|
+
# run (a bullet may hold staccato) but may never sit *inside* one --
|
|
3337
|
+
# three bullets in a row are a list, which is what the comment above
|
|
3338
|
+
# always intended and the pattern did not enforce.
|
|
3339
|
+
pattern: /(?:\A|(?<=\n\n)|(?<=[.!?])[ \t]{0,2}\r?\n|(?<=[.!?])[ \t]{1,2})[ \t]{0,4}
|
|
3340
|
+
(?:[-*+•][ \t]+|o[ \t]+(?=[A-Z])|\d+[.)][ \t]+|[A-Za-z][.)][ \t]+(?=[A-Z]))?\K
|
|
3341
|
+
(?:[A-Za-z][^.!?\n"“”0-9]{3,29}(?<![^A-Za-z'’][A-Za-z])\.
|
|
3342
|
+
(?:[ \t]{1,2}|\r?\n(?!\s*\n)[ \t]{0,4})
|
|
3343
|
+
(?![-*+•][ \t]|o[ \t]+[A-Z]|\d+[.)][ \t]|[A-Za-z][.)][ \t]+[A-Z])){2}
|
|
3344
|
+
[A-Za-z][^.!?\n"“”0-9]{3,29}(?<![^A-Za-z'’][A-Za-z])\.(?=\s|\z)/x,
|
|
3345
|
+
message: "A run of three short sentences reads as AI staccato.",
|
|
3346
|
+
suggestion: "Join two of them, or give one of them a subordinate clause.",
|
|
3347
|
+
# Two whole-run exclusions, because each is a property of the run and
|
|
3348
|
+
# not of any one sentence in it. Three single-word "sentences" in a row
|
|
3349
|
+
# is a citation line ("Natl. Inst. Stand. Technol."), never staccato --
|
|
3350
|
+
# one single-word sentence is the archetypal kicker and stays. Three
|
|
3351
|
+
# lowercase-led sentences in a row is transcribed speech ("we all get
|
|
3352
|
+
# gas. we go to divert to Albany."); a run that reaches a capital
|
|
3353
|
+
# anywhere is prose, so identifier-initial writing ("npm was slow. git
|
|
3354
|
+
# blame helped. We moved on.") is untouched.
|
|
3355
|
+
skip: [
|
|
3356
|
+
/\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)\./,
|
|
3357
|
+
/\A\S+\.[ \t\r\n]+\S+\.[ \t\r\n]+\S+\.\z/,
|
|
3358
|
+
/\A[a-z][^.!?]*\.\s+[a-z][^.!?]*\.\s+[a-z][^.!?]*\.\z/
|
|
3359
|
+
],
|
|
3360
|
+
examples_bad: [
|
|
3361
|
+
"Nobody used it. A named owner. Then a review.",
|
|
3362
|
+
"The draft sat on one desk. Nobody else saw it. So it never shipped.",
|
|
3363
|
+
"Drafts only at first. Widen after a month. Two people sign off.",
|
|
3364
|
+
# A hard-wrapped run survives one newline.
|
|
3365
|
+
"Nobody used it. A named owner.\nThen a review.",
|
|
3366
|
+
# A bullet may hold a run.
|
|
3367
|
+
"- Nobody used it. A named owner. Then a review.",
|
|
3368
|
+
# An acronym ends a sentence; only a lone initial does not.
|
|
3369
|
+
"Nobody used it. We shipped it to QA. Then a review.",
|
|
3370
|
+
# A one-word sentence is the kicker, not an abbreviation, so only a
|
|
3371
|
+
# run made entirely of them is excluded.
|
|
3372
|
+
"The fix landed. Agreed. We moved on.",
|
|
3373
|
+
# A sentence may end on a possessive; that is not a bare letter.
|
|
3374
|
+
"We shipped a fix. The bug was Ana's. Nobody cared.",
|
|
3375
|
+
# Identifier-initial prose is the register this tool is aimed at, so a
|
|
3376
|
+
# run only counts as transcript when nothing in it reaches a capital.
|
|
3377
|
+
"git blame helped. npm was slow. We moved on."
|
|
3378
|
+
],
|
|
3379
|
+
examples_ok: [
|
|
3380
|
+
"Nobody used it. A named owner and a quarterly review that the whole team can see.",
|
|
3381
|
+
# Dialogue.
|
|
3382
|
+
"\"Go now.\" \"I will.\" \"Then go.\"",
|
|
3383
|
+
# A quotation mark inside a sentence.
|
|
3384
|
+
"He said \"go\" today. Then a review. So it ended.",
|
|
3385
|
+
# A paragraph break ends the run.
|
|
3386
|
+
"Nobody used it. A named owner.\n\nThen a review.",
|
|
3387
|
+
# Questions and exclamations are a different device.
|
|
3388
|
+
"Who owns it? Nobody. Who checks it? Nobody.",
|
|
3389
|
+
# Two short sentences are a pair.
|
|
3390
|
+
"Ship it. Then find out if it was worth the effort and the wait.",
|
|
3391
|
+
# One sentence over thirty characters breaks the run.
|
|
3392
|
+
"Nobody used it. A named owner reviewed the draft again. Then a review.",
|
|
3393
|
+
# Numbers are data, and an initial is not a sentence end.
|
|
3394
|
+
"Hold cash. Expected value: 1000. Expected tax: none.",
|
|
3395
|
+
"Alan W. Prosser loves officer Kane. Nobody else does.",
|
|
3396
|
+
# An abbreviation is not a sentence end.
|
|
3397
|
+
"She wrote to Prof. Ellis at the Dept. Nobody replied to her.",
|
|
3398
|
+
# The tail of a hard-wrapped long sentence does not open a run.
|
|
3399
|
+
"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.",
|
|
3400
|
+
# Blanked text after a newline cannot weld two sentences.
|
|
3401
|
+
"Nobody used it. A named owner.\n Then a review.",
|
|
3402
|
+
# Consecutive bullets are a list.
|
|
3403
|
+
"- Fast setup.\n- No config.\n- Free tier.\n",
|
|
3404
|
+
# Document furniture, not staccato. Wording follows a NASA hazard
|
|
3405
|
+
# report form, a NIST publication citation line and a NIST control
|
|
3406
|
+
# enumeration -- US government works.
|
|
3407
|
+
"Acronym e. OpNom f. Hazard System record number g.",
|
|
3408
|
+
"Phone q. Fax r. e-mail s.",
|
|
3409
|
+
"Natl. Inst. Stand. Technol.",
|
|
3410
|
+
"What type of event occurred; b. When the event occurred; c. Where the event occurred; d.",
|
|
3411
|
+
"o Shop was not a clean area. o Lighting was not adequate. o Space was limited.",
|
|
3412
|
+
# Consecutive bullets are a list, whatever each one says.
|
|
3413
|
+
"- Nobody used it here.\n- A named owner was set.\n- Then a review happened.",
|
|
3414
|
+
# Transcribed speech has the staccato shape and is not prose.
|
|
3415
|
+
# Wording follows an NTSB cockpit voice recorder transcript.
|
|
3416
|
+
"we all get gas. we go to divert to Albany. we pick up a load."
|
|
3417
|
+
],
|
|
3418
|
+
rationale: "Short sentences in a row borrow force from their rhythm, and a model " \
|
|
3419
|
+
"falls into the rhythm whenever it wants to sound decisive. People do it " \
|
|
3420
|
+
"on purpose, about thirty times per million words, so one flag is a " \
|
|
3421
|
+
"question; a draft where the flag repeats should be read as a warning."
|
|
3422
|
+
),
|
|
1264
3423
|
Rule.new(
|
|
1265
3424
|
id: "em-dash",
|
|
1266
3425
|
category: "structure",
|
|
@@ -1436,6 +3595,98 @@ module Sloplint
|
|
|
1436
3595
|
"narrator who never appears. Careful writers put the verb in its own clause " \
|
|
1437
3596
|
"with a subject, or leave the significance to the reader."
|
|
1438
3597
|
),
|
|
3598
|
+
Rule.new(
|
|
3599
|
+
id: "trailing-restatement",
|
|
3600
|
+
category: "structure",
|
|
3601
|
+
severity: "info",
|
|
3602
|
+
default_on: false,
|
|
3603
|
+
# The restating tail: "…, which means working through the process
|
|
3604
|
+
# rather than around it". A regex sees the connective and not whether
|
|
3605
|
+
# the tail says the head again, and the sentence that prompted the rule
|
|
3606
|
+
# shares no content words between the two, so no overlap test reaches
|
|
3607
|
+
# it either. Hence off by default.
|
|
3608
|
+
#
|
|
3609
|
+
# Three connectives, and four participle frames that make the same
|
|
3610
|
+
# move. Nothing before the comma is inspected: a gloss ("_ma_, which
|
|
3611
|
+
# means hand") and a real tell after a code span or a bold phrase end
|
|
3612
|
+
# on the same characters, so a guard there costs more hits than it
|
|
3613
|
+
# saves. Glosses are a known cost of the rule. The participle
|
|
3614
|
+
# "meaning" must open on one of a closed set of determiners and
|
|
3615
|
+
# pronouns, which keeps out the noun ("meaning of"), the intention
|
|
3616
|
+
# ("meaning to come back") and the bare-noun gloss ("meaning six"); a
|
|
3617
|
+
# gloss that takes an article ("meaning the red stick") gets through.
|
|
3618
|
+
# Each participle frame needs a pronoun object and a closing word,
|
|
3619
|
+
# because the bare participle is ordinary English ("she left, leaving
|
|
3620
|
+
# the door open"). The closer on "making" is a comparative or
|
|
3621
|
+
# (im)possible and must end the clause or lead into "to", "for" or
|
|
3622
|
+
# "than", since "-er" alone is also "wonder", "offer" and "her". These
|
|
3623
|
+
# verbs stay off trailing-significance-participle, whose list is closed
|
|
3624
|
+
# to verbs an event can be the subject of.
|
|
3625
|
+
pattern: /,#{WRAP_GAP}+
|
|
3626
|
+
(?:which#{WRAP_GAP}+means\b
|
|
3627
|
+
|which#{WRAP_GAP}+is#{WRAP_GAP}+to#{WRAP_GAP}+say\b
|
|
3628
|
+
|meaning#{WRAP_GAP}+(?:that|the|a|an|this|these|those|it|he|she|we|you|they|there|nothing|every|each|any|all|most|some|your|our|their|its)\b
|
|
3629
|
+
|making#{WRAP_GAP}+(?:it|them|us|you|the(?:#{WRAP_GAP}+[\w'’-]+){1,2})#{WRAP_GAP}+
|
|
3630
|
+
(?:[a-z]+er|(?:im)?possible|(?:more|less)(?:#{WRAP_GAP}+[a-z]+)?)
|
|
3631
|
+
(?=#{WRAP_GAP}+(?:to|for|than)\b|[ \t]*[.,;:!?)]|[ \t]*\r?\n|[ \t]*\z)
|
|
3632
|
+
|allowing#{WRAP_GAP}+(?:it|them|us|you)#{WRAP_GAP}+to\b
|
|
3633
|
+
|giving#{WRAP_GAP}+(?:them|us|you)#{WRAP_GAP}+(?:more|less|time|room)\b
|
|
3634
|
+
|leaving#{WRAP_GAP}+(?:them|us|you)#{WRAP_GAP}+with(?:out)?\b)/ix,
|
|
3635
|
+
message: "Trailing clause that says the sentence again, or hangs a result off it.",
|
|
3636
|
+
suggestion: "Cut the tail, or if it states a real consequence, make it its own sentence.",
|
|
3637
|
+
examples_bad: [
|
|
3638
|
+
"We moved the checks into the build step, which means the errors show up before anyone opens a review.",
|
|
3639
|
+
"The new queue drains in order, which is to say nothing jumps ahead of an older job.",
|
|
3640
|
+
"Every job now records its own start time, meaning the log tells you when the run began.",
|
|
3641
|
+
"He signed at once, meaning he had read it already.",
|
|
3642
|
+
"We rebuilt the parser, making the whole pipeline faster.",
|
|
3643
|
+
"The cache is local now, making it easier to reason about.",
|
|
3644
|
+
"The change is small, making it more robust.",
|
|
3645
|
+
"The lock is per row, making it impossible for two writers to collide.",
|
|
3646
|
+
"The cache now lives beside the worker, allowing us to skip the round trip.",
|
|
3647
|
+
"The report ships on Fridays, giving them more time to read it.",
|
|
3648
|
+
"The old flags are gone, leaving you with one switch to learn.",
|
|
3649
|
+
# Nothing before the comma is inspected.
|
|
3650
|
+
"The flag defaults to `false`, which means nothing is written to disk.",
|
|
3651
|
+
"- **Cache is local**, which means the round trip is gone.",
|
|
3652
|
+
"We fixed the bug (the null check), which means the crash is gone.",
|
|
3653
|
+
# Either gap may hard-wrap.
|
|
3654
|
+
"We moved the checks into the build step,\nwhich means the errors show up first.",
|
|
3655
|
+
"We moved the checks into the build step, which\nmeans the errors show up first."
|
|
3656
|
+
],
|
|
3657
|
+
examples_ok: [
|
|
3658
|
+
# "meaning" outside the closed set: the bare-noun gloss, the intention, the noun.
|
|
3659
|
+
"They count on with tatisitupe, meaning six.",
|
|
3660
|
+
"It is an old word, meaning caves.",
|
|
3661
|
+
"He left the room, meaning to come back before dark.",
|
|
3662
|
+
"She read on, meaning of the word aside, and let it pass.",
|
|
3663
|
+
"He shrugged, meaning no harm by it.",
|
|
3664
|
+
# The bare participles are ordinary English.
|
|
3665
|
+
"She left the room, leaving the door open.",
|
|
3666
|
+
"Cut the paper around the frame, leaving a margin for pasting.",
|
|
3667
|
+
"He handed over the keys, giving her a nod.",
|
|
3668
|
+
"The lid lifts off, allowing the steam to escape.",
|
|
3669
|
+
# A pronoun object with no closing word, or the wrong one.
|
|
3670
|
+
"Rub down the leather, making it as smooth as possible.",
|
|
3671
|
+
"The crowd parted, making room for more chairs.",
|
|
3672
|
+
"He pulled the cork, giving it time to breathe.",
|
|
3673
|
+
"He shut the door, leaving them to it.",
|
|
3674
|
+
"The porter took the trunk, leaving it with the station master.",
|
|
3675
|
+
# "-er" that is not a comparative, and a comparative that does not close the clause.
|
|
3676
|
+
"The rain kept up all week, making you wonder whether the trip was worth it.",
|
|
3677
|
+
"She turned the coat inside out, making it her own.",
|
|
3678
|
+
"They argued, making the same point over and over.",
|
|
3679
|
+
"The tide turned, making it matter less than before.",
|
|
3680
|
+
# A gerund list.
|
|
3681
|
+
"The work involves cutting, making and sanding the parts.",
|
|
3682
|
+
# A paragraph break is not a comma.
|
|
3683
|
+
"The checks moved into the build step\n\nWhich means the errors show up first."
|
|
3684
|
+
],
|
|
3685
|
+
rationale: "The tail after the connective says the head again in other words, and a " \
|
|
3686
|
+
"model adds one whenever a sentence feels short of a point. Careful writers " \
|
|
3687
|
+
"use the same connective to state a consequence, and the pattern cannot " \
|
|
3688
|
+
"tell the two apart, so the rule is off by default."
|
|
3689
|
+
),
|
|
1439
3690
|
# ── hedging ───────────────────────────────────────────────────────────
|
|
1440
3691
|
Rule.new(
|
|
1441
3692
|
id: "vague-attribution",
|