zero-slop 2.12.23 → 2.12.24

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
@@ -14,7 +14,7 @@ Zero Slop is a free, open-source agent skill that finds and removes AI slop whil
14
14
 
15
15
  <p align="center">
16
16
  <a href="https://github.com/manavmishra/ZeroSlop/actions/workflows/validate.yml"><img alt="Validate" src="https://github.com/manavmishra/ZeroSlop/actions/workflows/validate.yml/badge.svg"></a>
17
- <img alt="Version 2.12.23" src="https://img.shields.io/badge/version-2.12.23-72528F?color=C15732">
17
+ <img alt="Version 2.12.24" src="https://img.shields.io/badge/version-2.12.24-72528F?color=C15732">
18
18
  <a href="https://www.npmjs.com/package/zero-slop"><img alt="npm version" src="https://img.shields.io/npm/v/zero-slop?color=C15732"></a>
19
19
  <a href="https://www.npmjs.com/package/zero-slop"><img alt="npm downloads" src="https://img.shields.io/npm/dm/zero-slop?color=17634F"></a>
20
20
  <a href="https://github.com/manavmishra/ZeroSlop/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/manavmishra/ZeroSlop?style=flat&color=C15732"></a>
@@ -176,7 +176,7 @@ python3 scripts/slopscore.py --batch drafts/ --gate 25
176
176
  The CLI sends a file to the hosted editor without changing the file on disk:
177
177
 
178
178
  ```sh
179
- npx --yes zero-slop@2.12.23 deslop draft.md --genre professional
179
+ npx --yes zero-slop@2.12.24 deslop draft.md --genre professional
180
180
  ```
181
181
 
182
182
  Use `-` for stdin and `--json` for structured output. `--require-approved` prints the result but exits nonzero when review is needed. Requires Node.js 22+; offline `score` also needs Python 3. [CLI options and privacy](docs/cli.md).
package/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: zero-slop
3
3
  license: MIT
4
4
  metadata:
5
- version: "2.12.23"
5
+ version: "2.12.24"
6
6
  author: manavmishra
7
7
  description: Edit drafts into natural prose, inspect AI-sounding patterns, or review how a specified audience might respond passage by passage. Zero Slop runs inside the user's existing AI assistant with local tools that protect source details. Use for humanizing or de-slopping writing, polishing outward-facing prose, social drafts, final editorial checks, or an explicit simulated reader review. Preserve facts, voice and format; reader simulations are hypotheses, not human feedback.
8
8
  ---
package/docs/cli.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Use `deslop` for hosted editing with MCP parity. Use `score` for offline checks.
4
4
 
5
5
  ```sh
6
- npm install --global zero-slop@2.12.23
6
+ npm install --global zero-slop@2.12.24
7
7
  zero-slop deslop draft.md --genre professional
8
8
  zero-slop deslop - --genre email < draft.txt
9
9
  zero-slop deslop draft.md --json --require-approved
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-slop",
3
- "version": "2.12.23",
3
+ "version": "2.12.24",
4
4
  "description": "Find and fix canned AI writing while preserving the facts, voice, and format.",
5
5
  "mcpServers": {
6
6
  "zero-slop": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-slop",
3
- "version": "2.12.23",
3
+ "version": "2.12.24",
4
4
  "description": "An editing skill, offline writing checks, and a CLI for the hosted Zero Slop MCP pipeline. Find stock AI phrasing, rewrite, and check source details.",
5
5
  "bin": {
6
6
  "zero-slop": "bin/zero-slop.mjs"
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "zero-slop",
4
- "version": "2.12.23",
4
+ "version": "2.12.24",
5
5
  "description": "Find and fix canned AI writing while preserving the facts, voice, and format.",
6
6
  "author": {
7
7
  "name": "Manav Mishra",
@@ -1265,8 +1265,30 @@ def facts(text, _other=""):
1265
1265
  # a feeling verb, because the goal is catching an INVENTED inner state, and the
1266
1266
  # comparison below cancels any that were already in the source.
1267
1267
  INTERIOR_STATE_RX = re.compile(
1268
- r"\b(?:I|we)\s+(?:genuinely\s+)?(?:was|were|am|felt|feel|got)\s+"
1269
- r"(?:(?:very|really|extremely|quite|so|genuinely)\s+)?(?P<state>[A-Za-z]+)", re.I)
1268
+ r"\b(?P<subject>I|we)(?:(?P<contraction>['’](?:m|re))|\s+"
1269
+ r"(?:genuinely\s+)?(?P<verb>was|were|am|are|felt|feel|got)(?P<negative>n['’]t)?)\s+"
1270
+ r"(?P<modifiers>(?:(?:very|really|extremely|quite|so|genuinely|not|never|hardly|"
1271
+ r"barely|somewhat|slightly|partly|mostly|almost|nearly|probably|possibly|"
1272
+ r"perhaps|usually|sometimes|occasionally)\s+)*)"
1273
+ r"(?P<state>[A-Za-z]+)\b", re.I)
1274
+ # New copula forms are limited to feelings, rather than treating "we're
1275
+ # exporting" or "we are product managers" as invented interior experience.
1276
+ # The older felt/was/am branches retain their existing experiential coverage.
1277
+ INTERIOR_EMOTIONS = frozenset("""afraid alarmed angry anxious ashamed astonished
1278
+ bewildered bitter bored calm comfortable confident confused content delighted
1279
+ depressed disappointed discouraged distressed eager elated embarrassed excited
1280
+ exhausted fearful familiar frustrated furious glad grateful happy hopeful humbled
1281
+ hurt isolated lonely miserable nervous overwhelmed panicked pleased proud relieved
1282
+ sad safe scared shocked surprised terrified thrilled tired uncomfortable uncertain
1283
+ uneasy unhappy unsafe upset worried""".split())
1284
+ INTERIOR_COSMETIC_INTENSITY = frozenset("very really extremely quite so genuinely".split())
1285
+ INTERIOR_CONTEXT_RX = re.compile(
1286
+ r"\b(?:if|unless|provided|assuming|when|once|until|whether|wish|wished|"
1287
+ r"may|might|could|would|should|possibly|probably|perhaps|reportedly|"
1288
+ r"allegedly|apparently)\b", re.I)
1289
+ INTERIOR_ATTRIBUTION_RX = re.compile(
1290
+ r"\b(?:said|says|wrote|writes|claimed|claims|reported|reports|told|tells|"
1291
+ r"according\s+to)\b", re.I)
1270
1292
  INTERIOR_COGNITION_RX = re.compile(
1271
1293
  r"\b(?:I|we)\s+(?P<cognition>remember(?:ed)?|recall(?:ed)?|realise(?:d)?|"
1272
1294
  r"realize(?:d)?|knew|fear(?:ed)?|hope(?:d)?|worr(?:y|ied)|panic(?:ked)?|"
@@ -1285,10 +1307,92 @@ COGNITION_CANON = {
1285
1307
  }
1286
1308
 
1287
1309
 
1310
+ def _quoted_interior_starts(text, starts):
1311
+ """Mark quotation/blockquote context in one forward delimiter scan."""
1312
+ quoted, opened = set(), [False, False, False, False]
1313
+ line_start = 0
1314
+ blockquote = False
1315
+ def word(value):
1316
+ return bool(value) and value in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
1317
+ for index, char in enumerate(text):
1318
+ if index == line_start:
1319
+ cursor = index
1320
+ while cursor < len(text) and text[cursor] in " \t":
1321
+ cursor += 1
1322
+ blockquote = cursor < len(text) and text[cursor] == ">"
1323
+ if index in starts and (blockquote or any(opened)):
1324
+ quoted.add(index)
1325
+ previous = text[index - 1] if index else ""
1326
+ following = text[index + 1] if index + 1 < len(text) else ""
1327
+ if char == "“":
1328
+ opened[0] = True
1329
+ elif char == "”":
1330
+ opened[0] = False
1331
+ elif char == "‘":
1332
+ opened[1] = True
1333
+ elif char == "’" and not (word(previous) and word(following)):
1334
+ opened[1] = False
1335
+ elif char == '"':
1336
+ opened[2] = not opened[2]
1337
+ elif char == "'":
1338
+ if opened[3] and not word(following):
1339
+ opened[3] = False
1340
+ elif not opened[3] and not word(previous):
1341
+ opened[3] = True
1342
+ if char == "\n":
1343
+ line_start = index + 1
1344
+ return quoted
1345
+
1346
+
1347
+ def _interior_states(text):
1348
+ # Literal code is protected by the structure gate, not an author's feeling.
1349
+ prose = INLINE_CODE_RX.sub(lambda m: " " * len(m.group(0)), _mask_fenced(text))
1350
+ matches = list(INTERIOR_STATE_RX.finditer(prose))
1351
+ quoted = _quoted_interior_starts(prose, {match.start() for match in matches})
1352
+ out, match_index, start = set(), 0, 0
1353
+ # Context is scanned once per sentence, avoiding a suffix scan per state.
1354
+ ends = [m.start() for m in re.finditer(r"[.!?](?=\s|$)|\n[ \t]*\n", prose)] + [len(prose)]
1355
+ for end in ends:
1356
+ sentence = prose[start:end]
1357
+ context = INTERIOR_CONTEXT_RX.search(sentence)
1358
+ attribution = INTERIOR_ATTRIBUTION_RX.search(sentence)
1359
+ context_signature = None
1360
+ if context:
1361
+ # Pin the exact qualification without copying a long sentence into
1362
+ # every state key or exposing it as a new diagnostics field.
1363
+ qualification = " ".join(sentence[context.start():].lower().split())
1364
+ context_signature = hashlib.sha256(qualification.encode("utf-8")).hexdigest()
1365
+ while match_index < len(matches) and matches[match_index].start() < end:
1366
+ match = matches[match_index]
1367
+ match_index += 1
1368
+ subject, state = match.group("subject").lower(), match.group("state").lower()
1369
+ contraction = (match.group("contraction") or "").replace("’", "'").lower()
1370
+ if contraction and contraction != ("'m" if subject == "i" else "'re"):
1371
+ continue
1372
+ if (contraction or match.group("verb").lower() == "are") and state not in INTERIOR_EMOTIONS:
1373
+ continue
1374
+ modifiers = [word.lower() for word in match.group("modifiers").split()
1375
+ if word.lower() not in INTERIOR_COSMETIC_INTENSITY]
1376
+ if match.group("negative"):
1377
+ modifiers.insert(0, "not")
1378
+ parts = [subject, *modifiers, state]
1379
+ if match.start() in quoted:
1380
+ parts.insert(0, "quoted")
1381
+ if attribution and start + attribution.start() < match.start():
1382
+ parts.insert(0, "attributed")
1383
+ if context_signature:
1384
+ # A hypothetical/qualified feeling must not license a new
1385
+ # unqualified assertion. Keep its actual context conservatively.
1386
+ parts.append("context=" + context_signature)
1387
+ out.add(":".join(parts))
1388
+ start = end + 1
1389
+ return out
1390
+
1391
+
1288
1392
  def interior_claims(text):
1289
1393
  """Inner-state assertions, reduced to a comparable core so paraphrase of an
1290
1394
  existing one does not read as a new invention."""
1291
- out = {m.group("state").lower() for m in INTERIOR_STATE_RX.finditer(text)}
1395
+ out = _interior_states(text)
1292
1396
  for m in INTERIOR_COGNITION_RX.finditer(text):
1293
1397
  word = m.group("cognition").lower()
1294
1398
  out.add(COGNITION_CANON.get(word, word))