sloplint 0.5.0 → 0.6.0

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