sloplint 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -0
- data/README.md +27 -21
- data/docs/SPEC.md +169 -108
- data/lib/sloplint/cli.rb +15 -10
- data/lib/sloplint/engine.rb +3 -2
- data/lib/sloplint/rules.rb +366 -164
- data/lib/sloplint/version.rb +1 -1
- metadata +1 -1
data/lib/sloplint/rules.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
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,
|
|
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: "
|
|
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: "
|
|
108
|
-
severity: "
|
|
109
|
+
category: "cadence",
|
|
110
|
+
severity: "warning",
|
|
111
|
+
confidence: "medium",
|
|
109
112
|
# The same cadence built from sentence fragments: "No fluff. No filler."
|
|
110
|
-
#
|
|
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: "
|
|
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: "
|
|
202
|
-
severity: "
|
|
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.
|
|
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: "
|
|
269
|
-
severity: "
|
|
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
|
-
#
|
|
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: "
|
|
331
|
+
category: "cadence",
|
|
326
332
|
severity: "info",
|
|
327
|
-
|
|
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: "
|
|
383
|
+
category: "cadence",
|
|
378
384
|
severity: "info",
|
|
379
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
668
|
-
severity: "
|
|
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
|
-
#
|
|
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: "
|
|
728
|
-
severity: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
906
|
+
"which is accepted."
|
|
892
907
|
),
|
|
893
908
|
Rule.new(
|
|
894
909
|
id: "hold-onto-that",
|
|
895
|
-
category: "
|
|
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: "
|
|
923
|
-
severity: "
|
|
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
|
|
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: "
|
|
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: "
|
|
1002
|
-
severity: "
|
|
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: "
|
|
1031
|
-
severity: "
|
|
1032
|
-
|
|
1033
|
-
#
|
|
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.
|
|
1056
|
-
"
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
1094
|
-
severity: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
1240
|
-
severity: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
1737
|
-
#
|
|
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: "
|
|
1786
|
-
severity: "
|
|
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: "
|
|
1824
|
-
"
|
|
1825
|
-
"
|
|
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: "
|
|
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: "
|
|
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.",
|
|
@@ -1858,10 +1895,64 @@ module Sloplint
|
|
|
1858
1895
|
rationale: "Labelling a claim as the portable lesson does the persuading that the claim " \
|
|
1859
1896
|
"should be doing — a model habit borrowed from thought-leader prose."
|
|
1860
1897
|
),
|
|
1898
|
+
Rule.new(
|
|
1899
|
+
id: "cataphoric-teaser",
|
|
1900
|
+
category: "self-rating",
|
|
1901
|
+
severity: "warning",
|
|
1902
|
+
confidence: "high",
|
|
1903
|
+
# The forward-pointing tease: "Here's what nobody tells you", "the
|
|
1904
|
+
# part most people get wrong". Three parts, each a closed frame. A
|
|
1905
|
+
# sentence-initial position, optionally opened by a forward demonstrative
|
|
1906
|
+
# (here's / this is; "that's what nobody understands" points backward
|
|
1907
|
+
# and is left alone). A noun slot: what / the part / the thing / the
|
|
1908
|
+
# bit / the secret. A subject that claims scarcity for the reader's
|
|
1909
|
+
# benefit: nobody, no one, most people, few people, hardly anyone,
|
|
1910
|
+
# they, people. Then up to two adverbs and a verb of telling, getting
|
|
1911
|
+
# wrong, talking about, admitting or noticing. The bare subject form
|
|
1912
|
+
# ("What nobody tells you ...") must also reach a copula or colon in
|
|
1913
|
+
# the same sentence, since that is what makes it a setup rather than a
|
|
1914
|
+
# plain report ("What nobody tells you gets forgotten."), and it may
|
|
1915
|
+
# not take "they": without a demonstrative in front, "what they don't
|
|
1916
|
+
# realize is" refers to the people the story is about, and narrative
|
|
1917
|
+
# says it. Read across 8.7 MB of RAID that was the one human hit.
|
|
1918
|
+
pattern: /(?:\A|[.!?]\s+|\n\s*\n|:\s+)\s*
|
|
1919
|
+
(?:(?<demo>(?:\w+,?\s+)?(?:here'?s|here\s+is|this\s+is)\s+)|(?-i:(?=[A-Z])))
|
|
1920
|
+
(?:what|the\s+(?:part|thing|bit|secret))\s+
|
|
1921
|
+
(?:nobody|no[-\s]?one|most\s+\w+(?:\s+\w+)?|few\s+people|hardly\s+anyone|almost\s+(?:nobody|no[-\s]?one)|people|(?(<demo>)they|(?!)))\s+
|
|
1922
|
+
(?:(?:will|won'?t|don'?t|doesn'?t|actually|ever|really|never|rarely|wants?\s+to)\s+){0,2}
|
|
1923
|
+
(?:tells?|gets?|talks?|says?|admits?|mentions?|warns?|teach(?:es)?|explains?|notices?|realizes?|understands?|hears?)\b
|
|
1924
|
+
(?(<demo>)|[^.!?\n:]{0,60}?(?:\s+is\b|:))/ix,
|
|
1925
|
+
message: "Cataphoric teaser promises a hidden insight before giving it.",
|
|
1926
|
+
suggestion: "Cut the tease and state the thing itself.",
|
|
1927
|
+
examples_bad: [
|
|
1928
|
+
"Here's what nobody tells you about hiring.",
|
|
1929
|
+
"Here is the part most people get wrong.",
|
|
1930
|
+
"This is the thing they don't tell you.",
|
|
1931
|
+
"What nobody tells you is that the migration is the easy part.",
|
|
1932
|
+
"The part nobody talks about is the rollback.",
|
|
1933
|
+
"What most senior engineers won't admit is that the estimate was a guess.",
|
|
1934
|
+
"So here's what few people realize: the cache is cold on every deploy."
|
|
1935
|
+
],
|
|
1936
|
+
examples_ok: [
|
|
1937
|
+
# Backward-pointing: the sentence reports, it does not tease.
|
|
1938
|
+
"That's what nobody understands.",
|
|
1939
|
+
"That is the part most people get wrong.",
|
|
1940
|
+
# A bare-subject sentence that never reaches a copula is a report.
|
|
1941
|
+
"What nobody tells you gets forgotten by lunch.",
|
|
1942
|
+
# Mid-sentence, the phrase is an object, not a setup.
|
|
1943
|
+
"I finally learned what nobody tells you about hiring.",
|
|
1944
|
+
# Lowercase after a hard wrap is a continuation, not a sentence start.
|
|
1945
|
+
"the reviewer read it and\nwhat nobody tells you is that she signed it anyway."
|
|
1946
|
+
],
|
|
1947
|
+
rationale: "The tease sells the claim as rare knowledge before the claim exists, so the " \
|
|
1948
|
+
"reader is asked to rate it before reading it. Models reach for it as an " \
|
|
1949
|
+
"opener because engagement training rewards the hook."
|
|
1950
|
+
),
|
|
1861
1951
|
Rule.new(
|
|
1862
1952
|
id: "is-is",
|
|
1863
|
-
category: "
|
|
1953
|
+
category: "cadence",
|
|
1864
1954
|
severity: "warning",
|
|
1955
|
+
confidence: "high",
|
|
1865
1956
|
# No anchor needed -- the doubled copula alone scored 0 across ~1.9M words.
|
|
1866
1957
|
# The comma is allowed because "What it is, is a mystery" grates the same
|
|
1867
1958
|
# way. Sentence and clause punctuation still block the weld ("what it is.
|
|
@@ -1896,8 +1987,9 @@ module Sloplint
|
|
|
1896
1987
|
|
|
1897
1988
|
Rule.new(
|
|
1898
1989
|
id: "earns-its-place",
|
|
1899
|
-
category: "
|
|
1990
|
+
category: "self-rating",
|
|
1900
1991
|
severity: "warning",
|
|
1992
|
+
confidence: "high",
|
|
1901
1993
|
# The possessive is the narrowing. "earned a place on the team" and
|
|
1902
1994
|
# "earn a place in the final" are ordinary; "earns its place" is the
|
|
1903
1995
|
# metaphor, a thing paying for the room it takes up.
|
|
@@ -1921,8 +2013,9 @@ module Sloplint
|
|
|
1921
2013
|
),
|
|
1922
2014
|
Rule.new(
|
|
1923
2015
|
id: "does-a-lot-of-work",
|
|
1924
|
-
category: "
|
|
2016
|
+
category: "self-rating",
|
|
1925
2017
|
severity: "warning",
|
|
2018
|
+
confidence: "high",
|
|
1926
2019
|
# Two arms, both narrowed away from the ordinary sense.
|
|
1927
2020
|
#
|
|
1928
2021
|
# "a lot of work" needs a locative ("here", "in that sentence") because
|
|
@@ -1954,8 +2047,9 @@ module Sloplint
|
|
|
1954
2047
|
),
|
|
1955
2048
|
Rule.new(
|
|
1956
2049
|
id: "failure-mode-here",
|
|
1957
|
-
category: "
|
|
2050
|
+
category: "borrowed-metaphor",
|
|
1958
2051
|
severity: "warning",
|
|
2052
|
+
confidence: "high",
|
|
1959
2053
|
# "here" is the whole narrowing, and it is doing a lot -- the bare "the
|
|
1960
2054
|
# failure mode is" is ordinary engineering writing about real systems,
|
|
1961
2055
|
# where the phrase means what it says. The deictic is what marks the
|
|
@@ -1981,8 +2075,9 @@ module Sloplint
|
|
|
1981
2075
|
),
|
|
1982
2076
|
Rule.new(
|
|
1983
2077
|
id: "thats-the-tension",
|
|
1984
|
-
category: "
|
|
2078
|
+
category: "closer",
|
|
1985
2079
|
severity: "warning",
|
|
2080
|
+
confidence: "high",
|
|
1986
2081
|
# Sentence-initial, only two nouns, and the noun must end the clause.
|
|
1987
2082
|
#
|
|
1988
2083
|
# "tradeoff" and "catch" were tried and cut: both are ordinary English
|
|
@@ -2015,8 +2110,9 @@ module Sloplint
|
|
|
2015
2110
|
),
|
|
2016
2111
|
Rule.new(
|
|
2017
2112
|
id: "right-up-until",
|
|
2018
|
-
category: "
|
|
2113
|
+
category: "closer",
|
|
2019
2114
|
severity: "warning",
|
|
2115
|
+
confidence: "high",
|
|
2020
2116
|
# The intensifier is the tell, not the reversal. "It works until it
|
|
2021
2117
|
# doesn't" is an old human idiom and stays clean; stacking "right up"
|
|
2022
2118
|
# in front of it is the model's version, and the two together are two
|
|
@@ -2041,8 +2137,9 @@ module Sloplint
|
|
|
2041
2137
|
),
|
|
2042
2138
|
Rule.new(
|
|
2043
2139
|
id: "two-things-true",
|
|
2044
|
-
category: "
|
|
2045
|
-
severity: "
|
|
2140
|
+
category: "false-concession",
|
|
2141
|
+
severity: "error",
|
|
2142
|
+
confidence: "high",
|
|
2046
2143
|
# Closed phrase, no anchor needed. The optional "both" and the optional
|
|
2047
2144
|
# "at once" tail are the two ways the sentence is padded; the count word
|
|
2048
2145
|
# is fixed at two, because "three things can be true" is someone
|
|
@@ -2067,8 +2164,9 @@ module Sloplint
|
|
|
2067
2164
|
),
|
|
2068
2165
|
Rule.new(
|
|
2069
2166
|
id: "notice-what-there",
|
|
2070
|
-
category: "
|
|
2167
|
+
category: "reader-address",
|
|
2071
2168
|
severity: "warning",
|
|
2169
|
+
confidence: "high",
|
|
2072
2170
|
# The self-referential half of the attention cue: the sentence points
|
|
2073
2171
|
# at the writing rather than at anything in the world. Two frames, both
|
|
2074
2172
|
# sentence-initial -- "notice what X did there" and the bare "read that
|
|
@@ -2096,8 +2194,9 @@ module Sloplint
|
|
|
2096
2194
|
),
|
|
2097
2195
|
Rule.new(
|
|
2098
2196
|
id: "notice-what",
|
|
2099
|
-
category: "
|
|
2197
|
+
category: "reader-address",
|
|
2100
2198
|
severity: "info",
|
|
2199
|
+
confidence: "medium",
|
|
2101
2200
|
# The ambiguous half of the pair, and it ships at info because the
|
|
2102
2201
|
# sentence-initial imperative is also how people point at something
|
|
2103
2202
|
# real: "Notice what happens around Q3", "Notice what is not on the
|
|
@@ -2128,8 +2227,9 @@ module Sloplint
|
|
|
2128
2227
|
),
|
|
2129
2228
|
Rule.new(
|
|
2130
2229
|
id: "none-of-this-is-to-say",
|
|
2131
|
-
category: "
|
|
2132
|
-
severity: "
|
|
2230
|
+
category: "false-concession",
|
|
2231
|
+
severity: "error",
|
|
2232
|
+
confidence: "high",
|
|
2133
2233
|
# Only the "none of" form. Every neighbouring phrasing is ordinary
|
|
2134
2234
|
# English by an order of magnitude -- "which is not to say", "this is
|
|
2135
2235
|
# not to say", "that's not to say" -- and admitting any of them would
|
|
@@ -2155,10 +2255,11 @@ module Sloplint
|
|
|
2155
2255
|
),
|
|
2156
2256
|
Rule.new(
|
|
2157
2257
|
id: "if-im-being-honest",
|
|
2158
|
-
category: "
|
|
2159
|
-
severity: "
|
|
2258
|
+
category: "false-concession",
|
|
2259
|
+
severity: "warning",
|
|
2260
|
+
confidence: "medium",
|
|
2160
2261
|
# The "being honest" frame only. Plain "to be honest" and "I'll be
|
|
2161
|
-
# honest" are how people talk and are excluded.
|
|
2262
|
+
# honest" are how people talk and are excluded. Medium confidence:
|
|
2162
2263
|
# writers really do say this out loud.
|
|
2163
2264
|
pattern: /\bif\s+(?:I['’]?m|I\s+am|we['’]?re|we\s+are)\s+(?:being\s+)?honest\b
|
|
2164
2265
|
|\bhonestly,\s+the\s+(?:answer|truth)\b/ix,
|
|
@@ -2180,8 +2281,9 @@ module Sloplint
|
|
|
2180
2281
|
),
|
|
2181
2282
|
Rule.new(
|
|
2182
2283
|
id: "honestly",
|
|
2183
|
-
category: "
|
|
2284
|
+
category: "self-rating",
|
|
2184
2285
|
severity: "warning",
|
|
2286
|
+
confidence: "high",
|
|
2185
2287
|
# The manner adverb, the way "cleanly" is the manner adverb: hung on a
|
|
2186
2288
|
# subject that cannot be honest. Two guards, no verb list.
|
|
2187
2289
|
#
|
|
@@ -2231,8 +2333,9 @@ module Sloplint
|
|
|
2231
2333
|
),
|
|
2232
2334
|
Rule.new(
|
|
2233
2335
|
id: "honest-x",
|
|
2234
|
-
category: "
|
|
2336
|
+
category: "self-rating",
|
|
2235
2337
|
severity: "warning",
|
|
2338
|
+
confidence: "high",
|
|
2236
2339
|
# The noun list is short on purpose, and the words left out are the
|
|
2237
2340
|
# point. "An honest answer", "an honest assessment" and "an honest
|
|
2238
2341
|
# account" are ordinary English about people -- six hits between them in
|
|
@@ -2274,8 +2377,9 @@ module Sloplint
|
|
|
2274
2377
|
),
|
|
2275
2378
|
Rule.new(
|
|
2276
2379
|
id: "most-honest-x",
|
|
2277
|
-
category: "
|
|
2278
|
-
severity: "
|
|
2380
|
+
category: "self-rating",
|
|
2381
|
+
severity: "error",
|
|
2382
|
+
confidence: "high",
|
|
2279
2383
|
# The superlative frame carries the tell on its own, so this noun list is
|
|
2280
2384
|
# wider than honest-x's -- "the most honest assessment" is self-ranking
|
|
2281
2385
|
# in a way "an honest assessment" is not. The list still has to keep out
|
|
@@ -2310,14 +2414,14 @@ module Sloplint
|
|
|
2310
2414
|
),
|
|
2311
2415
|
Rule.new(
|
|
2312
2416
|
id: "genuinely",
|
|
2313
|
-
category: "
|
|
2417
|
+
category: "self-rating",
|
|
2314
2418
|
severity: "info",
|
|
2419
|
+
confidence: "low",
|
|
2315
2420
|
# Off by default. There is no narrowing here: the word is ordinary
|
|
2316
2421
|
# English at every frequency we measured, and the difference between
|
|
2317
2422
|
# the tell and the real use is whether a contrast exists in the
|
|
2318
2423
|
# surrounding argument, which no regex can see. Selectable when a
|
|
2319
2424
|
# writer wants every occurrence listed back to them.
|
|
2320
|
-
default_on: false,
|
|
2321
2425
|
pattern: /\bgenuinely\b/i,
|
|
2322
2426
|
message: '"Genuinely" asserts sincerity instead of earning it (heuristic; high false-positive).',
|
|
2323
2427
|
suggestion: "Cut the adverb. If the sentence needs it, the claim is doing the work.",
|
|
@@ -2339,8 +2443,9 @@ module Sloplint
|
|
|
2339
2443
|
),
|
|
2340
2444
|
Rule.new(
|
|
2341
2445
|
id: "actually-not-x",
|
|
2342
|
-
category: "
|
|
2446
|
+
category: "false-correction",
|
|
2343
2447
|
severity: "warning",
|
|
2448
|
+
confidence: "high",
|
|
2344
2449
|
# Two markers that each correct the reader, doubled up in one clause:
|
|
2345
2450
|
# the adverb and the trailing "…, not X". Bare "actually" is not the
|
|
2346
2451
|
# tell and is not matched, because "the build actually failed on the
|
|
@@ -2405,8 +2510,9 @@ module Sloplint
|
|
|
2405
2510
|
),
|
|
2406
2511
|
Rule.new(
|
|
2407
2512
|
id: "and-thats-fine",
|
|
2408
|
-
category: "
|
|
2409
|
-
severity: "
|
|
2513
|
+
category: "false-concession",
|
|
2514
|
+
severity: "warning",
|
|
2515
|
+
confidence: "medium",
|
|
2410
2516
|
# Three narrowings, and the rule needs all of them. "and" is required:
|
|
2411
2517
|
# bare "that's fine" is a reply people write constantly. The match must
|
|
2412
2518
|
# open a sentence and close it, so the concessive clause -- "and that's
|
|
@@ -2430,8 +2536,9 @@ module Sloplint
|
|
|
2430
2536
|
),
|
|
2431
2537
|
Rule.new(
|
|
2432
2538
|
id: "and-nothing-else",
|
|
2433
|
-
category: "
|
|
2539
|
+
category: "closer",
|
|
2434
2540
|
severity: "warning",
|
|
2541
|
+
confidence: "high",
|
|
2435
2542
|
# This rule is deliberately wide, and the cost is known. There is no verb
|
|
2436
2543
|
# list and no imperative requirement, so the only narrowing is structural
|
|
2437
2544
|
# -- which means the pattern cannot tell a leaked instruction from the
|
|
@@ -2489,8 +2596,9 @@ module Sloplint
|
|
|
2489
2596
|
),
|
|
2490
2597
|
Rule.new(
|
|
2491
2598
|
id: "nothing-else-frag",
|
|
2492
|
-
category: "
|
|
2599
|
+
category: "closer",
|
|
2493
2600
|
severity: "warning",
|
|
2601
|
+
confidence: "high",
|
|
2494
2602
|
# The same exclusion as its own sentence: "Return the JSON. Nothing
|
|
2495
2603
|
# else." Built on the no-x-no-y-frag template -- the fragment must start
|
|
2496
2604
|
# at a sentence boundary and the separator is at most two spaces or one
|
|
@@ -2526,18 +2634,18 @@ module Sloplint
|
|
|
2526
2634
|
# letter guard is there to exclude.
|
|
2527
2635
|
"and so on; nothing more."
|
|
2528
2636
|
],
|
|
2529
|
-
rationale: "The fragment form of the same leaked instruction, and it reads the same
|
|
2530
|
-
"a second sentence that only says the first one was complete.
|
|
2531
|
-
"
|
|
2532
|
-
"
|
|
2533
|
-
"
|
|
2534
|
-
"
|
|
2637
|
+
rationale: "The fragment form of the same leaked instruction, and it reads the same " \
|
|
2638
|
+
"way: a second sentence that only says the first one was complete. The " \
|
|
2639
|
+
"capital letter and the whole-sentence requirement keep it off the " \
|
|
2640
|
+
"continuation sense, which is where ordinary prose puts the phrase: one hit " \
|
|
2641
|
+
"in 1.92M words of public-domain prose and one in 461k words of pre-2022 " \
|
|
2642
|
+
"Hacker News."
|
|
2535
2643
|
),
|
|
2536
|
-
# ── puffery ───────────────────────────────────────────────────────────
|
|
2537
2644
|
Rule.new(
|
|
2538
2645
|
id: "puffery-words",
|
|
2539
2646
|
category: "puffery",
|
|
2540
|
-
severity: "
|
|
2647
|
+
severity: "error",
|
|
2648
|
+
confidence: "high",
|
|
2541
2649
|
# "nestled" alone is the literal verb as often as the puffery sense --
|
|
2542
2650
|
# a head nestling against a shoulder, a kitten nestling into a blanket
|
|
2543
2651
|
# -- so it requires a following in/among/between, the same shape the
|
|
@@ -2573,7 +2681,8 @@ module Sloplint
|
|
|
2573
2681
|
Rule.new(
|
|
2574
2682
|
id: "stands-serves-as",
|
|
2575
2683
|
category: "puffery",
|
|
2576
|
-
severity: "
|
|
2684
|
+
severity: "warning",
|
|
2685
|
+
confidence: "medium",
|
|
2577
2686
|
pattern: /\b(?:stands|serves)\s+as\b|\bis\s+a\s+(?:testament|reminder)\s+to\b/i,
|
|
2578
2687
|
message: '"stands/serves as", "is a testament/reminder to" is puffed AI framing.',
|
|
2579
2688
|
suggestion: "Say what it does, not what it 'stands as'.",
|
|
@@ -2584,7 +2693,8 @@ module Sloplint
|
|
|
2584
2693
|
Rule.new(
|
|
2585
2694
|
id: "vital-role",
|
|
2586
2695
|
category: "puffery",
|
|
2587
|
-
severity: "
|
|
2696
|
+
severity: "error",
|
|
2697
|
+
confidence: "high",
|
|
2588
2698
|
pattern: /\bplays?\s+a\s+(?:vital|crucial|pivotal|significant|key|central)\s+role\b/i,
|
|
2589
2699
|
message: '"plays a vital/crucial/… role" is a stock AI puffery phrase.',
|
|
2590
2700
|
suggestion: "State the specific role or effect instead.",
|
|
@@ -2595,7 +2705,8 @@ module Sloplint
|
|
|
2595
2705
|
Rule.new(
|
|
2596
2706
|
id: "underscores-highlights",
|
|
2597
2707
|
category: "puffery",
|
|
2598
|
-
severity: "
|
|
2708
|
+
severity: "warning",
|
|
2709
|
+
confidence: "medium",
|
|
2599
2710
|
# "underscored/underscoring" are unambiguously the verb and flag bare.
|
|
2600
2711
|
# "underscore/underscores" is also the character noun (a leading
|
|
2601
2712
|
# underscore, snake_case docs), so those forms require a following
|
|
@@ -2636,15 +2747,16 @@ module Sloplint
|
|
|
2636
2747
|
"Numbers accept underscores\n\nThe next section covers floats."
|
|
2637
2748
|
],
|
|
2638
2749
|
rationale: "Models reach for 'underscore' as an all-purpose emphasis verb -- findings " \
|
|
2639
|
-
"underscore, outages underscore -- asserting significance without earning
|
|
2640
|
-
"Sincere journalistic and academic use exists
|
|
2641
|
-
"
|
|
2642
|
-
"
|
|
2750
|
+
"underscore, outages underscore -- asserting significance without earning " \
|
|
2751
|
+
"it. Sincere journalistic and academic use exists: a flag means the move is " \
|
|
2752
|
+
"present, not that it's slop. The character noun never takes the verb's " \
|
|
2753
|
+
"frame and stays out."
|
|
2643
2754
|
),
|
|
2644
2755
|
Rule.new(
|
|
2645
2756
|
id: "rich-tapestry",
|
|
2646
2757
|
category: "puffery",
|
|
2647
|
-
severity: "
|
|
2758
|
+
severity: "error",
|
|
2759
|
+
confidence: "high",
|
|
2648
2760
|
pattern: /\brich\s+tapestry\b|\btapestry\s+of\b/i,
|
|
2649
2761
|
message: '"rich tapestry"/"tapestry of" is a signature AI cliché.',
|
|
2650
2762
|
suggestion: "Cut the metaphor; name the actual things.",
|
|
@@ -2653,11 +2765,11 @@ module Sloplint
|
|
|
2653
2765
|
rationale: "'tapestry of' is one of the most reliable single-phrase model tells."
|
|
2654
2766
|
),
|
|
2655
2767
|
|
|
2656
|
-
# ── structure ─────────────────────────────────────────────────────────
|
|
2657
2768
|
Rule.new(
|
|
2658
2769
|
id: "not-just-x-but-y",
|
|
2659
|
-
category: "
|
|
2770
|
+
category: "false-correction",
|
|
2660
2771
|
severity: "warning",
|
|
2772
|
+
confidence: "high",
|
|
2661
2773
|
# Two branches, both anchored on an explicit escalation word. (1) The
|
|
2662
2774
|
# copula escalation: "is not just/only/merely/simply/solely A … but B".
|
|
2663
2775
|
# (2) "not because A, but because B". The escalation word is what makes
|
|
@@ -2701,10 +2813,11 @@ module Sloplint
|
|
|
2701
2813
|
),
|
|
2702
2814
|
Rule.new(
|
|
2703
2815
|
id: "not-x-but-y",
|
|
2704
|
-
category: "
|
|
2705
|
-
severity: "
|
|
2816
|
+
category: "false-correction",
|
|
2817
|
+
severity: "warning",
|
|
2818
|
+
confidence: "medium",
|
|
2706
2819
|
# The bare corrective: "is not A but B", no escalation word, comma or no
|
|
2707
|
-
# comma.
|
|
2820
|
+
# comma. Medium confidence, because the line between a corrective ("not an
|
|
2708
2821
|
# accident but a strategy") and an ordinary concession ("not warm but the
|
|
2709
2822
|
# fire helped") is syntactic, and a regex cannot see syntax. What is here
|
|
2710
2823
|
# is a set of cheap narrowings that cut the worst of the noise: A is
|
|
@@ -2718,7 +2831,8 @@ module Sloplint
|
|
|
2718
2831
|
# ("was not warm but the fire helped") or a bare lexical verb ("was not
|
|
2719
2832
|
# perfect but got us there"), because neither is distinguishable from the
|
|
2720
2833
|
# corrective by surface form. That is the cost of the rule and the reason
|
|
2721
|
-
#
|
|
2834
|
+
# its confidence is medium: the agent reading the flag has the context
|
|
2835
|
+
# to judge, and
|
|
2722
2836
|
# should. Do not chase these by growing the B-list -- every word added
|
|
2723
2837
|
# silently narrows recall with nothing pinning it.
|
|
2724
2838
|
pattern: /(?:\bis|\bare|\bwas|\bwere|\bisn['’]t|\baren['’]t|\bwasn['’]t|\bweren['’]t|
|
|
@@ -2765,15 +2879,16 @@ module Sloplint
|
|
|
2765
2879
|
"escalation word dropped, and models reach for it constantly. It is the " \
|
|
2766
2880
|
"noisiest rule in the catalog by design: a loose version scored 174 hits " \
|
|
2767
2881
|
"before narrowing, cut to 15 here -- but not all 15 are correctives. Some " \
|
|
2768
|
-
"are concessions with an elided subject (Walden's " \
|
|
2769
|
-
"
|
|
2770
|
-
"
|
|
2771
|
-
"
|
|
2882
|
+
"are concessions with an elided subject (Walden's 'It was not lonely, but " \
|
|
2883
|
+
"made all the earth lonely beneath it'), which no surface pattern can tell " \
|
|
2884
|
+
"apart from the real thing. A flag here means 'this has the shape', not " \
|
|
2885
|
+
"'this is slop'."
|
|
2772
2886
|
),
|
|
2773
2887
|
Rule.new(
|
|
2774
2888
|
id: "isnt-x-its-y",
|
|
2775
|
-
category: "
|
|
2776
|
-
severity: "
|
|
2889
|
+
category: "false-correction",
|
|
2890
|
+
severity: "warning",
|
|
2891
|
+
confidence: "medium",
|
|
2777
2892
|
# The corrective with the conjunction dropped: one clause rejects a
|
|
2778
2893
|
# description, the next supplies the replacement through a second copula
|
|
2779
2894
|
# ("It isn't the tool. It's the habit."). Every copula tense is covered,
|
|
@@ -2789,7 +2904,8 @@ module Sloplint
|
|
|
2789
2904
|
# guards cut a loose version from 19 hits to 4 over 889k words of
|
|
2790
2905
|
# 19th-century fiction.
|
|
2791
2906
|
#
|
|
2792
|
-
# What survives is the reason
|
|
2907
|
+
# What survives is the reason the confidence is medium. An adjective
|
|
2908
|
+
# outside the list
|
|
2793
2909
|
# still slips through, and some hits are the same shape written by a
|
|
2794
2910
|
# person -- "was not the wife; it was the children" is Conan Doyle. A flag
|
|
2795
2911
|
# here says the sentence has the frame, not that a model wrote it.
|
|
@@ -2861,13 +2977,14 @@ module Sloplint
|
|
|
2861
2977
|
"one, so the sentence sounds like a correction while correcting nobody. It " \
|
|
2862
2978
|
"is 'not A but B' with the conjunction dropped and the second half promoted " \
|
|
2863
2979
|
"to its own clause, which is the form models reach for most. Ordinary prose " \
|
|
2864
|
-
"contrasts two things this way too, so the rule
|
|
2865
|
-
"
|
|
2980
|
+
"contrasts two things this way too, so the rule reports the shape, not a " \
|
|
2981
|
+
"verdict."
|
|
2866
2982
|
),
|
|
2867
2983
|
Rule.new(
|
|
2868
2984
|
id: "not-by-x-but-by-y",
|
|
2869
|
-
category: "
|
|
2870
|
-
severity: "
|
|
2985
|
+
category: "false-correction",
|
|
2986
|
+
severity: "warning",
|
|
2987
|
+
confidence: "medium",
|
|
2871
2988
|
# The corrective built on a repeated preposition: "not by A, but by B",
|
|
2872
2989
|
# "not from A but from B". The copula rules above cannot see it because
|
|
2873
2990
|
# nothing precedes "not" but the verb or a dash, so the anchor here is
|
|
@@ -2908,16 +3025,16 @@ module Sloplint
|
|
|
2908
3025
|
"We won not by luck\n\nBut by then it hardly mattered."
|
|
2909
3026
|
],
|
|
2910
3027
|
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
|
-
"
|
|
2913
|
-
"
|
|
2914
|
-
"
|
|
3028
|
+
"preposition, which is how a model corrects a claim about means or cause " \
|
|
3029
|
+
"('not by luck, but by design'). People write it too -- Thoreau and Melville " \
|
|
3030
|
+
"both lean on it -- and the rule reports the shape; a human reader decides " \
|
|
3031
|
+
"whether it earned its place."
|
|
2915
3032
|
),
|
|
2916
3033
|
Rule.new(
|
|
2917
3034
|
id: "rule-of-three",
|
|
2918
|
-
category: "
|
|
3035
|
+
category: "cadence",
|
|
2919
3036
|
severity: "info",
|
|
2920
|
-
|
|
3037
|
+
confidence: "low",
|
|
2921
3038
|
pattern: /\b[\w'-]+,\s+[\w'-]+,\s+(?:and\s+)?[\w'-]+[.!?]/,
|
|
2922
3039
|
message: "Three single words in a comma series closing a sentence (heuristic; high false-positive).",
|
|
2923
3040
|
suggestion: "Fine in moderation; watch for the AI habit of ending on triplets.",
|
|
@@ -2943,8 +3060,9 @@ module Sloplint
|
|
|
2943
3060
|
# CLAUDE.md: some tells can't be regexes; this was one.
|
|
2944
3061
|
Rule.new(
|
|
2945
3062
|
id: "everyone-nobody",
|
|
2946
|
-
category: "
|
|
3063
|
+
category: "cadence",
|
|
2947
3064
|
severity: "warning",
|
|
3065
|
+
confidence: "high",
|
|
2948
3066
|
# The comma-spliced antithesis on quantifier subjects: "Everyone wants
|
|
2949
3067
|
# the dashboard, nobody maintains it." One clause opens on
|
|
2950
3068
|
# everyone/everybody, the other on nobody/no one/none or "one N"
|
|
@@ -3015,9 +3133,84 @@ module Sloplint
|
|
|
3015
3133
|
"or give the problem its own sentence."
|
|
3016
3134
|
),
|
|
3017
3135
|
Rule.new(
|
|
3018
|
-
id: "
|
|
3019
|
-
category: "
|
|
3136
|
+
id: "punch-sentence",
|
|
3137
|
+
category: "cadence",
|
|
3020
3138
|
severity: "info",
|
|
3139
|
+
confidence: "medium",
|
|
3140
|
+
# The mid-paragraph punch: a verbless beat of one to three words wedged
|
|
3141
|
+
# between two long sentences. "...for the third time that quarter. Not
|
|
3142
|
+
# anymore. The new rota puts..." mic-drop-closer and
|
|
3143
|
+
# bare-auxiliary-closer take the long-then-short shape at the end of a
|
|
3144
|
+
# paragraph; this is the one in the middle, which the closer rules
|
|
3145
|
+
# refuse. The setup is the shared sixty-character prefix, and a
|
|
3146
|
+
# sentence of forty or more characters must follow on the same
|
|
3147
|
+
# paragraph, so a closer and a run of short sentences (short-run's
|
|
3148
|
+
# tell) are both left to their own rules.
|
|
3149
|
+
#
|
|
3150
|
+
# Read against RAID, the first draft fired on human prose at four
|
|
3151
|
+
# times its model rate, and every kind of hit is now a guard:
|
|
3152
|
+
#
|
|
3153
|
+
# A title or initial before the stop ("Mrs. Cadaver.", "John F.
|
|
3154
|
+
# Kennedy.", "St. Petersburg.") makes the proper name after it look
|
|
3155
|
+
# like a one-word sentence, so the stop the punch follows may not
|
|
3156
|
+
# close a capitalised word of one to four letters, or a lone lowercase
|
|
3157
|
+
# letter ("Swift v. Tyson"). The same guard on
|
|
3158
|
+
# the punch's own last word ("When Mr.", "Felix and Rev.") is why a
|
|
3159
|
+
# lone word must be five letters or more when capitalised; a lowercase
|
|
3160
|
+
# last word ("Not anymore.") is never an abbreviation.
|
|
3161
|
+
#
|
|
3162
|
+
# A short clause with a pronoun subject ("He agrees.", "She complies.",
|
|
3163
|
+
# "Both die.") is how a plot summary and a news brief move the story
|
|
3164
|
+
# along, and an imperative with a name or a noun subject ("Dawes
|
|
3165
|
+
# refuses.", "Set aside.") reads the same to a regex as a beat, so the
|
|
3166
|
+
# punch is two closed shapes: a negator (Not, No, Never, Nothing,
|
|
3167
|
+
# None) with up to two lowercase words after it, or one capitalised
|
|
3168
|
+
# word of five letters or more. A one-word imperative in procedural
|
|
3169
|
+
# prose ("Drain.", "Repeat.") still lands in the second shape, at about
|
|
3170
|
+
# one per 25,000 words of recipes, which the info severity allows for.
|
|
3171
|
+
pattern: /#{SENTENCE_OF_SIXTY_CHARACTERS_ENDING_IN_PUNCTUATION_AND_SPACE}\K
|
|
3172
|
+
(?<![A-Z]\.[ \t]|[A-Z][a-z]\.[ \t]|[A-Z][a-z]{2}\.[ \t]|[A-Z][a-z]{3}\.[ \t]|[ \t][a-z]\.[ \t]
|
|
3173
|
+
|[A-Z]\.[ \t]{2}|[A-Z][a-z]\.[ \t]{2}|[A-Z][a-z]{2}\.[ \t]{2}|[A-Z][a-z]{3}\.[ \t]{2}|[ \t][a-z]\.[ \t]{2})
|
|
3174
|
+
(?:(?:Not|No|Never|Nothing|None)(?:(?:[ \t]|\r?\n(?!\s*\n)[ \t]*)[a-z][A-Za-z'’-]*){0,2}
|
|
3175
|
+
|[A-Z][A-Za-z'’-]{4,})\.
|
|
3176
|
+
(?=[ \t]{1,2}[A-Z](?:[^.!?\n]|\r?\n(?!\s*\n)){40,})/x,
|
|
3177
|
+
message: "A verbless beat of three words or fewer between two long sentences is the AI punch.",
|
|
3178
|
+
suggestion: "Fold the beat into the sentence before or after it, or cut it.",
|
|
3179
|
+
examples_bad: [
|
|
3180
|
+
"The on-call engineer had been paged for the same cron job three nights running that week. Not anymore. The job now checks the lock file before it starts and exits quietly.",
|
|
3181
|
+
"The reviewers spent the whole afternoon arguing about whether the migration should run in one pass. Never again. The first half ran on Tuesday and the second half is still waiting on a sign-off.",
|
|
3182
|
+
"Every team we talked to had a wiki page describing the release process in loving detail. No exceptions. The page had not been touched since the person who wrote it left the company.",
|
|
3183
|
+
"The retry loop was supposed to give up after five attempts and page someone with the error. Simple. Instead it kept going all night because the counter reset on every restart."
|
|
3184
|
+
],
|
|
3185
|
+
examples_ok: [
|
|
3186
|
+
# Paragraph-final: that is the closer rules' shape, not this one.
|
|
3187
|
+
"The reviewers spent the whole afternoon arguing about whether the migration should run in one pass. Not anymore.",
|
|
3188
|
+
# Followed by another short sentence: short-run's territory.
|
|
3189
|
+
"The reviewers spent the whole afternoon arguing about whether the migration should run in one pass. Not anymore. Nobody minded. The second half is still waiting on a sign-off from the platform team.",
|
|
3190
|
+
# A short clause with a pronoun subject is narrative, not a beat.
|
|
3191
|
+
"The reviewers spent the whole afternoon arguing about whether the migration should run in one pass. It didn't. The first half ran on Tuesday and the second half is still waiting on a sign-off.",
|
|
3192
|
+
"Every team we talked to had a wiki page describing the release process in loving detail. Nobody read it. The page had not been touched since the person who wrote it left the company.",
|
|
3193
|
+
# A title or initial with its complement on the far side of the stop.
|
|
3194
|
+
"The committee met for the last time on a wet Thursday in the back room of the library annex. Mr. Whitaker moved that the minutes be approved as read and the motion carried without a vote.",
|
|
3195
|
+
"The only survivor of the crash on the coast road that spring was the widow of the lighthouse keeper, Mrs. Cadaver. The inquest sat for two days in the town hall and returned no verdict at all.",
|
|
3196
|
+
"The freight line ran west from the river through three counties before it reached the yards. St. Louis was the end of the line for most of the crews and the start of it for the rest.",
|
|
3197
|
+
"The freight line ran west from the river through three counties before it reached the yards. Cyrus M. Jarrett owned the yards and every siding on the line from the river to the state line.",
|
|
3198
|
+
"The rule that a federal court sitting in diversity applies general common law was set out in Swift v. Tyson. The decision stood for nearly a century before the court overruled it in Erie.",
|
|
3199
|
+
# Digits are data, and a quotation mark is dialogue.
|
|
3200
|
+
"The freight line ran west from the river through three counties before it reached the yards. Fig. 2 shows the route as it stood in 1890 and the sidings that were added after the flood.",
|
|
3201
|
+
"The freight line ran west from the river through three counties before it reached the yards. \"Enough.\" The foreman waved the crew back toward the sheds and nobody argued with him.",
|
|
3202
|
+
# Four or more words is a sentence, not a beat.
|
|
3203
|
+
"The freight line ran west from the river through three counties before it reached the yards. The crews liked it. The foreman waved the crew back toward the sheds and nobody argued with him."
|
|
3204
|
+
],
|
|
3205
|
+
rationale: "The beat withholds the verb and the object the long sentence set up and " \
|
|
3206
|
+
"hands the reader a pause instead. People write one on purpose; a draft " \
|
|
3207
|
+
"that keeps doing it is the tell, which is why it ships at info."
|
|
3208
|
+
),
|
|
3209
|
+
Rule.new(
|
|
3210
|
+
id: "np-fragment-and",
|
|
3211
|
+
category: "cadence",
|
|
3212
|
+
severity: "warning",
|
|
3213
|
+
confidence: "medium",
|
|
3021
3214
|
# A whole sentence that is two noun phrases and an "and": "A named owner
|
|
3022
3215
|
# and a quarterly review." It is the fix half of a model's
|
|
3023
3216
|
# problem-then-fix pair, with the verb left for the reader to supply.
|
|
@@ -3028,7 +3221,7 @@ module Sloplint
|
|
|
3028
3221
|
# anywhere in it, contractions included, so "A man and a woman were
|
|
3029
3222
|
# there." and "A man and a woman aren't here." never match.
|
|
3030
3223
|
#
|
|
3031
|
-
#
|
|
3224
|
+
# Medium confidence, and this is why: a lexical verb is invisible to the
|
|
3032
3225
|
# pattern, so "A car and a truck collided." has the same shape and
|
|
3033
3226
|
# flags. The corpora say that sentence is rare (one hit in 1.25M words
|
|
3034
3227
|
# of public-domain prose, most of it narrative), but it is a complete
|
|
@@ -3071,8 +3264,9 @@ module Sloplint
|
|
|
3071
3264
|
),
|
|
3072
3265
|
Rule.new(
|
|
3073
3266
|
id: "quip-question",
|
|
3074
|
-
category: "
|
|
3267
|
+
category: "reader-address",
|
|
3075
3268
|
severity: "info",
|
|
3269
|
+
confidence: "medium",
|
|
3076
3270
|
# The verbless question that opens a pitch: "No invite?", "New to the
|
|
3077
3271
|
# tool?", "Still stuck?", "Ready to start?". It must start a sentence,
|
|
3078
3272
|
# open on one of a short list of words, run one to four more words, and
|
|
@@ -3118,8 +3312,9 @@ module Sloplint
|
|
|
3118
3312
|
),
|
|
3119
3313
|
Rule.new(
|
|
3120
3314
|
id: "mic-drop-closer",
|
|
3121
|
-
category: "
|
|
3315
|
+
category: "cadence",
|
|
3122
3316
|
severity: "info",
|
|
3317
|
+
confidence: "medium",
|
|
3123
3318
|
# The kicker: a sentence of at least sixty characters, then a closer of
|
|
3124
3319
|
# two to eight words that ends the paragraph and opens on a quantifier
|
|
3125
3320
|
# ("Nothing here needs a new login.", "Most teams end up
|
|
@@ -3142,7 +3337,7 @@ module Sloplint
|
|
|
3142
3337
|
# position." The quantifiers carry the tell; the demonstratives are
|
|
3143
3338
|
# ordinary, and no examples_bad used one.
|
|
3144
3339
|
#
|
|
3145
|
-
#
|
|
3340
|
+
# Medium confidence, and the rationale says why: people end paragraphs this
|
|
3146
3341
|
# way too, at about 150 per million words on Hacker News. One is
|
|
3147
3342
|
# nothing. A draft where most paragraphs end this way is the tell, and
|
|
3148
3343
|
# an agent that sees the flag repeat should read the family as a
|
|
@@ -3199,9 +3394,10 @@ module Sloplint
|
|
|
3199
3394
|
"warning, and the fix is usually to delete the closer outright."
|
|
3200
3395
|
),
|
|
3201
3396
|
Rule.new(
|
|
3202
|
-
id: "
|
|
3203
|
-
category: "
|
|
3397
|
+
id: "bare-auxiliary-closer",
|
|
3398
|
+
category: "cadence",
|
|
3204
3399
|
severity: "info",
|
|
3400
|
+
confidence: "medium",
|
|
3205
3401
|
# The same long-sentence-then-short-closer shape as mic-drop-closer,
|
|
3206
3402
|
# but the tell lives in the verb, not the subject. mic-drop-closer's
|
|
3207
3403
|
# closer opens on a quantifier and keeps a full verb with its object
|
|
@@ -3278,14 +3474,15 @@ module Sloplint
|
|
|
3278
3474
|
],
|
|
3279
3475
|
rationale: "The closer withholds exactly the thing the long setup built toward -- the " \
|
|
3280
3476
|
"verb and its object are gone, and only the bare confirmation that something " \
|
|
3281
|
-
"happened is left standing.
|
|
3282
|
-
"
|
|
3283
|
-
"
|
|
3477
|
+
"happened is left standing. Plenty of ordinary writing drops the verb the " \
|
|
3478
|
+
"same way when it confirms an expectation, and one flag proves nothing; a " \
|
|
3479
|
+
"draft where it repeats is the tell."
|
|
3284
3480
|
),
|
|
3285
3481
|
Rule.new(
|
|
3286
3482
|
id: "short-run",
|
|
3287
|
-
category: "
|
|
3483
|
+
category: "cadence",
|
|
3288
3484
|
severity: "info",
|
|
3485
|
+
confidence: "medium",
|
|
3289
3486
|
# Three consecutive sentences of thirty characters or fewer, each
|
|
3290
3487
|
# opening on a letter and closing on a full stop, with no quotation
|
|
3291
3488
|
# mark or digit in any of them: "Nobody used it. A named owner. Then a
|
|
@@ -3422,8 +3619,9 @@ module Sloplint
|
|
|
3422
3619
|
),
|
|
3423
3620
|
Rule.new(
|
|
3424
3621
|
id: "em-dash",
|
|
3425
|
-
category: "
|
|
3622
|
+
category: "punctuation",
|
|
3426
3623
|
severity: "info",
|
|
3624
|
+
confidence: "medium",
|
|
3427
3625
|
pattern: /—/,
|
|
3428
3626
|
message: "Em dash — an AI punctuation tell.",
|
|
3429
3627
|
suggestion: "Recast with a comma, parentheses, or a separate sentence.",
|
|
@@ -3442,8 +3640,9 @@ module Sloplint
|
|
|
3442
3640
|
),
|
|
3443
3641
|
Rule.new(
|
|
3444
3642
|
id: "em-dash-overuse",
|
|
3445
|
-
category: "
|
|
3643
|
+
category: "punctuation",
|
|
3446
3644
|
severity: "warning",
|
|
3645
|
+
confidence: "high",
|
|
3447
3646
|
pattern: /—(?:[^\n]|\n(?!\s*\n))*—(?:[^\n]|\n(?!\s*\n))*—/,
|
|
3448
3647
|
message: "Three or more em dashes in one paragraph — an AI punctuation tell.",
|
|
3449
3648
|
suggestion: "Recast with commas, parentheses, or separate sentences.",
|
|
@@ -3465,8 +3664,9 @@ module Sloplint
|
|
|
3465
3664
|
|
|
3466
3665
|
Rule.new(
|
|
3467
3666
|
id: "question-isnt",
|
|
3468
|
-
category: "
|
|
3469
|
-
severity: "
|
|
3667
|
+
category: "false-correction",
|
|
3668
|
+
severity: "warning",
|
|
3669
|
+
confidence: "medium",
|
|
3470
3670
|
# The resolving clause is required, so a plain rhetorical question never
|
|
3471
3671
|
# matches. "The real question is" is ordinary English and is excluded by
|
|
3472
3672
|
# the adjacency of "question" to the negated copula.
|
|
@@ -3492,8 +3692,9 @@ module Sloplint
|
|
|
3492
3692
|
),
|
|
3493
3693
|
Rule.new(
|
|
3494
3694
|
id: "less-about-more-about",
|
|
3495
|
-
category: "
|
|
3496
|
-
severity: "
|
|
3695
|
+
category: "false-correction",
|
|
3696
|
+
severity: "warning",
|
|
3697
|
+
confidence: "medium",
|
|
3497
3698
|
# The full frame is required at both ends. Bare "less about" and bare
|
|
3498
3699
|
# "more about" are ordinary English on their own, and the subject slot
|
|
3499
3700
|
# is limited to the pronouns so that a sentence with a real subject --
|
|
@@ -3519,8 +3720,9 @@ module Sloplint
|
|
|
3519
3720
|
),
|
|
3520
3721
|
Rule.new(
|
|
3521
3722
|
id: "trailing-significance-participle",
|
|
3522
|
-
category: "
|
|
3523
|
-
severity: "
|
|
3723
|
+
category: "puffery",
|
|
3724
|
+
severity: "error",
|
|
3725
|
+
confidence: "high",
|
|
3524
3726
|
# The verb list is closed and short on purpose. Wikipedia's "signs of AI
|
|
3525
3727
|
# writing" names eight watch words for this construction; half of them
|
|
3526
3728
|
# did not survive probing. In 1.85M words of pre-2022 Hacker News and
|
|
@@ -3597,9 +3799,9 @@ module Sloplint
|
|
|
3597
3799
|
),
|
|
3598
3800
|
Rule.new(
|
|
3599
3801
|
id: "trailing-restatement",
|
|
3600
|
-
category: "
|
|
3601
|
-
severity: "
|
|
3602
|
-
|
|
3802
|
+
category: "closer",
|
|
3803
|
+
severity: "warning",
|
|
3804
|
+
confidence: "low",
|
|
3603
3805
|
# The restating tail: "…, which means working through the process
|
|
3604
3806
|
# rather than around it". A regex sees the connective and not whether
|
|
3605
3807
|
# the tail says the head again, and the sentence that prompted the rule
|
|
@@ -3687,11 +3889,11 @@ module Sloplint
|
|
|
3687
3889
|
"use the same connective to state a consequence, and the pattern cannot " \
|
|
3688
3890
|
"tell the two apart, so the rule is off by default."
|
|
3689
3891
|
),
|
|
3690
|
-
# ── hedging ───────────────────────────────────────────────────────────
|
|
3691
3892
|
Rule.new(
|
|
3692
3893
|
id: "vague-attribution",
|
|
3693
|
-
category: "
|
|
3694
|
-
severity: "
|
|
3894
|
+
category: "false-concession",
|
|
3895
|
+
severity: "error",
|
|
3896
|
+
confidence: "high",
|
|
3695
3897
|
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
3898
|
message: "Vague attribution ('some critics argue', 'it is widely…') — an AI hedging tell.",
|
|
3697
3899
|
suggestion: "Name the source, or drop the appeal to unnamed authority.",
|