zero-slop 2.8.2 → 2.8.4
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 +19 -17
- package/SKILL.md +5 -5
- package/data/corpus/performed-register/README.md +1 -1
- package/data/corpus/performed-register/mechanical/social-stage-stack.txt +3 -0
- package/data/learned-log.md +11 -0
- package/data/patterns.json +36 -0
- package/package.json +2 -1
- package/scripts/register.py +81 -8
- package/scripts/slopscore.py +1 -1
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
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.
|
|
8
|
+
<img alt="version" src="https://img.shields.io/badge/version-2.8.4-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
|
-
<a href="https://zero-slop.ai/try/"><img alt="This README scores
|
|
11
|
+
<a href="https://zero-slop.ai/try/"><img alt="This README scores 11.6 out of 100 on the slop score, inside the human range" src="https://img.shields.io/badge/slop%20score-11.6%2F100-0f7d55"></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
Less slop, more pop in your writing.
|
|
@@ -114,7 +114,7 @@ The badge above is this README's own, from `npx zero-slop score README.md`.
|
|
|
114
114
|
Put yours up the same way:
|
|
115
115
|
|
|
116
116
|
```md
|
|
117
|
-
[](https://zero-slop.ai/try/)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
Colours are the scorer's bands: `0f7d55` under 25, `b8860b` under 60, `b0502c`
|
|
@@ -122,7 +122,7 @@ above.
|
|
|
122
122
|
|
|
123
123
|
## The slop that Zero Slop catches
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
294 weighted patterns and a 96-term lexicon, including:
|
|
126
126
|
|
|
127
127
|
1. Binary contrasts: "It's not X. It's Y."
|
|
128
128
|
2. Throat-clearing openers: "Here's the thing," "Let me be clear"
|
|
@@ -228,33 +228,35 @@ proof of general accuracy.
|
|
|
228
228
|
|
|
229
229
|
### Speed
|
|
230
230
|
|
|
231
|
-
One busy Apple silicon Mac. Meter: 1,000 documents in
|
|
232
|
-
second), 15,201 words in 0.
|
|
233
|
-
pass
|
|
234
|
-
second), 0.
|
|
235
|
-
8,000 words: 0.
|
|
236
|
-
|
|
231
|
+
One busy Apple silicon Mac. Meter: 1,000 documents in 1.9537 seconds (511.8 per
|
|
232
|
+
second), 15,201 words in 0.3179 seconds, worst stress case 2.4137 seconds.
|
|
233
|
+
Reading pass: 0.7741 seconds for the same 1,000 (1291.9 per
|
|
234
|
+
second), 0.1051 for the same large document, linear to 96,000 words. Learning
|
|
235
|
+
pass, 8,000 words: 0.1597 seconds. Across 24 interleaved runs against 2.7.7 we measured
|
|
236
|
+
1.29% lower median throughput; that is machine noise, not a speed claim. Editing
|
|
237
237
|
time is excluded.
|
|
238
238
|
|
|
239
239
|
### Reading-pass accuracy
|
|
240
240
|
|
|
241
241
|
The reading pass budgets antithesis pairs by frequency, so the count has to be right
|
|
242
|
-
before the budget means anything.
|
|
242
|
+
before the budget means anything. On 75 labelled pairs in
|
|
243
243
|
[`bench/antithesis/`](bench/antithesis/):
|
|
244
244
|
|
|
245
245
|
| Reading pass | 2.8.0 | now |
|
|
246
246
|
|---|---:|---:|
|
|
247
|
-
| Recall, all shapes | 40.0% |
|
|
247
|
+
| Recall, all shapes | 40.0% | 91.2% |
|
|
248
248
|
| Recall, shapes in reach | 44.4% | 100% |
|
|
249
249
|
| Precision | 80.0% | 100% |
|
|
250
250
|
| False positives | 3 | 0 |
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
2.8.3 added the families the 58-pair corpus never tested, where 2.8.2 scores 67.5%
|
|
253
|
+
precision.
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+

|
|
256
|
+
|
|
257
|
+
Bare subject swap and the weak isocolon stay out of reach and count against recall: both
|
|
258
|
+
are identical to ordinary prose on every lexical statistic. Maintainer labels on
|
|
259
|
+
constructed pairs, so this is a regression floor, not field accuracy.
|
|
258
260
|
|
|
259
261
|
### Current models
|
|
260
262
|
|
package/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: zero-slop
|
|
3
3
|
license: MIT
|
|
4
4
|
metadata:
|
|
5
|
-
version: "2.8.
|
|
5
|
+
version: "2.8.4"
|
|
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
|
---
|
|
@@ -91,8 +91,8 @@ local and AI responsibilities distinct:
|
|
|
91
91
|
and directly fix stumbles, repetition, weak transitions, and awkward flow.
|
|
92
92
|
7. **Verifier — local tools plus the AI assistant.** Check the exact final text
|
|
93
93
|
against the source for the writing score, facts, meaning, qualifiers, voice,
|
|
94
|
-
format, and structure.
|
|
95
|
-
runs again.
|
|
94
|
+
format, and structure. This is a release gate, not advice: a warning blocks
|
|
95
|
+
delivery. Any repair returns through roles 5 and 6 before role 7 runs again.
|
|
96
96
|
8. **Fresh-eyes finalizer — a new AI pass.** Read the verified text as a first-time
|
|
97
97
|
reader, apply only safe final polish, and approve it without changes. A role 8
|
|
98
98
|
edit restarts roles 5 through 8; the finalizer never bypasses verification.
|
|
@@ -192,7 +192,7 @@ Run the heuristic surface scorer on the draft:
|
|
|
192
192
|
python3 <skill-root>/scripts/slopscore.py --explain <file> # any cwd; or pipe via stdin
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
-
Every channel runs on every draft: the pattern meter (
|
|
195
|
+
Every channel runs on every draft: the pattern meter (294 weighted tells plus
|
|
196
196
|
a 96-term lexicon and 26 context-gated riders), rhythm and burstiness,
|
|
197
197
|
long-form word variety, followability, formatting
|
|
198
198
|
densities, and register. Each one is interpretable: pattern-meter hits come
|
|
@@ -933,7 +933,7 @@ the AI model already running in the assistant or rewrite this `SKILL.md`.
|
|
|
933
933
|
- `references/tells.md` — the master taxonomy (113 tells, 6 families) with fixes.
|
|
934
934
|
It is the human-readable catalogue; `data/patterns.json` is its machine
|
|
935
935
|
implementation. Together with the reviewed shared overlay, the current
|
|
936
|
-
release carries
|
|
936
|
+
release carries 294 weighted regexes because some tells need more than one.
|
|
937
937
|
- `references/rewrite-moves.md` — the positive program: the six ladder rungs
|
|
938
938
|
expanded, with before/after pairs and voice calibration.
|
|
939
939
|
- `references/platforms.md` — LinkedIn, X/Twitter, email, blog, newsletter,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# performed-register — should-flag corpus
|
|
2
2
|
|
|
3
|
-
Thirty-
|
|
3
|
+
Thirty-six spans a human editor flagged as slop on sight while the scorer
|
|
4
4
|
passed every one clean (9.5–10.9, zero pattern hits). All come from
|
|
5
5
|
zero-slop.ai blog drafts written in August 2026 — first-party text, so no
|
|
6
6
|
external registry entry applies. They share one register: **performed-writer
|
package/data/learned-log.md
CHANGED
|
@@ -390,3 +390,14 @@ example that motivated it.
|
|
|
390
390
|
alone: paragraph-order dependence, unsupported novelty, self-labelled
|
|
391
391
|
significance, moral-adjective category errors, recap-flattery, and
|
|
392
392
|
wall-of-text replies.
|
|
393
|
+
- 2026-09-03 — v2.8.4 social-stage false negative. A long promotional social
|
|
394
|
+
post scored 15.0 because its only machine-readable signal was one candour
|
|
395
|
+
phrase; repeated validation, staged negative fragments, a less-like/more-like
|
|
396
|
+
comparison, and a generic "real conversations" close were left entirely to
|
|
397
|
+
the contextual reader. Added four narrow anchors whose combined weight finds
|
|
398
|
+
that stack without making any one ordinary phrase decisive. Also fixed the
|
|
399
|
+
reading pass so curly contractions, irregular past-tense verbs, and Unicode
|
|
400
|
+
dash bullets are not misreported as verbless prose. The reproduced miss moves
|
|
401
|
+
from 15.0 to 50.2, the edited version remains below the 25-point line, every
|
|
402
|
+
known-human control remains below it, and the exact finishing pipeline now
|
|
403
|
+
treats a verifier warning as a blocked release rather than advisory copy.
|
package/data/patterns.json
CHANGED
|
@@ -162,6 +162,15 @@
|
|
|
162
162
|
"first_seen": "2026-08-03",
|
|
163
163
|
"last_confirmed": "2026-08-15"
|
|
164
164
|
},
|
|
165
|
+
{
|
|
166
|
+
"name": "social-validation",
|
|
167
|
+
"cat": "performed",
|
|
168
|
+
"rx": "\\b(?:i (?:know|realize) i(?:'|\u2019)m not alone|you(?:'|\u2019)re not (?:alone|imagining it)|if you(?:'|\u2019)ve (?:felt|noticed) [\\w '\u2019-]{2,36}, you(?:'|\u2019)re not (?:alone|imagining it))\\b",
|
|
169
|
+
"hints": ["not alone", "not imagining it"],
|
|
170
|
+
"w": 2.5,
|
|
171
|
+
"first_seen": "2026-09-03",
|
|
172
|
+
"last_confirmed": "2026-09-03"
|
|
173
|
+
},
|
|
165
174
|
{
|
|
166
175
|
"name": "faux-insight",
|
|
167
176
|
"cat": "performed",
|
|
@@ -226,6 +235,15 @@
|
|
|
226
235
|
"first_seen": "2026-08-03",
|
|
227
236
|
"last_confirmed": "2026-08-03"
|
|
228
237
|
},
|
|
238
|
+
{
|
|
239
|
+
"name": "staged-negative-ladder",
|
|
240
|
+
"cat": "contrast",
|
|
241
|
+
"rx": "(?:^|[.!?]\\s+|\\n)[\u2013\u2014*+\u2022-]*\\s*not in (?:an? |the )?[\\w '\u2019-]{2,32}\\.\\s*(?:[\u2013\u2014*+\u2022-]+\\s*)?not in (?:an? |the )?[\\w '\u2019-]{2,32}\\.\\s*(?:[\u2013\u2014*+\u2022-]+\\s*)?but in (?:an? |the )?[\\w '\u2019-]{2,48}",
|
|
242
|
+
"hints": ["not in a", "not in the"],
|
|
243
|
+
"w": 5,
|
|
244
|
+
"first_seen": "2026-09-03",
|
|
245
|
+
"last_confirmed": "2026-09-03"
|
|
246
|
+
},
|
|
229
247
|
{
|
|
230
248
|
"name": "this-isnt-about",
|
|
231
249
|
"cat": "contrast",
|
|
@@ -515,6 +533,15 @@
|
|
|
515
533
|
"first_seen": "2026-08-14",
|
|
516
534
|
"last_confirmed": "2026-08-14"
|
|
517
535
|
},
|
|
536
|
+
{
|
|
537
|
+
"name": "less-like-more-like",
|
|
538
|
+
"cat": "contrast",
|
|
539
|
+
"rx": "\\b(?:feel(?:s|ing)?|seem(?:s|ing)?) less like (?:an? |the )?[\\w '\u2019-]{2,48}(?:,| and)? (?:an? )?(?:little )?more like (?:an? |the )?[\\w '\u2019-]{2,48}",
|
|
540
|
+
"hints": ["less like", "more like"],
|
|
541
|
+
"w": 4,
|
|
542
|
+
"first_seen": "2026-09-03",
|
|
543
|
+
"last_confirmed": "2026-09-03"
|
|
544
|
+
},
|
|
518
545
|
{
|
|
519
546
|
"name": "thats-not-thats",
|
|
520
547
|
"cat": "contrast",
|
|
@@ -1177,6 +1204,15 @@
|
|
|
1177
1204
|
"first_seen": "2026-08-14",
|
|
1178
1205
|
"last_confirmed": "2026-08-14"
|
|
1179
1206
|
},
|
|
1207
|
+
{
|
|
1208
|
+
"name": "real-conversation-closer",
|
|
1209
|
+
"cat": "performed",
|
|
1210
|
+
"rx": "\\bwhere the (?:real|genuine|meaningful) (?:conversation|conversations|connection|connections) (?:is|are) (?:starting to |finally )?(?:happen|happening|taking place)\\b",
|
|
1211
|
+
"hints": ["real conversation", "genuine conversation", "meaningful conversation"],
|
|
1212
|
+
"w": 3,
|
|
1213
|
+
"first_seen": "2026-09-03",
|
|
1214
|
+
"last_confirmed": "2026-09-03"
|
|
1215
|
+
},
|
|
1180
1216
|
{
|
|
1181
1217
|
"name": "recipe-for",
|
|
1182
1218
|
"cat": "cliche",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-slop",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.4",
|
|
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"
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"!scripts/build_plugin.py",
|
|
48
48
|
"!scripts/build_skill_zip.py",
|
|
49
49
|
"!scripts/check_svg.py",
|
|
50
|
+
"!scripts/check_release_surfaces.py",
|
|
50
51
|
"!scripts/contextual.py",
|
|
51
52
|
"!scripts/contextual-signals.md",
|
|
52
53
|
"!references/contextual-signals.md",
|
package/scripts/register.py
CHANGED
|
@@ -95,9 +95,13 @@ NEGATION = re.compile(r"\b(?:not|never|cannot)\b|\w+n['’]t\b", re.I)
|
|
|
95
95
|
# figure's usual carrier and is absent from ordinary negated prose ("The server
|
|
96
96
|
# did not respond. We restarted it.").
|
|
97
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(
|
|
98
|
+
RX_MARKED_OPEN = re.compile(
|
|
99
|
+
r"^\W*not\s+(?!all\b|every\b|only\b|just\b|much\b|many\b|most\b|enough\b"
|
|
100
|
+
r"|yet\b|quite\b|nearly\b|entirely\b|\w+ly\b)", re.I)
|
|
99
101
|
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*$"
|
|
102
|
+
r"\b(?:was|were|is|are|did|does|do|has|have|had|will|can|could|would)\s+not\W*$"
|
|
103
|
+
r"|\b(?:wasn|weren|isn|aren|didn|doesn|don|hasn|haven|hadn|won|can|couldn|wouldn)"
|
|
104
|
+
r"['\u2019]t\W*$", re.I)
|
|
101
105
|
# The two template shapes the meter already anchors (this-is-what-looks-like,
|
|
102
106
|
# no-x-had-to). The meter scores them as spans; the register pass has to COUNT
|
|
103
107
|
# them, because tells.md budgets the family by frequency and a span hit is not
|
|
@@ -138,7 +142,20 @@ def _common_prefix(a: list[str], b: list[str]) -> int:
|
|
|
138
142
|
# splitter or prose_of -- both are shared by every family, and moving either one
|
|
139
143
|
# pushed four to eleven documents over budget on rate alone -- this family
|
|
140
144
|
# rejects the glued span itself. Nothing else sees the change.
|
|
141
|
-
RX_SCAFFOLD = re.compile(r"^\s*(?:[#>|]|[-*+]\s|\d+\.\s)
|
|
145
|
+
RX_SCAFFOLD = re.compile(r"^\s*(?:[#>|]|[-*+]\s|\d+\.\s|\*\*)|\n\s*(?:[#>|]|[-*+]\s|\d+\.\s|\*\*)")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Cardinals and calendar words. Their presence in the part of a pair that
|
|
149
|
+
# differs marks a specification rather than a rhetorical figure.
|
|
150
|
+
ENUMERATED_VALUE = frozenset("""
|
|
151
|
+
one two three four five six seven eight nine ten eleven twelve twenty thirty
|
|
152
|
+
forty fifty sixty seventy eighty ninety hundred thousand million billion
|
|
153
|
+
first second third fourth fifth
|
|
154
|
+
monday tuesday wednesday thursday friday saturday sunday
|
|
155
|
+
january february march april may june july august september october november
|
|
156
|
+
december hour hours minute minutes day days week weeks month months year years
|
|
157
|
+
am pm noon midnight
|
|
158
|
+
""".split())
|
|
142
159
|
|
|
143
160
|
|
|
144
161
|
def antithesis_pairs(prose: str) -> list[str]:
|
|
@@ -189,7 +206,24 @@ def antithesis_pairs(prose: str) -> list[str]:
|
|
|
189
206
|
# role never certifies its own output." shares "role" and is ordinary
|
|
190
207
|
# prose. The frame is a repeated opening, a copula on both sides, or two
|
|
191
208
|
# words in common.
|
|
192
|
-
|
|
209
|
+
# `prefix >= 1` counted a shared stopword as a frame, so "It does not
|
|
210
|
+
# run on Windows. It runs on Linux and macOS." qualified on "it" alone.
|
|
211
|
+
# Requiring a CONTENT frame instead was too strict: "We do not guess. We
|
|
212
|
+
# measure." shares only "we" and is the figure.
|
|
213
|
+
#
|
|
214
|
+
# What separates them is the payoff, not the frame. The figure lands its
|
|
215
|
+
# twist in a breath -- "We measure." "She proved it." -- while ordinary
|
|
216
|
+
# negated prose just carries on at normal length. So a stopword frame is
|
|
217
|
+
# allowed, but only when the second half is that short.
|
|
218
|
+
content_prefix = any(w not in ANTITHESIS_STOP for w in ta[:prefix])
|
|
219
|
+
shared_content = {w for w in shared if w not in ANTITHESIS_STOP}
|
|
220
|
+
# A repeated CONTENT opening is anaphora -- "The report ... The report
|
|
221
|
+
# ..." -- which the isocolon branch below already refuses for exactly
|
|
222
|
+
# this reason. Here it was being read as a frame, i.e. as evidence for
|
|
223
|
+
# the figure rather than against it.
|
|
224
|
+
strong_frame = len(shared_content) >= 2 and not content_prefix
|
|
225
|
+
weak_frame = prefix >= 1 or len(shared) >= 2
|
|
226
|
+
if negated and na <= 10 and ((strong_frame and nb <= 8) or (weak_frame and nb <= 5)):
|
|
193
227
|
out.append(f"{first} {second}"); consumed = i + 1; continue
|
|
194
228
|
# A copula on both sides is the weakest of the three frames, so it only
|
|
195
229
|
# counts when the halves are staccato-short. "Passwords are never stored
|
|
@@ -217,6 +251,20 @@ def antithesis_pairs(prose: str) -> list[str]:
|
|
|
217
251
|
shared_open = any(w not in ANTITHESIS_STOP for w in ta[:prefix])
|
|
218
252
|
if shared_open or head == tail or len(head) < 2 or len(tail) < 2:
|
|
219
253
|
continue
|
|
254
|
+
# Enumeration wears the same clothes as isocolon: one frame, both
|
|
255
|
+
# arguments swapped, the same word overlap. "The free tier includes ten
|
|
256
|
+
# seats. The pro tier includes fifty seats." is structurally identical
|
|
257
|
+
# to "A junior engineer reads the error. A senior engineer reads the
|
|
258
|
+
# stack trace." -- four shared words and 0.67 overlap in both.
|
|
259
|
+
#
|
|
260
|
+
# What separates them is what varies. A specification varies a VALUE:
|
|
261
|
+
# a quantity, a weekday, a time. The figure varies a CONCEPT. So a
|
|
262
|
+
# cardinal or a calendar word in the part that differs means this is a
|
|
263
|
+
# table written as prose, and the pass stays quiet.
|
|
264
|
+
differing = set(head) | set(tail)
|
|
265
|
+
if differing & ENUMERATED_VALUE or any(
|
|
266
|
+
any(ch.isdigit() for ch in w) for w in differing):
|
|
267
|
+
continue
|
|
220
268
|
if len(shared) >= 2 and len(shared) / min(len(head), len(tail)) >= 0.5:
|
|
221
269
|
out.append(f"{first} {second}")
|
|
222
270
|
consumed = i + 1
|
|
@@ -226,7 +274,7 @@ def antithesis_pairs(prose: str) -> list[str]:
|
|
|
226
274
|
# "X, not Y." and "A rather than B." The corrective appositive. Each instance is
|
|
227
275
|
# usually careful writing, which is why no pattern list contains it.
|
|
228
276
|
RX_SUBTRACTIVE = re.compile(
|
|
229
|
-
r"[^.\n]{3,90}
|
|
277
|
+
r"[^.\n]{3,90}?[,\u2014\u2013]\s*(?:not|never)\s+[^.\n]{3,60}[.\n]"
|
|
230
278
|
r"|[^.\n]{3,70}\brather than\b[^.\n]{3,50}[.\n]",
|
|
231
279
|
re.I,
|
|
232
280
|
)
|
|
@@ -332,6 +380,26 @@ FINITE_VERB = re.compile(
|
|
|
332
380
|
r"|gate[sd]?|gives?|runs?|gets?|gave|gone)\b"
|
|
333
381
|
r"|\b\w+(?:s|ed|es)\b", re.I)
|
|
334
382
|
|
|
383
|
+
# Curly apostrophes are ordinary prose, not token boundaries. A contracted
|
|
384
|
+
# auxiliary makes the clause finite even when the main verb is irregular or a
|
|
385
|
+
# participle ("I’ve found", "you’re seeing"). The old tokeniser split those
|
|
386
|
+
# into ``I``/``ve`` and ``you``/``re`` and then reported complete sentences as
|
|
387
|
+
# fragments. Keep this conservative: a possible possessive false negative is
|
|
388
|
+
# safer than telling a writer that a grammatical sentence has no verb.
|
|
389
|
+
CONTRACTED_FINITE = re.compile(r"\b[A-Za-z][\w-]*['’](?:m|re|ve|s|d|ll)\b", re.I)
|
|
390
|
+
# Only forms that are rarely ordinary nouns or adjectives belong here. Ambiguous
|
|
391
|
+
# forms such as ``cut``, ``left``, ``read``, ``set``, and ``thought`` made real
|
|
392
|
+
# fragments look grammatical ("Nothing left in the queue"). Contractions are
|
|
393
|
+
# handled separately above, so this small list only closes clear past-tense
|
|
394
|
+
# holes in uncontracted sentences.
|
|
395
|
+
IRREGULAR_FINITE = frozenset("""
|
|
396
|
+
arose became began blew broke brought bought caught chose came dealt did drew
|
|
397
|
+
drank drove ate fell fed felt fought found flew forgot forgave froze gave got
|
|
398
|
+
grew had heard held hid kept knew laid led lent lost made meant met paid rode
|
|
399
|
+
ran said saw sold sent shook showed sang sat slept spoke spent stood stole
|
|
400
|
+
struck swam took taught told threw understood woke wore won wrote was were went
|
|
401
|
+
""".split())
|
|
402
|
+
|
|
335
403
|
# The -s half of that catch-all cannot tell a verb from a plural noun, and a
|
|
336
404
|
# plural noun in a fragment made the whole fragment invisible: "Same compound,
|
|
337
405
|
# three identifiers." and "Same assay, two units." both read as verbed on
|
|
@@ -386,6 +454,11 @@ def _looks_like_noun(sent: str, start: int) -> bool:
|
|
|
386
454
|
|
|
387
455
|
|
|
388
456
|
def _has_finite_verb(sent: str) -> bool:
|
|
457
|
+
if CONTRACTED_FINITE.search(sent):
|
|
458
|
+
return True
|
|
459
|
+
if any(word.lower() in IRREGULAR_FINITE
|
|
460
|
+
for word in re.findall(r"[A-Za-z][\w'’-]*", sent)):
|
|
461
|
+
return True
|
|
389
462
|
for match in FINITE_VERB.finditer(sent):
|
|
390
463
|
word = match.group(0)
|
|
391
464
|
# An explicit auxiliary or an -ed form is a verb wherever it appears.
|
|
@@ -408,10 +481,10 @@ def _sentences(prose: str) -> list[str]:
|
|
|
408
481
|
def verbless_fragments(prose: str) -> list[str]:
|
|
409
482
|
out = []
|
|
410
483
|
for sent in _sentences(prose):
|
|
411
|
-
words = re.findall(r"[A-Za-z][\w'
|
|
484
|
+
words = re.findall(r"[A-Za-z][\w'’-]*", sent)
|
|
412
485
|
if not (3 <= len(words) <= 12):
|
|
413
486
|
continue
|
|
414
|
-
if sent.rstrip().endswith(":") or sent.lstrip().startswith(("-", "*", "#", "|")):
|
|
487
|
+
if sent.rstrip().endswith(":") or sent.lstrip().startswith(("-", "–", "—", "•", "*", "#", "|")):
|
|
415
488
|
continue
|
|
416
489
|
if IMPERATIVE_OPENER.match(sent):
|
|
417
490
|
continue
|
|
@@ -514,7 +587,7 @@ def prose_of(text: str) -> str:
|
|
|
514
587
|
return "\n".join(
|
|
515
588
|
line
|
|
516
589
|
for line in text.split("\n")
|
|
517
|
-
if not line.strip().startswith(("|", "![", ">", " "))
|
|
590
|
+
if not line.strip().startswith(("|", "![", ">", "-", "–", "—", "•", "*", " "))
|
|
518
591
|
)
|
|
519
592
|
|
|
520
593
|
|
package/scripts/slopscore.py
CHANGED
|
@@ -900,7 +900,7 @@ def render_portfolio(result):
|
|
|
900
900
|
# broetry is a slop tell, not a machine tell: LinkedIn writers invented it
|
|
901
901
|
# years before GPT-3, and it demonstrably performs on the platform. Whether to
|
|
902
902
|
# trade reach for a human voice is the author's call, not the meter's.
|
|
903
|
-
STRUCT_MARK = re.compile(r"^\s*(?:[
|
|
903
|
+
STRUCT_MARK = re.compile(r"^\s*(?:[-–—*+•>#]|\d+[.)]|\|)")
|
|
904
904
|
DIALOGUE_OPEN = re.compile("^[\"“‘']")
|
|
905
905
|
|
|
906
906
|
|