zero-slop 2.8.0 → 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/README.md CHANGED
@@ -5,7 +5,7 @@
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.8.0-72528F">
8
+ <img alt="version" src="https://img.shields.io/badge/version-2.8.1-72528F">
9
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
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
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>
@@ -191,10 +191,9 @@ with [`scripts/register.py`](scripts/register.py) running the reading pass.
191
191
 
192
192
  The saved replay ran Zero Slop, [avoid-ai-writing](https://github.com/conorbronsdon/avoid-ai-writing),
193
193
  [no-ai-slop](https://github.com/petergyang/no-ai-slop) and
194
- [humanizer](https://github.com/blader/humanizer) on the same 18 drafts, each with
195
- GPT-5.4, high reasoning, batches of three, and pinned instructions. The Zero Slop
196
- outputs came from v2.5.9; later releases rescore those frozen outputs but do not
197
- 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.
198
197
 
199
198
  | Method | Mean writing score ↓ | Passed Zero Slop's local gates | Source check passed | Average length change |
200
199
  |---|---:|---:|---:|---:|
@@ -218,23 +217,44 @@ rates, and a method-hidden quality ranking.
218
217
  ![Method-hidden quality ranking, lower is better](assets/bench-blind-quality.png)
219
218
 
220
219
  This is a small LLM-reviewed regression study. It measures neither field accuracy nor a
221
- universal ranking. Drafts, mappings, verdicts, hashes, version records, and limits are
222
- 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
223
221
  Zero Slop v2.6.0 and is preserved in
224
222
  [`bench/incumbent-blind-replay/`](bench/incumbent-blind-replay/).
225
223
 
226
224
  For the 38-item editorial panel, the current scorer matched the prior 84.2% result.
227
225
  All frozen scores stayed unchanged, all 18 human controls remained below the gate,
228
226
  and all 18 obvious search cases remained above it. These fixed-sample checks are not
229
- proof of general accuracy. We recorded 1.86% lower median throughput across 31 interleaved
230
- local runs; treat that as machine noise, not a speed claim.
227
+ proof of general accuracy.
231
228
 
232
229
  ### Speed
233
230
 
234
- On one busy Apple silicon Mac: 1,000 documents in 3.2233 seconds (310.2 per second),
235
- a 15,201-word document in 0.4757 seconds, the slowest stress case in 3.3487 seconds,
236
- and an 8,000-word learning pass in 0.2284 seconds. Editing time is excluded; expect
237
- 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.
238
258
 
239
259
  ### Current models
240
260
 
package/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: zero-slop
3
3
  license: MIT
4
4
  metadata:
5
- version: "2.8.0"
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.8.0",
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"
@@ -81,22 +81,41 @@ SHORT_FLOORS = {
81
81
  ANTITHESIS_STOP = frozenset("""
82
82
  a an the this that these those it its is are was were be been being am do does did
83
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 their there here
84
+ we you they he she i us our your their his her them me my there here
85
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
86
87
  """.split())
87
88
 
88
- # The negation can open the pair ("Not perfect. Honest.") or land at the end of
89
- # it ("The draft was cheap. The signal it sent was not."). Both are in tells.md.
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)
90
98
  RX_MARKED_OPEN = re.compile(r"^\W*not\b", re.I)
91
- RX_MARKED_CLOSE = re.compile(r"\b(?:was|were|is|are|did|does|do|has|have|had|will)\s+not\W*$", 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)
92
108
 
93
109
 
94
- def _antithesis_content(sent: str) -> list[str]:
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]:
95
115
  """Content words, with a trailing -s folded away so a verb frame still
96
116
  matches when only its agreement changed: 'let' and 'lets' are one frame."""
97
117
  out = []
98
- for word in re.findall(r"[A-Za-z][\w'-]*", sent):
99
- word = word.lower()
118
+ for word in tokens:
100
119
  if word in ANTITHESIS_STOP:
101
120
  continue
102
121
  if len(word) > 3 and word.endswith("s") and not word.endswith("ss"):
@@ -105,35 +124,102 @@ def _antithesis_content(sent: str) -> list[str]:
105
124
  return out
106
125
 
107
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
+
108
144
  def antithesis_pairs(prose: str) -> list[str]:
109
- """Adjacent balanced sentences: the marked form, and the swapped-argument one."""
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
+ """
110
157
  out = []
111
158
  sents = _sentences(prose)
112
- for first, second in zip(sents, sents[1:]):
113
- n_first, n_second = len(first.split()), len(second.split())
114
- # Both halves have to be short enough to read as one figure. A short
115
- # line beside a paragraph-length sentence is not a balanced pair.
116
- if not (2 <= n_first <= 14 and 1 <= n_second <= 14):
117
- continue
118
- # Marked: the twist is announced, at either end of the pair.
119
- if RX_MARKED_OPEN.match(first) and n_second <= 10:
120
- out.append(f"{first} {second}")
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:
121
168
  continue
122
- if RX_MARKED_CLOSE.search(second) and n_first <= 10:
123
- out.append(f"{first} {second}")
124
- continue
125
- head, tail = _antithesis_content(first), _antithesis_content(second)
126
- if len(head) < 2 or len(tail) < 2:
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):
127
173
  continue
128
- # Identical content is repetition, not antithesis: the figure needs the
129
- # arguments to have changed.
130
- if head == tail:
174
+ if RX_SCAFFOLD.search(first) or RX_SCAFFOLD.search(second):
131
175
  continue
176
+ head, tail = cont[i], cont[i + 1]
132
177
  shared = set(head) & set(tail)
133
- # A shared frame carrying most of the shorter sentence, with the
134
- # arguments changed: that is the figure, not an accidental echo.
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
135
220
  if len(shared) >= 2 and len(shared) / min(len(head), len(tail)) >= 0.5:
136
221
  out.append(f"{first} {second}")
222
+ consumed = i + 1
137
223
  return out
138
224
 
139
225