zero-slop 2.5.8

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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/SKILL.md +803 -0
  4. package/data/corpus/community-register/README.md +14 -0
  5. package/data/corpus/community-register/judgment/communicative-drift.txt +1 -0
  6. package/data/corpus/community-register/judgment/reflexive-agreement.txt +1 -0
  7. package/data/corpus/community-register/judgment/rhetorical-scale-mismatch.txt +1 -0
  8. package/data/corpus/must-not-flag/README.md +49 -0
  9. package/data/corpus/must-not-flag/esl-engineer-email.txt +7 -0
  10. package/data/corpus/must-not-flag/exec-memo.txt +9 -0
  11. package/data/corpus/must-not-flag/federalist.txt +5 -0
  12. package/data/corpus/must-not-flag/gettysburg.txt +1 -0
  13. package/data/corpus/must-not-flag/grant-abstract.txt +5 -0
  14. package/data/corpus/must-not-flag/ml-methods.txt +1 -0
  15. package/data/corpus/must-not-flag/personal-essay.txt +1 -0
  16. package/data/corpus/must-not-flag/press-release.txt +7 -0
  17. package/data/corpus/must-not-flag/recipe.txt +11 -0
  18. package/data/corpus/must-not-flag/sre-runbook.txt +1 -0
  19. package/data/corpus/must-not-flag/technical-postmortem.txt +1 -0
  20. package/data/corpus/must-not-flag/terse-engineer-note.txt +1 -0
  21. package/data/corpus/must-not-flag-shape/README.md +22 -0
  22. package/data/corpus/must-not-flag-shape/aphorism-list.txt +9 -0
  23. package/data/corpus/must-not-flag-shape/changelog.txt +13 -0
  24. package/data/corpus/must-not-flag-shape/dialogue.txt +19 -0
  25. package/data/corpus/must-not-flag-shape/lyrics.txt +15 -0
  26. package/data/corpus/must-not-flag-shape/poem.txt +8 -0
  27. package/data/corpus/must-not-flag-shape/sms-transcript.txt +15 -0
  28. package/data/corpus/performed-register/README.md +38 -0
  29. package/data/corpus/performed-register/judgment/detection-fragile.txt +1 -0
  30. package/data/corpus/performed-register/judgment/draft-cheap-signal.txt +1 -0
  31. package/data/corpus/performed-register/judgment/ear-chiasmus.txt +1 -0
  32. package/data/corpus/performed-register/judgment/fight-against-tagline.txt +1 -0
  33. package/data/corpus/performed-register/judgment/fine-beat.txt +1 -0
  34. package/data/corpus/performed-register/judgment/glow-finishing.txt +1 -0
  35. package/data/corpus/performed-register/judgment/jargon-compression.txt +1 -0
  36. package/data/corpus/performed-register/judgment/not-perfect-honest.txt +1 -0
  37. package/data/corpus/performed-register/judgment/punctuation-convict.txt +1 -0
  38. package/data/corpus/performed-register/judgment/recipe-ingredients.txt +1 -0
  39. package/data/corpus/performed-register/judgment/replay-lands.txt +1 -0
  40. package/data/corpus/performed-register/judgment/rhymes-with.txt +1 -0
  41. package/data/corpus/performed-register/judgment/rhythm-prints.txt +1 -0
  42. package/data/corpus/performed-register/judgment/same-leak.txt +1 -0
  43. package/data/corpus/performed-register/judgment/tools-work.txt +1 -0
  44. package/data/corpus/performed-register/judgment/tuning-gradients.txt +1 -0
  45. package/data/corpus/performed-register/judgment/verdict-arithmetic.txt +1 -0
  46. package/data/corpus/performed-register/judgment/whole-product.txt +1 -0
  47. package/data/corpus/performed-register/judgment/words-cheap.txt +1 -0
  48. package/data/corpus/performed-register/mechanical/argue-with-artifact.txt +1 -0
  49. package/data/corpus/performed-register/mechanical/bill-lands.txt +1 -0
  50. package/data/corpus/performed-register/mechanical/billed-to-reader.txt +1 -0
  51. package/data/corpus/performed-register/mechanical/economics-brutal.txt +1 -0
  52. package/data/corpus/performed-register/mechanical/has-receipts.txt +1 -0
  53. package/data/corpus/performed-register/mechanical/hired-adversary.txt +1 -0
  54. package/data/corpus/performed-register/mechanical/hyperbole-universal.txt +1 -0
  55. package/data/corpus/performed-register/mechanical/metaphor-sender-name.txt +1 -0
  56. package/data/corpus/performed-register/mechanical/minding-own-business.txt +1 -0
  57. package/data/corpus/performed-register/mechanical/on-the-tin.txt +1 -0
  58. package/data/corpus/performed-register/mechanical/opens-the-hood.txt +1 -0
  59. package/data/corpus/performed-register/mechanical/turns-out-payoff.txt +1 -0
  60. package/data/corpus/performed-register/mechanical/vibe-check.txt +1 -0
  61. package/data/corpus/performed-register/mechanical/vibe-isnt-a-vibe.txt +1 -0
  62. package/data/corpus/performed-register/mechanical/vibe-number-attached.txt +1 -0
  63. package/data/corpus/performed-register/mechanical/where-slop-lives.txt +1 -0
  64. package/data/learned-log.md +339 -0
  65. package/data/learned.json +128 -0
  66. package/data/patterns.json +2298 -0
  67. package/package.json +54 -0
  68. package/references/copy-desk.md +87 -0
  69. package/references/evidence.md +382 -0
  70. package/references/overcorrection.md +77 -0
  71. package/references/platforms.md +102 -0
  72. package/references/readalong.md +118 -0
  73. package/references/rewrite-moves.md +177 -0
  74. package/references/tells.md +190 -0
  75. package/scripts/calibrate.py +274 -0
  76. package/scripts/learn.py +1588 -0
  77. package/scripts/predictability.py +227 -0
  78. package/scripts/rerank.py +150 -0
  79. package/scripts/safeio.py +146 -0
  80. package/scripts/slopscore.py +1352 -0
  81. package/scripts/version_check.py +104 -0
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env python3
2
+ """calibrate — derive tell weights from evidence instead of intuition.
3
+
4
+ Two jobs, both stdlib-only and offline:
5
+
6
+ 1. WEIGHTS FROM DATA. Given a folder of known-human writing and a folder of
7
+ known-AI drafts, compute each lexicon term's *excess frequency* in the AI
8
+ set — the method Kobak et al. used to find the LLM vocabulary in 15M
9
+ PubMed abstracts. Terms the two corpora use equally get weight ~0. Terms
10
+ the AI set over-uses get weight proportional to the log-ratio. This makes
11
+ the meter self-calibrating: point it at this month's model output and the
12
+ weights follow the current era, not 2024's.
13
+
14
+ 2. FALSE-POSITIVE REGRESSION. Any pattern change is checked against a
15
+ corpus of writing that must never be flagged. A pattern that fires on
16
+ known-good human prose is rejected before it can ship. This is what makes
17
+ continuous updating safe rather than reckless.
18
+
19
+ Usage:
20
+ python3 calibrate.py --human dir/ --ai dir/ [--out data/calibrated.json]
21
+ python3 calibrate.py --selftest # run the FP regression only
22
+ python3 calibrate.py --decay # age out unconfirmed patterns
23
+
24
+ Provenance: every weight written carries `first_seen`, `last_confirmed`, and
25
+ `n_obs`, so a future maintainer can tell a well-evidenced tell from a guess.
26
+ """
27
+ import json
28
+ import math
29
+ import re
30
+ import sys
31
+ import argparse
32
+ from collections import Counter
33
+ from datetime import date
34
+ from pathlib import Path
35
+
36
+ from safeio import atomic_write_text, file_locks
37
+
38
+ DATA = Path(__file__).resolve().parent.parent / "data"
39
+ WORD = re.compile(r"[a-z’']+")
40
+ MIN_OBS = 5 # a term needs this many AI-side occurrences to earn a weight
41
+ MAX_WEIGHT = 6.0 # cap so no single word can convict alone
42
+ DECAY_MONTHS = 18 # unconfirmed patterns lose half their weight after this
43
+
44
+
45
+ def _json_texts(obj):
46
+ """Extract prose from supported JSON corpus shapes without scoring JSON syntax."""
47
+ if isinstance(obj, str):
48
+ return [obj]
49
+ values = obj.values() if isinstance(obj, dict) else obj if isinstance(obj, list) else []
50
+ texts = []
51
+ for value in values:
52
+ if isinstance(value, str):
53
+ texts.append(value)
54
+ elif isinstance(value, dict) and isinstance(value.get("draft"), str):
55
+ texts.append(value["draft"])
56
+ return texts
57
+
58
+
59
+ def read_corpus(d):
60
+ root = Path(d)
61
+ if not root.exists():
62
+ raise ValueError(f"corpus directory does not exist: {root}")
63
+ if not root.is_dir():
64
+ raise ValueError(f"corpus path is not a directory: {root}")
65
+ texts = []
66
+ for p in sorted(root.rglob("*")):
67
+ if p.suffix.lower() in (".txt", ".md", ".json") and p.is_file():
68
+ try:
69
+ t = p.read_text(errors="ignore")
70
+ except OSError as exc:
71
+ raise ValueError(f"cannot read corpus file: {p}") from exc
72
+ if p.suffix.lower() == ".json":
73
+ try:
74
+ extracted = _json_texts(json.loads(t))
75
+ except json.JSONDecodeError as exc:
76
+ raise ValueError(f"invalid JSON corpus file: {p}") from exc
77
+ texts.extend(value for value in extracted if value.strip())
78
+ elif t.strip():
79
+ texts.append(t)
80
+ if not texts:
81
+ raise ValueError(f"corpus contains no non-empty .txt, .md, or supported JSON text: {root}")
82
+ return texts
83
+
84
+
85
+ def freqs(texts):
86
+ c = Counter()
87
+ total = 0
88
+ for t in texts:
89
+ w = WORD.findall(t.lower())
90
+ c.update(w)
91
+ total += len(w)
92
+ if total == 0:
93
+ raise ValueError("corpus contains no words")
94
+ return c, total
95
+
96
+
97
+ def excess_weights(human_dir, ai_dir):
98
+ hc, hn = freqs(read_corpus(human_dir))
99
+ ac, an = freqs(read_corpus(ai_dir))
100
+ out = {}
101
+ for term, n_ai in ac.items():
102
+ if n_ai < MIN_OBS or len(term) < 4:
103
+ continue
104
+ p_ai = n_ai / an
105
+ p_hu = (hc.get(term, 0) + 0.5) / hn # smoothed: unseen != impossible
106
+ ratio = p_ai / p_hu
107
+ if ratio <= 2.0: # not over-represented enough
108
+ continue
109
+ w = round(min(math.log2(ratio) * 1.2, MAX_WEIGHT), 1)
110
+ if w >= 1.0:
111
+ out[term] = {"w": w, "n_obs": n_ai, "ratio": round(ratio, 1)}
112
+ return out, hn, an
113
+
114
+
115
+ def shape_selftest():
116
+ """Run the shape channel against genres that mimic broetry structurally.
117
+
118
+ Hostile setting: every sample is declared `social`, the only genre where
119
+ the channel engages. Anything that flags here is a documented boundary,
120
+ not a silent one.
121
+ """
122
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
123
+ import slopscore
124
+ d = DATA / "corpus" / "must-not-flag-shape"
125
+ if not d.exists():
126
+ print("no shape corpus — shape channel untested")
127
+ return 1
128
+ KNOWN = {"lyrics"} # see data/corpus/must-not-flag-shape/README.md
129
+ flagged, total = [], 0
130
+ files = sorted(d.glob("*.txt"))
131
+ if not files:
132
+ print(f"shape corpus at {d} is empty — shape channel untested")
133
+ return 1
134
+ for p in files:
135
+ total += 1
136
+ m = slopscore.shape_metrics(p.read_text(), genre="social")
137
+ if m.get("broetry"):
138
+ tag = "known boundary" if p.stem in KNOWN else "REGRESSION"
139
+ flagged.append((p.stem, tag))
140
+ print(f" {tag:14s} {p.name} solo={m['solo_frac']} run={m['max_fragment_run']}")
141
+ new = [f for f, t in flagged if t == "REGRESSION"]
142
+ print(f"shape regression: {total - len(flagged)}/{total} silent, "
143
+ f"{len([f for f,t in flagged if t=='known boundary'])} known boundary, "
144
+ f"{len(new)} new")
145
+ return 1 if new else 0
146
+
147
+
148
+ def selftest(fp_dir=None):
149
+ """No pattern may fire on writing that must never be flagged."""
150
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
151
+ import slopscore
152
+ data = slopscore.load_patterns()
153
+ fp_dir = Path(fp_dir or DATA / "corpus" / "must-not-flag")
154
+ if not fp_dir.exists():
155
+ print(f"no false-positive corpus at {fp_dir} — nothing to check")
156
+ return 1
157
+ files = sorted(p for p in fp_dir.rglob("*")
158
+ if p.is_file() and p.suffix.lower() in (".txt", ".md")
159
+ and p.name.lower() != "readme.md")
160
+ if not files:
161
+ print(f"false-positive corpus at {fp_dir} is empty — nothing to check")
162
+ return 1
163
+ failures = []
164
+ for p in files:
165
+ r = slopscore.score_text(p.read_text(), data)
166
+ if r["ai_likelihood"] > 25:
167
+ failures.append((p.name, r["ai_likelihood"],
168
+ [h["name"] for h in r["hits"]][:4]))
169
+ for name, score, hits in failures:
170
+ print(f" FAIL {score:5.1f} {name} ← {', '.join(hits) or 'no lexical hits'}")
171
+ n = len(files)
172
+ print(f"false-positive regression: {n - len(failures)}/{n} passed")
173
+ return 1 if failures else 0
174
+
175
+
176
+ def decay():
177
+ """Halve the weight of patterns not re-confirmed within DECAY_MONTHS."""
178
+ p = DATA / "learned.json"
179
+ with file_locks([p]):
180
+ try:
181
+ d = json.loads(p.read_text())
182
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
183
+ raise SystemExit(f"cannot read valid learned state at {p}: {exc}") from exc
184
+ if not isinstance(d, dict) or not isinstance(d.get("patterns", []), list):
185
+ raise SystemExit(f"invalid learned-state schema at {p}")
186
+ for index, pat in enumerate(d.get("patterns", [])):
187
+ if (not isinstance(pat, dict)
188
+ or not isinstance(pat.get("w"), (int, float))
189
+ or isinstance(pat.get("w"), bool)
190
+ or not math.isfinite(pat["w"])
191
+ or not 0 <= pat["w"] <= 10
192
+ or ("last_confirmed" in pat
193
+ and not isinstance(pat["last_confirmed"], str))
194
+ or ("decayed" in pat and not isinstance(pat["decayed"], str))):
195
+ raise SystemExit(
196
+ f"invalid learned pattern at index {index} in {p}"
197
+ )
198
+ today = date.today()
199
+ changed = 0
200
+ for index, pat in enumerate(d.get("patterns", [])):
201
+ lc = pat.get("last_confirmed")
202
+ if not lc:
203
+ continue
204
+ try:
205
+ confirmed = date.fromisoformat(lc)
206
+ except ValueError as exc:
207
+ raise SystemExit(
208
+ f"invalid last_confirmed date at pattern index {index}"
209
+ ) from exc
210
+ y, m = confirmed.year, confirmed.month
211
+ months = (today.year - y) * 12 + (today.month - m)
212
+ weight = pat["w"]
213
+ # One stale interval earns one decay. Re-running the command must be
214
+ # idempotent; a later confirmation clears this marker and starts a
215
+ # fresh interval.
216
+ if months > DECAY_MONTHS and weight > 0.5 and not pat.get("decayed"):
217
+ pat["w"] = round(weight / 2, 2)
218
+ pat["decayed"] = str(today)
219
+ changed += 1
220
+ atomic_write_text(p, json.dumps(d, indent=1) + "\n")
221
+ print(f"decayed {changed} pattern(s) unconfirmed for over {DECAY_MONTHS} months")
222
+ return 0
223
+
224
+
225
+ def main(argv=None):
226
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
227
+ mode = ap.add_mutually_exclusive_group()
228
+ mode.add_argument("--selftest", action="store_true")
229
+ mode.add_argument("--decay", action="store_true")
230
+ ap.add_argument("--human", metavar="DIR")
231
+ ap.add_argument("--ai", metavar="DIR")
232
+ ap.add_argument("--out", metavar="FILE")
233
+ args = ap.parse_args(argv)
234
+ if args.selftest:
235
+ fp_status = selftest()
236
+ shape_status = shape_selftest()
237
+ return 1 if fp_status or shape_status else 0
238
+ if args.decay:
239
+ return decay()
240
+ if not args.human or not args.ai:
241
+ ap.error("calibration needs --human DIR and --ai DIR")
242
+ try:
243
+ weights, hn, an = excess_weights(args.human, args.ai)
244
+ except ValueError as exc:
245
+ ap.error(str(exc))
246
+ print(f"human corpus {hn:,} words · ai corpus {an:,} words")
247
+ print(f"terms over-represented in AI text (ratio > 2, n >= {MIN_OBS}): {len(weights)}")
248
+ for t, v in sorted(weights.items(), key=lambda kv: -kv[1]["w"])[:25]:
249
+ print(f" {v['w']:4.1f} {t:18s} {v['ratio']:6.1f}x n={v['n_obs']}")
250
+ out = Path(args.out) if args.out else DATA / "calibrated.json"
251
+ protected = {(DATA / "patterns.json").resolve(), (DATA / "learned.json").resolve()}
252
+ if out.resolve() in protected:
253
+ ap.error(f"refusing to overwrite tracked detector state: {out}; write a separate calibration file")
254
+ today = str(date.today())
255
+ payload = {"_comment": "Weights derived from corpus excess frequency by "
256
+ "calibrate.py. Merge into learned.json after "
257
+ "--selftest passes.",
258
+ "calibrated": today,
259
+ "lexicon": {t: v["w"] for t, v in weights.items()},
260
+ "provenance": {t: dict(v, first_seen=today, last_confirmed=today)
261
+ for t, v in weights.items()}}
262
+ try:
263
+ with file_locks([out]):
264
+ atomic_write_text(out, json.dumps(payload, indent=1) + "\n")
265
+ except OSError as exc:
266
+ ap.error(f"cannot write calibration output {out}: {exc}")
267
+ print(f"\nwrote {out}")
268
+ print("next: review, merge the lexicon into data/learned.json, then run "
269
+ "`python3 calibrate.py --selftest` before shipping")
270
+ return 0
271
+
272
+
273
+ if __name__ == "__main__":
274
+ sys.exit(main())