sloplint 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 846ce6e663bb01d1dd3e054f328eaccb423745aeb34def94b8ce33253d960c23
4
- data.tar.gz: f165880bb800c66a57d88ffd7e9876c4426a7607a6f4b68582d8f0365c728956
3
+ metadata.gz: 2dff756c71ef10086a95d721978cb5bd0f3a73ae28f46637f91b13b016aa461b
4
+ data.tar.gz: 543c0b9005b4e7b90d35c5595ee812f77d849513163d222b1ae04cb8fa48b8c0
5
5
  SHA512:
6
- metadata.gz: ef1d295149a0b032fc000795316d385331a679aa246dcf4177c902a4589745e91f63d728e7a5b6ceb0c5323902c3079b1e588580f5bbbc08e07f9a19d2560a00
7
- data.tar.gz: bd0530c68b5dac9587580b79fffdd52bfed59ab847c18ecd1e3a164c906cd6f745cb7409278283876b3dcdf5ba7309459b422e047cc73c2406984569b6215bfa
6
+ metadata.gz: 537e1d987a39cb4b9c488b69ea4aa7e5381b3594d9565eb78324640389e6eae7fa9cf47c6c93921048b396de24178b969f6ac36685dec2606845c9e00694b543
7
+ data.tar.gz: 9d4d9845f6a540954446dd2a5513cd0dc0fd9c28a8e689ac6a64a975f03bf68e1478c62be96e2d09ac1784e83bb421c667898f88e89aceb6a63d729ffce17270
data/CHANGELOG.md CHANGED
@@ -3,6 +3,36 @@
3
3
  All notable changes to this project are documented here. Format loosely
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ## [0.3.0] - 2026-08-02
9
+
10
+ - New rule `load-bearing` (`rhetorical-tic`, `warning`): "load-bearing" used
11
+ as a borrowed metaphor for anything important, outside its literal
12
+ construction sense (a wall, a column, a beam). Guards on both sides —
13
+ a building noun right after it, or the predicate form ("the wall is
14
+ load-bearing") — leave the construction sense clean.
15
+ - New rule `is-is` (`rhetorical-tic`, `warning`): the doubled copula, "what it
16
+ is is a mistake" / "the thing is, is that...". No anchor needed -- the bare
17
+ pattern scored 0 hits across ~1.9M words of public-domain and modern prose.
18
+ - `exactly-the` replaced by the broader `exact-exactly` (`rhetorical-tic`,
19
+ now `info`): flags "exact/exactly" generally, not just the fixed "exactly
20
+ the point/kind/problem/…" phrase shape, with an allowlist for the places
21
+ it's doing real work (numbers, times, same/opposite/way/etc.). Severity
22
+ drops to `info` because a common word will still slip through on cases
23
+ the allowlist hasn't seen yet.
24
+ - `thats-not-nothing` replaced by the broader `not-nothing`
25
+ (`rhetorical-tic`, `warning`): catches the "is not nothing" litotes
26
+ regardless of subject ("Fifty basis points is not nothing"), not only
27
+ the demonstrative-subject form ("that's not nothing"). Personal-subject
28
+ litotes ("he was not nothing to her") and the "there is not nothing"
29
+ philosophy frame are allowlisted as legitimate human use.
30
+ - `underscores-highlights` broadened (`puffery`, still `info`): "underscores
31
+ the need for", "underscored how fragile", and "underscoring the urgency"
32
+ now match, not only the fixed "underscores/highlights/emphasizes its/the/
33
+ their importance/significance" phrase. The character-noun sense
34
+ ("a leading underscore", snake_case) stays excluded.
35
+
6
36
  ## [0.2.0] - 2026-07-28
7
37
 
8
38
  - `is-real-and-not` and `worth-naming` move from `warning` to `info`. Both
data/README.md CHANGED
@@ -51,10 +51,10 @@ $ printf 'The report is a rich tapestry of vibrant detail.\nThat is exactly the
51
51
  why: Travel-brochure adjectives and phrases that models reach for and careful writers avoid.
52
52
  fix: Replace with a concrete, specific detail or cut it.
53
53
 
54
- -:2:9: warning exactly-the "exactly the point/kind/problem/…" is an overused LLM emphasis tic.
55
- excerpt: …tapestry of vibrant detail. That is [exactly the point] I keep making about it.
56
- why: 'exactly the X' is a model intensifier that a careful writer rarely leans on.
57
- fix: Drop 'exactly the'; state the point without the intensifier.
54
+ -:2:9: info exact-exactly "exact/exactly" is reflexive emphasis unless it names something checkable.
55
+ excerpt: …tapestry of vibrant detail. That is [exactly] the point I keep making about it.
56
+ why: Models reach for 'exact/exactly' as filler emphasis on a claim with nothing to check; it earns its place only next to a number, a name, or a stated identity.
57
+ fix: Cut it, or replace with the number, name, or match it's supposed to be precise about.
58
58
  ```
59
59
 
60
60
  The brackets mark the match; the rest is there so you can see what you're fixing without opening the file — including *why*, so an agent doesn't have to run `explain` separately to decide whether a flag is worth acting on.
@@ -128,9 +128,9 @@ An unknown id or category in `--select`/`--ignore` is a usage error (exit 2, nam
128
128
 
129
129
  ## The rule catalog
130
130
 
131
- 27 rules across four categories. `sloplint rules` prints them; `sloplint rules --json` gives an agent the enumerable form.
131
+ 29 rules across four categories. `sloplint rules` prints them; `sloplint rules --json` gives an agent the enumerable form.
132
132
 
133
- - **rhetorical-tic** (16) the cadence patterns: `no-x-no-y`, `no-x-no-y-frag`, `thats-the-whole`, `thats-how-x`, `announced-takeaway`, `exactly-the`, `you-already-know`, `sit-with-that`, `thats-not-nothing`, and more.
133
+ - **rhetorical-tic** (18) the cadence patterns: `no-x-no-y`, `no-x-no-y-frag`, `thats-the-whole`, `thats-how-x`, `announced-takeaway`, `exact-exactly`, `load-bearing`, `you-already-know`, `sit-with-that`, `not-nothing`, `is-is` (doubled copula), and more.
134
134
  - **puffery** (5) Wikipedia's "signs of AI writing": `puffery-words` (vibrant, nestled, groundbreaking, in the heart of), `rich-tapestry`, `vital-role`, `stands-serves-as`, `underscores-highlights`.
135
135
  - **structure** (5) `not-just-x-but-y`, `not-x-but-y` (the bare corrective), `em-dash` (any em dash), `em-dash-overuse` (three or more in one paragraph), and `rule-of-three`.
136
136
  - **hedging** (1) `vague-attribution`: "some critics argue," "it is widely regarded."
data/docs/SPEC.md CHANGED
@@ -275,7 +275,8 @@ false-positive risk, which none has demonstrated.
275
275
  | `is-real-and-not` | "the X is real, and/not…", "is the real … and it" | skip "real estate/time"; `info` |
276
276
  | `the-punchline-is` | "the punchline is/:/?" | |
277
277
  | `worth-naming` | "worth naming", "it's worth naming that…", "Worth naming:" | skip "naming names"; `info` |
278
- | `thats-not-nothing` | "that/this/it/which is not nothing" | |
278
+ | `not-nothing` | copula + "not nothing" litotes, any subject | skip personal/there subjects |
279
+ | `is-is` | doubled copula: "what it is is …", "the thing is, is that …" | comma optional |
279
280
 
280
281
  ### puffery (Wikipedia: Signs of AI writing)
281
282
 
@@ -284,7 +285,7 @@ Single flat rule per word-cluster, matched as whole words:
284
285
  - `puffery-words` — boasts a, vibrant, rich (history/cultural/tapestry), nestled (gated to a following in/among/between, so the literal verb — a head nestled against a shoulder — doesn't count), in the heart of (gated to a place object), groundbreaking, renowned, diverse array, breathtaking, natural beauty, stands as a testament, indelible mark, deeply rooted.
285
286
  - `stands-serves-as` — "stands as / serves as", "is a testament/reminder to".
286
287
  - `vital-role` — "plays a (vital/crucial/pivotal/significant/key) role".
287
- - `underscores-highlights` — "underscores/highlights/emphasizes its (importance/significance)".
288
+ - `underscores-highlights` — "underscore(s)" + determiner and "underscored/underscoring" anywhere (the emphasis verb); "highlights/emphasizes its (importance/significance)" stays narrow.
288
289
  - `rich-tapestry` — "rich tapestry", "tapestry of".
289
290
 
290
291
  ### structure
@@ -33,9 +33,8 @@ module Sloplint
33
33
  examples_bad: ["No fluff, no filler, no jargon."],
34
34
  examples_ok: ["No parking on Sundays."],
35
35
  rationale: "Asyndetic negation chains are a signature model cadence, near-absent from " \
36
- "human prose at any length -- 24 hits in 1.02M words across Austen, Melville, " \
37
- "Madison, Thoreau, and Emerson combined. A careful writer occasionally stacks " \
38
- "two (and, rarely, more), but a model reaches for the pattern constantly."
36
+ "human prose at any length. A careful writer occasionally stacks two (and, " \
37
+ "rarely, more), but a model reaches for the pattern constantly."
39
38
  ),
40
39
  Rule.new(
41
40
  id: "no-x-no-y-frag",
@@ -76,11 +75,9 @@ module Sloplint
76
75
  ],
77
76
  rationale: "The fragment form of the same cadence, and the weaker signal of the two: the " \
78
77
  "comma chain is one authored sentence, while this is just short sentences in " \
79
- "sequence, which human prose also does. It scored 1 hit in a 744k-word " \
80
- "five-book probe (Hamilton's 'no common coin; no common judicatory', itself " \
81
- "the chain in question), so it is rare in careful writing -- but a deliberate " \
82
- "staccato run ('No one moved. No one spoke.') has the identical shape and is " \
83
- "not a tell. Treat a flag here as a question, not a verdict."
78
+ "sequence, which human prose also does. It's rare in careful writing, but a " \
79
+ "deliberate staccato run ('No one moved. No one spoke.') has the identical " \
80
+ "shape and is not a tell. Treat a flag here as a question, not a verdict."
84
81
  ),
85
82
  Rule.new(
86
83
  id: "thats-the-whole",
@@ -215,27 +212,141 @@ module Sloplint
215
212
  "which sense it's in."
216
213
  ),
217
214
  Rule.new(
218
- id: "thats-not-nothing",
215
+ id: "not-nothing",
219
216
  category: "rhetorical-tic",
220
217
  severity: "warning",
221
- pattern: /\b(?:that|this|it|which)(?:'s| is)\s+not\s+nothing\b/i,
222
- message: '"…that\'s not nothing" is a stock LLM understatement.',
218
+ # Two branches: spelled-out copula with an optional captured subject, and
219
+ # the contracted "X's not nothing". The optional subject + skip: is the
220
+ # load-bearing idiom -- skip: only sees matched text, so the personal
221
+ # subjects a human litotes takes (I/he/she/we/you/they) must be inside
222
+ # the match to be dropped there. The contracted branch captures the word
223
+ # before 's for the same reason ("she's not nothing" must reach the
224
+ # skip). "there" is guarded too: "there is/there's not nothing" is the
225
+ # philosophy frame or dialect, not the closer. "not for nothing" and
226
+ # "something and not nothing" never match at all -- the copula must sit
227
+ # adjacent to "not nothing". Token gaps cross a hard-wrapped line but
228
+ # never a paragraph break, so a paragraph ending "...was not" cannot weld
229
+ # onto one starting "Nothing ...".
230
+ pattern: /\b(?:(?:i|he|she|we|you|they|there)(?:[ \t]|\r?\n(?![ \t]*\r?\n))+)?
231
+ (?:am|is|are|was|were)(?:[ \t]|\r?\n(?![ \t]*\r?\n))+
232
+ not(?:[ \t]|\r?\n(?![ \t]*\r?\n))+nothing\b
233
+ |\b[\w-]+['’]s(?:[ \t]|\r?\n(?![ \t]*\r?\n))+
234
+ not(?:[ \t]|\r?\n(?![ \t]*\r?\n))+nothing\b/ix,
235
+ skip: [/\A(?:i|he|she|we|you|they|there)\b/i],
236
+ message: '"…is not nothing" is a stock LLM understatement.',
223
237
  suggestion: "State the magnitude directly instead of the litotes.",
224
- examples_bad: ["We cut latency in half, and that's not nothing."],
225
- examples_ok: ["That is not enough to matter."],
226
- rationale: "The 'not nothing' litotes is a recognizable model closer."
238
+ examples_bad: [
239
+ "We cut latency in half, and that's not nothing.",
240
+ "Fifty basis points is not nothing.",
241
+ "Three years of runway is not nothing.",
242
+ "A million users is not nothing.",
243
+ "The margin was not nothing.",
244
+ # Hard-wrapped Markdown: same paragraph, still the tell.
245
+ "The gain was not\nnothing, the report said."
246
+ ],
247
+ examples_ok: [
248
+ "That is not enough to matter.",
249
+ # Personal-subject litotes is a human literary move, not the closer
250
+ # tic -- spelled out and contracted.
251
+ "She told him he was not nothing to her.",
252
+ "She's not nothing to me, whatever they say.",
253
+ # "not for nothing": the intervening word breaks adjacency.
254
+ "It was not for nothing that he trained all winter.",
255
+ # The philosophy frame, both shapes it takes.
256
+ "Why is there something and not nothing?",
257
+ "There is not nothing; being persists.",
258
+ # Dialect double negative, not the litotes.
259
+ "There's not nothing we can do.",
260
+ # A paragraph break never welds a match.
261
+ "The answer was not\n\nNothing prepared us for it."
262
+ ],
263
+ rationale: "The 'not nothing' litotes is a recognizable model closer; the subject " \
264
+ "varies ('that's not nothing', 'fifty basis points is not nothing') but " \
265
+ "the move is the same. Personal subjects are carved out: 'he was not " \
266
+ "nothing to her' is a human literary litotes, not the closer."
227
267
  ),
228
268
 
229
269
  Rule.new(
230
- id: "exactly-the",
270
+ id: "exact-exactly",
271
+ category: "rhetorical-tic",
272
+ severity: "info",
273
+ pattern: /\bexact(?:ly)?\b(?!\s*(?:(?:the\s+)?(?:same|opposite|science|change|replica|copy|location|coordinates|way)\b|[$\d]|noon\b|midnight\b|o'?\s*clock\b))/i,
274
+ message: '"exact/exactly" is reflexive emphasis unless it names something checkable.',
275
+ suggestion: "Cut it, or replace with the number, name, or match it's supposed to be precise about.",
276
+ examples_bad: [
277
+ "That's exactly the point.", "We proved exactly the point we needed.",
278
+ "This is exactly the kind of thing we warned about.",
279
+ "That's the exact problem with the old system.",
280
+ "That's exactly right.", "I know exactly why this happened."
281
+ ],
282
+ examples_ok: [
283
+ "She folded it exactly the way he showed her.",
284
+ "The bill came to exactly $42.",
285
+ "We agreed on the exact same design.",
286
+ "It's not an exact science.",
287
+ "Please bring exact change for the bus.",
288
+ "The museum built an exact replica of the ship.",
289
+ "This is an exact copy of the original.",
290
+ "He wanted the exact opposite of what she suggested.",
291
+ "Rescue teams pinpointed the exact location of the wreck.",
292
+ "GPS gave us the exact coordinates of the site.",
293
+ "The train left at exactly noon.",
294
+ "They agreed to meet at exactly midnight.",
295
+ "The meeting starts at exactly 3 o'clock.",
296
+ "She has exacting standards for her students."
297
+ ],
298
+ rationale: "Models reach for 'exact/exactly' as filler emphasis on a claim with nothing to " \
299
+ "check; it earns its place only next to a number, a name, or a stated identity."
300
+ ),
301
+ Rule.new(
302
+ id: "load-bearing",
231
303
  category: "rhetorical-tic",
232
304
  severity: "warning",
233
- pattern: /\b(?:(?:that's|this is|it's)\s+)?exactly\s+the\s+(?:point|kind|type|sort|problem|question|issue|opposite|reason|thing|tension)\b/i,
234
- message: '"exactly the point/kind/problem/…" is an overused LLM emphasis tic.',
235
- suggestion: "Drop 'exactly the'; state the point without the intensifier.",
236
- examples_bad: ["That's exactly the point.", "We proved exactly the point we needed."],
237
- examples_ok: ["She folded it exactly the way he showed her."],
238
- rationale: "'exactly the X' is a model intensifier that a careful writer rarely leans on."
305
+ # Two guards, both structural, over the same noun list so they can't
306
+ # drift apart. Forward: a physical building part right after it is the
307
+ # literal sense, checked with a negative lookahead. Backward: the
308
+ # predicate form ("the wall is load-bearing") is literal too, but a
309
+ # fixed-width lookbehind covering this many noun x tense combinations
310
+ # trips a real Ruby/Onigmo lookbehind bug on some inputs (RegexpError
311
+ # at match time, not compile time -- reproduced on em-dash's own
312
+ # fixtures). Pulling the noun+copula into the pattern itself as an
313
+ # optional leading group sidesteps lookbehind entirely: when present,
314
+ # it's captured as part of the match, and skip: (which only ever sees
315
+ # matched text, never surrounding context) drops it there instead.
316
+ pattern: /\b(?:(?:wall|column|beam|post|pillar|joist|stud|masonry|partition|
317
+ structure|frame|footing|foundation|member)s?\s+
318
+ (?:is|are|was|were)\s+)?
319
+ load[-\s]?bearing\b
320
+ (?!\s+(?:wall|column|beam|post|pillar|joist|stud|masonry|partition|
321
+ structure|frame|footing|foundation|member)s?\b)/ix,
322
+ skip: [/\A(?:wall|column|beam|post|pillar|joist|stud|masonry|partition|
323
+ structure|frame|footing|foundation|member)s?\s+
324
+ (?:is|are|was|were)\s+load/ix],
325
+ message: '"load-bearing" outside construction is a borrowed metaphor.',
326
+ suggestion: "Say what the thing holds up, or what breaks without it.",
327
+ examples_bad: [
328
+ "That comma is load-bearing.",
329
+ "The load-bearing assumption is that users read the docs.",
330
+ "Half the argument rests on one load-bearing word.",
331
+ "This paragraph is the load-bearing part of the essay.",
332
+ "Trust was the load-bearing element of the whole deal.",
333
+ "The qualifier is doing load bearing work here."
334
+ ],
335
+ examples_ok: [
336
+ "They knocked out a load-bearing wall during the remodel.",
337
+ "The inspector flagged a cracked load-bearing column.",
338
+ "Steel load-bearing beams replaced the old timber.",
339
+ "The load-bearing masonry dates to 1890.",
340
+ "Those columns are load-bearing.",
341
+ "The interior wall is load-bearing, so it stays.",
342
+ "The beam is load-bearing.",
343
+ "That stud was load-bearing, so removing it needed a header beam.",
344
+ "Those joists were load-bearing, engineers confirmed after inspection.",
345
+ "The masonry was load-bearing in the original 1890 structure."
346
+ ],
347
+ rationale: "'Load-bearing' is a construction term for a wall or column that holds up the " \
348
+ "structure. Models borrow it as a metaphor for anything important, which just " \
349
+ "restates the sentence's importance without saying what actually holds it up."
239
350
  ),
240
351
  Rule.new(
241
352
  id: "thats-how-x",
@@ -271,6 +382,41 @@ module Sloplint
271
382
  rationale: "Labelling a claim as the portable lesson does the persuading that the claim " \
272
383
  "should be doing — a model habit borrowed from thought-leader prose."
273
384
  ),
385
+ Rule.new(
386
+ id: "is-is",
387
+ category: "rhetorical-tic",
388
+ severity: "warning",
389
+ # No anchor needed -- the doubled copula alone scored 0 across ~1.9M words.
390
+ # The comma is allowed because "What it is, is a mystery" grates the same
391
+ # way. Sentence and clause punctuation still block the weld ("what it is.
392
+ # Is that…", "here's what it is: is anyone…"), and \b on both ends keeps
393
+ # the pattern out of "his island". \s+ needs no paragraph-break guard: a
394
+ # false match would need a paragraph that begins with "is ".
395
+ pattern: /\bis,?\s+is\b/i,
396
+ message: 'Doubled copula ("is is" / "is, is") — spoken cadence on the page.',
397
+ suggestion: "Drop the cleft and say it straight, or delete the second 'is'.",
398
+ examples_bad: [
399
+ "What this really is is a bet on distribution.",
400
+ "What it is is a rounding error with a press release.",
401
+ "What it is, is a failure of nerve.",
402
+ "The thing is, is that nobody checked the logs."
403
+ ],
404
+ examples_ok: [
405
+ # Walden: the cleft with a single copula, which is ordinary English.
406
+ "We have heard of this virtue, but we know not what it is.",
407
+ # Sentence and clause punctuation can't be welded across.
408
+ "I know what it is. Is that a problem?",
409
+ "Here's what it is: is anyone actually reading this?",
410
+ # Both \b anchors matter -- "his island" contains the literal string "is is".
411
+ "He sailed to his island at dawn."
412
+ ],
413
+ rationale: "Three things produce a doubled copula and all three read as unedited. The " \
414
+ "wh-cleft ('what it is is a mistake') is grammatical -- 'what it is' is the " \
415
+ "subject and the second 'is' is the verb -- and it is the model's version, a " \
416
+ "frame that stages a definition instead of asserting one, the same move as " \
417
+ "'the punchline is'. The NP form ('the thing is, is that') is the true double " \
418
+ "copula and a spoken disfluency. The third is a typo."
419
+ ),
274
420
 
275
421
  # ── puffery ───────────────────────────────────────────────────────────
276
422
  Rule.new(
@@ -335,12 +481,50 @@ module Sloplint
335
481
  id: "underscores-highlights",
336
482
  category: "puffery",
337
483
  severity: "info",
338
- pattern: /\b(?:underscores|highlights|emphasizes)\s+(?:its|the|their)\s+(?:importance|significance)\b/i,
339
- message: '"underscores/highlights its importance" is stock AI framing.',
484
+ # "underscored/underscoring" are unambiguously the verb and flag bare.
485
+ # "underscore/underscores" is also the character noun (a leading
486
+ # underscore, snake_case docs), so those forms require a following
487
+ # determiner/wh-word -- the frame the emphasis verb takes, which the
488
+ # noun never precedes. That guard also skips bare-noun objects
489
+ # ("underscores concerns about"), an accepted miss: widening the frame
490
+ # to catch them readmits the noun sense. Token gaps cross a
491
+ # hard-wrapped line but never a paragraph break. highlights/emphasizes
492
+ # stay narrowed to the importance/significance frame -- both verbs are
493
+ # too common in ordinary prose to match broadly.
494
+ pattern: /\bunderscor(?:ed|ing)\b
495
+ |\bunderscores?(?:[ \t]|\r?\n(?![ \t]*\r?\n))+
496
+ (?:a|an|the|its|his|her|their|our|your|my|this|that|these|those|what|why|how|just)\b
497
+ |\b(?:highlights|emphasizes)\s+(?:its|the|their)\s+(?:importance|significance)\b/ix,
498
+ message: '"underscores" as emphasis (or "highlights/emphasizes its importance") is stock AI framing.',
340
499
  suggestion: "Show why it matters rather than asserting that it does.",
341
- examples_bad: ["This underscores its importance to the field."],
342
- examples_ok: ["She highlights the key line in yellow."],
343
- rationale: "The 'underscores its importance' move asserts significance without earning it."
500
+ examples_bad: [
501
+ "This underscores its importance to the field.",
502
+ "The delay underscores the need for better tooling.",
503
+ "The outage underscored how fragile the pipeline was.",
504
+ "Underscoring the urgency, the board met twice.",
505
+ "The results underscore a deeper problem with the method.",
506
+ "The report underscores just how far behind we are.",
507
+ "It underscores that the market has moved on.",
508
+ "The essay highlights its importance at length.",
509
+ "This emphasizes the significance of early testing."
510
+ ],
511
+ examples_ok: [
512
+ "Replace each space with an underscore.",
513
+ "Variable names use underscores instead of dashes.",
514
+ "The underscore character separates words in snake case.",
515
+ "Prefix private methods with a leading underscore.",
516
+ "Ruby numeric literals accept underscores for readability.",
517
+ "Two underscores mark a dunder method in Python.",
518
+ "She highlights the key line in yellow.",
519
+ "The paper emphasizes the method, not the results.",
520
+ # A paragraph break never welds the noun onto the next sentence.
521
+ "Numbers accept underscores\n\nThe next section covers floats."
522
+ ],
523
+ rationale: "Models reach for 'underscore' as an all-purpose emphasis verb -- findings " \
524
+ "underscore, outages underscore -- asserting significance without earning it. " \
525
+ "Sincere journalistic and academic use exists, hence info: a flag means the " \
526
+ "move is present, not that it's slop. The character noun never takes the " \
527
+ "verb's frame and stays out."
344
528
  ),
345
529
  Rule.new(
346
530
  id: "rich-tapestry",
@@ -397,8 +581,8 @@ module Sloplint
397
581
  "copula, which is the specific shape models overuse. Correlative 'not only… " \
398
582
  "but' joining two verb phrases or clauses, without a preceding copula, is " \
399
583
  "ordinary and common in formal human prose; requiring the escalation word " \
400
- "keeps those out. 'not because but because' scored 2 hits in a 744k-word " \
401
- "five-book probe (Austen, Melville, Hamilton/Madison, Thoreau, Emerson)."
584
+ "keeps those out. The same logic covers 'not because A, but because B': the " \
585
+ "escalation word is what marks it deliberate."
402
586
  ),
403
587
  Rule.new(
404
588
  id: "not-x-but-y",
@@ -464,9 +648,9 @@ module Sloplint
464
648
  ],
465
649
  rationale: "The bare 'is not A but B' corrective is the 'not just… but' move with the " \
466
650
  "escalation word dropped, and models reach for it constantly. It is the " \
467
- "noisiest rule in the catalog by design: a loose version scored 174 hits in a " \
468
- "744k-word five-book probe, and the narrowings here cut that to 15 -- but not " \
469
- "all 15 are correctives. Some are concessions with an elided subject (Walden's " \
651
+ "noisiest rule in the catalog by design: a loose version scored 174 hits " \
652
+ "before narrowing, cut to 15 here -- but not all 15 are correctives. Some " \
653
+ "are concessions with an elided subject (Walden's " \
470
654
  "'It was not lonely, but made all the earth lonely beneath it'), which no " \
471
655
  "surface pattern can tell apart from the real thing. Hence info: a flag here " \
472
656
  "means 'this has the shape', not 'this is slop'."
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sloplint
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sloplint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Jackson