sloplint 0.7.0 → 0.8.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.
@@ -6,12 +6,14 @@ module Sloplint
6
6
  # count_group: a Regexp scanned over the matched text to tally items; when set,
7
7
  # the Note carries `count` and the message may interpolate %{count}.
8
8
  # skip: Regexps that, if they match the matched text, drop the note (exclusions).
9
- # default_on: false keeps a noisy rule out of the default run (still selectable).
9
+ # severity: what the construct costs the prose -- "error", "warning", "info".
10
+ # confidence: how likely a match is a false positive -- "high", "medium", "low".
11
+ # A "low" rule stays out of the default run; --strict or its own id turns it on.
10
12
  Rule = Data.define(
11
- :id, :category, :severity, :pattern, :message, :suggestion,
12
- :examples_bad, :examples_ok, :count_group, :skip, :rationale, :default_on
13
+ :id, :category, :severity, :confidence, :pattern, :message, :suggestion,
14
+ :examples_bad, :examples_ok, :count_group, :skip, :rationale
13
15
  ) do
14
- def initialize(count_group: nil, skip: [], rationale: nil, default_on: true, **rest)
16
+ def initialize(count_group: nil, skip: [], rationale: nil, **rest)
15
17
  super
16
18
  end
17
19
  end
@@ -83,11 +85,11 @@ module Sloplint
83
85
  "|time|world|numbers?|roots?|money|estate|user"
84
86
 
85
87
  RULES = [
86
- # ── rhetorical-tic ────────────────────────────────────────────────────
87
88
  Rule.new(
88
89
  id: "no-x-no-y",
89
- category: "rhetorical-tic",
90
+ category: "cadence",
90
91
  severity: "warning",
92
+ confidence: "high",
91
93
  # Comma chains only: "no fluff, no filler, no jargon". The comma is the
92
94
  # evidence -- it makes the parallelism deliberate. Fragment chains split
93
95
  # by sentence punctuation are a separate, quieter rule (no-x-no-y-frag),
@@ -104,10 +106,11 @@ module Sloplint
104
106
  ),
105
107
  Rule.new(
106
108
  id: "no-x-no-y-frag",
107
- category: "rhetorical-tic",
108
- severity: "info",
109
+ category: "cadence",
110
+ severity: "warning",
111
+ confidence: "medium",
109
112
  # The same cadence built from sentence fragments: "No fluff. No filler."
110
- # Ships at info, not warning, because the shape is genuinely ambiguous --
113
+ # Medium confidence, because the shape is genuinely ambiguous --
111
114
  # two short "no" sentences in a row is also just writing ("No one moved.
112
115
  # No one spoke."). The agent reading the flag decides; see rationale.
113
116
  #
@@ -147,8 +150,9 @@ module Sloplint
147
150
  ),
148
151
  Rule.new(
149
152
  id: "thats-the-whole",
150
- category: "rhetorical-tic",
153
+ category: "closer",
151
154
  severity: "warning",
155
+ confidence: "high",
152
156
  # The nouns are WHOLE_CLOSERS. "value" and "fix" are the closers agents
153
157
  # write in technical prose ("That's the whole fix"), where the
154
158
  # contraction keeps them out of is-the-whole-x, which sees only "is".
@@ -198,8 +202,9 @@ module Sloplint
198
202
  ),
199
203
  Rule.new(
200
204
  id: "is-the-whole-x",
201
- category: "rhetorical-tic",
202
- severity: "info",
205
+ category: "closer",
206
+ severity: "warning",
207
+ confidence: "medium",
203
208
  # The generic form of thats-the-whole: a subject, then "is the whole /
204
209
  # real / actual / entire N" with N from a closed abstract list. "That
205
210
  # periodicity is the whole tell.", "Consistency is the real test." The
@@ -216,7 +221,7 @@ module Sloplint
216
221
  # "cost" because "the real deal", "the real thing" and "the whole cost"
217
222
  # are idioms or quantities. The noun may not run on into a compound
218
223
  # ("problem-solver"). Gaps may cross a hard-wrapped newline but never
219
- # a paragraph break. Ships at info because "the real question" and
224
+ # a paragraph break. Medium confidence: "the real question" and
220
225
  # "the whole point" are also how people talk.
221
226
  pattern: /(?<![\w'’-])
222
227
  (?!(?:that|this)#{WRAP_GAP}+(?:is)#{WRAP_GAP}+(?:the)#{WRAP_GAP}+(?:whole)#{WRAP_GAP}+(?:#{WHOLE_CLOSERS})(?![\w'’-]))
@@ -265,8 +270,9 @@ module Sloplint
265
270
  ),
266
271
  Rule.new(
267
272
  id: "bare-equative",
268
- category: "rhetorical-tic",
269
- severity: "info",
273
+ category: "closer",
274
+ severity: "warning",
275
+ confidence: "medium",
270
276
  # A sentence that opens on an abstract head noun and equates it with
271
277
  # something: "The tell here is the periodicity.", "The problem is not
272
278
  # the tool.", "The lesson is the handoff." The head noun list is closed
@@ -280,8 +286,8 @@ module Sloplint
280
286
  # answer is a mess"), a pointing complement ("the same", "the one",
281
287
  # "the first"), and a proper noun ("the Slack thread") are all out.
282
288
  # Gaps may cross a hard-wrapped newline but never a paragraph break.
283
- # Ships at info: "The problem is the cost" is how people write too, at
284
- # about four per million words on Hacker News; it is the density that
289
+ # Medium confidence: "The problem is the cost" is how people write too,
290
+ # at about four per million words on Hacker News; it is the density that
285
291
  # tells.
286
292
  pattern: /(?:^|(?<=[.!?])[ \t]{1,2})(?:[-*+•][ \t]+|\d+[.)][ \t]+)?\KThe(?:[ \t]|\r?\n(?!\s*\n))+
287
293
  (?: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))+
@@ -322,9 +328,9 @@ module Sloplint
322
328
  ),
323
329
  Rule.new(
324
330
  id: "epistrophe",
325
- category: "rhetorical-tic",
331
+ category: "cadence",
326
332
  severity: "info",
327
- default_on: false,
333
+ confidence: "low",
328
334
  # Two clauses that end on the same two-word phrase, the second closing
329
335
  # the sentence: "built for one desk, and almost no job is done at one
330
336
  # desk." Two backreferences catch the repeat, one per word, so the
@@ -374,9 +380,9 @@ module Sloplint
374
380
  ),
375
381
  Rule.new(
376
382
  id: "phrase-echo",
377
- category: "rhetorical-tic",
383
+ category: "cadence",
378
384
  severity: "info",
379
- default_on: false,
385
+ confidence: "low",
380
386
  # The same three words again a few paragraphs on. Three consecutive
381
387
  # words, each four characters or more and lowercase-led, one of them
382
388
  # six letters with nothing but letters, and the same three again
@@ -458,8 +464,9 @@ module Sloplint
458
464
  ),
459
465
  Rule.new(
460
466
  id: "did-not-x-did-not-y",
461
- category: "rhetorical-tic",
467
+ category: "cadence",
462
468
  severity: "warning",
469
+ confidence: "high",
463
470
  pattern: /\b(?:did\s+not|didn't)\s+[\w'-]+(?:,?\s+(?:and\s+)?(?:did\s+not|didn't)\s+[\w'-]+)+/i,
464
471
  message: '"did not X, did not Y" chain (%{count} items) reads as AI cadence.',
465
472
  suggestion: "Cut the chain or make it one plain sentence.",
@@ -470,8 +477,9 @@ module Sloplint
470
477
  ),
471
478
  Rule.new(
472
479
  id: "from-x-to-y-chain",
473
- category: "rhetorical-tic",
480
+ category: "cadence",
474
481
  severity: "warning",
482
+ confidence: "high",
475
483
  # Two or more "from X to Y" spans in a row, comma-separated: "from
476
484
  # private notes to shared files, from personal memory to team context".
477
485
  # Each span is "from", one to three words, "to", one to three words; the
@@ -546,8 +554,9 @@ module Sloplint
546
554
  ),
547
555
  Rule.new(
548
556
  id: "one-x-one-y",
549
- category: "rhetorical-tic",
557
+ category: "cadence",
550
558
  severity: "warning",
559
+ confidence: "high",
551
560
  # Three or more "one X" items in a comma chain that stands on its own:
552
561
  # "One owner, one repository, one weekly prune." or, after a colon,
553
562
  # "the setup is narrow: one reviewer, one queue, one deadline". The
@@ -617,8 +626,9 @@ module Sloplint
617
626
  ),
618
627
  Rule.new(
619
628
  id: "and-what-it-should",
620
- category: "rhetorical-tic",
629
+ category: "closer",
621
630
  severity: "warning",
631
+ confidence: "high",
622
632
  # The elliptical tail: "List what the assistant knows about the client,
623
633
  # and what it should." The second "what" clause borrows its verb from the
624
634
  # first and ends on a bare modal or a negated auxiliary, so the sentence
@@ -664,8 +674,9 @@ module Sloplint
664
674
  ),
665
675
  Rule.new(
666
676
  id: "abstract-lives-in",
667
- category: "rhetorical-tic",
668
- severity: "info",
677
+ category: "puffery",
678
+ severity: "warning",
679
+ confidence: "medium",
669
680
  # An abstraction given an address: "the craft that lives between the two
670
681
  # desks", "its context lives in a folder nobody else can open", "the
671
682
  # value sits in the follow-up". The subject list is closed and abstract,
@@ -681,7 +692,7 @@ module Sloplint
681
692
  # lies in the assumption") and "resides in" are ordinary English for
682
693
  # where a fault or an authority is, at any register.
683
694
  #
684
- # Ships at info. The same shape states where information literally is
695
+ # Medium confidence. The same shape states where information literally is
685
696
  # ("the knowledge lives in our heads", "the instructions live in the
686
697
  # README"), and a sample of pre-2022 Hacker News biased toward the
687
698
  # construction turns up a few of those per million words, all human.
@@ -724,8 +735,9 @@ module Sloplint
724
735
  ),
725
736
  Rule.new(
726
737
  id: "the-x-is-the-x",
727
- category: "rhetorical-tic",
728
- severity: "warning",
738
+ category: "cadence",
739
+ severity: "error",
740
+ confidence: "high",
729
741
  # The repeated-head equative: "the reason it holds up is the reason the
730
742
  # other half happens", "the problem with A is the problem with B". The
731
743
  # same abstract head noun sits on both sides of the copula, caught by a
@@ -787,8 +799,9 @@ module Sloplint
787
799
  ),
788
800
  Rule.new(
789
801
  id: "same-determiner-chain",
790
- category: "rhetorical-tic",
802
+ category: "cadence",
791
803
  severity: "info",
804
+ confidence: "medium",
792
805
  # The quiet cousin of one-x-one-y and no-x-no-y: three or more items in
793
806
  # a comma chain that all open on the same determiner or quantifier,
794
807
  # caught with a backreference: "every faculty, every thought, every
@@ -849,8 +862,9 @@ module Sloplint
849
862
  ),
850
863
  Rule.new(
851
864
  id: "dont-verb-it",
852
- category: "rhetorical-tic",
865
+ category: "false-correction",
853
866
  severity: "warning",
867
+ confidence: "high",
854
868
  pattern: /\b(?:don't|do\s+not)\s+(\w+)\s+it\b[^.!?]*[.!?]\s*\1\s+it\b/i,
855
869
  message: '"Don\'t X it. X it Y." reframing is a stock LLM move.',
856
870
  suggestion: "Drop the fake reframe and state the claim once.",
@@ -860,8 +874,9 @@ module Sloplint
860
874
  ),
861
875
  Rule.new(
862
876
  id: "sit-with-that",
863
- category: "rhetorical-tic",
877
+ category: "reader-address",
864
878
  severity: "warning",
879
+ confidence: "high",
865
880
  # Two branches. The deictic object is the tic anywhere in a sentence, so
866
881
  # "sit with that/this/it" needs no anchor. Anything else needs the
867
882
  # sentence-initial imperative, which is where the tic lives and where the
@@ -888,12 +903,13 @@ module Sloplint
888
903
  "The object does not matter: the tic is telling the reader to dwell instead " \
889
904
  "of giving them something to dwell on. The sentence-initial imperative does " \
890
905
  "flag the plain companion sense (\"Sit with the baby while I run out\"), " \
891
- "which is accepted -- hence warning, not error."
906
+ "which is accepted."
892
907
  ),
893
908
  Rule.new(
894
909
  id: "hold-onto-that",
895
- category: "rhetorical-tic",
910
+ category: "reader-address",
896
911
  severity: "warning",
912
+ confidence: "high",
897
913
  # Sentence-initial imperative only. Past tense and subordinate clauses
898
914
  # ("she held on to that letter", "if you hold onto that phrase") are a
899
915
  # different construction, not the tell. Everything AFTER that/this is
@@ -919,8 +935,9 @@ module Sloplint
919
935
  ),
920
936
  Rule.new(
921
937
  id: "cleanly",
922
- category: "rhetorical-tic",
923
- severity: "info",
938
+ category: "self-rating",
939
+ severity: "warning",
940
+ confidence: "medium",
924
941
  # The bare adverb was the whole rule, and the engineering idioms were
925
942
  # flagged on purpose. Technical prose says that was the wrong call: a
926
943
  # patch that applies cleanly, a build that compiles cleanly and a gear
@@ -936,7 +953,7 @@ module Sloplint
936
953
  # parts. The clause-final form ("the objection breaks down cleanly, and
937
954
  # neither half survives") is the same tell and is left out.
938
955
  #
939
- # The frame is not the sense, and this rule ships at info because of it.
956
+ # The frame is not the sense, and the confidence here is medium because of it.
940
957
  # "The argument splits cleanly into two parts" and "the gear retracted
941
958
  # cleanly into the well" are one pattern apart only in their subject,
942
959
  # and a regex cannot see the subject -- the same limit that keeps the
@@ -971,8 +988,9 @@ module Sloplint
971
988
  ),
972
989
  Rule.new(
973
990
  id: "clean-count",
974
- category: "rhetorical-tic",
991
+ category: "self-rating",
975
992
  severity: "warning",
993
+ confidence: "high",
976
994
  # Needs a partition noun. The bare count reaches the laundry: Ulysses
977
995
  # has "four clean strokes", Jane Eyre "two clean tuckers".
978
996
  pattern: /\b(?:two|three|four|five|six|seven|2|3|4|5|6|7)\s+
@@ -998,8 +1016,9 @@ module Sloplint
998
1016
  ),
999
1017
  Rule.new(
1000
1018
  id: "cleanest-x",
1001
- category: "rhetorical-tic",
1002
- severity: "warning",
1019
+ category: "self-rating",
1020
+ severity: "error",
1021
+ confidence: "high",
1003
1022
  # Noun list only. "The cleanest way to install the driver" and "the
1004
1023
  # cleanest cut of meat" are ordinary English, so "way" is admitted only
1005
1024
  # in front of a speech verb, and the concrete-capable nouns (cut, line,
@@ -1027,10 +1046,11 @@ module Sloplint
1027
1046
  ),
1028
1047
  Rule.new(
1029
1048
  id: "clean-x",
1030
- category: "rhetorical-tic",
1031
- severity: "info",
1032
- # The quiet half of cleanest-x, at info because the positive degree is
1033
- # where ordinary usage lives. "A clean separation of concerns" is
1049
+ category: "self-rating",
1050
+ severity: "warning",
1051
+ confidence: "medium",
1052
+ # The quiet half of cleanest-x, at medium confidence because the positive
1053
+ # degree is where ordinary usage lives. "A clean separation of concerns" is
1034
1054
  # standard engineering English, so "separation" stays out of the list;
1035
1055
  # "break" is admitted only in "clean break between", never bare, because
1036
1056
  # "make a clean break with the past" is an idiom and not a tell.
@@ -1052,13 +1072,14 @@ module Sloplint
1052
1072
  "The report gives a clean bill of health."
1053
1073
  ],
1054
1074
  rationale: "'Clean' in front of an idea is evaluation, not description -- it says the " \
1055
- "writer approves, and nothing about the idea. It ships at info because the " \
1056
- "same words carry a plain sense a regex cannot separate from the tic."
1075
+ "writer approves, and nothing about the idea. The same words carry a plain " \
1076
+ "sense a regex cannot separate from the tic."
1057
1077
  ),
1058
1078
  Rule.new(
1059
1079
  id: "you-already-know",
1060
- category: "rhetorical-tic",
1080
+ category: "reader-address",
1061
1081
  severity: "warning",
1082
+ confidence: "high",
1062
1083
  pattern: /\byou\s+already\s+know\b/i,
1063
1084
  message: '"You already know…" is a stock LLM rhetorical setup.',
1064
1085
  suggestion: "Just make the point; don't tell the reader they know it.",
@@ -1068,8 +1089,9 @@ module Sloplint
1068
1089
  ),
1069
1090
  Rule.new(
1070
1091
  id: "is-the-entire",
1071
- category: "rhetorical-tic",
1092
+ category: "closer",
1072
1093
  severity: "warning",
1094
+ confidence: "high",
1073
1095
  pattern: /\bis\s+the\s+entire\s+(?:point|game|thing|business\s+model|deal|story)\b/i,
1074
1096
  message: '"X is the entire point/game/…" is an LLM emphasis tic.',
1075
1097
  suggestion: "State the point plainly without the superlative frame.",
@@ -1079,8 +1101,9 @@ module Sloplint
1079
1101
  ),
1080
1102
  Rule.new(
1081
1103
  id: "the-entire-is",
1082
- category: "rhetorical-tic",
1104
+ category: "closer",
1083
1105
  severity: "warning",
1106
+ confidence: "high",
1084
1107
  pattern: /\bthe\s+entire\s+(?:point|game|thing|business\s+model|deal|story)\s+is\b/i,
1085
1108
  message: '"The entire point/game/… is" is an LLM emphasis tic.',
1086
1109
  suggestion: "State the point plainly without the superlative frame.",
@@ -1090,8 +1113,9 @@ module Sloplint
1090
1113
  ),
1091
1114
  Rule.new(
1092
1115
  id: "is-real-and-not",
1093
- category: "rhetorical-tic",
1094
- severity: "info",
1116
+ category: "false-concession",
1117
+ severity: "warning",
1118
+ confidence: "medium",
1095
1119
  pattern: /\bis\s+real,?\s+(?:and|but|not)\b/i,
1096
1120
  message: '"The X is real, and…" is a stock LLM concession move.',
1097
1121
  suggestion: "Drop the 'is real, and' scaffolding; assert the point directly.",
@@ -1112,8 +1136,9 @@ module Sloplint
1112
1136
  ),
1113
1137
  Rule.new(
1114
1138
  id: "real-x-real-y",
1115
- category: "rhetorical-tic",
1139
+ category: "cadence",
1116
1140
  severity: "info",
1141
+ confidence: "medium",
1117
1142
  # Ships at info, not warning: the only two hits the probe found in
1118
1143
  # 1.28M words were both false positives, and one corpus of one
1119
1144
  # register is thin evidence next to honestly, which sits at warning
@@ -1210,8 +1235,9 @@ module Sloplint
1210
1235
  ),
1211
1236
  Rule.new(
1212
1237
  id: "the-punchline-is",
1213
- category: "rhetorical-tic",
1238
+ category: "self-rating",
1214
1239
  severity: "warning",
1240
+ confidence: "high",
1215
1241
  # Same reveal, three nouns. "honest answer" and "honest version" join
1216
1242
  # "punchline" because they do the identical job: rate the sentence as
1217
1243
  # the candid one before the reader gets it. "short version" was tried
@@ -1236,8 +1262,9 @@ module Sloplint
1236
1262
  ),
1237
1263
  Rule.new(
1238
1264
  id: "worth-naming",
1239
- category: "rhetorical-tic",
1240
- severity: "info",
1265
+ category: "self-rating",
1266
+ severity: "warning",
1267
+ confidence: "medium",
1241
1268
  # Widened to optionally include a trailing "names" so the "naming
1242
1269
  # names" idiom is part of the matched text -- skip: checks the matched
1243
1270
  # text itself, and the tighter /\bworth\s+naming\b/ never captured
@@ -1274,8 +1301,9 @@ module Sloplint
1274
1301
  ),
1275
1302
  Rule.new(
1276
1303
  id: "worth-saying-plainly",
1277
- category: "rhetorical-tic",
1304
+ category: "self-rating",
1278
1305
  severity: "warning",
1306
+ confidence: "high",
1279
1307
  # Two branches, both sentence-initial.
1280
1308
  #
1281
1309
  # First: evaluative adjective + speech verb + manner adverb, all three
@@ -1327,8 +1355,9 @@ module Sloplint
1327
1355
  ),
1328
1356
  Rule.new(
1329
1357
  id: "not-nothing",
1330
- category: "rhetorical-tic",
1358
+ category: "false-concession",
1331
1359
  severity: "warning",
1360
+ confidence: "high",
1332
1361
  # Two branches: spelled-out copula with an optional captured subject, and
1333
1362
  # the contracted "X's not nothing". The optional subject + skip: is the
1334
1363
  # load-bearing idiom -- skip: only sees matched text, so the personal
@@ -1382,8 +1411,9 @@ module Sloplint
1382
1411
 
1383
1412
  Rule.new(
1384
1413
  id: "exact-exactly",
1385
- category: "rhetorical-tic",
1414
+ category: "self-rating",
1386
1415
  severity: "info",
1416
+ confidence: "medium",
1387
1417
  # Rewritten after 2.2M words of technical prose left ~111 of 125 hits
1388
1418
  # false. The shape was the problem, not the entries: the pattern matched
1389
1419
  # "exact" everywhere and subtracted an allow-list, and an open pattern
@@ -1489,8 +1519,9 @@ module Sloplint
1489
1519
  ),
1490
1520
  Rule.new(
1491
1521
  id: "load-bearing",
1492
- category: "rhetorical-tic",
1522
+ category: "borrowed-metaphor",
1493
1523
  severity: "warning",
1524
+ confidence: "high",
1494
1525
  # Two guards, both structural, over the same noun list so they can't
1495
1526
  # drift apart. Forward: a physical building part right after it is the
1496
1527
  # literal sense, checked with a negative lookahead. Backward: the
@@ -1539,8 +1570,9 @@ module Sloplint
1539
1570
  ),
1540
1571
  Rule.new(
1541
1572
  id: "intersection-of",
1542
- category: "rhetorical-tic",
1573
+ category: "borrowed-metaphor",
1543
1574
  severity: "warning",
1575
+ confidence: "high",
1544
1576
  # "at" is not load-bearing -- "explores the intersection of art and
1545
1577
  # technology" is the same move -- so the anchor is "the intersection of"
1546
1578
  # and the two guards carry the whole burden of separating the literal
@@ -1626,8 +1658,9 @@ module Sloplint
1626
1658
  ),
1627
1659
  Rule.new(
1628
1660
  id: "impact-verb",
1629
- category: "rhetorical-tic",
1661
+ category: "borrowed-metaphor",
1630
1662
  severity: "warning",
1663
+ confidence: "high",
1631
1664
  # "impact" and "impacts" are also nouns, so they only count as verbs
1632
1665
  # behind an auxiliary or a subject pronoun. One optional pronoun may sit
1633
1666
  # between the auxiliary and the verb ("does this impact the date");
@@ -1731,10 +1764,11 @@ module Sloplint
1731
1764
  Rule.new(
1732
1765
  id: "impact-noun-vague",
1733
1766
  category: "puffery",
1734
- severity: "warning",
1767
+ severity: "error",
1768
+ confidence: "high",
1735
1769
  # Only the puffed shapes: an intensity adjective, or make/have plus an
1736
- # article. "the impact of X" is left to impact-noun-bare, which sits at
1737
- # info because it is the standard word in research prose. "positive" and
1770
+ # article. "the impact of X" is left to impact-noun-bare, which takes the
1771
+ # bare form because it is the standard word in research prose. "positive" and
1738
1772
  # "negative" stay out of the adjective list on purpose -- they name a
1739
1773
  # direction, which is more than the intensity words do. "statistically"
1740
1774
  # is pulled into the match as an optional leading word so skip: can see
@@ -1782,8 +1816,9 @@ module Sloplint
1782
1816
  ),
1783
1817
  Rule.new(
1784
1818
  id: "impact-noun-bare",
1785
- category: "rhetorical-tic",
1786
- severity: "info",
1819
+ category: "borrowed-metaphor",
1820
+ severity: "warning",
1821
+ confidence: "medium",
1787
1822
  # Two shapes, each needing its own anchor: a measuring verb in front, or
1788
1823
  # "of" behind. That is what keeps the collision sense clear without a
1789
1824
  # list of collision verbs -- "the impact crushed the front bumper" has
@@ -1820,14 +1855,15 @@ module Sloplint
1820
1855
  "Torque the bolts with an impact wrench.",
1821
1856
  "Will the impact be permanent?"
1822
1857
  ],
1823
- rationale: "This one sits at info because 'the impact of X on Y' is the ordinary word in " \
1824
- "research and policy writing, not a tell. Elsewhere it postpones the sentence: " \
1825
- "the writer announces that an effect exists and stops before naming it."
1858
+ rationale: "'The impact of X on Y' is the ordinary word in research and policy writing, " \
1859
+ "not a tell. Elsewhere it postpones the sentence: the writer announces that " \
1860
+ "an effect exists and stops before naming it."
1826
1861
  ),
1827
1862
  Rule.new(
1828
1863
  id: "thats-how-x",
1829
- category: "rhetorical-tic",
1864
+ category: "closer",
1830
1865
  severity: "warning",
1866
+ confidence: "high",
1831
1867
  pattern: /(?:\A|[.!?]\s+|\n\s*\n)\s*(?:that|this)(?:'s| is)\s+how\b/i,
1832
1868
  message: '"That\'s how…" opening a sentence is a stock LLM aphorism closer.',
1833
1869
  suggestion: "Cut the closer, or replace it with the concrete result you mean.",
@@ -1843,8 +1879,9 @@ module Sloplint
1843
1879
  ),
1844
1880
  Rule.new(
1845
1881
  id: "announced-takeaway",
1846
- category: "rhetorical-tic",
1882
+ category: "self-rating",
1847
1883
  severity: "warning",
1884
+ confidence: "high",
1848
1885
  pattern: /(?:\A|[.!?]\s+|\n\s*\n)\s*(?:here'?s\s+)?the\s+(?:loop|pattern|trick|lesson|takeaway|playbook|framing|insight|kicker)\b[^.!?\n]{0,60}:/i,
1849
1886
  message: "Colon-led takeaway label announces the lesson before making it.",
1850
1887
  suggestion: "Give the observation first; let the reader decide it's the takeaway.",
@@ -1860,8 +1897,9 @@ module Sloplint
1860
1897
  ),
1861
1898
  Rule.new(
1862
1899
  id: "is-is",
1863
- category: "rhetorical-tic",
1900
+ category: "cadence",
1864
1901
  severity: "warning",
1902
+ confidence: "high",
1865
1903
  # No anchor needed -- the doubled copula alone scored 0 across ~1.9M words.
1866
1904
  # The comma is allowed because "What it is, is a mystery" grates the same
1867
1905
  # way. Sentence and clause punctuation still block the weld ("what it is.
@@ -1896,8 +1934,9 @@ module Sloplint
1896
1934
 
1897
1935
  Rule.new(
1898
1936
  id: "earns-its-place",
1899
- category: "rhetorical-tic",
1937
+ category: "self-rating",
1900
1938
  severity: "warning",
1939
+ confidence: "high",
1901
1940
  # The possessive is the narrowing. "earned a place on the team" and
1902
1941
  # "earn a place in the final" are ordinary; "earns its place" is the
1903
1942
  # metaphor, a thing paying for the room it takes up.
@@ -1921,8 +1960,9 @@ module Sloplint
1921
1960
  ),
1922
1961
  Rule.new(
1923
1962
  id: "does-a-lot-of-work",
1924
- category: "rhetorical-tic",
1963
+ category: "self-rating",
1925
1964
  severity: "warning",
1965
+ confidence: "high",
1926
1966
  # Two arms, both narrowed away from the ordinary sense.
1927
1967
  #
1928
1968
  # "a lot of work" needs a locative ("here", "in that sentence") because
@@ -1954,8 +1994,9 @@ module Sloplint
1954
1994
  ),
1955
1995
  Rule.new(
1956
1996
  id: "failure-mode-here",
1957
- category: "rhetorical-tic",
1997
+ category: "borrowed-metaphor",
1958
1998
  severity: "warning",
1999
+ confidence: "high",
1959
2000
  # "here" is the whole narrowing, and it is doing a lot -- the bare "the
1960
2001
  # failure mode is" is ordinary engineering writing about real systems,
1961
2002
  # where the phrase means what it says. The deictic is what marks the
@@ -1981,8 +2022,9 @@ module Sloplint
1981
2022
  ),
1982
2023
  Rule.new(
1983
2024
  id: "thats-the-tension",
1984
- category: "rhetorical-tic",
2025
+ category: "closer",
1985
2026
  severity: "warning",
2027
+ confidence: "high",
1986
2028
  # Sentence-initial, only two nouns, and the noun must end the clause.
1987
2029
  #
1988
2030
  # "tradeoff" and "catch" were tried and cut: both are ordinary English
@@ -2015,8 +2057,9 @@ module Sloplint
2015
2057
  ),
2016
2058
  Rule.new(
2017
2059
  id: "right-up-until",
2018
- category: "rhetorical-tic",
2060
+ category: "closer",
2019
2061
  severity: "warning",
2062
+ confidence: "high",
2020
2063
  # The intensifier is the tell, not the reversal. "It works until it
2021
2064
  # doesn't" is an old human idiom and stays clean; stacking "right up"
2022
2065
  # in front of it is the model's version, and the two together are two
@@ -2041,8 +2084,9 @@ module Sloplint
2041
2084
  ),
2042
2085
  Rule.new(
2043
2086
  id: "two-things-true",
2044
- category: "rhetorical-tic",
2045
- severity: "warning",
2087
+ category: "false-concession",
2088
+ severity: "error",
2089
+ confidence: "high",
2046
2090
  # Closed phrase, no anchor needed. The optional "both" and the optional
2047
2091
  # "at once" tail are the two ways the sentence is padded; the count word
2048
2092
  # is fixed at two, because "three things can be true" is someone
@@ -2067,8 +2111,9 @@ module Sloplint
2067
2111
  ),
2068
2112
  Rule.new(
2069
2113
  id: "notice-what-there",
2070
- category: "rhetorical-tic",
2114
+ category: "reader-address",
2071
2115
  severity: "warning",
2116
+ confidence: "high",
2072
2117
  # The self-referential half of the attention cue: the sentence points
2073
2118
  # at the writing rather than at anything in the world. Two frames, both
2074
2119
  # sentence-initial -- "notice what X did there" and the bare "read that
@@ -2096,8 +2141,9 @@ module Sloplint
2096
2141
  ),
2097
2142
  Rule.new(
2098
2143
  id: "notice-what",
2099
- category: "rhetorical-tic",
2144
+ category: "reader-address",
2100
2145
  severity: "info",
2146
+ confidence: "medium",
2101
2147
  # The ambiguous half of the pair, and it ships at info because the
2102
2148
  # sentence-initial imperative is also how people point at something
2103
2149
  # real: "Notice what happens around Q3", "Notice what is not on the
@@ -2128,8 +2174,9 @@ module Sloplint
2128
2174
  ),
2129
2175
  Rule.new(
2130
2176
  id: "none-of-this-is-to-say",
2131
- category: "rhetorical-tic",
2132
- severity: "warning",
2177
+ category: "false-concession",
2178
+ severity: "error",
2179
+ confidence: "high",
2133
2180
  # Only the "none of" form. Every neighbouring phrasing is ordinary
2134
2181
  # English by an order of magnitude -- "which is not to say", "this is
2135
2182
  # not to say", "that's not to say" -- and admitting any of them would
@@ -2155,10 +2202,11 @@ module Sloplint
2155
2202
  ),
2156
2203
  Rule.new(
2157
2204
  id: "if-im-being-honest",
2158
- category: "rhetorical-tic",
2159
- severity: "info",
2205
+ category: "false-concession",
2206
+ severity: "warning",
2207
+ confidence: "medium",
2160
2208
  # The "being honest" frame only. Plain "to be honest" and "I'll be
2161
- # honest" are how people talk and are excluded. info, not warning:
2209
+ # honest" are how people talk and are excluded. Medium confidence:
2162
2210
  # writers really do say this out loud.
2163
2211
  pattern: /\bif\s+(?:I['’]?m|I\s+am|we['’]?re|we\s+are)\s+(?:being\s+)?honest\b
2164
2212
  |\bhonestly,\s+the\s+(?:answer|truth)\b/ix,
@@ -2180,8 +2228,9 @@ module Sloplint
2180
2228
  ),
2181
2229
  Rule.new(
2182
2230
  id: "honestly",
2183
- category: "rhetorical-tic",
2231
+ category: "self-rating",
2184
2232
  severity: "warning",
2233
+ confidence: "high",
2185
2234
  # The manner adverb, the way "cleanly" is the manner adverb: hung on a
2186
2235
  # subject that cannot be honest. Two guards, no verb list.
2187
2236
  #
@@ -2231,8 +2280,9 @@ module Sloplint
2231
2280
  ),
2232
2281
  Rule.new(
2233
2282
  id: "honest-x",
2234
- category: "rhetorical-tic",
2283
+ category: "self-rating",
2235
2284
  severity: "warning",
2285
+ confidence: "high",
2236
2286
  # The noun list is short on purpose, and the words left out are the
2237
2287
  # point. "An honest answer", "an honest assessment" and "an honest
2238
2288
  # account" are ordinary English about people -- six hits between them in
@@ -2274,8 +2324,9 @@ module Sloplint
2274
2324
  ),
2275
2325
  Rule.new(
2276
2326
  id: "most-honest-x",
2277
- category: "rhetorical-tic",
2278
- severity: "warning",
2327
+ category: "self-rating",
2328
+ severity: "error",
2329
+ confidence: "high",
2279
2330
  # The superlative frame carries the tell on its own, so this noun list is
2280
2331
  # wider than honest-x's -- "the most honest assessment" is self-ranking
2281
2332
  # in a way "an honest assessment" is not. The list still has to keep out
@@ -2310,14 +2361,14 @@ module Sloplint
2310
2361
  ),
2311
2362
  Rule.new(
2312
2363
  id: "genuinely",
2313
- category: "rhetorical-tic",
2364
+ category: "self-rating",
2314
2365
  severity: "info",
2366
+ confidence: "low",
2315
2367
  # Off by default. There is no narrowing here: the word is ordinary
2316
2368
  # English at every frequency we measured, and the difference between
2317
2369
  # the tell and the real use is whether a contrast exists in the
2318
2370
  # surrounding argument, which no regex can see. Selectable when a
2319
2371
  # writer wants every occurrence listed back to them.
2320
- default_on: false,
2321
2372
  pattern: /\bgenuinely\b/i,
2322
2373
  message: '"Genuinely" asserts sincerity instead of earning it (heuristic; high false-positive).',
2323
2374
  suggestion: "Cut the adverb. If the sentence needs it, the claim is doing the work.",
@@ -2339,8 +2390,9 @@ module Sloplint
2339
2390
  ),
2340
2391
  Rule.new(
2341
2392
  id: "actually-not-x",
2342
- category: "rhetorical-tic",
2393
+ category: "false-correction",
2343
2394
  severity: "warning",
2395
+ confidence: "high",
2344
2396
  # Two markers that each correct the reader, doubled up in one clause:
2345
2397
  # the adverb and the trailing "…, not X". Bare "actually" is not the
2346
2398
  # tell and is not matched, because "the build actually failed on the
@@ -2405,8 +2457,9 @@ module Sloplint
2405
2457
  ),
2406
2458
  Rule.new(
2407
2459
  id: "and-thats-fine",
2408
- category: "rhetorical-tic",
2409
- severity: "info",
2460
+ category: "false-concession",
2461
+ severity: "warning",
2462
+ confidence: "medium",
2410
2463
  # Three narrowings, and the rule needs all of them. "and" is required:
2411
2464
  # bare "that's fine" is a reply people write constantly. The match must
2412
2465
  # open a sentence and close it, so the concessive clause -- "and that's
@@ -2430,8 +2483,9 @@ module Sloplint
2430
2483
  ),
2431
2484
  Rule.new(
2432
2485
  id: "and-nothing-else",
2433
- category: "rhetorical-tic",
2486
+ category: "closer",
2434
2487
  severity: "warning",
2488
+ confidence: "high",
2435
2489
  # This rule is deliberately wide, and the cost is known. There is no verb
2436
2490
  # list and no imperative requirement, so the only narrowing is structural
2437
2491
  # -- which means the pattern cannot tell a leaked instruction from the
@@ -2489,8 +2543,9 @@ module Sloplint
2489
2543
  ),
2490
2544
  Rule.new(
2491
2545
  id: "nothing-else-frag",
2492
- category: "rhetorical-tic",
2546
+ category: "closer",
2493
2547
  severity: "warning",
2548
+ confidence: "high",
2494
2549
  # The same exclusion as its own sentence: "Return the JSON. Nothing
2495
2550
  # else." Built on the no-x-no-y-frag template -- the fragment must start
2496
2551
  # at a sentence boundary and the separator is at most two spaces or one
@@ -2526,18 +2581,18 @@ module Sloplint
2526
2581
  # letter guard is there to exclude.
2527
2582
  "and so on; nothing more."
2528
2583
  ],
2529
- rationale: "The fragment form of the same leaked instruction, and it reads the same way: " \
2530
- "a second sentence that only says the first one was complete. It ships at " \
2531
- "warning rather than info because the capital letter and the whole-sentence " \
2532
- "requirement keep it off the continuation sense, which is where ordinary prose " \
2533
- "puts the phrase: one hit in 1.92M words of public-domain prose and one in " \
2534
- "461k words of pre-2022 Hacker News."
2584
+ rationale: "The fragment form of the same leaked instruction, and it reads the same " \
2585
+ "way: a second sentence that only says the first one was complete. The " \
2586
+ "capital letter and the whole-sentence requirement keep it off the " \
2587
+ "continuation sense, which is where ordinary prose puts the phrase: one hit " \
2588
+ "in 1.92M words of public-domain prose and one in 461k words of pre-2022 " \
2589
+ "Hacker News."
2535
2590
  ),
2536
- # ── puffery ───────────────────────────────────────────────────────────
2537
2591
  Rule.new(
2538
2592
  id: "puffery-words",
2539
2593
  category: "puffery",
2540
- severity: "warning",
2594
+ severity: "error",
2595
+ confidence: "high",
2541
2596
  # "nestled" alone is the literal verb as often as the puffery sense --
2542
2597
  # a head nestling against a shoulder, a kitten nestling into a blanket
2543
2598
  # -- so it requires a following in/among/between, the same shape the
@@ -2573,7 +2628,8 @@ module Sloplint
2573
2628
  Rule.new(
2574
2629
  id: "stands-serves-as",
2575
2630
  category: "puffery",
2576
- severity: "info",
2631
+ severity: "warning",
2632
+ confidence: "medium",
2577
2633
  pattern: /\b(?:stands|serves)\s+as\b|\bis\s+a\s+(?:testament|reminder)\s+to\b/i,
2578
2634
  message: '"stands/serves as", "is a testament/reminder to" is puffed AI framing.',
2579
2635
  suggestion: "Say what it does, not what it 'stands as'.",
@@ -2584,7 +2640,8 @@ module Sloplint
2584
2640
  Rule.new(
2585
2641
  id: "vital-role",
2586
2642
  category: "puffery",
2587
- severity: "warning",
2643
+ severity: "error",
2644
+ confidence: "high",
2588
2645
  pattern: /\bplays?\s+a\s+(?:vital|crucial|pivotal|significant|key|central)\s+role\b/i,
2589
2646
  message: '"plays a vital/crucial/… role" is a stock AI puffery phrase.',
2590
2647
  suggestion: "State the specific role or effect instead.",
@@ -2595,7 +2652,8 @@ module Sloplint
2595
2652
  Rule.new(
2596
2653
  id: "underscores-highlights",
2597
2654
  category: "puffery",
2598
- severity: "info",
2655
+ severity: "warning",
2656
+ confidence: "medium",
2599
2657
  # "underscored/underscoring" are unambiguously the verb and flag bare.
2600
2658
  # "underscore/underscores" is also the character noun (a leading
2601
2659
  # underscore, snake_case docs), so those forms require a following
@@ -2636,15 +2694,16 @@ module Sloplint
2636
2694
  "Numbers accept underscores\n\nThe next section covers floats."
2637
2695
  ],
2638
2696
  rationale: "Models reach for 'underscore' as an all-purpose emphasis verb -- findings " \
2639
- "underscore, outages underscore -- asserting significance without earning it. " \
2640
- "Sincere journalistic and academic use exists, hence info: a flag means the " \
2641
- "move is present, not that it's slop. The character noun never takes the " \
2642
- "verb's frame and stays out."
2697
+ "underscore, outages underscore -- asserting significance without earning " \
2698
+ "it. Sincere journalistic and academic use exists: a flag means the move is " \
2699
+ "present, not that it's slop. The character noun never takes the verb's " \
2700
+ "frame and stays out."
2643
2701
  ),
2644
2702
  Rule.new(
2645
2703
  id: "rich-tapestry",
2646
2704
  category: "puffery",
2647
- severity: "warning",
2705
+ severity: "error",
2706
+ confidence: "high",
2648
2707
  pattern: /\brich\s+tapestry\b|\btapestry\s+of\b/i,
2649
2708
  message: '"rich tapestry"/"tapestry of" is a signature AI cliché.',
2650
2709
  suggestion: "Cut the metaphor; name the actual things.",
@@ -2653,11 +2712,11 @@ module Sloplint
2653
2712
  rationale: "'tapestry of' is one of the most reliable single-phrase model tells."
2654
2713
  ),
2655
2714
 
2656
- # ── structure ─────────────────────────────────────────────────────────
2657
2715
  Rule.new(
2658
2716
  id: "not-just-x-but-y",
2659
- category: "structure",
2717
+ category: "false-correction",
2660
2718
  severity: "warning",
2719
+ confidence: "high",
2661
2720
  # Two branches, both anchored on an explicit escalation word. (1) The
2662
2721
  # copula escalation: "is not just/only/merely/simply/solely A … but B".
2663
2722
  # (2) "not because A, but because B". The escalation word is what makes
@@ -2701,10 +2760,11 @@ module Sloplint
2701
2760
  ),
2702
2761
  Rule.new(
2703
2762
  id: "not-x-but-y",
2704
- category: "structure",
2705
- severity: "info",
2763
+ category: "false-correction",
2764
+ severity: "warning",
2765
+ confidence: "medium",
2706
2766
  # The bare corrective: "is not A but B", no escalation word, comma or no
2707
- # comma. Ships at info because the line between a corrective ("not an
2767
+ # comma. Medium confidence, because the line between a corrective ("not an
2708
2768
  # accident but a strategy") and an ordinary concession ("not warm but the
2709
2769
  # fire helped") is syntactic, and a regex cannot see syntax. What is here
2710
2770
  # is a set of cheap narrowings that cut the worst of the noise: A is
@@ -2718,7 +2778,8 @@ module Sloplint
2718
2778
  # ("was not warm but the fire helped") or a bare lexical verb ("was not
2719
2779
  # perfect but got us there"), because neither is distinguishable from the
2720
2780
  # corrective by surface form. That is the cost of the rule and the reason
2721
- # it is info: the agent reading the flag has the context to judge, and
2781
+ # its confidence is medium: the agent reading the flag has the context
2782
+ # to judge, and
2722
2783
  # should. Do not chase these by growing the B-list -- every word added
2723
2784
  # silently narrows recall with nothing pinning it.
2724
2785
  pattern: /(?:\bis|\bare|\bwas|\bwere|\bisn['’]t|\baren['’]t|\bwasn['’]t|\bweren['’]t|
@@ -2765,15 +2826,16 @@ module Sloplint
2765
2826
  "escalation word dropped, and models reach for it constantly. It is the " \
2766
2827
  "noisiest rule in the catalog by design: a loose version scored 174 hits " \
2767
2828
  "before narrowing, cut to 15 here -- but not all 15 are correctives. Some " \
2768
- "are concessions with an elided subject (Walden's " \
2769
- "'It was not lonely, but made all the earth lonely beneath it'), which no " \
2770
- "surface pattern can tell apart from the real thing. Hence info: a flag here " \
2771
- "means 'this has the shape', not 'this is slop'."
2829
+ "are concessions with an elided subject (Walden's 'It was not lonely, but " \
2830
+ "made all the earth lonely beneath it'), which no surface pattern can tell " \
2831
+ "apart from the real thing. A flag here means 'this has the shape', not " \
2832
+ "'this is slop'."
2772
2833
  ),
2773
2834
  Rule.new(
2774
2835
  id: "isnt-x-its-y",
2775
- category: "structure",
2776
- severity: "info",
2836
+ category: "false-correction",
2837
+ severity: "warning",
2838
+ confidence: "medium",
2777
2839
  # The corrective with the conjunction dropped: one clause rejects a
2778
2840
  # description, the next supplies the replacement through a second copula
2779
2841
  # ("It isn't the tool. It's the habit."). Every copula tense is covered,
@@ -2789,7 +2851,8 @@ module Sloplint
2789
2851
  # guards cut a loose version from 19 hits to 4 over 889k words of
2790
2852
  # 19th-century fiction.
2791
2853
  #
2792
- # What survives is the reason for info. An adjective outside the list
2854
+ # What survives is the reason the confidence is medium. An adjective
2855
+ # outside the list
2793
2856
  # still slips through, and some hits are the same shape written by a
2794
2857
  # person -- "was not the wife; it was the children" is Conan Doyle. A flag
2795
2858
  # here says the sentence has the frame, not that a model wrote it.
@@ -2861,13 +2924,14 @@ module Sloplint
2861
2924
  "one, so the sentence sounds like a correction while correcting nobody. It " \
2862
2925
  "is 'not A but B' with the conjunction dropped and the second half promoted " \
2863
2926
  "to its own clause, which is the form models reach for most. Ordinary prose " \
2864
- "contrasts two things this way too, so the rule ships at info: it reports " \
2865
- "the shape, not a verdict."
2927
+ "contrasts two things this way too, so the rule reports the shape, not a " \
2928
+ "verdict."
2866
2929
  ),
2867
2930
  Rule.new(
2868
2931
  id: "not-by-x-but-by-y",
2869
- category: "structure",
2870
- severity: "info",
2932
+ category: "false-correction",
2933
+ severity: "warning",
2934
+ confidence: "medium",
2871
2935
  # The corrective built on a repeated preposition: "not by A, but by B",
2872
2936
  # "not from A but from B". The copula rules above cannot see it because
2873
2937
  # nothing precedes "not" but the verb or a dash, so the anchor here is
@@ -2908,16 +2972,16 @@ module Sloplint
2908
2972
  "We won not by luck\n\nBut by then it hardly mattered."
2909
2973
  ],
2910
2974
  rationale: "The 'not A but B' corrective with the copula swapped for a repeated " \
2911
- "preposition, which is how a model corrects a claim about means or " \
2912
- "cause ('not by luck, but by design'). People write it too -- Thoreau " \
2913
- "and Melville both lean on it -- so the rule ships at info. It reports " \
2914
- "the shape; a human reader decides whether it earned its place."
2975
+ "preposition, which is how a model corrects a claim about means or cause " \
2976
+ "('not by luck, but by design'). People write it too -- Thoreau and Melville " \
2977
+ "both lean on it -- and the rule reports the shape; a human reader decides " \
2978
+ "whether it earned its place."
2915
2979
  ),
2916
2980
  Rule.new(
2917
2981
  id: "rule-of-three",
2918
- category: "structure",
2982
+ category: "cadence",
2919
2983
  severity: "info",
2920
- default_on: false,
2984
+ confidence: "low",
2921
2985
  pattern: /\b[\w'-]+,\s+[\w'-]+,\s+(?:and\s+)?[\w'-]+[.!?]/,
2922
2986
  message: "Three single words in a comma series closing a sentence (heuristic; high false-positive).",
2923
2987
  suggestion: "Fine in moderation; watch for the AI habit of ending on triplets.",
@@ -2943,8 +3007,9 @@ module Sloplint
2943
3007
  # CLAUDE.md: some tells can't be regexes; this was one.
2944
3008
  Rule.new(
2945
3009
  id: "everyone-nobody",
2946
- category: "structure",
3010
+ category: "cadence",
2947
3011
  severity: "warning",
3012
+ confidence: "high",
2948
3013
  # The comma-spliced antithesis on quantifier subjects: "Everyone wants
2949
3014
  # the dashboard, nobody maintains it." One clause opens on
2950
3015
  # everyone/everybody, the other on nobody/no one/none or "one N"
@@ -3016,8 +3081,9 @@ module Sloplint
3016
3081
  ),
3017
3082
  Rule.new(
3018
3083
  id: "np-fragment-and",
3019
- category: "structure",
3020
- severity: "info",
3084
+ category: "cadence",
3085
+ severity: "warning",
3086
+ confidence: "medium",
3021
3087
  # A whole sentence that is two noun phrases and an "and": "A named owner
3022
3088
  # and a quarterly review." It is the fix half of a model's
3023
3089
  # problem-then-fix pair, with the verb left for the reader to supply.
@@ -3028,7 +3094,7 @@ module Sloplint
3028
3094
  # anywhere in it, contractions included, so "A man and a woman were
3029
3095
  # there." and "A man and a woman aren't here." never match.
3030
3096
  #
3031
- # Ships at info, and this is why: a lexical verb is invisible to the
3097
+ # Medium confidence, and this is why: a lexical verb is invisible to the
3032
3098
  # pattern, so "A car and a truck collided." has the same shape and
3033
3099
  # flags. The corpora say that sentence is rare (one hit in 1.25M words
3034
3100
  # of public-domain prose, most of it narrative), but it is a complete
@@ -3071,8 +3137,9 @@ module Sloplint
3071
3137
  ),
3072
3138
  Rule.new(
3073
3139
  id: "quip-question",
3074
- category: "structure",
3140
+ category: "reader-address",
3075
3141
  severity: "info",
3142
+ confidence: "medium",
3076
3143
  # The verbless question that opens a pitch: "No invite?", "New to the
3077
3144
  # tool?", "Still stuck?", "Ready to start?". It must start a sentence,
3078
3145
  # open on one of a short list of words, run one to four more words, and
@@ -3118,8 +3185,9 @@ module Sloplint
3118
3185
  ),
3119
3186
  Rule.new(
3120
3187
  id: "mic-drop-closer",
3121
- category: "structure",
3188
+ category: "cadence",
3122
3189
  severity: "info",
3190
+ confidence: "medium",
3123
3191
  # The kicker: a sentence of at least sixty characters, then a closer of
3124
3192
  # two to eight words that ends the paragraph and opens on a quantifier
3125
3193
  # ("Nothing here needs a new login.", "Most teams end up
@@ -3142,7 +3210,7 @@ module Sloplint
3142
3210
  # position." The quantifiers carry the tell; the demonstratives are
3143
3211
  # ordinary, and no examples_bad used one.
3144
3212
  #
3145
- # Ships at info, and the rationale says why: people end paragraphs this
3213
+ # Medium confidence, and the rationale says why: people end paragraphs this
3146
3214
  # way too, at about 150 per million words on Hacker News. One is
3147
3215
  # nothing. A draft where most paragraphs end this way is the tell, and
3148
3216
  # an agent that sees the flag repeat should read the family as a
@@ -3199,9 +3267,10 @@ module Sloplint
3199
3267
  "warning, and the fix is usually to delete the closer outright."
3200
3268
  ),
3201
3269
  Rule.new(
3202
- id: "ellipsis-closer",
3203
- category: "structure",
3270
+ id: "bare-auxiliary-closer",
3271
+ category: "cadence",
3204
3272
  severity: "info",
3273
+ confidence: "medium",
3205
3274
  # The same long-sentence-then-short-closer shape as mic-drop-closer,
3206
3275
  # but the tell lives in the verb, not the subject. mic-drop-closer's
3207
3276
  # closer opens on a quantifier and keeps a full verb with its object
@@ -3278,14 +3347,15 @@ module Sloplint
3278
3347
  ],
3279
3348
  rationale: "The closer withholds exactly the thing the long setup built toward -- the " \
3280
3349
  "verb and its object are gone, and only the bare confirmation that something " \
3281
- "happened is left standing. Ships at info: plenty of ordinary writing drops " \
3282
- "the verb the same way when it confirms an expectation, and one flag proves " \
3283
- "nothing; a draft where it repeats is the tell."
3350
+ "happened is left standing. Plenty of ordinary writing drops the verb the " \
3351
+ "same way when it confirms an expectation, and one flag proves nothing; a " \
3352
+ "draft where it repeats is the tell."
3284
3353
  ),
3285
3354
  Rule.new(
3286
3355
  id: "short-run",
3287
- category: "structure",
3356
+ category: "cadence",
3288
3357
  severity: "info",
3358
+ confidence: "medium",
3289
3359
  # Three consecutive sentences of thirty characters or fewer, each
3290
3360
  # opening on a letter and closing on a full stop, with no quotation
3291
3361
  # mark or digit in any of them: "Nobody used it. A named owner. Then a
@@ -3422,8 +3492,9 @@ module Sloplint
3422
3492
  ),
3423
3493
  Rule.new(
3424
3494
  id: "em-dash",
3425
- category: "structure",
3495
+ category: "punctuation",
3426
3496
  severity: "info",
3497
+ confidence: "medium",
3427
3498
  pattern: /—/,
3428
3499
  message: "Em dash — an AI punctuation tell.",
3429
3500
  suggestion: "Recast with a comma, parentheses, or a separate sentence.",
@@ -3442,8 +3513,9 @@ module Sloplint
3442
3513
  ),
3443
3514
  Rule.new(
3444
3515
  id: "em-dash-overuse",
3445
- category: "structure",
3516
+ category: "punctuation",
3446
3517
  severity: "warning",
3518
+ confidence: "high",
3447
3519
  pattern: /—(?:[^\n]|\n(?!\s*\n))*—(?:[^\n]|\n(?!\s*\n))*—/,
3448
3520
  message: "Three or more em dashes in one paragraph — an AI punctuation tell.",
3449
3521
  suggestion: "Recast with commas, parentheses, or separate sentences.",
@@ -3465,8 +3537,9 @@ module Sloplint
3465
3537
 
3466
3538
  Rule.new(
3467
3539
  id: "question-isnt",
3468
- category: "structure",
3469
- severity: "info",
3540
+ category: "false-correction",
3541
+ severity: "warning",
3542
+ confidence: "medium",
3470
3543
  # The resolving clause is required, so a plain rhetorical question never
3471
3544
  # matches. "The real question is" is ordinary English and is excluded by
3472
3545
  # the adjacency of "question" to the negated copula.
@@ -3492,8 +3565,9 @@ module Sloplint
3492
3565
  ),
3493
3566
  Rule.new(
3494
3567
  id: "less-about-more-about",
3495
- category: "structure",
3496
- severity: "info",
3568
+ category: "false-correction",
3569
+ severity: "warning",
3570
+ confidence: "medium",
3497
3571
  # The full frame is required at both ends. Bare "less about" and bare
3498
3572
  # "more about" are ordinary English on their own, and the subject slot
3499
3573
  # is limited to the pronouns so that a sentence with a real subject --
@@ -3519,8 +3593,9 @@ module Sloplint
3519
3593
  ),
3520
3594
  Rule.new(
3521
3595
  id: "trailing-significance-participle",
3522
- category: "structure",
3523
- severity: "warning",
3596
+ category: "puffery",
3597
+ severity: "error",
3598
+ confidence: "high",
3524
3599
  # The verb list is closed and short on purpose. Wikipedia's "signs of AI
3525
3600
  # writing" names eight watch words for this construction; half of them
3526
3601
  # did not survive probing. In 1.85M words of pre-2022 Hacker News and
@@ -3597,9 +3672,9 @@ module Sloplint
3597
3672
  ),
3598
3673
  Rule.new(
3599
3674
  id: "trailing-restatement",
3600
- category: "structure",
3601
- severity: "info",
3602
- default_on: false,
3675
+ category: "closer",
3676
+ severity: "warning",
3677
+ confidence: "low",
3603
3678
  # The restating tail: "…, which means working through the process
3604
3679
  # rather than around it". A regex sees the connective and not whether
3605
3680
  # the tail says the head again, and the sentence that prompted the rule
@@ -3687,11 +3762,11 @@ module Sloplint
3687
3762
  "use the same connective to state a consequence, and the pattern cannot " \
3688
3763
  "tell the two apart, so the rule is off by default."
3689
3764
  ),
3690
- # ── hedging ───────────────────────────────────────────────────────────
3691
3765
  Rule.new(
3692
3766
  id: "vague-attribution",
3693
- category: "hedging",
3694
- severity: "warning",
3767
+ category: "false-concession",
3768
+ severity: "error",
3769
+ confidence: "high",
3695
3770
  pattern: /\bsome\s+(?:critics|experts|observers|scholars|analysts)\s+(?:argue|say|believe|contend|maintain)\b|\bit\s+is\s+widely\s+(?:regarded|considered|seen|believed|acknowledged)\b|\bmany\s+would\s+argue\b/i,
3696
3771
  message: "Vague attribution ('some critics argue', 'it is widely…') — an AI hedging tell.",
3697
3772
  suggestion: "Name the source, or drop the appeal to unnamed authority.",