zero-slop 2.7.8 → 2.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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Manav Mishra
3
+ Copyright (c) 2026 Garage Capital Ventures
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -5,23 +5,65 @@
5
5
  <img alt="tests" src="https://img.shields.io/badge/tests-passing-227B5B">
6
6
  <img alt="dependencies" src="https://img.shields.io/badge/runtime%20dependencies-0-227B5B">
7
7
  <img alt="privacy" src="https://img.shields.io/badge/learning-private-227B5B">
8
- <img alt="version" src="https://img.shields.io/badge/version-2.7.8-72528F">
8
+ <img alt="version" src="https://img.shields.io/badge/version-2.8.1-72528F">
9
+ <a href="https://hol.org/guard/plugins"><img alt="Listed in the HOL plugin registry" src="https://img.shields.io/badge/HOL%20registry-listed-2C6E8F"></a>
10
+ <a href="https://github.com/hashgraph-online/awesome-ai-plugins#tools--integrations"><img alt="Listed in awesome-ai-plugins" src="https://img.shields.io/badge/awesome--ai--plugins-listed-2C6E8F"></a>
11
+ <a href="https://zero-slop.ai/try/"><img alt="This README scores 12.0 out of 100 on the slop score, inside the human range" src="https://img.shields.io/badge/slop%20score-12.0%2F100-0f7d55"></a>
9
12
  </p>
10
13
 
11
- Less slop, more pop.
14
+ Less slop, more pop in your writing.
12
15
 
13
16
  Zero Slop finds stock phrasing, mechanical rhythm, vague claims, and canned
14
- formatting, then gives your AI assistant a guarded editing workflow. Its
15
- MIT-licensed local checks run offline; your existing AI assistant does the editing.
16
- See the benchmark at [zero-slop.ai](https://zero-slop.ai).
17
+ formatting, then gives your AI assistant or agent harness an editing workflow on guardrails. Its
18
+ MIT-licensed local checks run offline; your existing AI assistant or harness does the editing.
19
+ Score a draft at [zero-slop.ai/try](https://zero-slop.ai/try/); benchmark at
20
+ [zero-slop.ai](https://zero-slop.ai).
17
21
 
18
- ![A scored sentence before and after editing](assets/demo.png)
22
+ ```sh
23
+ npx zero-slop install
24
+ ```
25
+
26
+ ![Scoring a launch post at 100.0, rewriting it to 9.5, with the 40% figure held by the fact gate](assets/zero-slop-demo.gif)
27
+
28
+ ### What it does, on a real draft
29
+
30
+ A launch post, as AI wrote it:
31
+
32
+ > We're thrilled to announce that our team has leveraged cutting-edge AI to deliver a seamless onboarding experience. It's not just a redesign, it's a fundamental reimagining of how users engage with our platform. In today's fast-paced landscape, the ability to iterate quickly is crucial. By harnessing the power of machine learning, we've been able to reduce setup time by 40%. Here's the thing nobody tells you: onboarding is where most products lose their users. We're excited to continue this journey with you.
33
+
34
+ `slopscore.py --explain` on that paragraph, unedited:
35
+
36
+ ```text
37
+ Writing score: 100.0/100 [major rewrite]
38
+ Flagged phrases : 11 across 83 words
39
+ Sentence variety: too even
40
+ Main issues : canned LinkedIn phrase, promotional language, manufactured
41
+ stakes, two-part contrast used as a formula, performed
42
+ writer's voice, buzzword used as promotion
43
+
44
+ Flagged phrases (11), strongest first:
45
+ "In today's fast-paced" manufactured stakes; start where the reader needs to start
46
+ "We're thrilled to" canned LinkedIn phrase; say what happened without the stock opening
47
+ "It's not just a redesign, it's"
48
+ two-part contrast used as a formula; state the claim once, plainly
49
+ "Here's the thing" performed writer's voice; say the thing plainly
50
+ 'cutting-edge' promotional language; name what it does; cut the adjectives
51
+ 'leveraged' buzzword used as promotion; use the plain word
52
+ ```
53
+
54
+ The rewrite, limited to the draft's stated claims:
55
+
56
+ > We used machine learning to reduce onboarding setup time by 40%.
57
+
58
+ ```text
59
+ Writing score: 9.5/100 [clear]
60
+ Flagged phrases : 0 across 10 words
61
+ ```
19
62
 
20
63
  ## Problem
21
64
 
22
65
  AI-assisted writing often converges on the same constructions: "It's not X. It's Y."
23
- "Here's the thing nobody tells you." One phrase proves little. Repetition across
24
- wording, rhythm, structure, and formatting is the problem.
66
+ "Here's the thing nobody tells you." One emdash is fine. Multiple emdashes, definitely slop. The same effect can come from repetition in the wording or the structure. Mechanical rhythm and overworked formatting can do it too.
25
67
 
26
68
  Zero Slop is an Agent Skill and ships no model. Claude, GPT, or another compatible
27
69
  model edits; local tools check names, numbers, quotations, links, code, tables, and
@@ -42,7 +84,10 @@ Or install it with `npx`:
42
84
  npx skills add manavmishra/ZeroSlop --global
43
85
  ```
44
86
 
45
- Or from the registry, which also installs the scorer as a command:
87
+ Or from the registry, which also installs the scorer as a command. Zero Slop is
88
+ listed in [awesome-ai-plugins](https://github.com/hashgraph-online/awesome-ai-plugins#tools--integrations)
89
+ and carries a public profile in the [HOL plugin registry](https://hol.org/guard/plugins),
90
+ where its trust score and scanner results are published:
46
91
 
47
92
  ```sh
48
93
  npx zero-slop install # add --harness codex|cursor|opencode|zed
@@ -63,6 +108,18 @@ You get the edited draft, before-and-after scores, and quoted flagged phrases.
63
108
  `/zero-slop inspect (your writing)` reviews without rewriting. For a folder,
64
109
  `slopscore.py --batch drafts/ --gate 25` fails above the threshold.
65
110
 
111
+ ## Show your score
112
+
113
+ The badge above is this README's own, from `npx zero-slop score README.md`.
114
+ Put yours up the same way:
115
+
116
+ ```md
117
+ [![Slop score](https://img.shields.io/badge/slop%20score-12.0%2F100-0f7d55)](https://zero-slop.ai/try/)
118
+ ```
119
+
120
+ Colours are the scorer's bands: `0f7d55` under 25, `b8860b` under 60, `b0502c`
121
+ above.
122
+
66
123
  ## The slop that Zero Slop catches
67
124
 
68
125
  290 weighted patterns and a 96-term lexicon, including:
@@ -94,7 +151,7 @@ Eight roles form one workflow. Each is a job rather than a service, run as its o
94
151
 
95
152
  | Role | Who does it | What happens |
96
153
  |---|---|---|
97
- | 1. Scorer | Local tools | Finds the exact wording, rhythm, readability, and formatting problems that raised the writing score. |
154
+ | 1. Scorer | Local tools | Finds the exact phrases behind the writing score, then checks pacing and readability. It also catches overworked formatting. |
98
155
  | 2. Interpreter | Your AI assistant | Reads the claims, purpose, audience, structure, and voice before changing anything. |
99
156
  | 3. Rewriter | Your AI assistant | Removes stock language and rebuilds order, rhythm, and tone without inventing detail. |
100
157
  | 4. Fact gate | Local tools | Rejects any version that changes names, numbers, quotations, links, code, tables, paths, or structure. |
@@ -134,10 +191,9 @@ with [`scripts/register.py`](scripts/register.py) running the reading pass.
134
191
 
135
192
  The saved replay ran Zero Slop, [avoid-ai-writing](https://github.com/conorbronsdon/avoid-ai-writing),
136
193
  [no-ai-slop](https://github.com/petergyang/no-ai-slop) and
137
- [humanizer](https://github.com/blader/humanizer) on the same 18 drafts, each with
138
- GPT-5.4, high reasoning, batches of three, and pinned instructions. The Zero Slop
139
- outputs came from v2.5.9; later releases rescore those frozen outputs but do not
140
- pretend to have regenerated them.
194
+ [humanizer](https://github.com/blader/humanizer) over 18 drafts with GPT-5.4, high
195
+ reasoning, and pinned instructions. Zero Slop's outputs came from v2.5.9; later
196
+ releases rescore those frozen outputs rather than pretend to regenerate them.
141
197
 
142
198
  | Method | Mean writing score ↓ | Passed Zero Slop's local gates | Source check passed | Average length change |
143
199
  |---|---:|---:|---:|---:|
@@ -161,23 +217,44 @@ rates, and a method-hidden quality ranking.
161
217
  ![Method-hidden quality ranking, lower is better](assets/bench-blind-quality.png)
162
218
 
163
219
  This is a small LLM-reviewed regression study. It measures neither field accuracy nor a
164
- universal ranking. Drafts, mappings, verdicts, hashes, version records, and limits are
165
- in [`bench/README.md`](bench/README.md). The separate method-hidden two-way replay used
220
+ universal ranking. Drafts, hashes, and limits are in [`bench/README.md`](bench/README.md). The separate method-hidden two-way replay used
166
221
  Zero Slop v2.6.0 and is preserved in
167
222
  [`bench/incumbent-blind-replay/`](bench/incumbent-blind-replay/).
168
223
 
169
224
  For the 38-item editorial panel, the current scorer matched the prior 84.2% result.
170
225
  All frozen scores stayed unchanged, all 18 human controls remained below the gate,
171
226
  and all 18 obvious search cases remained above it. These fixed-sample checks are not
172
- proof of general accuracy. We recorded 6.51% higher median throughput across 12 interleaved
173
- local runs; treat that as machine noise, not a speed claim.
227
+ proof of general accuracy.
174
228
 
175
229
  ### Speed
176
230
 
177
- On one busy Apple silicon Mac: 1,000 documents in 3.2233 seconds (310.2 per second),
178
- a 15,201-word document in 0.4757 seconds, the slowest stress case in 3.3487 seconds,
179
- and an 8,000-word learning pass in 0.2284 seconds. Editing time is excluded; expect
180
- different results on other machines and loads.
231
+ One busy Apple silicon Mac. Meter: 1,000 documents in 2.5035 seconds (399.4 per
232
+ second), 15,201 words in 0.4301 seconds, worst stress case 3.2518 seconds. Reading
233
+ pass, untimed on record until now: 0.8304 seconds for the same 1,000 (1,204.2 per
234
+ second), 0.1359 for the same large document, linear to 96,000 words. Learning pass,
235
+ 8,000 words: 0.2027 seconds. Across 31 interleaved runs against 2.7.7 we measured
236
+ 0.07% higher median throughput; that is machine noise, not a speed claim. Editing
237
+ time is excluded.
238
+
239
+ ### Reading-pass accuracy
240
+
241
+ The reading pass budgets antithesis pairs by frequency, so the count has to be right
242
+ before the budget means anything. It had never been measured. On 58 labelled pairs in
243
+ [`bench/antithesis/`](bench/antithesis/):
244
+
245
+ | Reading pass | 2.8.0 | now |
246
+ |---|---:|---:|
247
+ | Recall, all shapes | 40.0% | 90.0% |
248
+ | Recall, shapes in reach | 44.4% | 100% |
249
+ | Precision | 80.0% | 100% |
250
+ | False positives | 3 | 0 |
251
+
252
+ ![Antithesis detection before and after, on 58 labelled pairs](assets/bench-antithesis.png)
253
+
254
+ Bare subject swap and the weak isocolon stay out of reach and count against recall: the
255
+ first is a judgment call in `references/tells.md`, the second is identical to ordinary
256
+ parallel prose on every lexical statistic. Maintainer labels on constructed pairs, so
257
+ this is a regression floor, not field accuracy.
181
258
 
182
259
  ### Current models
183
260
 
package/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: zero-slop
3
3
  license: MIT
4
4
  metadata:
5
- version: "2.7.8"
5
+ version: "2.8.1"
6
6
  author: manavmishra
7
7
  description: Turn drafts into sharp, natural prose or inspect them without rewriting. Zero Slop runs inside the user's existing AI assistant; Claude, GPT, or another compatible model reads and edits in context while local tools point to exact phrases and protect the source. Use when the user asks to humanize or de-slop writing, inspect AI-sounding patterns, fix text that reads like ChatGPT, polish outward-facing prose, draft social or LinkedIn content, or apply a final quality check to prose the agent generated. The workflow preserves facts, voice, and format and learns privately from repeated, reason-labelled human edits.
8
8
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-slop",
3
- "version": "2.7.8",
3
+ "version": "2.8.1",
4
4
  "description": "An Agent Skill that scores AI-sounding prose locally, guides the host AI through an editorial rewrite, and checks changed source details. The local tools run offline with zero dependencies.",
5
5
  "bin": {
6
6
  "zero-slop": "bin/zero-slop.mjs"
@@ -50,6 +50,7 @@
50
50
  "!scripts/contextual.py",
51
51
  "!scripts/contextual-signals.md",
52
52
  "!references/contextual-signals.md",
53
+ "!scripts/make-readme-gif.mjs",
53
54
  "!scripts/growth-snapshot.mjs",
54
55
  "!data/voices/",
55
56
  "!**/__pycache__/"
@@ -35,6 +35,7 @@ import sys
35
35
  # certified-human samples would let these budgets be derived rather than argued.
36
36
  MIN_WORDS = 300
37
37
  BUDGETS = {
38
+ "antithesis_pair": (0.0, 2),
38
39
  "subtractive_contrast": (6.0, 3),
39
40
  "comma_series": (26.0, 8),
40
41
  "significance_scaffolding": (0.0, 1),
@@ -61,6 +62,167 @@ SHORT_FLOORS = {
61
62
  "referent_cluster": 2,
62
63
  }
63
64
 
65
+ # eval.md A1 / SKILL.md step 2.1: antithesis pairs -- two balanced statements,
66
+ # the second landing the twist. Budget is ONE per piece, and "three or more under
67
+ # 500 words means the register failed whatever the score said."
68
+ #
69
+ # This report had no row for that family. RX_SUBTRACTIVE below is a different
70
+ # check -- eval.md A2, the corrective appositive, judged on density -- and it was
71
+ # carrying the whole contrast family's name in the report while A1 went
72
+ # unmeasured. A 479-word manifesto with five antithesis pairs in it therefore
73
+ # printed "Binary contrasts: 1 found, ok" and nothing else about contrast.
74
+ #
75
+ # Two of the four shapes are reachable without a parser, and only those two ship:
76
+ # marked staccato -- "Not perfect. Honest."
77
+ # adjacent isocolon -- one verb frame, both arguments swapped: "Speed is moving
78
+ # fast. Velocity is moving fast in the same direction."
79
+ # Bare subject swap and unmarked reversal stay the reader's judgment, exactly as
80
+ # references/tells.md already says they must.
81
+ ANTITHESIS_STOP = frozenset("""
82
+ a an the this that these those it its is are was were be been being am do does did
83
+ to of in on at by for with from as and or but so if then than not no nor yet
84
+ we you they he she i us our your their his her them me my there here
85
+ one two three first second next last own same very just only also more most less
86
+ every all each any some many few way thing
87
+ """.split())
88
+
89
+ # Negation, including the contracted forms. "isn't", "don't" and "won't" carry
90
+ # the figure exactly as "is not" does, and the shipped detector matched none of
91
+ # them: "Slop isn't a vibe. It's measurable." is a documented anchor in
92
+ # references/tells.md and it walked straight past.
93
+ NEGATION = re.compile(r"\b(?:not|never|cannot)\b|\w+n['’]t\b", re.I)
94
+ # A copula in both halves is the "X is A. Y is B." frame, which is the marked
95
+ # figure's usual carrier and is absent from ordinary negated prose ("The server
96
+ # did not respond. We restarted it.").
97
+ COPULA = re.compile(r"\b(?:is|are|was|were|be|been|am)\b|\w+['’]s\b|\w+n['’]t\b", re.I)
98
+ RX_MARKED_OPEN = re.compile(r"^\W*not\b", re.I)
99
+ RX_MARKED_CLOSE = re.compile(
100
+ r"\b(?:was|were|is|are|did|does|do|has|have|had|will|can|could|would)\s+not\W*$", re.I)
101
+ # The two template shapes the meter already anchors (this-is-what-looks-like,
102
+ # no-x-had-to). The meter scores them as spans; the register pass has to COUNT
103
+ # them, because tells.md budgets the family by frequency and a span hit is not
104
+ # a count.
105
+ RX_CONTRASTIVE = re.compile(r"^\W*(?:but|yet|however)\b", re.I)
106
+ RX_STOCK_CLOSER = re.compile(r"\bthis is what\b.{0,60}?\blooks? like\b", re.I)
107
+ RX_UNMARKED_REVERSAL = re.compile(r"^\W*no\s+[\w-]+(?:\s+[\w-]+){0,3}\s+had to\b", re.I)
108
+
109
+
110
+ def _tokens(sent: str) -> list[str]:
111
+ return [w.lower() for w in re.findall(r"[A-Za-z][\w'’-]*", sent)]
112
+
113
+
114
+ def _antithesis_content(tokens: list[str]) -> list[str]:
115
+ """Content words, with a trailing -s folded away so a verb frame still
116
+ matches when only its agreement changed: 'let' and 'lets' are one frame."""
117
+ out = []
118
+ for word in tokens:
119
+ if word in ANTITHESIS_STOP:
120
+ continue
121
+ if len(word) > 3 and word.endswith("s") and not word.endswith("ss"):
122
+ word = word[:-1]
123
+ out.append(word)
124
+ return out
125
+
126
+
127
+ def _common_prefix(a: list[str], b: list[str]) -> int:
128
+ n = 0
129
+ for x, y in zip(a, b):
130
+ if x != y:
131
+ break
132
+ n += 1
133
+ return n
134
+
135
+
136
+ # A heading or a list lead-in carries no terminal punctuation, so it arrives
137
+ # from _sentences glued to the paragraph beneath it. Rather than change the
138
+ # splitter or prose_of -- both are shared by every family, and moving either one
139
+ # pushed four to eleven documents over budget on rate alone -- this family
140
+ # rejects the glued span itself. Nothing else sees the change.
141
+ RX_SCAFFOLD = re.compile(r"^\s*(?:[#>|]|[-*+]\s|\d+\.\s)|\*\*|\n\s*(?:[#>|]|[-*+]\s|\d+\.\s)")
142
+
143
+
144
+ def antithesis_pairs(prose: str) -> list[str]:
145
+ """Adjacent balanced sentences where the second lands the twist.
146
+
147
+ Recall is bounded and the bound is a property of the figure, not of the
148
+ implementation. "A meter reports a number. A reader reports a feeling." and
149
+ "The report lists every vendor. The appendix lists every contract." are the
150
+ same construction to every lexical statistic -- same lengths, same one
151
+ shared word, same 0.33 overlap -- and only the first is antithesis. What
152
+ separates them is semantic opposition, which no word count can see. So the
153
+ marked shapes, where a negation anchors the figure, are matched broadly;
154
+ the unmarked ones are matched only where the parallel is strong enough to
155
+ be structural. Bare subject swap stays the reader's call, as tells.md says.
156
+ """
157
+ out = []
158
+ sents = _sentences(prose)
159
+ toks = [_tokens(s) for s in sents]
160
+ cont = [_antithesis_content(t) for t in toks]
161
+ # Matches do not overlap. The figure is a pair, so a sentence that has
162
+ # already landed one twist cannot also be the setup for the next: three
163
+ # short consecutive sentences were producing two pairs out of one figure
164
+ # and inflating a rate the budget reads directly.
165
+ consumed = -1
166
+ for i in range(len(sents) - 1):
167
+ if i <= consumed:
168
+ continue
169
+ first, second = sents[i], sents[i + 1]
170
+ ta, tb = toks[i], toks[i + 1]
171
+ na, nb = len(ta), len(tb)
172
+ if not (2 <= na <= 14 and 1 <= nb <= 14):
173
+ continue
174
+ if RX_SCAFFOLD.search(first) or RX_SCAFFOLD.search(second):
175
+ continue
176
+ head, tail = cont[i], cont[i + 1]
177
+ shared = set(head) & set(tail)
178
+ prefix = _common_prefix(ta, tb)
179
+ negated = bool(NEGATION.search(first) or NEGATION.search(second))
180
+
181
+ # Marked, announced at the open or the close of the pair.
182
+ if RX_MARKED_OPEN.match(first) and nb <= 10:
183
+ out.append(f"{first} {second}"); consumed = i + 1; continue
184
+ if RX_MARKED_CLOSE.search(second) and na <= 10:
185
+ out.append(f"{first} {second}"); consumed = i + 1; continue
186
+ # Marked, carried inside the pair. A negation alone is ordinary prose,
187
+ # so the halves also have to be short AND share a frame. One shared
188
+ # topic word is not a frame: "The AI roles supply judgment. A generating
189
+ # role never certifies its own output." shares "role" and is ordinary
190
+ # prose. The frame is a repeated opening, a copula on both sides, or two
191
+ # words in common.
192
+ if negated and na <= 10 and nb <= 8 and (prefix >= 1 or len(shared) >= 2):
193
+ out.append(f"{first} {second}"); consumed = i + 1; continue
194
+ # A copula on both sides is the weakest of the three frames, so it only
195
+ # counts when the halves are staccato-short. "Passwords are never stored
196
+ # in plain text. They are hashed with a per-user salt." is two copulas
197
+ # and a negation and no figure at all.
198
+ if (negated and na <= 7 and nb <= 6
199
+ and COPULA.search(first) and COPULA.search(second)):
200
+ out.append(f"{first} {second}"); consumed = i + 1; continue
201
+ # A contrastive opener on the second half is the twist announced by a
202
+ # conjunction rather than by the negation's position: "That creates
203
+ # speed. But speed is not velocity."
204
+ if (negated and RX_CONTRASTIVE.match(second) and na <= 8 and nb <= 7
205
+ and (shared or COPULA.search(second))):
206
+ out.append(f"{first} {second}"); consumed = i + 1; continue
207
+ # The two template shapes the meter anchors, counted here.
208
+ if RX_STOCK_CLOSER.search(second) or RX_UNMARKED_REVERSAL.match(first):
209
+ out.append(f"{first} {second}"); consumed = i + 1; continue
210
+ # Isocolon: one frame, both arguments swapped. A repeated opening is
211
+ # restatement rather than a swap, but only when the repeated part
212
+ # carries meaning: "Version one shipped in March. Version two shipped in
213
+ # June." repeats the subject and is enumeration, while "A junior
214
+ # engineer reads the error. A senior engineer reads the stack trace."
215
+ # repeats only the article and is the figure. So the test is whether any
216
+ # CONTENT word opens both halves, not whether any token does.
217
+ shared_open = any(w not in ANTITHESIS_STOP for w in ta[:prefix])
218
+ if shared_open or head == tail or len(head) < 2 or len(tail) < 2:
219
+ continue
220
+ if len(shared) >= 2 and len(shared) / min(len(head), len(tail)) >= 0.5:
221
+ out.append(f"{first} {second}")
222
+ consumed = i + 1
223
+ return out
224
+
225
+
64
226
  # "X, not Y." and "A rather than B." The corrective appositive. Each instance is
65
227
  # usually careful writing, which is why no pattern list contains it.
66
228
  RX_SUBTRACTIVE = re.compile(
@@ -142,6 +304,28 @@ RX_INFLATION = re.compile(
142
304
  r"(?:improvement|progress|difference|impact|result|results|value|win|shift"
143
305
  r"|change|benefit|breakthrough|game.?changer)\b", re.I)
144
306
 
307
+ # An imperative is the bare base form, so it carries none of the inflection
308
+ # (-s, -ed, -es) or the auxiliaries FINITE_VERB looks for: "Play to win." and
309
+ # "Build durable growth." were both being reported as verbless fragments. On the
310
+ # seven-line manifesto that surfaced this, two of the five hits were imperatives,
311
+ # so 40% of the document's one finding was wrong. A closed list is the same
312
+ # device FINITE_VERB already is, and it only ever fires at a sentence opening.
313
+ IMPERATIVE_OPENER = re.compile(
314
+ r"^\W*(?:and|but|so|then|now|first|next|finally)?\W*"
315
+ r"(?:add|aim|allow|apply|ask|assume|avoid|begin|book|break|bring|build|buy"
316
+ r"|call|change|check|choose|clean|clear|click|close|collect|come|compare"
317
+ r"|consider|copy|count|cover|create|cut|decide|define|delete|deliver|design"
318
+ r"|do|draw|drive|drop|edit|enter|expect|explain|fill|find|finish|fix|focus"
319
+ r"|follow|forget|get|give|go|grab|grow|handle|help|hire|hold|imagine|include"
320
+ r"|install|invest|join|keep|know|launch|lead|learn|leave|let|listen|look"
321
+ r"|love|make|meet|move|name|note|notice|open|pay|pick|plan|play|point|prefer"
322
+ r"|press|pull|push|put|read|remember|remove|repeat|replace|report|reset"
323
+ r"|return|review|run|save|say|scale|see|select|sell|send|set|share|ship|show"
324
+ r"|sign|skip|solve|sort|speak|spend|stand|start|stay|stop|study|take|talk"
325
+ r"|teach|tell|test|think|throw|touch|track|treat|trust|try|turn|update|use"
326
+ r"|wait|walk|want|watch|win|write)\b",
327
+ re.I)
328
+
145
329
  FINITE_VERB = re.compile(
146
330
  r"\b(?:is|are|was|were|be|been|being|has|have|had|do|does|did|can|could|will"
147
331
  r"|would|shall|should|may|might|must|gets?|goes|comes?|makes?|takes?|gives?"
@@ -161,6 +345,8 @@ def verbless_fragments(prose: str) -> list[str]:
161
345
  continue
162
346
  if sent.rstrip().endswith(":") or sent.lstrip().startswith(("-", "*", "#", "|")):
163
347
  continue
348
+ if IMPERATIVE_OPENER.match(sent):
349
+ continue
164
350
  if not FINITE_VERB.search(sent):
165
351
  out.append(sent)
166
352
  return out
@@ -307,6 +493,7 @@ def measure(text: str) -> dict:
307
493
  triads = [" ".join(m.group(0).split()) for m in NEGATION_TRIAD.finditer(prose)]
308
494
  dangling = dangling_pointers(text)
309
495
  fragments = verbless_fragments(prose)
496
+ antithesis = antithesis_pairs(prose)
310
497
  thin = thin_sections(text)
311
498
  clusters = referent_clusters(text)
312
499
  table_uniformity, column = table_row_uniformity(text)
@@ -321,6 +508,7 @@ def measure(text: str) -> dict:
321
508
  "thin_section": {"count": len(thin), "per_1k": per_k(len(thin)), "hits": thin[:6]},
322
509
  "referent_cluster": {"count": len(clusters), "per_1k": per_k(len(clusters)), "hits": clusters[:3]},
323
510
  "table_uniformity": {"share": table_uniformity, "column": column},
511
+ "antithesis_pair": {"count": len(antithesis), "per_1k": per_k(len(antithesis)), "hits": antithesis[:6]},
324
512
  "subtractive_contrast": {"count": len(subtractive), "per_1k": per_k(len(subtractive)), "hits": subtractive[:12]},
325
513
  "comma_series": {"count": len(series), "per_1k": per_k(len(series))},
326
514
  "significance_scaffolding": {"count": len(significance), "per_1k": per_k(len(significance)), "hits": significance[:6]},
@@ -354,7 +542,8 @@ LABEL = {
354
542
  "verbless_fragment": "Verbless fragments",
355
543
  "thin_section": "Headings over a sentence or two",
356
544
  "referent_cluster": "One thing under several names",
357
- "subtractive_contrast": "Binary contrasts",
545
+ "antithesis_pair": "Antithesis pairs",
546
+ "subtractive_contrast": "Binary contrasts (X, not Y)",
358
547
  "comma_series": "Comma-series density",
359
548
  "significance_scaffolding": "Announced significance",
360
549
  "classifier_scaffolding": "Graded not delivered (stems)",