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 +1 -1
- package/README.md +99 -22
- package/SKILL.md +1 -1
- package/package.json +2 -1
- package/scripts/register.py +190 -1
package/LICENSE
CHANGED
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.
|
|
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
|
|
15
|
-
MIT-licensed local checks run offline; your existing AI assistant does the editing.
|
|
16
|
-
|
|
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
|
-
|
|
22
|
+
```sh
|
|
23
|
+
npx zero-slop install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+

|
|
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
|
|
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
|
+
[](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
|
|
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)
|
|
138
|
-
|
|
139
|
-
|
|
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
|

|
|
162
218
|
|
|
163
219
|
This is a small LLM-reviewed regression study. It measures neither field accuracy nor a
|
|
164
|
-
universal ranking. Drafts,
|
|
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.
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+

|
|
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.
|
|
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.
|
|
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__/"
|
package/scripts/register.py
CHANGED
|
@@ -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
|
-
"
|
|
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)",
|