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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +27 -21
- data/docs/SPEC.md +165 -108
- data/lib/sloplint/cli.rb +15 -10
- data/lib/sloplint/engine.rb +3 -2
- data/lib/sloplint/rules.rb +239 -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.",
|
|
@@ -1860,8 +1897,9 @@ module Sloplint
|
|
|
1860
1897
|
),
|
|
1861
1898
|
Rule.new(
|
|
1862
1899
|
id: "is-is",
|
|
1863
|
-
category: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
2045
|
-
severity: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
2132
|
-
severity: "
|
|
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: "
|
|
2159
|
-
severity: "
|
|
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.
|
|
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: "
|
|
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: "
|
|
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: "
|
|
2278
|
-
severity: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
2409
|
-
severity: "
|
|
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: "
|
|
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: "
|
|
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
|
|
2530
|
-
"a second sentence that only says the first one was complete.
|
|
2531
|
-
"
|
|
2532
|
-
"
|
|
2533
|
-
"
|
|
2534
|
-
"
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
2640
|
-
"Sincere journalistic and academic use exists
|
|
2641
|
-
"
|
|
2642
|
-
"
|
|
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: "
|
|
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: "
|
|
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: "
|
|
2705
|
-
severity: "
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
"
|
|
2770
|
-
"
|
|
2771
|
-
"
|
|
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: "
|
|
2776
|
-
severity: "
|
|
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
|
|
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
|
|
2865
|
-
"
|
|
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: "
|
|
2870
|
-
severity: "
|
|
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
|
-
"
|
|
2913
|
-
"
|
|
2914
|
-
"
|
|
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: "
|
|
2982
|
+
category: "cadence",
|
|
2919
2983
|
severity: "info",
|
|
2920
|
-
|
|
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: "
|
|
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: "
|
|
3020
|
-
severity: "
|
|
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
|
-
#
|
|
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: "
|
|
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: "
|
|
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
|
-
#
|
|
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: "
|
|
3203
|
-
category: "
|
|
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.
|
|
3282
|
-
"
|
|
3283
|
-
"
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
3469
|
-
severity: "
|
|
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: "
|
|
3496
|
-
severity: "
|
|
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: "
|
|
3523
|
-
severity: "
|
|
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: "
|
|
3601
|
-
severity: "
|
|
3602
|
-
|
|
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: "
|
|
3694
|
-
severity: "
|
|
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.",
|